update 2026-10-12 14:54 - · -

IT 로드맵의 백엔드 구역이다.

요청 하나가 들어와서 응답 하나가 나가기까지를 맡는 자리다. 언어와 런타임이 아래를 받치고, 프레임워크가 그 위에서 내 코드를 불러 준다.

지금 들어갈 수 있는 곳은 둘이다.


크기의 단위 — 변수에서 쿠버네티스까지 세 줄로 선다

주문 API 하나를 만든다고 하자. 나는 주문에 관한 코드를 order/ 폴더에 모으고, 그 안의 domain/ 폴더에 Order 클래스를 만들어 cancel() 메서드를 쓴다. 다 만든 앱은 서버(24시간 켜 둔 컴퓨터)에서 실행해 둔다. 브라우저나 다른 프로그램이 HTTP로 「이 주문 취소해 줘」 하고 요청을 보내면, 앱이 그걸 받아 처리하고 응답을 돌려준다. 그래서 main이 끝나면 꺼지는 연습용 프로그램과 달리, 서버의 앱은 요청을 기다리며 계속 켜져 있다. 이렇게 실행해 켜 두는 것을 「띄운다」고 한다.

이 일 하나에 크기가 다른 이름이 줄줄이 붙는다. 변수 · 메서드 · 클래스 · 패키지 · 앱, 서버 쪽으로 가면 컨테이너 · 쿠버네티스까지. 처음에는 이 이름들을 작은 것부터 큰 것까지 한 줄로 세우려 했다. 그런데 세워 보니 한 줄이 아니라 세 줄이었다. 같은 코드를 두고 묻는 질문이 셋이라서다.

  • 실행 — 컴퓨터가 내 코드를 실제로 어떻게 돌리나
  • 설계 — 내 코드가 무엇으로 이루어져 있나
  • 짓기 — 다 만든 앱을 어디서 실행해 두나

세 질문의 답은 서로를 대신하지 않는다. 클래스를 어떻게 나누든 CPU가 코드를 도는 방식은 그대로고, 서버를 몇 대 쓰든 클래스 모양은 그대로다. 그래서 크기의 단위를 세 줄로 따로 세웠다. 아래 그림에서 칸은 위로 갈수록 크다. 다만 「크다」의 뜻이 단위마다 달라서, 위아래 칸이 무슨 관계인지를 줄마다 머리에 한 줄씩 적었다. 점선 밑줄이 있는 이름을 누르면 뜻이 나온다.

실행 단위 컴퓨터가 내 코드를 실제로 어떻게 돌리나 내가 고르지 않는다 · 명령이 어떻게 바뀔지는 컴파일러 · JVM · CPU가 정한다 위 칸 하나를 풀면 아래 칸 여러 개가 나온다
.class 파일javac가 .java 하나를 번역해 만든 파일 — 안에 바이트코드 명령이 줄줄이 들어 있다 바이트코드 명령JVM이 알아듣는 명령 하나 — 예: 「두 수를 더해라」 기계어 명령CPU가 읽고 그대로 하는 명령 하나. 바이트코드 명령 하나가 기계어 명령 여러 개로 풀리고, CPU가 그걸 읽는다
설계 단위 내 코드가 무엇으로 이루어져 있나 내가 고른다 · 무엇을 어떻게 나눌지 아래 칸이 여러 개 모여 위 칸이 된다
시스템서로 HTTP로 부르며 함께 일하는 앱 전부 — 지금 우리는 앱 하나 애플리케이션Spring Boot 앱 하나 — 클래스들을 .jar 파일 하나로 묶은 것여기서 나온 .jar가 짓기로 넘어간다 도메인 패키지order/ — 주문 · 회원처럼 업무 하나의 코드를 모은 폴더. 여기서 「도메인」은 업무라는 뜻이다 계층 패키지order/api/ · order/domain/ … — 그 안을 요청 받기 · 규칙 · 저장처럼 역할로 나눈 폴더 클래스 몇 개의 협력클래스 몇 개를 엮는 모양 — 디자인 패턴 클래스Order 메서드cancel() 변수 · 문장int total = 0;
다루는 곳이 페이지
짓기 단위 다 만든 앱을 어디서 실행해 두나 내가 고른다 · 똑같은 앱을 몇 개 띄울지, 서버를 몇 대 둘지 이미지를 켜면 컨테이너다(같은 것의 꺼진 상태와 켜진 상태). 그 위로는 담는 범위가 넓어진다 — Pod → 서버 한 대 → 서버 묶음
클러스터노드 여러 대를 한 덩어리로 관리 노드Pod가 올라가는 서버 한 대 — 운영체제와 하드웨어를 가진 칸이라 여기서 땅에 닿는다 Pod컨테이너를 담는 껍데기 — 보통 하나를 담고, 꼭 붙어 다녀야 하는 둘(앱과, 앱이 남긴 실행 기록을 모아 보내는 프로그램)을 함께 담기도 한다 컨테이너이미지를 켜 둔 것 — 안의 JVM이 켜져서 .jar를 돌리고 있다 이미지.jar · JVM · JVM이 돌 때 찾는 기본 파일(리눅스의 기본 명령어 · 설정 파일 같은 운영체제 파일 일부)을 묶은 꾸러미설계에서 넘어온 .jar를 여기서 싼다
도시와 땅 운영체제(도시)와 메모리 · 디스크(땅) — 세 단위의 어느 칸도 아니고 셋이 서는 바닥

