Architecture
Context & Scope
이 문서는 Project-Auth-Server의 레이어 구조와 조립 방식을 설명합니다.
핵심 질문은 "왜 이 구조를 택했고, 실제로 의존 방향을 어떻게 통제하는가"입니다.
문서 작성 기준은 docs/documentation-guide.md를 따릅니다.
Scope
- 포함: 레이어 책임, 의존 방향, 웹 앱과 migration 앱의 조립 방식
- 제외: 개별 인증 플로우, 운영 환경별 GitOps 선언 상세
Why
이 프로젝트는 인증/인가 서버이기 때문에 비즈니스 규칙, HTTP 계약, 외부 기술 구현이 빠르게 섞이기 쉽습니다.
특히 보안, DB migration, 외부 시스템 연동이 함께 존재하면 "편해서 한 군데에 몰아넣은 코드"가 빠르게 늘어납니다.
이 구조가 해결하려는 문제는 다음과 같습니다.
- 도메인 규칙이 Spring MVC, JPA, 보안 프레임워크에 오염되는 문제
- 응답 계약과 비즈니스 규칙이 뒤섞이는 문제
- migration 같은 운영성 실행을 웹 애플리케이션과 강하게 결합하는 문제
- 기술 교체 영향 범위를 좁히기 어려운 문제
What
한눈에 보는 의존 흐름
flowchart LR
Bootstrap[bootstrap]
Presentation[presentation]
Infrastructure[infrastructure]
Application[application]
Domain[domain]
Bootstrap --> Presentation
Bootstrap --> Infrastructure
Bootstrap --> Application
Bootstrap -. compileOnly .-> Domain
Presentation --> Application
Infrastructure --> Application
Infrastructure --> Domain
Application --> Domain
핵심은 의존 방향이 항상 안쪽으로만 흐른다는 점입니다.
domain은 가장 안쪽에 있고 아무 것도 모릅니다.application은 유스케이스와 포트를 통해 도메인 규칙을 조합합니다.presentation,infrastructure는 바깥쪽 어댑터로서application에 의존합니다.bootstrap은 실제 Spring Boot 실행 모듈로, 바깥 레이어들을 조립합니다.
레이어별 책임
domain
도메인 모델과 순수 규칙만 둡니다.
- 예:
User,UserEmail,UserName,UserPasswordPolicy - 금지: Spring annotation, JPA annotation, HTTP/Servlet 타입
application
“무슨 일을 한다”를 담당합니다.
- 유스케이스
- 커맨드/결과 DTO
- 포트(
port/in,port/out) - 비즈니스 에러 코드/예외
여기서는 더 이상 HTTP status를 다루지 않습니다.
에러는 code, message만 가지고 있고, HTTP status 매핑은 바깥쪽 presentation에서 담당합니다.
presentation
HTTP 입출력과 API 응답 계약만 담당합니다.
- Controller
- Request/Response DTO
ApiResultApiSuccessCodeValidationExceptionHandlerRequestExceptionHandlerApplicationExceptionHandlerApiErrorHttpStatusMapper
즉 API 응답 모양과 HTTP status는 이 레이어의 책임입니다.
infrastructure
기술 구현체만 담당합니다.
- JPA repository adapter
- persistence mapper/entity
- password encoder adapter
- JWT/Vault integration
즉 application이 정의한 포트를 실제 기술로 연결합니다.
bootstrap
전체를 조립합니다.
- Spring Boot entrypoint
- configuration
- security wiring
presentation이 직접 의존할 수 없는 기술 예외를 HTTP 경계에서 번역하는 bootstrap 전용 web adapter- migration 전용 app entrypoint
이 프로젝트에서는 일반 웹 애플리케이션 진입점과 별도로 MigrationApplication을 둬서 DB migration을 전용 실행 단위로 분리했습니다.
InfrastructureExceptionHandler가 bootstrap에 있는 이유도 여기 있습니다.
이 핸들러는 HTTP 응답을 만들지만, 이를 presentation으로 옮기면 presentation -> infrastructure 의존이 생겨 현재 ArchUnit 규칙을 깨게 됩니다.
그래서 security filter chain 예외나 InfrastructureException처럼 presentation이 직접 알 수 없는 타입을 HTTP로 번역하는 adapter는 bootstrap이 맡습니다.
How
웹 앱과 migration 앱의 조립
flowchart TD
Web[AuthApplication]
Migration[MigrationApplication]
App[application]
Infra[infrastructure]
Pres[presentation]
Domain[domain]
DB[(PostgreSQL)]
Web --> Pres
Web --> App
Web --> Infra
Pres --> App
Infra --> App
Infra --> Domain
App --> Domain
Infra --> DB
Migration --> App
Migration --> Infra
Migration --> DB
이 구조의 의도는 이렇습니다.
- 웹 앱은
presentation + application + infrastructure를 조립 - migration 앱은 웹 어댑터 없이 DB migration만 수행
즉 migration을 위해 auth-server 전체 웹 컨텍스트를 억지로 띄우지 않도록 분리했습니다.
레이어 규칙을 어떻게 유지하는가
구조는 설명만으로 유지되지 않기 때문에, ArchUnit 테스트로 핵심 규칙을 검증합니다.
- 위치:
bootstrap/src/test/java/com/project/auth/architecture/LayerDependencyArchitectureTest.java
현재 강제하는 규칙:
domain은 Spring/JPA/Servlet에 의존하지 않는다application은presentation/infrastructure에 의존하지 않는다presentation은domain/infrastructure에 직접 의존하지 않는다bootstrap만config패키지를 조립 지점으로 사용한다
즉 이 문서는 "설계 설명"이고, ArchUnit은 "설계 위반 방지 장치"입니다.
Alternatives Considered
대안 1: 패키지 수준 구분만 두고 단일 모듈에 집중
- 장점: 초기 개발 속도가 빠르고 진입 장벽이 낮습니다.
- 단점: 시간이 지나면 HTTP, DB, 도메인 규칙이 같은 계층에서 섞이기 쉽습니다.
- 결론: 지금 프로젝트처럼 인증, 보안, 외부 시스템 연동이 많은 경우 경계가 빨리 무너질 가능성이 높아 채택하지 않았습니다.
대안 2: 웹 앱과 migration을 같은 진입점에서 처리
- 장점: 실행 경로가 단순합니다.
- 단점: migration만 수행해도 웹 관련 빈과 설정이 함께 로딩될 수 있습니다.
- 결론: 운영 절차를 분리하고 실패 반경을 줄이기 위해 별도
MigrationApplication을 유지합니다.
Result / Trade-offs
이 구조로 얻는 이점은 다음과 같습니다.
- 도메인/유스케이스가 웹 프레임워크에 오염되지 않습니다.
- 기술 교체 영향 범위를 주로
infrastructure로 제한할 수 있습니다. - 응답 계약과 비즈니스 규칙의 경계를 분명히 할 수 있습니다.
- migration 같은 운영성 실행을 별도 진입점으로 분리할 수 있습니다.
동시에 감수하는 비용도 있습니다.
- 모듈과 패키지 경계를 지키기 위한 학습 비용이 있습니다.
- 단기적으로는 파일 수와 설정 포인트가 늘어납니다.
- 구조를 설명하는 문서와 ArchUnit 테스트를 함께 관리해야 합니다.