헥사고날
고친 사람 github-actions[bot]
헥사고날은 업무 규칙을 데이터베이스 같은 바깥 기술에서 떼어 놓는 설계 방식입니다. 업무 규칙은 바깥에 바라는 일을 인터페이스로만 적어 둡니다. 바깥 기술은 그 인터페이스에 맞춰 끼워 넣습니다. 그래서 저장소를 바꾸거나 테스트용 가짜를 끼워도 업무 규칙 코드는 바뀌지 않습니다.
쉽고 빠른 이해
헥사고날은 업무 규칙이 바깥 기술을 모르게 만드는 설계입니다. 주문 코드는 「주문을 저장해 줘」라는 인터페이스만 압니다. 그 뒤에서 데이터베이스가 받는지 메모리 목록이 받는지는 모릅니다.
업무 규칙이 데이터베이스 코드를 직접 부르면 둘이 엉겨 붙습니다. 저장 기술을 바꿀 때 업무 코드까지 고쳐야 합니다. 업무 규칙 하나를 시험하려 해도 데이터베이스를 띄워야 합니다.
어떻게 도나.
- 업무 규칙이 바깥에 바라는 일을 인터페이스로 정합니다. 이 인터페이스를 포트라고 부릅니다
- 바깥 기술마다 그 인터페이스를 구현한 코드를 만듭니다. 이 코드를 어댑터라고 부릅니다
- 프로그램을 띄울 때 어느 어댑터를 끼울지 고릅니다. 운영에서는 데이터베이스 어댑터를, 테스트에서는 메모리 어댑터를 끼웁니다
대가도 있습니다. 인터페이스와 클래스가 늘어납니다. 업무 객체를 저장용 모양이나 응답용 모양으로 옮겨 담는 코드도 어댑터마다 붙습니다.
그래서 업무 규칙이 크고 바깥 기술이 여럿 붙는 서비스에서 이 구조를 씁니다. 데이터를 넣고 꺼내기만 하는 서비스에서는 대가만 남습니다.
상세
이 절은 헥사고날이 코드를 안쪽과 바깥쪽으로 가르는 방식을 봅니다. 두 쪽을 잇는 포트와 어댑터를 차례로 풉니다. 코드가 서로를 아는 방향은 짧은 자바 코드로 확인합니다. 예로는 주문을 받는 쇼핑몰 서비스 하나를 끝까지 씁니다.
헥사고날은 헥사고날 아키텍처(Hexagonal Architecture)를 줄여 부르는 말입니다. 같은 구조를 포트와 어댑터(Ports and Adapters)라고도 부릅니다.
안쪽의 업무 규칙
업무 규칙은 이 서비스가 왜 있는지를 담은 코드입니다. 비즈니스 로직이나 도메인 로직이라고도 부릅니다. 쇼핑몰이면 「주문 수량은 한 개 이상이어야 한다」, 「이미 발송한 주문은 취소할 수 없다」가 업무 규칙입니다.
헥사고날은 이 업무 규칙을 한가운데 안쪽에 둡니다. 업무 규칙이 바깥과 주고받을 일을 적은 인터페이스도 함께 안쪽에 둡니다. 이 인터페이스가 다음 소절의 포트입니다.
나머지 기술 코드는 전부 바깥쪽입니다. 요청을 받는 웹 컨트롤러, 주문을 담는 데이터베이스, 결제를 맡기는 외부 결제사, 알림을 보내는 메일 서버가 모두 바깥에 섭니다.
안과 밖을 가르는 것은 기술이 업무 규칙과 따로 바뀌기 때문입니다. 주문을 다른 데이터베이스로 옮겨도 「주문 수량은 한 개 이상이어야 한다」는 바뀌지 않습니다. 결제사를 바꿔도 마찬가지입니다.
둘이 한 코드에 섞여 있으면 이 차이가 문제가 됩니다. 업무 코드 한가운데 쿼리 문자열과 요청 본문을 읽는 코드가 들어 있으면 저장 기술을 바꿀 때 업무 코드까지 고칩니다. 업무 규칙 하나를 시험하려 해도 웹 서버와 데이터베이스를 띄워야 합니다.
포트
포트는 안쪽에 속한 코드입니다. 안쪽이 바깥과 주고받을 일을 적어 둔 인터페이스입니다. 「주문을 저장한다」, 「결제를 요청한다」처럼 안쪽의 말로 적습니다. 테이블 이름이나 요청 주소 같은 기술의 말은 포트에 나오지 않습니다.
포트가 있으면 업무 규칙이 바깥 기술의 이름을 몰라도 됩니다. 업무 규칙은 포트만 부릅니다. 그 포트 뒤에 무엇이 붙었는지는 업무 규칙이 신경 쓰지 않습니다.
네트워크에서 말하는 포트와는 이름만 같습니다. 이 문서의 포트는 코드 안의 인터페이스입니다.
어댑터
어댑터는 포트와 바깥 기술 사이에서 말을 옮겨 주는 바깥 코드입니다. 데이터베이스 어댑터는 「주문을 저장한다」를 받아 테이블에 행을 넣는 쿼리로 바꿉니다. 웹 어댑터는 들어온 요청 본문을 읽어 안쪽이 아는 주문 객체로 바꿉니다.
한 포트에 어댑터를 여럿 만들 수 있습니다. 「주문을 저장한다」 포트에 데이터베이스 어댑터와 메모리 어댑터를 따로 둘 수 있습니다. 메모리 어댑터는 데이터베이스 대신 프로그램 메모리의 목록에 주문을 담습니다. 어느 쪽을 끼워도 안쪽 코드는 한 줄도 안 바뀝니다.
부르는 쪽과 불리는 쪽
바깥에 선 것들은 두 부류로 갈립니다. 하나는 안쪽을 부르는 쪽입니다. 웹 컨트롤러, 정해진 시각에 도는 배치 작업, 테스트 코드가 여기 듭니다. 다른 하나는 안쪽이 부르는 쪽으로, 데이터베이스와 결제사와 메일 서버가 여기 듭니다.
부르는 쪽의 이름은 주도하는 쪽(driving side)입니다. 불리는 쪽은 주도되는 쪽(driven side)입니다. 어댑터도 어느 쪽에 서느냐에 따라 주도하는 어댑터와 주도되는 어댑터로 나뉩니다.
아래 그림은 가운데 업무 규칙과 둘레의 바깥을 한 장에 담았습니다. 화살표는 부르는 방향입니다.
flowchart TD
subgraph 주도하는쪽["주도하는 쪽"]
W["웹 컨트롤러"]
B["배치 작업"]
T["테스트 코드"]
end
R{{"업무 규칙"}}
subgraph 주도되는쪽["주도되는 쪽"]
D["데이터베이스"]
P["결제사"]
E["메일 서버"]
end
W --> R
B --> R
T --> R
R --> D
R --> P
R --> E
주도하는 쪽은 위에서 업무 규칙을 부릅니다. 업무 규칙은 아래의 주도되는 쪽을 부릅니다. 가운데가 왜 육각형인지는 「육각형이라는 이름」 소절에서 봅니다.
포트도 이 두 쪽을 따라 둘로 나뉩니다. 주도하는 쪽이 부르는 포트는 안쪽이 바깥에 내놓는 기능입니다. 「주문을 넣는다」 같은 이 포트가 입력 포트입니다.
안쪽이 주도되는 쪽을 부를 때 거치는 포트는 안쪽이 일을 하려고 바깥에 바라는 기능입니다. 「주문을 저장한다」 같은 이 포트가 출력 포트입니다.
아래 그림은 주문 요청 하나가 흐르는 길을 위에서 아래로 그렸습니다.
flowchart TD
W["웹 어댑터"]
subgraph 안쪽["안쪽"]
IP["입력 포트 · 주문을 넣는다"]
R["업무 규칙"]
OP["출력 포트 · 주문을 저장한다"]
end
D["데이터베이스 어댑터"]
M["메모리 어댑터"]
W --> IP
IP --> R
R --> OP
OP --> D
OP --> M
웹 어댑터가 입력 포트를 부르면 업무 규칙이 돕니다. 업무 규칙은 수량을 검사한 뒤 출력 포트를 부릅니다. 출력 포트 아래의 두 어댑터 중 그때 끼워 둔 하나가 저장을 맡습니다.
안쪽을 향하는 의존성
위 그림의 화살표는 부르는 방향입니다. 코드가 서로를 아는 방향은 이와 다릅니다. 코드 A 가 코드 B 의 이름을 알고 가져다 쓰면 A 가 B 에 의존한다고 합니다. 이 방향을 의존성의 방향이라고 합니다.
데이터베이스 어댑터는 바깥에 있으면서 안쪽의 출력 포트를 구현합니다. 그래서 어댑터가 안쪽을 압니다. 안쪽은 어댑터를 모릅니다. 부르는 방향은 안에서 밖이지만 의존성은 밖에서 안을 향합니다.
먼저 안쪽 코드입니다. 출력 포트 하나와 그 포트를 쓰는 업무 규칙입니다.
// 안쪽 · 출력 포트
interface OrderStore {
void save(Order order);
}
// 안쪽 · 업무 규칙
class PlaceOrder {
private final OrderStore store;
PlaceOrder(OrderStore store) {
this.store = store;
}
void place(Order order) {
if (order.quantity() < 1) {
throw new IllegalArgumentException();
}
store.save(order);
}
}
PlaceOrder 는 OrderStore 라는 인터페이스만 압니다. 데이터베이스라는 낱말이 이 코드 어디에도 없습니다. 무엇에 저장할지는 생성자로 받은 store 가 정합니다.
다음은 바깥의 데이터베이스 어댑터입니다.
// 바깥 · 데이터베이스 어댑터
class DbOrderStore implements OrderStore {
public void save(Order order) {
// 주문을 테이블 행으로 바꿔 넣는다
}
}
implements OrderStore 한 줄이 의존성의 방향을 보여 줍니다. 바깥의 어댑터가 안쪽 인터페이스의 이름을 압니다. 안쪽 코드에는 DbOrderStore 라는 이름이 한 번도 나오지 않습니다.
앞의 흐름 그림은 부르는 방향을 그렸습니다. 아래 그림은 같은 코드가 서로를 아는 방향, 곧 의존성의 방향을 그립니다. 메모리 어댑터 MemoryOrderStore 의 코드는 「테스트에서 얻는 것」 소절에서 봅니다.
flowchart TD
subgraph 안쪽["안쪽"]
R["PlaceOrder · 업무 규칙"]
OP["OrderStore · 출력 포트"]
end
D["DbOrderStore · 데이터베이스 어댑터"]
M["MemoryOrderStore · 메모리 어댑터"]
R -->|쓴다| OP
OP ~~~ D
OP ~~~ M
D -->|구현한다| OP
M -->|구현한다| OP
상자 배치는 흐름 그림과 같습니다. 어댑터 쪽 화살표만 뒤집혀 모두 안쪽을 향합니다. 안쪽 상자에서 밖으로 나가는 의존성은 하나도 없습니다.
의존성 역전 원칙
의존성 역전 원칙은 업무 규칙 같은 안쪽 코드가 저장 같은 바깥 기술에 의존하지 않게 하는 원칙입니다. 둘 다 인터페이스에 의존하게 만듭니다. 그 인터페이스는 업무 규칙 쪽에 둡니다. 앞에서 본 OrderStore 가 그런 인터페이스입니다.
이 원칙이 무엇을 뒤집는지는 레이어드와 견주면 보입니다. 레이어드는 코드를 계층으로 나눠 위아래로 쌓는 구조입니다. 위 계층이 아래 계층을 부릅니다. 부르는 위 계층이 아래 계층의 이름도 압니다.
흔히 업무 규칙을 담은 계층 아래에 데이터를 저장하는 계층을 둡니다. 그래서 레이어드에서는 업무 규칙이 저장 코드에 의존합니다. 부르는 방향과 의존성의 방향이 둘 다 위에서 아래입니다.
헥사고날은 이 원칙으로 저장 쪽 의존성의 방향만 뒤집습니다. 부르는 방향은 바뀌지 않습니다. 앞 그림처럼 모든 의존성이 업무 규칙을 향합니다.
조립 코드
안쪽이 어댑터를 모르면 누군가는 둘을 이어 줘야 합니다. 프로그램이 뜰 때 어댑터 객체를 만들어 업무 규칙에 넘기는 코드가 이 일을 맡습니다. 이 조립 코드는 안쪽도 어댑터도 아닌 가장 바깥에 둡니다.
의존성 주입은 객체가 쓸 다른 객체를 밖에서 만들어 넘겨주는 방식입니다. 위의 PlaceOrder 가 생성자로 store 를 받은 것이 의존성 주입입니다. 조립을 대신 해 주는 의존성 주입 컨테이너를 쓰기도 합니다.
테스트에서 얻는 것
헥사고날이 노린 것 하나는 업무 규칙을 바깥 없이 돌려 보는 것입니다. 사람이 화면으로 쓰든 다른 프로그램이나 테스트가 부르든 같은 업무 규칙이 돌게 합니다. 메모리 어댑터를 끼워 업무 규칙만 시험하는 테스트로 이것을 확인합니다.
테스트 더블은 테스트에서 진짜 대신 끼우는 가짜 객체입니다. 메모리 어댑터가 그런 가짜입니다. 받은 주문을 목록에 담기만 합니다.
class MemoryOrderStore implements OrderStore {
final List<Order> saved = new ArrayList<>();
public void save(Order order) {
saved.add(order);
}
}
이제 테스트가 PlaceOrder 를 직접 부릅니다. 이 예에서는 입력 포트 인터페이스를 따로 두지 않았습니다. PlaceOrder 의 place 가 그 역할을 겸합니다. 웹 서버도 데이터베이스도 띄우지 않습니다.
var store = new MemoryOrderStore();
var placeOrder = new PlaceOrder(store);
placeOrder.place(new Order("A-1", 2));
store.saved.size(); // 1
수량이 2인 주문을 넣었더니 목록에 한 건이 담겼습니다. 업무 규칙이 검사를 통과시키고 저장을 부른 것입니다. 수량을 0으로 넣으면 place 가 예외를 던집니다. 목록은 그대로 비어 있습니다.
육각형이라는 이름
헥사고날은 육각형 모양이라는 뜻의 영어 hexagonal 을 소리 나는 대로 적은 말입니다. 이 구조를 그릴 때 업무 규칙을 육각형 안에 넣습니다. 포트는 그 둘레에 붙여 그립니다. 이름은 이 그림에서 왔습니다.
육각형의 여섯이라는 수에는 뜻이 없습니다. 계층을 위아래로 쌓는 그림에서는 바깥이 위와 아래 둘뿐입니다. 둘레가 넓은 도형을 고르면 포트를 필요한 만큼 둘러 그릴 수 있습니다. 「부르는 쪽과 불리는 쪽」 그림의 가운데 육각형이 그 모양입니다.
쓰면 나빠지는 것
헥사고날은 기술을 떼어 놓는 대신 코드를 늘립니다. 늘어나는 것은 셋입니다.
첫째는 인터페이스와 클래스 수입니다. 저장 하나에도 포트 인터페이스와 어댑터 클래스가 따로 생깁니다. 바깥 기술이 늘 때마다 이 짝이 하나씩 붙습니다.
둘째는 모양을 옮겨 담는 코드입니다. 안쪽의 주문 객체는 기술을 모릅니다. 그래서 테이블 모양 객체나 응답 모양 객체를 어댑터 쪽에 따로 둡니다. 주문 객체와 이 객체 사이에서 값을 옮겨 담습니다.
이렇게 옮겨 담는 일이 매핑입니다. 아래 그림은 주문 하나가 요청에서 테이블까지 옮겨 담기는 길입니다.
flowchart TD
Q["요청 본문 · 웹 어댑터 쪽"]
O["주문 객체 · 안쪽"]
T["테이블 모양 객체 · 데이터베이스 어댑터 쪽"]
Q -->|웹 어댑터가 옮겨 담는다| O
O -->|데이터베이스 어댑터가 옮겨 담는다| T
주문에 필드 하나를 더하면 그 필드가 지나는 곳을 모두 고칩니다. 이 그림에서는 모양 셋과 매핑 둘입니다.
셋째는 코드를 따라 읽는 수고입니다. 업무 규칙을 읽다가 포트 메서드를 만나면 그 뒤에 어떤 어댑터가 끼워졌는지 한 번 더 찾아야 합니다. 조립 코드를 열어 봐야 그 답이 나옵니다.
쓰는 곳과 안 쓰는 곳
업무 규칙이 크고 바깥 기술이 여럿 붙는 서비스에서 씁니다. 결제사를 바꾸거나 저장소를 옮겨도 고칠 곳이 어댑터 하나로 모입니다. 업무 규칙을 데이터베이스 없이 시험하고 싶을 때도 이 구조를 고릅니다.
데이터를 받아 테이블에 넣고 꺼내 보여주기만 하는 서비스는 사정이 다릅니다. 지킬 업무 규칙이 거의 없어서 포트와 어댑터가 테이블 하나를 몇 겹으로 감싸기만 합니다. 이런 서비스는 흔히 레이어드로 짓습니다.
같은 생각에서 나온 구조
클린 아키텍처와 어니언 아키텍처도 업무 규칙을 가운데 두고 모든 의존성을 안쪽으로 모읍니다. 안쪽을 몇 겹으로 나누는지와 겹마다 붙인 이름이 헥사고날과 다릅니다.
관련 항목
헥사고날을 이루는 구성 요소
입력 포트 · 출력 포트 · 주도하는 어댑터 · 주도되는 어댑터 · 비즈니스 로직 · 도메인 모델 · 유스케이스
헥사고날이 기대는 설계 원칙
의존성 역전 원칙 · 의존성 주입 · 의존성 · 인터페이스 · 관심사 분리 · 결합도 · 응집도 · SOLID
어댑터를 짓는 데 쓰는 설계 패턴
어댑터 패턴 · 리포지토리 패턴 · DTO · 데이터 매퍼 · 부패 방지 계층
업무 규칙을 떼어 시험하는 테스트 수단
테스트 더블 · 목 객체 · 단위 테스트 · 통합 테스트 · 테스트 주도 개발
헥사고날과 같은 목적을 두고 겨루는 아키텍처 양식
레이어드 · 클린 아키텍처 · 어니언 아키텍처 · 수직 슬라이스 아키텍처 · MVC · 모듈러 모놀리스
헥사고날과 짝지어 쓰는 설계 방법
도메인 주도 설계 · 경계 컨텍스트 · CQRS · 이벤트 주도 · 마이크로서비스
헥사고날이 속하는 상위 분류
소프트웨어 아키텍처 · 아키텍처 양식 · 아키텍처 패턴
다른 이름: 헥사고날 아키텍처 · 육각형 아키텍처 · 포트와 어댑터 · 포트 앤 어댑터 · Hexagonal Architecture · Ports and Adapters