설계 단위와 짓기 단위는 애플리케이션에서 만난다. 내가 쓴 클래스들은 Gradle 같은 빌드 도구가 .jar 파일 하나로 묶는다. 애플리케이션 칸의 결과물이 이 .jar다. 짓기는 Docker로 그 .jar를 이미지로 싸는 데서 시작한다. 설계 단위 맨 위의 시스템 칸은 이 .jar를 몇 종류 만들지를 정하는 칸이라, 앱이 여럿이면 .jar도 이미지도 여럿이 된다.

세 단위는 운영체제와 하드웨어 위에 선다

운영체제와 하드웨어는 세 단위의 어느 칸도 아니고 세 단위가 서는 바닥이다. 비유로는 하드웨어가 땅이고, 운영체제는 그 땅 위에 맨 먼저 깔리는 도시 — 관청과 기반시설(도로 · 관로)이다. 컴퓨터 한 대가 땅 한 필지다.

땅은 세 겹이다. 지상의 땅이 메모리고, 지하 1층은 메모리가 아닌 하드웨어(CPU 같은 장비)가 있는 장비층, 가장 깊은 지하 2층 서고가 디스크(SSD)다. 서고에는 프로그램 꾸러미(실행 파일 — 책 상자와 「어느 책을 땅 어디에 꽂아라」는 배치표)와 데이터 원본이 잠들어 있다. 프로그램을 켜면 도시가 꾸러미의 배치표를 읽고 지상에 도서관을 세워 책을 꽂는다 — 실행 중인 프로그램이 곧 지상에 선 도서관이다. 그래서 전기가 나가면 지상의 도서관은 통째로 사라지고 지하만 남는다. 다시 켜면 지하의 꾸러미로 도서관을 새로 세운다.

운영체제도 지하의 꾸러미로 짓는 프로그램인데, 도서관이 아니라 도시인 이유는 권한이다. 켤 때 맨 먼저 깔리고, 땅을 직접 만지고 지하로 관로를 내리는 일은 도시만 한다. CPU가 이 차이를 실제로 강제한다 — 도시의 책(커널 코드)을 읽는 모드와 도서관의 책을 읽는 모드가 따로 있다. 다른 프로그램은 모두 도시가 나눠 준 부지에 서고, 땅과 지하는 도시의 관로로만 쓴다. 셸처럼 운영체제와 함께 오는 프로그램도 있지만 그건 보통 도서관이다. 도시 역할은 커널이 한다.

코드를 실제로 읽고 그대로 하는 것은 CPU다. 세계관으로는 지상 벌판 바로 밑, 지하 1층 장비층의 작업대다. 메모리 칸을 하나도 차지하지 않고, 바로 위 지상의 칸을 땅 밑 운반로로 복사해 내려받아 읽는다. 내 코드는 도서관 책장의 책이고, 작업대가 책갈피(스레드)가 가리키는 쪽을 복사해 와서 한 줄씩 읽는다. 책이 작업대로 가는 것도, 도서관이 작업대에 일을 보내는 것도 아니다.

