레이어드
고친 사람 github-actions[bot]
레이어드는 애플리케이션 코드를 하는 일에 따라 위아래 계층으로 나눠 짓는 결정입니다. 요청을 받는 코드, 업무 규칙, 저장하는 코드를 각각 한 계층에 두는 식입니다. 위 계층은 바로 아래 계층만 부릅니다. 그래서 한 계층을 고쳐도 나머지가 덜 흔들립니다.
쉽고 빠른 이해
레이어드는 코드를 요청 받기, 업무 규칙, 저장이라는 세 덩이로 나눠 위에서 아래로만 부르게 합니다. 주문 조회 요청이 오면 요청 받기를 맡은 컨트롤러가 먼저 받습니다. 컨트롤러는 업무 규칙을 맡은 서비스에 넘깁니다. 서비스는 저장을 맡은 리포지토리에게 주문을 꺼내 달라고 합니다.
한 함수에 세 가지 일을 섞어 두면 데이터베이스를 바꿀 때 요청을 다루는 코드까지 뒤져야 합니다. 업무 규칙 하나를 시험하려 해도 서버와 데이터베이스를 다 띄워야 합니다.
- 하는 일마다 계층을 하나씩 둡니다
- 위 계층은 바로 아래 계층만 부릅니다
- 아래 계층은 누가 자기를 부르는지 모릅니다
대가는 늘어나는 코드입니다. 아래로 넘기기만 하는 메서드가 생깁니다. 필드 하나를 더하려 해도 계층마다 파일을 하나씩 고치게 됩니다. 데이터를 넣고 꺼내는 일이 대부분이면 이 대가가 작습니다. 업무 규칙이 크고 붙는 바깥 기술이 많으면 다른 구조를 함께 봅니다.
상세
계층은 큰 일을 위아래로 나눴을 때 그중 한 덩이입니다. 위 덩이는 자기 몫만 합니다. 나머지는 아래 덩이에 맡깁니다. 주문 조회라면 요청을 받는 덩이, 업무 규칙을 따지는 덩이, 저장소에서 꺼내는 덩이가 따로 섭니다.
레이어드라는 이름은 영어 layered 를 소리대로 적은 것입니다. 레이어드 아키텍처나 계층형 아키텍처라고도 부릅니다.
이 절은 주문 번호를 받아 주문 내용을 돌려주는 기능 하나를 가지고 봅니다. 이 기능을 계층 없이 한 함수로 짰을 때와 계층으로 나눴을 때를 견줍니다.
그다음 계층 사이의 규칙 둘과 레이어드가 치르는 대가를 봅니다. 끝으로 의존 방향을 뒤집은 구조와 어떻게 갈리는지 봅니다.
한 함수에 뭉친 세 가지 일
가장 짧게 짜면 함수 하나가 모든 일을 합니다. 아래 코드는 HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜) 요청에서 번호를 꺼내 데이터베이스를 조회한 뒤 응답을 만듭니다.
Response getOrder(Request req) {
long id = Long.parseLong(req.param("id"));
Row row = db.query(
"SELECT * FROM orders WHERE id = ?", id);
if (row.get("status").equals("CANCELED"))
return Response.status(410);
return Response.ok(toBody(row));
}
이 함수 안에는 성격이 다른 일이 셋 섞여 있습니다. 첫째는 요청을 해석하고 응답을 만드는 일입니다. 둘째는 「취소된 주문은 내주지 않는다」는 업무 규칙입니다. 셋째는 SQL(Structured Query Language, 구조화 질의 언어)로 데이터를 읽는 일입니다.
셋이 한 함수에 있으면 하나를 고칠 때 나머지까지 건드립니다. 저장소를 다른 데이터베이스로 바꾸면 요청을 다루는 줄 사이에서 SQL 을 찾아 고쳐야 합니다. 업무 규칙 한 줄을 시험하려 해도 서버와 데이터베이스를 다 띄워야 합니다.
성격이 다른 일을 서로 다른 코드에 두는 원칙을 관심사 분리라고 부릅니다. 한쪽이 바뀌어도 다른 쪽이 안 따라 바뀌게 하려는 것입니다. 레이어드는 이 원칙을 위아래 방향으로 적용합니다.
흔히 쓰는 세 계층
백엔드 애플리케이션은 대개 세 계층으로 나눕니다. 이름이 팀마다 조금씩 다르므로 이 문서에서 쓸 이름을 먼저 정합니다.
| 이 문서의 이름 | 맡는 일 | 흔히 보는 다른 이름 |
|---|---|---|
| 표현 계층 | 요청을 해석하고 응답 모양을 만든다 | 컨트롤러 · 프레젠테이션 계층 |
| 업무 계층 | 업무 규칙을 따지고 일의 순서를 정한다 | 서비스 · 비즈니스 계층 |
| 영속 계층 | 데이터베이스에 읽고 쓴다 | 리포지토리 · 데이터 접근 계층 |
업무 계층이 다루는 규칙을 비즈니스 로직이라고 합니다. 「취소된 주문은 내주지 않는다」처럼 기술과 상관없이 업무가 정한 규칙입니다. 저장소를 바꾸든 화면을 바꾸든 이 규칙은 그대로 남아야 합니다.
프로그램이 꺼져도 데이터가 남아 있는 성질을 영속성이라고 부릅니다. 영속 계층이라는 이름이 여기서 왔습니다. 이 계층은 데이터를 데이터베이스에 넣고 꺼내는 일만 맡습니다.
데이터베이스 자체를 맨 아래 계층으로 세어 넷으로 부르기도 합니다. 계층 수는 정해진 것이 없습니다. 몇 개로 세든 하는 일마다 계층 하나를 둔다는 생각은 같습니다.
flowchart TD
C["클라이언트"] -->|"HTTP 요청"| P["표현 계층 · 컨트롤러"]
P -->|"주문 찾아 줘"| B["업무 계층 · 서비스"]
B -->|"주문 꺼내 줘"| D["영속 계층 · 리포지토리"]
D -->|"SQL"| DB[("데이터베이스")]
요청은 그림처럼 위에서 아래로 한 칸씩 내려갑니다. 결과는 같은 길을 거꾸로 올라옵니다. 앞의 함수를 이 세 계층으로 나누면 아래처럼 됩니다.
// 표현 계층
Response getOrder(Request req) {
long id = Long.parseLong(req.param("id"));
return Response.ok(service.find(id));
}
// 업무 계층
Order find(long id) {
Order o = repository.findById(id);
if (o.isCanceled()) throw new Gone();
return o;
}
// 영속 계층
Order findById(long id) {
return db.query("SELECT ...", id);
}
앞의 함수는 취소된 주문에 410 을 돌려줬습니다. 410 은 요청한 것이 사라져 더는 없다는 뜻의 HTTP 응답 코드입니다. 이 코드의 이름이 Gone 입니다.
업무 계층은 응답 코드를 모릅니다. 그래서 410 을 돌려주는 대신 같은 이름의 Gone 예외를 던집니다.
이 예외를 410 응답으로 바꾸는 일은 표현 계층 쪽이 맡습니다. 아래 그림은 취소된 주문 하나를 따라가며
결과가 올라오는 길을 보입니다.
sequenceDiagram
participant 클라이언트
participant 컨트롤러
participant 서비스
participant 리포지토리
클라이언트->>컨트롤러: 주문 7번 조회
컨트롤러->>서비스: find(7)
서비스->>리포지토리: findById(7)
리포지토리-->>서비스: 주문 · 상태는 취소됨
Note over 서비스: 취소된 주문이라 Gone 예외를 던진다
서비스-->>컨트롤러: Gone 예외
Note over 컨트롤러: Gone 을 410 응답으로 바꾼다
컨트롤러-->>클라이언트: 410 응답
업무 규칙 한 줄은 업무 계층에만 남았습니다. SQL 은 영속 계층에만 있습니다. 이제 저장소를 바꿀 때는 영속 계층부터 엽니다. 영속 계층이 업무 계층에 내주는 메서드 모양이 그대로면 다른 계층은 손대지 않습니다.
취소 규칙을 시험할 때는 가짜 리포지토리를 넣어 업무 계층만 돌립니다. 서버도 데이터베이스도 띄우지 않습니다.
부르는 방향은 위에서 아래
첫째 규칙은 방향입니다. 위 계층은 아래 계층을 부르지만 아래 계층은 위에 누가 있는지 모릅니다. 영속 계층 코드에는 요청이나 응답이라는 낱말이 나오지 않습니다.
A 가 B 를 불러야 제 일을 할 수 있으면 A 가 B 에 의존한다고 말합니다. 이 관계를 의존성이라고 부릅니다. 레이어드에서 의존성은 언제나 아래를 향합니다.
방향이 한쪽이면 바꿀 때 번지는 범위도 한쪽입니다. 표현 계층을 웹 요청 대신 메시지 큐에서 꺼낸 메시지를 받게 바꿔도 업무 계층과 영속 계층은 손대지 않습니다. 영속 계층의 메서드 모양이 바뀌면 바로 위 업무 계층만 고칩니다.
아래 계층이 위를 부르기 시작하면 두 계층이 서로 의존하게 됩니다. 그러면 어느 쪽도 혼자 떼어내거나 바꿀 수 없습니다.
닫힌 계층과 열린 계층
둘째 규칙은 건너뛰기입니다. 요청이 반드시 거쳐 가야 하는 계층을 닫힌 계층이라고 부릅니다. 표현 계층은 영속 계층을 바로 부르지 못하고 업무 계층을 거쳐야 합니다.
닫아 두는 까닭은 규칙이 빠지지 않게 하려는 것입니다. 컨트롤러가 리포지토리를 바로 부르면 코드는 짧아집니다. 대신 취소 규칙을 건너뛰므로 취소된 주문까지 응답에 나갑니다.
이렇게 정해진 순서를 뛰어넘는 것을 계층 위반이라고 합니다. 한 번 생기면 저장소를 바꿀 때 컨트롤러까지 뒤져야 하는 처음 상태로 돌아갑니다.
건너뛰어도 되는 계층은 열린 계층이라고 부릅니다. 날짜 계산이나 암호화처럼 업무 규칙 여러 곳이 같이 쓰는 기능이 흔한 예입니다. 이 기능을 공통 기능 계층으로 떼어 업무 계층과 영속 계층 사이에 끼워 넣습니다. 그러면 계층이 넷이 됩니다.
업무 계층은 날짜 계산이 필요할 때 공통 기능 계층을 부릅니다. 주문을 꺼낼 때는 이 계층을 거치지 않고 영속 계층을 바로 부릅니다. 공통 기능 계층을 열어 두었기 때문입니다. 업무 계층은 여전히 닫혀 있으므로 표현 계층은 영속 계층을 바로 부를 수 없습니다.
flowchart TD
P["표현 계층"] --> B["업무 계층 · 닫힘"]
B --> S["공통 기능 계층 · 열림"]
S --> D["영속 계층"]
B -.->|"건너뛰어도 된다"| D
P -.-x|"막힘 · 업무 계층을 거쳐야 한다"| D
어느 계층을 열지는 미리 정해 두어야 합니다. 정하지 않고 하나씩 열다 보면 나눈 효과가 사라집니다.
계층 사이를 오가는 데이터 모양
계층마다 다루기 편한 데이터 모양이 다릅니다. 영속 계층은 테이블의 행에 맞춘 객체를 씁니다. 표현 계층은 응답에 내보낼 필드만 담은 객체를 씁니다.
계층 사이에서 데이터를 옮기려고 만든 객체를 DTO(Data Transfer Object, 데이터 전송 객체)라고 부릅니다. 필드만 있고 규칙은 없는 단순한 그릇입니다.
모양을 따로 두면 테이블 칼럼 이름이 바뀌어도 응답 모양은 안 바뀝니다. 대가는 옮겨 담는 코드입니다. 행 객체를 DTO 로 바꾸는 변환이 계층 경계마다 생깁니다.
레이어드가 치르는 대가
계층을 나누면 한 계층을 고칠 때 다른 계층을 덜 건드립니다. 대신 코드 양과 거쳐 가는 단계가 늘어납니다. 그 대가는 셋입니다.
첫째는 거쳐 가기만 하는 코드입니다. 조회 기능에서는 업무 계층이 따질 규칙이 없는 때가 많습니다. 그러면 서비스 메서드는 리포지토리를 부르고 결과를 돌려주기만 합니다. 계층 수만큼 파일과 메서드가 늘지만 하는 일은 없습니다.
둘째는 기능 하나가 모든 계층을 가로지른다는 점입니다. 주문에 「배송 메모」 필드를 하나 더하면 테이블, 영속 계층 객체, 업무 계층, DTO, 컨트롤러를 차례로 고칩니다. 계층을 기능이 아니라 기술을 기준으로 나눴기 때문입니다.
계층마다 따로 배포하면 이 대가가 더 커집니다. 계층 하나를 프로그램 하나로 떼어 따로 올리면, 기능 하나를 고칠 때마다 세 프로그램을 모두 새로 올려야 합니다.
시스템을 따로 배포하는 작은 프로그램 여럿으로 쪼개는 방식을 마이크로서비스라고 합니다. 이 이름의 서비스는 업무 계층의 서비스가 아니라 따로 배포하는 프로그램 하나를 뜻합니다. 마이크로서비스가 프로그램을 계층이 아니라 업무 단위로 자르는 까닭이 앞 문단에 있습니다.
셋째는 업무 계층이 영속 계층에 의존한다는 점입니다. 앞에서 저장소를 바꿀 때 영속 계층부터 연다고 했습니다. 이 말은 영속 계층이 업무 계층에 내주는 메서드와 타입이 그대로일 때만 맞습니다.
영속 계층이 저장 기술의 객체를 그대로 올려 보내면 이 조건이 깨집니다. 첫 함수의 Row 처럼 저장
기술이 정한 행 객체나 그 기술이 던지는 예외가 그런 객체입니다. 업무 규칙 코드가 이런 타입을 들여오면
저장 기술을 바꿀 때 업무 계층까지 고쳐야 합니다.
의존 방향을 뒤집은 구조
업무 계층이 영속 계층에 의존하는 대가를 줄이려고 의존성의 방향을 뒤집는 방법이 있습니다. 업무 계층이 「주문을 찾아 주는 것」이라는 약속을 먼저 정합니다. 영속 계층은 그 약속을 지키는 코드를 씁니다.
메서드 이름과 모양만 정해 두고 몸통은 비워 둔 이 약속을 인터페이스라고 합니다. 인터페이스가 있으면 부르는 쪽은 그 뒤에 어떤 코드가 있는지 몰라도 됩니다.
인터페이스를 업무 계층이 가지므로 영속 계층이 업무 계층에 의존하게 됩니다. 업무 계층은 이제 저장 기술의 타입을 들여오지 않습니다. 이렇게 방향을 뒤집는 원칙이 의존성 역전 원칙입니다.
flowchart TD
P1["표현 계층"] --> B1["업무 계층"]
B1 --> D1["영속 계층"]
subgraph 레이어드["레이어드 · 의존성이 아래로"]
P1
B1
D1
end
D1 ~~~ P2
P2["표현 계층"] --> R
D2["영속 계층"] -->|"약속을 지킨다"| I
subgraph 뒤집은뒤["뒤집은 뒤 · 의존성이 업무 계층으로"]
P2
D2
subgraph B2["업무 계층"]
R["업무 규칙"]
I["인터페이스 · 주문을 찾아 주는 것"]
end
end
위쪽 그림에서 업무 계층은 영속 계층을 알고 있습니다. 아래쪽 그림에서는 인터페이스가 업무 규칙과 함께 업무 계층 안에 있습니다. 영속 계층은 그 인터페이스를 알고 거기에 맞춰 코드를 씁니다.
이 그림의 화살표는 부르는 방향이 아니라 의존하는 방향입니다. 부르는 쪽은 여전히 업무 계층입니다. 업무 계층이 인터페이스의 메서드를 부르면 영속 계층의 코드가 돕니다.
계층의 이름과 맡는 일은 같습니다. 화살표 하나의 방향만 바뀌었습니다.
이 생각을 구조 전체로 밀고 간 것이 헥사고날입니다. 클린 아키텍처도 같은 생각에서 나왔습니다. 둘 다 업무 규칙을 한가운데에 두고 저장소와 웹을 바깥에 둡니다. 모든 의존성이 안쪽 업무 규칙을 향합니다.
계층과 티어
레이어드의 계층은 코드를 나눈 논리적 단위입니다. 세 계층이 한 프로세스 안에서 한꺼번에 돌아도 레이어드입니다.
이와 달리 서로 다른 기계나 프로세스에 나눠 놓은 단위를 티어라고 부릅니다. 브라우저, 애플리케이션 서버, 데이터베이스 서버로 나눈 구조가 3티어 아키텍처입니다.
flowchart TD
subgraph T1["브라우저 티어"]
W["브라우저"]
end
subgraph T2["애플리케이션 서버 티어"]
P["표현 계층"] --> B["업무 계층"]
B --> D["영속 계층"]
end
subgraph T3["데이터베이스 서버 티어"]
DB[("데이터베이스")]
end
W --> P
D --> DB
레이어드의 세 계층은 그림처럼 애플리케이션 서버 티어 하나 안에 모두 들어갑니다. 우리말로 둘 다 「계층」이라 부르는 일이 많아서 자주 섞입니다.
네트워크 통신을 일곱 계층으로 나눈 OSI 모델(Open Systems Interconnection, 개방형 시스템 상호 연결)에도 표현 계층이 있습니다. 이쪽은 데이터를 바이트로 적는 방식을 맞추는 계층이라 이 문서의 표현 계층과 이름만 같습니다.
고를 만한 상황과 다른 길을 볼 상황
업무 규칙이 적고 데이터를 넣고 꺼내는 일이 대부분이면 레이어드가 치르는 대가가 작습니다. 계층이 셋뿐이라 새로 온 사람도 코드가 어디 있을지 짐작합니다. 한 번에 배포하는 모놀리스 안에서도, 마이크로서비스로 쪼갠 프로그램 하나 안에서도 이렇게 나눕니다.
업무 규칙이 크고 결제사, 메시지 큐, 여러 저장소처럼 붙는 바깥 기술이 많으면 업무 계층이 아래 기술에 의존하는 대가가 커집니다. 업무 계층이 여러 저장 기술을 들여오게 되기 때문입니다. 이때는 의존성을 뒤집는 헥사고날 쪽을 봅니다.
기능 하나를 고칠 때 계층을 가로지르는 일이 잦으면 코드를 기능 단위로 먼저 묶는 방법도 있습니다. 기능마다 폴더를 두고 그 안에서만 계층을 나누는 구조를 수직 슬라이스 아키텍처라고 부릅니다.
레이어드와 이 구조들은 모두 시스템을 어떤 모양으로 지을지 정하는 결정입니다. 이 결정을 소프트웨어 아키텍처라고 부릅니다.
관련 항목
레이어드를 이루는 구성 요소
계층 · 컨트롤러 · 서비스 계층 · 리포지토리 패턴 · 데이터 접근 계층 · DAO · 비즈니스 로직 · 영속성
레이어드가 따르는 설계 원칙
관심사 분리 · 추상화 · 정보 은닉 · 결합도 · 응집도 · 의존성 · 인터페이스 · 의존성 역전 원칙 · 의존성 주입
레이어드와 맞세워지는 아키텍처 양식
헥사고날 · 포트와 어댑터 · 클린 아키텍처 · 어니언 아키텍처 · 수직 슬라이스 아키텍처 · 마이크로서비스 · 모놀리스 · 모듈러 모놀리스 · MVC
레이어드가 속하는 상위 분류
소프트웨어 아키텍처 · 아키텍처 양식 · 아키텍처 패턴 · 디자인 패턴
레이어드에서 자주 생기는 문제
계층 위반 · 싱크홀 안티패턴 · 빈약한 도메인 모델 · 누수 추상화 · 순환 의존성
계층 사이에서 데이터를 옮기는 수단
DTO · 엔티티 · 매퍼 · ORM · 값 객체
레이어드의 계층과 헷갈리는 이름
티어 · 3티어 아키텍처 · N티어 아키텍처 · OSI 모델 · 표현 계층 · 계층 구조
다른 이름: layered architecture · 레이어드 아키텍처 · 계층형 아키텍처 · 계층 아키텍처 · layered pattern