백엔드
IT 로드맵의 백엔드 구역이다.
요청 하나가 들어와서 응답 하나가 나가기까지를 맡는 자리다. 언어와 런타임이 아래를 받치고, 프레임워크가 그 위에서 내 코드를 불러 준다.
지금 들어갈 수 있는 곳은 둘이다.
-
자바
java한 줄이 실행되기까지 JDK 안에서 벌어지는 일을 맡는다. 컴파일 · 클래스 파일 · 스택 · 힙 · GC · JIT까지 따라갔다. - 프레임워크 내가 안 짠 코드가 내 코드를 어떻게 불러 주는지를 맡는다. 자동 구성 · 빈 · 프록시 · 톰캣 · 요청 왕복까지 따라갔다.
크기의 단위 — 변수에서 쿠버네티스까지 세 줄로 선다
주문 API 하나를 만든다고 하자. 나는 주문에 관한 코드를 order/ 폴더에 모으고, 그 안의 domain/ 폴더에 Order 클래스를 만들어 cancel() 메서드를 쓴다. 다 만든 앱은 서버(24시간 켜 둔 컴퓨터)에서 실행해 둔다. 브라우저나 다른 프로그램이 HTTP로 「이 주문 취소해 줘」 하고 요청을 보내면, 앱이 그걸 받아 처리하고 응답을 돌려준다. 그래서 main이 끝나면 꺼지는 연습용 프로그램과 달리, 서버의 앱은 요청을 기다리며 계속 켜져 있다. 이렇게 실행해 켜 두는 것을 「띄운다」고 한다.
이 일 하나에 크기가 다른 이름이 줄줄이 붙는다. 변수 · 메서드 · 클래스 · 패키지 · 앱, 서버 쪽으로 가면 컨테이너 · 쿠버네티스까지. 처음에는 이 이름들을 작은 것부터 큰 것까지 한 줄로 세우려 했다. 그런데 세워 보니 한 줄이 아니라 세 줄이었다. 같은 코드를 두고 묻는 질문이 셋이라서다.
- 실행 — 컴퓨터가 내 코드를 실제로 어떻게 돌리나
- 설계 — 내 코드가 무엇으로 이루어져 있나
- 짓기 — 다 만든 앱을 어디서 실행해 두나
세 질문의 답은 서로를 대신하지 않는다. 클래스를 어떻게 나누든 CPU가 코드를 도는 방식은 그대로고, 서버를 몇 대 쓰든 클래스 모양은 그대로다. 그래서 크기의 단위를 세 줄로 따로 세웠다. 아래 그림에서 칸은 위로 갈수록 크다. 다만 「크다」의 뜻이 단위마다 달라서, 위아래 칸이 무슨 관계인지를 줄마다 머리에 한 줄씩 적었다. 점선 밑줄이 있는 이름을 누르면 뜻이 나온다.
.jar 파일 하나로 묶은 것여기서 나온 .jar가 짓기로 넘어간다
도메인 패키지order/ — 주문 · 회원처럼 업무 하나의 코드를 모은 폴더. 여기서 「도메인」은 업무라는 뜻이다
계층 패키지order/api/ · order/domain/ … — 그 안을 요청 받기 · 규칙 · 저장처럼 역할로 나눈 폴더
클래스 몇 개의 협력클래스 몇 개를 엮는 모양 — 디자인 패턴
클래스Order
메서드cancel()
변수 · 문장int total = 0;
설계 단위와 짓기 단위는 애플리케이션에서 만난다. 내가 쓴 클래스들은 Gradle 같은 빌드 도구가 .jar 파일 하나로 묶는다. 애플리케이션 칸의 결과물이 이 .jar다. 짓기는 Docker로 그 .jar를 이미지로 싸는 데서 시작한다. 설계 단위 맨 위의 시스템 칸은 이 .jar를 몇 종류 만들지를 정하는 칸이라, 앱이 여럿이면 .jar도 이미지도 여럿이 된다.
세 단위는 운영체제와 하드웨어 위에 선다
운영체제와 하드웨어는 세 단위의 어느 칸도 아니고 세 단위가 서는 바닥이다. 비유로는 하드웨어가 땅이고, 운영체제는 그 땅 위에 맨 먼저 깔리는 도시 — 관청과 기반시설(도로 · 관로)이다. 컴퓨터 한 대가 땅 한 필지다.
땅은 세 겹이다. 지상의 땅이 메모리고, 지하 1층은 메모리가 아닌 하드웨어(CPU 같은 장비)가 있는 장비층, 가장 깊은 지하 2층 서고가 디스크(SSD)다. 서고에는 프로그램 꾸러미(실행 파일 — 책 상자와 「어느 책을 땅 어디에 꽂아라」는 배치표)와 데이터 원본이 잠들어 있다. 프로그램을 켜면 도시가 꾸러미의 배치표를 읽고 지상에 도서관을 세워 책을 꽂는다 — 실행 중인 프로그램이 곧 지상에 선 도서관이다. 그래서 전기가 나가면 지상의 도서관은 통째로 사라지고 지하만 남는다. 다시 켜면 지하의 꾸러미로 도서관을 새로 세운다.
운영체제도 지하의 꾸러미로 짓는 프로그램인데, 도서관이 아니라 도시인 이유는 권한이다. 켤 때 맨 먼저 깔리고, 땅을 직접 만지고 지하로 관로를 내리는 일은 도시만 한다. CPU가 이 차이를 실제로 강제한다 — 도시의 책(커널 코드)을 읽는 모드와 도서관의 책을 읽는 모드가 따로 있다. 다른 프로그램은 모두 도시가 나눠 준 부지에 서고, 땅과 지하는 도시의 관로로만 쓴다. 셸처럼 운영체제와 함께 오는 프로그램도 있지만 그건 보통 도서관이다. 도시 역할은 커널이 한다.
코드를 실제로 읽고 그대로 하는 것은 CPU다. 세계관으로는 지상 벌판 바로 밑, 지하 1층 장비층의 작업대다. 메모리 칸을 하나도 차지하지 않고, 바로 위 지상의 칸을 땅 밑 운반로로 복사해 내려받아 읽는다. 내 코드는 도서관 책장의 책이고, 작업대가 책갈피(스레드)가 가리키는 쪽을 복사해 와서 한 줄씩 읽는다. 책이 작업대로 가는 것도, 도서관이 작업대에 일을 보내는 것도 아니다.
내 코드가 파일을 읽거나 요청에 응답을 돌려보낼 때, 내 앱 같은 보통 프로그램은 디스크나 랜 카드(네트워크 선을 꽂는 부품)를 직접 만지지 않는다. 나는 자바 코드로 「파일을 연다」고만 쓰고, JVM이 운영체제에게 「이 파일 좀 열어 줘」 하고 부탁한다. 그러면 CPU가 운영체제의 코드를 읽으며 하드웨어를 움직인다. 그 부탁이 시스템 콜, 곧 책 속의 「도시 지침서를 펴라」는 문장이다. 이 문장을 만나면 같은 작업대가 도시 지침서를 이어서 읽고 돌아온다. 도서관에 있는 건 이 문장 하나뿐이고, 관로와 운반은 도시가 맡는다. 그래서 아래 그림은 위에서부터 「도시 지침서를 펴라」 → 도시 → 지상의 땅 → 지하 2층 서고 순으로 쌓인다. CPU가 있는 장비층은 이 그림에 따로 칸을 두지 않았다 — 위에서 말한 대로 지상 바로 밑이다.
예외가 하나 있다. 더하기 · 비교 같은 계산은 이 문장 없이 작업대가 바로 읽고 한다. 도시 지침서를 펴는 건 파일 · 네트워크처럼 CPU 말고 다른 장치를 써야 할 때다.
단위마다 이 땅에 닿는 자리가 다르다.
.jar가 윈도우에서도 리눅스에서도 돈다
땅을 공부하는 곳은 하드웨어 & OS 구역이다.
설계 단위 — 변수에서 시스템까지 백엔드 코드가 쌓이는 순서
세 단위 중 내가 직접 고를 것이 가장 많은 쪽이 설계 단위다. 변수와 문장이 모여 메서드가 되고, 메서드가 모여 클래스가 되고, 클래스 몇 개가 짝을 지어 함께 일하고, 그런 클래스들이 모여 패키지가 되고, 패키지가 모여 앱이 된다. 칸마다 정해야 하는 것이 다르다. 그 칸에서 무엇을 정해야 하는지, 이미 정한 글이 있으면 어디인지를 한 줄씩 적었다.
패키지 칸 둘은 이 블로그의 패키지 구조 표준이 쓰는 말이다. 도메인 패키지는 주문 · 회원처럼 업무 하나의 코드를 모은 폴더다. 계층 패키지는 그 안을 역할로 다시 나눈 폴더 넷이다 — 요청을 받는 api/, 일의 순서를 짜는 application/, 주문 규칙을 담는 domain/, DB에 저장하는 infra/. 주문 취소로 치면 application/은 「주문을 찾고 → 취소하고 → 저장한다」는 순서를, domain/은 「배송이 시작된 주문은 취소할 수 없다」는 규칙을 담는다. 요청이 api/에서 infra/까지 층을 차례로 내려가서 「계층」이라 부른다. 이름이 헷갈리는데, 도메인 패키지의 「도메인」은 업무(주문)라는 뜻이고, 계층 패키지 중 domain/은 그 업무의 규칙을 담는 층 하나의 이름이다.
.jar 파일 하나로 묶고, 터미널(명령어를 쳐서 컴퓨터를 다루는 창)에서 java -jar 앱.jar로 켠다. 이 명령은 JVM을 켜고, 그 JVM이 jar 안의 main을 찾아 실행하게 한다 — 앱 하나가 곧 JVM 하나다. IntelliJ 같은 개발 도구(IDE)의 실행 버튼이 하는 일과 같다
앱 안을 무엇으로 먼저 자르나 — 하는 일로(요청 받기 · 규칙 처리 · 저장) 자르나, 업무로(주문 · 회원) 자르나. 우리는 업무로 먼저 잘랐다 — 그래서 바로 아래 칸이 도메인 패키지, 그 아래가 계층 패키지다
책 배치 — 도서관 한 채
order/ · member/ — 업무 하나의 코드를 모은 폴더
업무 영역을 어디서 끊나 — 주문과 결제를 한 폴더에 둘까, 둘로 나눌까
패키지 구조 표준 원칙 1
order/api/ · order/application/ · order/domain/ · order/infra/
어느 폴더가 어느 폴더를 import해도 되나 — 자바는 다 허락하지만, 주문 규칙(domain/)이 저장 코드(infra/)를 직접 가져다 쓰면 저장 방식(예: 쓰는 DB 종류)을 바꿀 때 주문 규칙 코드까지 같이 고쳐야 한다. 가져다 쓴 클래스가 바뀌면 가져다 쓴 쪽도 깨지기 때문이다
패키지 구조 표준 원칙 2
OrderService와, Spring이 그 앞에 끼우는 프록시 — @Transactional이 이렇게 돈다
클래스 몇 개를 어떤 관계로 엮나 — 이미 이름이 붙은 해결 모양(디자인 패턴)이 있으면 그걸 쓴다
디자인 패턴 — 아래 절
Order · OrderItem
클래스 하나가 어떤 값과 규칙을 맡나, 어떤 클래스들을 함께 다루나 — 주문을 취소하면 주문 항목의 상태도 같이 바뀌어야 해서 Order와 OrderItem은 늘 함께 다룬다(애그리거트)
애그리거트 경계 표준
order.cancel()
메서드 하나가 한 가지 일만 하나
아직 다룬 글이 없다
- 맨 아랫줄은 코딩 스타일이다. 위로 올라갈수록 앱 전체의 뼈대(아키텍처)를 정하는 칸이 되고, 맨 아랫줄은 한 줄 한 줄의 모양만 정한다. 그래서 같은 단위에 두되 점선으로 떼어 따로 부른다. 섞으면 「헥사고날이냐 3계층이냐」 같은 앱 전체의 모양과 「변수에
final을 붙이냐」 같은 한 줄의 모양이 같은 무게로 보인다 - 「책 배치」 꼬리표가 붙은 네 칸은 아래 아키텍처 절에서 다시 나온다. 거기서는 앱 구조를 도서관에 빗대서 시스템을 도서관 단지, 애플리케이션을 도서관 한 채, 도메인 패키지를 책장, 계층 패키지를 책장 칸이라고 부른다. 그보다 작은 칸에도 같은 비유를 이어 붙였다 — 클래스 몇 개의 협력은 책 몇 권의 엮음, 클래스는 책 한 권, 메서드는 장(章), 변수와 문장은 문장 · 낱말이다. 칸마다 그림은 세계관에 있다
- 메서드 칸은 아직 다룬 글이 없다. 한 메서드가 한 가지 일만 하는지는 지금 코드 리뷰에서 눈으로만 본다
디자인 패턴은 클래스 몇 개의 협력 크기다 — 책 몇 권을 엮는 모양
코드를 짜다 보면 같은 종류의 고민이 자꾸 나온다. 「원래 클래스는 그대로 두고, 그 앞뒤에 일을 하나 더 끼우고 싶다」 같은 것이다. 이런 고민마다 잘 통한 해결 모양이 있고, 그 모양에 이름을 붙여 둔 것이 디자인 패턴이다. 유명한 것은 네 저자의 책 『Design Patterns』(1994)에 실린 23개다. 저자 넷을 Gang of Four(네 명의 패거리)라는 별명으로 불러서 GoF 패턴이라고도 한다.
설계 단위에서는 클래스 하나보다 크고 패키지보다 작은 칸, 곧 「클래스 몇 개의 협력」에 놓인다. 도서관으로 치면 책장 칸을 어떻게 나누느냐가 아니라, 책 몇 권을 「저 책 몇 장을 펴라」로 어떻게 엮느냐다. 책장 칸보다 작으니 어느 칸(패키지)에든 들어간다. 그래서 같은 패턴을 domain/에서도 infra/에서도 쓴다.
우리 코드에도 이미 하나가 돈다. @Transactional을 붙인 클래스는 Spring이 프록시로 감싼다. 프록시 패턴은 23개 중 하나이고, 모양은 이렇다. 실제로 Spring이 만드는 클래스의 이름과 코드는 다르고, 모양만 옮겼다.
먼저 알아 둘 것이 하나 있다. Spring 앱에서는 내가 new OrderService()를 쓰지 않는다. Spring이 앱을 켤 때 내 클래스들의 객체를 직접 만들어서, 필요한 곳의 변수에 넣어 준다(의존성 주입). 이때 @Transactional이 붙은 클래스를 보면, Spring은 아래의 OrderServiceProxy 같은 클래스를 실행 중에 새로 만들어(실행 중에 만든 클래스) 원래 객체 대신 그 객체를 넣는다.
// 내가 쓴 클래스 — 주문 취소만 한다
class OrderService {
void cancel(Long orderId) { /* DB에 있는 주문의 상태를 「취소」로 바꾼다 */ }
}
// Spring이 앱을 켤 때 만드는 클래스 — 원래 객체를 들고 있다가 일을 넘긴다
class OrderServiceProxy extends OrderService {
private final OrderService target; // Spring이 먼저 만들어 둔 원래 OrderService 객체
private final TxManager tx; // 트랜잭션을 열고 닫는 객체 (설명용 이름)
@Override
void cancel(Long orderId) {
tx.begin(); // 앞에 일을 하나 끼운다 — 트랜잭션을 연다
try {
target.cancel(orderId); // 진짜 일은 원래 객체에게 시킨다
tx.commit(); // 잘 끝나면 DB에 바꾼 것을 확정한다
} catch (RuntimeException e) {
tx.rollback(); // 도중에 실패하면 DB에 바꾼 것을 전부 되돌린다
throw e; // 실패했다는 사실은 부른 쪽에 그대로 알린다
}
}
}
// 주문 취소를 부르는 쪽
class OrderController {
private final OrderService orderService; // Spring이 여기에 프록시 객체를 넣어 준다
void cancelOrder(Long orderId) {
orderService.cancel(orderId); // 그래서 실제로는 프록시의 cancel이 불린다
}
}
OrderService는 내가 쓴 클래스다. 주문 취소만 하고 트랜잭션은 모른다- Spring은
OrderService객체(target)를 먼저 하나 만들고, 그걸 감싼OrderServiceProxy객체를 또 하나 만든다. 프록시는OrderService를 상속해서 겉모습이 똑같다 OrderController의orderService변수에 Spring이 넣어 준 것이 이 프록시 객체다. 그래서orderService.cancel()을 부르면 프록시가 먼저 받는다- 프록시는 트랜잭션을 열고 → 원래 객체의
cancel을 부르고 → 잘 끝나면 확정(커밋)하고, 실패하면 되돌린다(롤백).tx의 메서드 이름은 설명하려고 붙인 것이고, 실제로는 Spring의 트랜잭션 관리 객체가 같은 일을 한다 RuntimeException만 잡는 것은 Spring의 기본 규칙을 옮긴 것이다. Spring은 체크하지 않는 예외가 나면 되돌리고, 체크 예외가 나면 기본으로는 되돌리지 않는다- 되돌린 뒤
throw e로 예외를 다시 던지는 것은, 부른 쪽(OrderController)도 취소가 실패했다는 것을 알아야 하기 때문이다. 여기서 삼키면 부른 쪽은 성공한 줄 안다 - 부르는 쪽도
OrderService도 바뀐 것이 없다
원래 클래스는 그대로 두고 앞뒤에 일을 끼운다 — 이것이 프록시라는 이름이 붙은 해결 모양이다. 도서관에 빗대면 원래 책과 같은 표지를 씌운 대리 책이다. 작업대가 대리 책의 cancel 장을 펴면 「트랜잭션을 연다 → 원래 책 cancel 장을 편다 → 확정한다」가 적혀 있다. 원래 책(OrderService)은 그대로다. 이 프록시가 실제로 어떻게 걸리는지는 @Transactional 프록시 증명에서 직접 돌려 봤다.
패턴에도 크기가 셋 있다. 『Pattern-Oriented Software Architecture』(1996)는 패턴을 아키텍처 패턴 · 디자인 패턴 · 관용구 셋으로 나눈다. 이게 설계 단위 칸과 그대로 겹친다.
- 아키텍처 패턴 — 책 배치 크기. 3계층 · 헥사고날처럼 앱 전체를 어떻게 나누나
- 디자인 패턴 — 책 몇 권의 엮음 크기. 위의 프록시처럼 클래스 몇 개를 어떻게 엮나
- 관용구 — 문장 크기. 한 줄의 굳어진 모양이다. 컬렉션을 돌 때
for (int i = 0; i < orders.size(); i++)로 번호를 세지 않고for (Order order : orders)로 쓰는 것이 그 예다. 번호를 직접 세다 하나 넘치거나 모자라는 실수가 없어서, 다들 이 모양으로 쓴다
헷갈리는 이름이 하나 있다. 헥사고날의 다른 이름 「포트와 어댑터」에 들어간 어댑터는 원래 책 몇 권 크기의 패턴이다 — 모양이 다른 두 쪽 사이에 끼워 서로 맞춰 주는 클래스다. 우리 코드에서는 domain/에 「주문을 저장해 줘(save(order))」라는 약속만 인터페이스로 적어 두고(목차만 있는 책), infra/에 그 약속을 DB가 알아듣는 말(SQL)로 바꿔 주는 구현을 둔다(그 목차를 채운 책). 이 짝이 Repository다. 바깥과 닿는 곳은 DB만이 아니다. 들어오는 쪽도 있다 — application/이 「주문 취소」라는 입구를 UseCase 인터페이스로 열어 두고, api/의 Controller가 웹 요청을 받아 그 입구 모양에 맞춰 넣는다. 결제 서버처럼 다른 회사의 서버를 부르는 곳에도 같은 짝을 둔다. 이렇게 바깥과 닿는 곳마다 짝을 두면 domain/은 바깥을 하나도 모르게 된다. 그 모양이 앱 전체에 퍼진 것이 헥사고날이다 — 책 몇 권 크기의 어댑터를 도서관 전체 크기로 키운 것이다.
설계 단위를 도서관으로 걸어 들어간다 — 단지에서 문장까지
위의 설계 단위를 도서관 비유 그대로 그림으로 걸어 들어가 본다. 그림 아래 「다음」을 누르면 도서관 단지에서 시작해 도서관 → 책장 → 책장 칸 → 책 몇 권 → 책 한 권 → 장 → 문장 순으로 한 칸씩 확대된다. 버튼 사이의 막대 줄이 여덟 칸 중 어디까지 왔는지를 보여 주고, 막대를 누르면 그 칸으로 바로 간다. 그 아래 카드가 지금 보는 칸의 설명이다. 처음에는 스크롤로 넘겼는데, 카드가 그림 위로 지나가 버려서 한 칸에 머물러 보기가 어려웠다. 그래서 세계관 그림처럼 버튼으로 바꿨다(2026-10-12).
시스템 도서관 단지
도서관 몇 채가 모인 단지가 시스템이다. 금빛 테두리가 우리 앱이고, 흐린 도서관들은 앱을 기능마다 따로 떼어 내면(마이크로서비스) 생길 도서관이다. 지금 우리 단지에는 도서관이 한 채뿐이다.
애플리케이션 도서관 한 채
도서관 한 채가 Spring Boot 앱 하나다. 클래스들을 .jar 파일 하나로 묶고 java -jar로 켜면, 도시(운영체제)가 JVM 도서관을 세우고 그 책들을 꽂는다. 이 도서관 안을 무엇으로 먼저 나눌지가 다음 질문이다.
도메인 패키지 책장
유리벽을 걷으면 층마다 책장이 있다. 우리는 책을 업무로 먼저 나눴다 — 주문 책장 order/, 회원 책장 member/. 책장마다 요청 받기부터 저장까지 칸이 다 있다.
계층 패키지 책장 칸
주문 책장의 칸 넷이 api/ · application/ · domain/ · infra/다. 금빛 점선이 「누가 누구의 책을 펴 보나」이고, 화살표가 전부 domain/을 향한다. 주문 규칙 책은 바깥 책을 모른다.
클래스 몇 개의 협력 책 몇 권의 엮음
디자인 패턴이 놓이는 크기다. OrderController 책과 OrderService 책 사이에 Spring이 같은 표지를 씌운 대리 책(프록시)을 끼운다. 대리 책을 펴면 「트랜잭션을 연다 → 원래 책을 편다 → 확정한다」가 적혀 있다.
클래스 책 한 권
domain/ 칸의 Order가 책 한 권이다. 옆의 OrderItem과 굵은 선으로 묶인 것은, 주문을 취소하면 주문 항목도 같이 바뀌어서 늘 함께 다루기 때문이다(애그리거트).
메서드 장(章)
책 앞면의 띠 하나가 장 하나, 곧 메서드 하나다 — cancel() · ship() · total(). 장 하나는 한 가지 일만 적는다. CPU가 책갈피(스레드)를 따라 그 장을 읽는다.
변수 · 문장 문장 · 낱말
표지를 열면 쪽마다 문장이 보인다 — status · items · total 같은 변수와, 그걸 다루는 코드 한 줄 한 줄이다. 여기서부터는 앱의 뼈대가 아니라 한 줄 한 줄의 모양, 곧 코딩 스타일의 크기다.
실행 단위와 짓기 단위는 따로 본다
- 실행 단위: 설계 단위와 재는 것이 다르다. 설계 단위는 내가 쓴 코드가 무엇으로 짜였는지를 보고, 실행 단위는 그 코드가 컴퓨터 안에서 어떤 모양으로 바뀌어 도는지를 본다.
javac가.java를 바이트코드로 번역하고, CPU는 기계어만 읽을 수 있어서, 처음에는 CPU가 JVM의 통역 코드(인터프리터)를 읽으며 바이트코드를 한 줄씩 푼다. 그러다 자주 도는 부분은 JIT가 통째로 기계어로 번역해 두고, 다음부터 CPU가 그 번역본을 바로 읽는다. 이 바뀜은 컴파일러와 JVM이 알아서 하고 내가 고르는 칸이 없다. 그래서 설계 단위와 섞지 않는다. Study · JDK가 JDK 안에서.java가.class가 되는 길을 따라갔다 - 짓기 단위: 설계 단위 맨 위 「시스템」과 짓기 단위 맨 위 「클러스터」는 다른 질문이다. 시스템은 앱을 몇 종류로 나누나고, 클러스터는 그 앱을 서버 몇 대에 몇 개씩 띄우나다. 그래서 마이크로서비스(앱을 여러 종류로 쪼개 따로 배포하는 방식, 설계)와 Pod 개수(짓기)도 다른 질문이다 — 앱이 한 종류여도 똑같은 Pod를 세 개 띄울 수 있다. 요청이 몰릴 때 셋이 나눠 받고, 하나가 죽어도 나머지 둘이 계속 응답하게 하려는 것이다. 들어온 요청을 셋에 골고루 나눠 주는 일은 쿠버네티스의 서비스(Service)라는 기능이 맡는다 — 셋 앞에 서서 요청을 받아 돌아가며 넘긴다. 자세한 것은 앱을 서버에 올리고 운영하는 일을 다루는 데브옵스 구역에 둔다
아키텍처 — 백엔드는 책 배치와 요청 길을 고른다
아키텍처를 고르는 일은 두 축으로 나뉜다 — 책 배치(패키지를 어떻게 나누나)와 요청 길(요청 하나가 어떤 길로 흐르나). 둘은 따로 고른다. 패키지를 어떻게 나누든 요청은 같은 길로 흘려도 되고, 반대도 된다.
백엔드 책 배치는 패키지 구조 표준이 이미 정해 놓았다. 다만 그 표준은 「3계층 · 헥사고날 · 클린 중에 이 모양을 골랐다」는 비교 없이 모양만 정했다. 그래서 백엔드는 「모르는 걸 공부해서 고르기」가 아니라 「이미 고른 걸 다른 안과 비교해 왜 이걸 골랐는지 채우기」다.
우리가 이미 정한 책 배치
order/
├── api/ Controller
├── application/ Service, UseCase 인터페이스
├── domain/ Domain, Repository 인터페이스
└── infra/ Entity, Repository 구현체
- 도메인별로 먼저 나눈다 —
order/·member/가 먼저고, 계층은 그 안에 있다 - 화살표가 전부
domain을 향한다 — Repository 인터페이스는domain에, 구현체는infra에 둔다.infra가domain을 바라보고, 그 반대는 없다 - 경계는 ArchUnit이 지킨다 — 사람이 기억하지 않고 테스트가 막는다
책 배치는 도서관 단지 · 도서관 · 책장, 세 가지 크기로 본다
책 배치의 이름들은 한 줄에 서 있지 않다. 바라보는 크기가 셋이다. 도서관으로 치면 도서관을 몇 채 지을지, 도서관 한 채 안의 책장을 무엇으로 나눌지, 책장 하나 안의 칸들이 서로 누구를 펴 보게 할지가 따로다. 그래서 크기마다 따로 하나씩 고르고, 큰 것부터 정한다.
| 크기 | 개발에서 | 묻는 것 | 선택지 | 우리 |
|---|---|---|---|---|
| 도서관 단지 | 시스템 | 도서관을 몇 채 짓나 — 앱을 몇 개로 나누나 | 모놀리스 · 모듈러 모놀리스 · 마이크로서비스 | 한 채, 책장 사이에 칸막이 |
| 도서관 한 채 | 앱 하나 | 책장을 무엇으로 나누나 — 앱 안을 무엇으로 먼저 자르나 | 계층별 · 기능별 · 수직 슬라이스 | 업무별 책장 = 기능별 (order/ 먼저) |
| 책장 | 기능 폴더 하나 | 책장 칸끼리 누구를 펴 보나 — 계층끼리 화살표가 어디를 향하나 | 3계층 · DDD 4계층 · 헥사고날 · 어니언 · 클린 | 칸 네 개, 화살표는 domain으로 |
책장 하나의 칸들이 곧 api · application · domain · infra 네 겹이다.
크기가 다르니 서로 대신하지 않는다. 도서관을 여러 채(마이크로서비스)로 지어도, 한 채 한 채 안에서 다시 「무엇으로 나누나」와 「칸들이 누구를 펴 보나」를 골라야 한다.
다만 경계가 딱 떨어지지 않는 곳이 둘 있다.
- 모듈러 모놀리스의 칸막이는 책장 사이에 세운다. 그래서 단지 크기의 선택이 도서관 크기의 「업무별 책장(기능별)」을 전제로 한다
- 수직 슬라이스는 칸 없는 책장이다. 유스케이스 한 권에 다 들어 있어서 책장 크기의 선택이 가벼워진다
도서관 단지 — 도서관을 몇 채 짓나
| 이름 | 도서관으로 | 개발에서 | 우리 |
|---|---|---|---|
| 모놀리스 | 한 채 | 앱 하나 | 앱은 하나다 |
| 모듈러 모놀리스 | 한 채, 책장 사이에 칸막이 | 앱은 하나, 모듈 사이에 벽을 세운다 (Spring Modulith) | ArchUnit으로 도메인 사이에 벽을 세우니 이쪽에 가깝다 |
| 마이크로서비스 | 여러 채, 서로는 도로로 요청서를 주고받는다 | 앱 여러 개, 따로 배포. 서로 네트워크로 부른다 |
도서관 한 채 — 책장을 무엇으로 나누나
| 이름 | 도서관으로 | 개발에서 | 우리 |
|---|---|---|---|
| 계층별 (package by layer) | 요청 받는 책은 전부 한 구역, 저장하는 책은 전부 다른 구역 | controller/ · service/ · repository/ |
|
| 기능별 (package by feature) | 주문 책장 · 회원 책장, 책장마다 칸이 다 있다 | order/ · member/ 먼저, 그 안에 계층 |
이것이다 |
| 수직 슬라이스 (Vertical Slice) | 칸 없는 책장, 유스케이스 한 권에 다 있다 | 유스케이스 하나(「주문 생성」)가 폴더 하나 |
계층별이 왜 불편한지는 도서관으로 보면 바로 보인다. 주문에 관한 책이 구역마다 흩어져 있으면, 주문 하나를 고치는 데 구역 세 곳을 오가야 한다. 계층별 폴더에서 기능 하나를 고치려면 폴더 세 곳을 다 여는 것과 같다.
책장 — 책장 칸끼리 누구를 펴 보나
| 이름 | 누가, 언제 | 모양 | 우리와의 관계 |
|---|---|---|---|
| 3계층 (Layered) | 오래된 기본형 | 화면 → 업무 → DB, 위에서 아래로만 부른다 | 출발점. Service가 DB 코드를 직접 부른다 |
| DDD 4계층 | 에릭 에반스, 2003 | UI · Application · Domain · Infrastructure | 우리 네 겹의 이름이 이것과 같다 |
| 헥사고날 (포트와 어댑터) | 앨리스터 코오번, 2005 | 도메인을 가운데 두고, 웹 · DB는 꽂았다 뺐다 하는 어댑터로 둔다 | 우리 화살표(infra → domain)가 이 생각이다 |
| 어니언 | 제프리 팔레르모, 2008 | 겹겹이, 바깥 겹은 안쪽 겹만 안다 | 헥사고날과 같은 생각을 겹으로 그렸다 |
| 클린 | 로버트 마틴, 2012 | 동심원, 의존은 안쪽으로만 | 헥사고날 · 어니언을 합쳐 정리했다 |
헥사고날의 원래 이름은 「포트와 어댑터」다. 도서관으로 치면 포트는 목차만 있는 책, 어댑터는 그 목차를 채운 책이다. 3계층은 domain 칸 책의 본문에 「MariaDB 책 37쪽을 펴라」가 박혀 있는 도서관이라, DB가 바뀌면 domain 책을 다시 써야 한다. 헥사고날은 domain 칸 책에 「저장 목차의 save 장을 펴라」만 적고, 그 장을 무슨 책이 채울지는 infra 칸이 정한다.
헥사고날 · 어니언 · 클린은 그림이 다를 뿐 핵심이 하나다 — 도메인은 바깥을 모른다. 우리 자리는 「DDD 4계층의 이름에 헥사고날의 화살표」다.
요청 길 — 정한 것과 안 정한 것
| 내용 | 어디에 | |
|---|---|---|
| 정했다 | 쓰기 입력은 Command, 읽기 입력은 Query로 이름을 나눈다. CQRS를 엄격히 따르는 건 아니다 | 계층별 DTO 네이밍 표준 |
| 정했다 | 애그리거트 하나에 안 담기는 조회 결과는 XxxView로 내보낸다 |
조회 전용 타입 표준 |
| 정했다 | 도메인끼리는 application에서만, 인터페이스로 만난다 |
패키지 구조 표준 |
| 안 정했다 | 요청 하나가 끝까지 한 줄로 이어지는 동기 호출 말고, 이벤트로 흘려 보내는 방식 |
아직 안 본 것 — 하나씩 비교해 채운다
위 표의 이름 · 연도 · 한 줄 설명은 원본을 열어 확인하지 않았다. 하나씩 글로 정리하면서 확인하고, 그때 「우리는 왜 이 모양인가」를 채운다.
| 축 | 볼 것 | 왜 |
|---|---|---|
| 책 배치 · 책장 | 3계층 → 헥사고날 | 화살표를 뒤집으면 무엇이 좋아지고 무엇을 잃나. 우리가 이미 뒤집었으니 그 이유를 적는다 |
| 책 배치 · 책장 | 어니언 · 클린 | 헥사고날과 정말 같은가, 다른 점은 무엇인가 |
| 책 배치 · 도서관 | 수직 슬라이스 | 기능별보다 더 잘게 자르면 무엇이 달라지나 |
| 책 배치 · 단지 | 모듈러 모놀리스 | ArchUnit으로 세운 벽과 Spring Modulith의 벽은 무엇이 다른가 |
| 요청 길 | 트랜잭션 스크립트 · 도메인 모델 | 업무 규칙을 Service에 두나, Domain 객체에 두나 |
| 요청 길 | 이벤트 · 메시지 | 안 정한 칸이다 |
| 요청 길 | CQRS | 이름만 나눈 지금과, 저장소까지 나누는 CQRS는 무엇이 다른가 |