내 코드가 파일을 읽거나 요청에 응답을 돌려보낼 때, 내 앱 같은 보통 프로그램은 디스크나 랜 카드(네트워크 선을 꽂는 부품)를 직접 만지지 않는다. 나는 자바 코드로 「파일을 연다」고만 쓰고, JVM이 운영체제에게 「이 파일 좀 열어 줘」 하고 부탁한다. 그러면 CPU가 운영체제의 코드를 읽으며 하드웨어를 움직인다. 그 부탁이 시스템 콜, 곧 책 속의 「도시 지침서를 펴라」는 문장이다. 이 문장을 만나면 같은 작업대가 도시 지침서를 이어서 읽고 돌아온다. 도서관에 있는 건 이 문장 하나뿐이고, 관로와 운반은 도시가 맡는다. 그래서 아래 그림은 위에서부터 「도시 지침서를 펴라」 → 도시 → 지상의 땅 → 지하 2층 서고 순으로 쌓인다. CPU가 있는 장비층은 이 그림에 따로 칸을 두지 않았다 — 위에서 말한 대로 지상 바로 밑이다.

예외가 하나 있다. 더하기 · 비교 같은 계산은 이 문장 없이 작업대가 바로 읽고 한다. 도시 지침서를 펴는 건 파일 · 네트워크처럼 CPU 말고 다른 장치를 써야 할 때다.

시스템 콜 도서관(프로그램)이 도시(운영체제)에 일을 부탁하는 유일한 자리 — 파일 저장, 네트워크로 보내기. 이 문장을 만나면 같은 CPU가 도시 지침서를 이어서 읽는다
운영체제 도시 역할은 커널이 한다 땅 위에 맨 먼저 깔린다. 도서관마다 부지를 나눠 주고, CPU가 어느 책갈피를 읽을지 차례를 정하고, 땅과 지하로 짐을 나르는 일을 혼자 맡는다
메모리 CPU가 바로 읽고 쓰는 땅 실행 중인 프로그램이 통째로 올라와 있다. 빠르지만 전기가 나가면 사라진다
디스크 SSD · 하드디스크 프로그램 꾸러미(실행 파일)와 데이터 원본이 잠든 곳. 느리지만 전기가 나가도 남는다

단위마다 이 땅에 닿는 자리가 다르다.

실행 단위 맨 아래 칸이 하드웨어(CPU)에 바로 닿는다 기계어 명령은 CPU에서 바로 돈다 — 더하기 같은 계산은 운영체제를 거치지 않는다. 파일 · 네트워크처럼 다른 하드웨어를 쓰는 일만 시스템 콜로 부탁한다. 다만 CPU 하나를 여러 프로그램이 돌아가며 쓴다. 운영체제는 정해 둔 시간(타임 슬라이스)이 지나면 타이머 신호(인터럽트)로 CPU를 되돌려 받아 다음 프로그램에 넘긴다. 운영체제가 보기에는 JVM도 프로세스(실행 중인 프로그램) 하나라서, JVM이 쓸 수 있는 메모리도 운영체제가 허락한 만큼이다
설계 단위 실행 단위를 거쳐서 닿는다 내 클래스와 패키지는 운영체제를 모른다. 파일을 열 때도 나는 자바 코드만 쓰고, 운영체제에 실제로 부탁하는 일은 JVM(실행 단위에서 바이트코드를 기계어로 바꾸는 프로그램)이 대신한다. 윈도우와 리눅스는 파일을 여는 방법(시스템 콜) 같은 것이 서로 다른데, JVM이 운영체제마다 따로 만들어져 있어서 그 차이를 맞춰 준다. 그래서 같은 .jar가 윈도우에서도 리눅스에서도 돈다
짓기 단위 노드 칸에서 닿는다 노드는 하드웨어와 운영체제를 가진 서버 한 대, 곧 땅 한 필지다. 짓기 단위는 크기 순으로 세워서 땅에 닿는 칸이 맨 아래가 아니라 노드다 — 이미지 · 컨테이너 · Pod는 노드보다 작아서 노드 안에 들어간다. 컨테이너도 운영체제 위에서 돌지만 그 운영체제는 노드의 것이다. 이미지에 든 운영체제 파일은 JVM이 돌 때 찾는 기본 파일일 뿐이고, 프로그램을 실제로 돌리는 커널과 하드웨어를 가진 칸은 노드뿐이다. 그래서 땅에 닿는 칸을 노드로 본다. 그리고 컨테이너 자체가 운영체제의 기능이다. 리눅스 운영체제가 프로그램끼리 칸막이를 쳐 주는 기능을 Docker가 빌려 쓰는 것이다. 윈도우에도 비슷한 기능이 있어 윈도우용 컨테이너가 따로 있지만, 서버 쪽 노드는 대부분 리눅스다. 그 칸막이가 어떻게 쳐지는지는 데브옵스 구역에서 다룬다

