3-Layer Architecture
3계층 아키텍처
Controller, Service, Data Access 계층으로 이어지는 요청과 응답 흐름을 확인합니다.
3계층 아키텍처는 요청 처리, 비즈니스 로직, 데이터 접근의 책임을 각각 분리하는 구조입니다. 브라우저의 요청은 Controller와 Service를 거쳐 Repository 또는 MyBatis Mapper에 전달되고, 처리 결과는 반대 방향으로 반환됩니다.
Controller Service Repository MyBatis Mapper Database
도메인 드리븐을 쓸 수 없는 자리. 외주처럼 구조를 우리가 정할 수 없는 일감에서는 네 겹으로 나누고 package-private으로 막는 방식을 강제할 수 없다.
Request Flow
브라우저의 HTTP 요청이 Controller와 Service를 거쳐 Data Access Layer에 닿고, 결과가 반대 방향으로 돌아온다. 계층 카드를 누르면 아래 표준 목록이 그 계층 것만 남긴다.
Request 사용자의 동작이 Database에 닿기까지
Browser → api · web → service → repository → Database순서로 흐른다. 계층을 고르면 아래 목록이 그 계층의 표준만 남긴다.
Response 조회한 데이터가 화면으로 돌아오기까지
- Database
- repository
- service
- apiweb
- Browser
돌아오는 길에는 Query Result, Entity / Record, 결과 객체, HTTP Response
순서로 타입이 바뀐다. 각 계층은 자기 타입만 알고, 옆 계층의 타입은 모른다.
패키징은 도메인 드리븐과 같다 — 최상위를 도메인으로 나누고 계층은 그 안에 둔다(order/api, order/service, order/repository). 다른 것은 그 안이 몇 겹이냐다. Repository, DAO, MyBatis Mapper는 모두 데이터 접근 책임을 수행한다. 기술 스택에 따라 이름과 구현 방식은 달라질 수 있어서, 이 셋을 서로 다른 계층으로 나누지 않고 하나로 묶었다. domain이 없으므로 Repository 인터페이스로 의존을 뒤집지 않고, 호출 방향과 의존 방향이 같다. 첫 칸의 api와 web은 계층이 둘이라는 뜻이 아니다 — 같은 Presentation 계층의 패키지 둘이고, 나가는 것이 JSON이냐 HTML이냐만 다르다.
Layer Responsibilities
세 계층이 각각 무엇을 맡고, 무엇을 맡지 않는지다. 계층이 흐려지는 자리는 대부분 「하지 않는다」 쪽에 있다.
-
Presentation Layer
api
HTTP 요청을 받아 입력값을 확인하고, 적절한 Service를 호출한 뒤 JSON 응답을 생성한다.
대표 경로
{도메인}/api여기서 한다
- Route 연결
- Request Parameter 수신
- Request DTO 변환
- 입력 형식 검증
- 인증 정보 수신
- Service 호출
- Response DTO 반환
- HTTP Status Code 지정
여기서 하지 않는다
- 핵심 비즈니스 로직
- 직접 SQL 실행
- Mapper 직접 호출
- 복잡한 데이터 가공
- 장시간 트랜잭션 처리
- 여러 업무 로직을 직접 조율
-
Business Layer
service
비즈니스 처리 순서를 조율하고, 트랜잭션 범위 안에서 Repository 또는 Mapper를 호출한다.
대표 경로
{도메인}/service여기서 한다
- 업무 흐름 조율
- 비즈니스 조건 검사
- 트랜잭션 관리
- 여러 Repository 또는 Mapper 호출
- 데이터 생성 및 변경
- 외부 API 호출 조율
- 업무 예외 발생
- 결과 데이터 구성
여기서 하지 않는다
- HTTP Request 직접 의존
- HTTP Response 직접 생성
- View 객체 직접 생성
- SQL 문자열 직접 작성
- Controller 역할 수행
- 프레젠테이션 전용 포맷에 강하게 의존
-
Data Access Layer
repository
Database에 접근하여 데이터를 조회하고 저장하며, 데이터 접근 구현을 상위 계층에서 분리한다.
대표 경로
{도메인}/repository여기서 한다
- SQL 실행
- 데이터 조회 · 저장 · 수정 · 삭제
- 검색 조건 적용
- Paging 처리
- Database Record 변환
- 영속성 기술 처리
여기서 하지 않는다
- HTTP 처리
- 사용자 메시지 결정
- 업무 흐름 조율
- Controller 호출
- Service 역할 수행
- 화면 전용 데이터 생성
- 복잡한 업무 정책 판단
Data Access Layer의 구현 방식
Repository, DAO, MyBatis Mapper는 모두 데이터 접근 책임을 수행합니다. 프로젝트의 기술 스택과 규칙에 따라 이름과 구현 방식은 달라질 수 있습니다. 셋을 서로 다른 계층으로 나누지 않는다 — 같은 자리의 다른 구현이다.
-
MyBatis · XML
ControllerServiceMapper InterfaceMapper XMLDatabase -
MyBatis · Annotation
ControllerServiceMapper InterfaceSQL AnnotationDatabase -
JPA · ORM
ControllerServiceRepositoryORMDatabase
Request Examples
실제 요청 하나가 어느 클래스를 어떤 순서로 지나가는지다. 예시를 고르면 위 다이어그램의 계층에 그 요청에서의 순서 번호가 붙는다.
GET /users/10
MyBatis Mapper
-
GET /users/10Request -
UserController.getUser()Request -
UserService.getUser()Request -
UserMapper.selectUserById()Request -
users 테이블 조회Request -
UserResponse 반환Response
POST /posts
MyBatis Mapper
-
POST /postsRequest -
PostController.createPost()Request -
PostService.createPost()Request -
PostMapper.insertPost()Request -
posts 테이블 저장Request -
생성 결과 반환Response
PATCH /orders/100/status
JPA Repository
-
PATCH /orders/100/statusRequest -
OrderController.updateStatus()Request -
OrderService.updateStatus()Request -
OrderRepository.updateStatus()Request -
orders 테이블 수정Request -
변경 결과 반환Response
Coding Standards
여기 실린 15개는 계층 구조를 전제하지 않아 이 구조에서도 그대로 성립하는 표준이다. 3계층만의 표준은 아직 정한 것이 없다 — 비어 있는 자리는 아직 정하지 않은 것에 적어뒀다.
계층을 가리지 않는 규칙
객체 생성, 예외, assert처럼 어느 계층에서 쓰든 같은 모양이어야 하는 것들이다. 위 다이어그램에는 자리가 없지만 세 계층 모두에 걸린다.
전체 표준 15개
-
팩토리 메소드로 VO 생성하기
DTO에서 VO로의 변환을 팩토리 메소드에 맡겨 Service의 변환 코드를 걷어낸다
자세히 보기 -
Null-free 객체 설계와 생성 철학
생성자가 객체의 완전성을 보장한다. 정적 팩토리, 불변성, 그리고 엔티티에서의 타협점
자세히 보기 -
Assert로 프로그래머의 실수를 잡아내기
내부 검증과 외부 검증의 경계를 나누고, 그 안쪽을 assert가 맡는다
자세히 보기 -
상속 vs 합성
DTO 공통 필드는 1레벨 상속으로 관리하고, 그 밖은 합성으로 간다
자세히 보기 -
애플리케이션 시간
사건 시각은 Service가 UTC Instant로 공급하고 날짜와 지역 시각은 의미에 맞는 타입으로 남긴다
자세히 보기 -
DB 시간 저장
사건 시점은 UTC 세션의 TIMESTAMP(6)로 왕복하고 행의 감사 시각은 DB가 만든다
자세히 보기 -
스키마 마이그레이션
스키마는 Flyway 한 벌이 소유하고 Hibernate와 테스트는 그걸 검증만 한다
자세히 보기 -
Swagger 문서화 표준
@Schema와 Bean Validation의 역할을 나누고, 공통 응답은 전역에서 관리한다
자세히 보기 -
Service 계층의 assert
메서드의 전제와 결과를 코드로 선언하고, 깨지는 순간 멈추게 하는 방법
자세히 보기 -
DTO는 생성자로만 만든다
setter와 기본 생성자를 없애면 객체가 불완전한 상태로 존재할 수 없다
자세히 보기 -
예외 처리 표준
분류가 끝난 실패만 ErrorCodeException으로 운반하고 상태 코드, 메시지, 로그는 ErrorCode에서 통제한다
자세히 보기 -
성공 응답 상태
처리 결과가 200·201·202를 가르고 실제 상태와 본문 status, 코드 앞 세 자리를 맞춘다. 봉투가 면제되는 응답은 304 하나뿐이다
자세히 보기 -
예외의 종류
예외는 발생 경계에서 번역하고 ErrorCode는 식별 가능한 업무 도메인까지 세분화한다
자세히 보기 -
컨트롤러 패키지 분리
JSON은 api, HTML은 web으로 가른다. 두 아키텍처가 들어오는 칸을 같은 이름으로 부른다
자세히 보기 -
인증 주체
LoginUser는 api에서 죽고, application으로는 userId만 간다
자세히 보기
이 계층에는 아직 정한 표준이 없다. 비어 있는 자리는 아직 정하지 않은 것에 모아둔다.
Dependency Rules
호출은 Controller에서 Data Access 쪽으로만 흐른다.
거꾸로 부르는 순간 계층이 아니라 그물이 된다.
허용 이 방향으로만 부른다
-
Controller호출 가능Service -
Service호출 가능Repository / Mapper
금지 거꾸로 부르지 않는다
-
Service호출 금지Controller -
Repository / Mapper호출 금지Service -
Repository / Mapper호출 금지Controller
- Controller가 Repository 또는 Mapper를 직접 호출하지 않는다.
- Controller에 핵심 비즈니스 로직을 작성하지 않는다.
- Service에 HTTP 전용 처리 로직을 작성하지 않는다.
- Repository 또는 Mapper에 업무 흐름을 작성하지 않는다.
- SQL은 Data Access Layer에서 관리한다.
- 트랜잭션 범위는 일반적으로 Service에서 관리한다.
- 계층 간 순환 참조를 만들지 않는다.
프로젝트에 조회 전용 구조나 별도의 Query Service 규칙이 이미 있다면 그 프로젝트의 코딩 스탠다드가 위 규칙보다 앞선다. 여기 적은 것은 아무 규칙도 없을 때의 기본값이다.
Related Architecture
더 엄격하게 의존성을 분리한 구조가 필요하다면 도메인 드리븐 아키텍처를 확인하세요.
3계층 아키텍처
- Controller, Service, Data Access 중심
- 구조가 단순하고 이해하기 쉬움
- 일반적인 업무 시스템에 적용하기 쉬움
- 빠른 개발과 유지보수에 적합
- 계층별 책임 분리가 핵심
도메인 드리븐 아키텍처
- Domain을 중심으로 의존성 방향을 엄격히 제어
- UseCase, Adapter, Infrastructure 등을 세분화
- 외부 기술로부터 핵심 비즈니스 규칙을 보호
- 구조와 추상화가 상대적으로 복잡함
모든 프로젝트에 복잡한 아키텍처가 필요한 것은 아닙니다. Controller, Service, Data Access의 책임이 명확히 분리되어 있다면 3계층 아키텍처도 충분히 안정적이고 유지보수하기 좋은 구조가 될 수 있습니다.
둘 중 하나가 더 낫다는 이야기가 아니다. 프로젝트 규모, 업무 복잡도, 팀의 숙련도에 따라 고르는 구조다. 이 저장소가 도메인 드리븐을 먼저 정한 것도 우열 때문이 아니라, 구조를 우리가 정할 수 있는 프로젝트가 먼저 있었기 때문이다.