땅을 공부하는 곳은 하드웨어 & OS 구역이다.

설계 단위 — 변수에서 시스템까지 백엔드 코드가 쌓이는 순서

세 단위 중 내가 직접 고를 것이 가장 많은 쪽이 설계 단위다. 변수와 문장이 모여 메서드가 되고, 메서드가 모여 클래스가 되고, 클래스 몇 개가 짝을 지어 함께 일하고, 그런 클래스들이 모여 패키지가 되고, 패키지가 모여 앱이 된다. 칸마다 정해야 하는 것이 다르다. 그 칸에서 무엇을 정해야 하는지, 이미 정한 글이 있으면 어디인지를 한 줄씩 적었다.

패키지 칸 둘은 이 블로그의 패키지 구조 표준이 쓰는 말이다. 도메인 패키지는 주문 · 회원처럼 업무 하나의 코드를 모은 폴더다. 계층 패키지는 그 안을 역할로 다시 나눈 폴더 넷이다 — 요청을 받는 api/, 일의 순서를 짜는 application/, 주문 규칙을 담는 domain/, DB에 저장하는 infra/. 주문 취소로 치면 application/은 「주문을 찾고 → 취소하고 → 저장한다」는 순서를, domain/은 「배송이 시작된 주문은 취소할 수 없다」는 규칙을 담는다. 요청이 api/에서 infra/까지 층을 차례로 내려가서 「계층」이라 부른다. 이름이 헷갈리는데, 도메인 패키지의 「도메인」은 업무(주문)라는 뜻이고, 계층 패키지 중 domain/은 그 업무의 규칙을 담는 층 하나의 이름이다.

크기 우리 코드에서는 이 크기에서 고르는 것 더 읽을 곳
시스템책 배치 · 도서관 단지 앱 몇 개 — 지금은 하나 앱을 몇 개로 나누나 — 앱 하나에 다 넣나(모놀리스), 기능마다 따로 떼어 내나(마이크로서비스) 책 배치 — 도서관 단지
애플리케이션책 배치 · 도서관 한 채 Spring Boot 앱 하나 — 클래스들을 .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() 메서드 하나가 한 가지 일만 하나 아직 다룬 글이 없다
변수 · 문장코딩 스타일 · 문장 · 낱말 final int maxRetry = 3; 이름을 어떻게 짓나, final(한 번 넣은 값을 못 바꾸게 막는 표시)을 붙이나 — 줄바꿈 · 띄어쓰기는 도구(포매터)가 맞춘다 코드 포매터 표준
  • 맨 아랫줄은 코딩 스타일이다. 위로 올라갈수록 앱 전체의 뼈대(아키텍처)를 정하는 칸이 되고, 맨 아랫줄은 한 줄 한 줄의 모양만 정한다. 그래서 같은 단위에 두되 점선으로 떼어 따로 부른다. 섞으면 「헥사고날이냐 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).

1 / 8 · 도서관 단지

시스템 도서관 단지

도서관 몇 채가 모인 단지가 시스템이다. 금빛 테두리가 우리 앱이고, 흐린 도서관들은 앱을 기능마다 따로 떼어 내면(마이크로서비스) 생길 도서관이다. 지금 우리 단지에는 도서관이 한 채뿐이다.

실행 단위와 짓기 단위는 따로 본다

  • 실행 단위: 설계 단위와 재는 것이 다르다. 설계 단위는 내가 쓴 코드가 무엇으로 짜였는지를 보고, 실행 단위는 그 코드가 컴퓨터 안에서 어떤 모양으로 바뀌어 도는지를 본다. 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는 무엇이 다른가

← IT 로드맵으로

최상단으로 이동했습니다!
확대 이미지

조회수: - · -

업데이트: