Files

8.6 KiB

Project-Auth-Server

Clean / Hexagonal Architecture 기반 Spring Boot 인증 서버입니다.
브라우저 로그인과 세션 확인은 Project-Infra의 Ingress 계층(oauth2-proxy + Keycloak)이 담당하고, 이 서버는 Keycloak이 발급한 JWT를 Spring Security Resource Server로 다시 검증합니다.

현재는 local / dev 환경에서 구조와 테스트 게이트를 검증한 단계입니다. GitHub Actions CI 자동화와 staging / prod 배포 검증은 다음 단계로 남겨두었습니다.

이 프로젝트에서 확인하고 싶었던 질문은 세 가지입니다.

  • 로그인 플로우를 애플리케이션 코드 밖으로 빼면 컨트롤러와 use case는 무엇만 책임지는가?
  • domain / application / presentation / infrastructure 경계를 테스트로 강제할 수 있는가?
  • 실패 응답, HTTP status, 내부 infrastructure error를 한 응답 경로에 섞지 않을 수 있는가?
Runtime Java 21, Spring Boot 4.0.3
Architecture Gradle multi-module, Clean / Hexagonal Architecture
Auth Spring Security Resource Server, Keycloak JWT, Nimbus JWT decoder
Data PostgreSQL 16, Spring Data JPA, Flyway
Tests ArchUnit, JUnit 5, AssertJ, jqwik, JaCoCo, PIT
Infra Kubernetes / Vault / Traefik 구성은 Project-Infra에서 관리

Architecture

Application 구조: Clean Architecture 5 모듈

의존 방향은 presentation / infrastructure → application → domain입니다.
domain은 Spring / JPA / Servlet을 모르고, bootstrap만 전체 모듈을 조립합니다.

5개 모듈로 나눈 이유는 비즈니스 규칙, use case, HTTP 계약, 외부 adapter, 실행 조립 책임을 서로 다른 변경 이유로 분리하기 위해서입니다.

레이어 규칙은 문서 약속으로만 두지 않고 LayerDependencyArchitectureTest로 검증합니다.

Auth Flow

sequenceDiagram
    autonumber
    actor User as 사용자
    participant Traefik as Traefik
    participant OAuth as oauth2-proxy
    participant KC as Keycloak
    participant App as auth-server

    User->>Traefik: GET /api/v1/auth/me
    Traefik->>OAuth: ForwardAuth
    OAuth-->>Traefik: authenticated
    Traefik->>App: Bearer JWT 전달
    App->>KC: JWKS 조회
    KC-->>App: public keys
    App-->>User: ApiResult<AuthenticatedUserResponse>

auth-server는 토큰을 발급하지 않습니다. Keycloak JWT의 issuer / signature / expiration / role claim을 검증하고, 검증된 principal을 application 계층으로 넘깁니다.


Engineering Decisions

1. 로그인 플로우는 Ingress 계층에 위임

spring-boot-starter-oauth2-client가 아니라 spring-boot-starter-oauth2-resource-server만 사용합니다.
브라우저 redirect, callback, session 확인은 oauth2-proxy + Keycloak이 담당하고, auth-server는 API 서버로서 JWT 검증에 집중합니다.

이렇게 나누면 컨트롤러는 로그인 상태를 만드는 코드가 아니라, 검증된 principal을 전제로 한 HTTP 계약에 집중할 수 있습니다.

2. 레이어 경계는 ArchUnit으로 검증

5개 모듈을 domain / application / presentation / infrastructure / bootstrap으로 분리했습니다.

대표 규칙:

  • domain은 Spring / JPA / Servlet 의존 금지
  • application은 presentation / infrastructure 의존 금지
  • presentation은 infrastructure 직접 의존 금지
  • bootstrap은 조립과 설정을 담당

잘못된 의존이 들어오면 테스트 단계에서 실패하도록 했습니다.

3. ErrorCode와 HTTP status 분리

BusinessExceptionErrorCode는 비즈니스 의미만 갖습니다.
HTTP status 매핑은 presentation 계층의 ApiErrorHttpStatusMapper가 담당합니다.

application 계층이 HTTP 프로토콜 세부사항을 알지 않게 만들기 위한 결정입니다.

4. 내부 infrastructure error는 클라이언트 응답 계약과 분리

DB / Vault / 외부 HTTP adapter에서 발생한 내부 오류는 클라이언트 응답으로 그대로 노출하지 않습니다.
ErrorCode는 sealed type이고, 클라이언트 응답에 노출 가능한 ClientFacingErrorCode와 내부 분류 전용 ExternalErrorCode로 갈라집니다.

ApiErrorHttpStatusMapper.map(...)ClientFacingErrorCode만 받습니다. 따라서 InfrastructureErrorCode 같은 내부 코드를 HTTP 응답 매퍼에 전달하는 코드는 런타임 검사가 아니라 컴파일 단계에서 거부됩니다.

실패 응답은 ApiResult 형태로 통일하되, 내부 원인 분류와 외부 응답 계약은 타입으로 분리했습니다.


Quick Start

H2 profile

가장 가볍게 애플리케이션 구조와 API를 확인하는 경로입니다.

./gradlew :bootstrap:bootRun
curl http://localhost:8080/actuator/health

Full local infra

PostgreSQL / Keycloak / Vault까지 함께 확인하려면 로컬 docker compose 환경을 사용합니다.

docker compose -f deploy/docker/docker-compose.yml --env-file .env.local up -d
set -a; source .env.local; set +a
./gradlew :bootstrap:bootRun
curl http://localhost:8080/actuator/health

자세한 Keycloak 로컬 설정은 Keycloak local setup을 참고합니다.


API

Method Path Auth Response
GET /api/v1/auth/me Bearer JWT (realm role: user) ApiResult<AuthenticatedUserResponse>
GET /actuator/health, /livez, /readyz Public health / probe
GET /swagger-ui.html, /v3/api-docs/** Public OpenAPI

모든 비공개 endpoint는 oauth2ResourceServer.jwt()로 보호합니다.


Build / Test

./gradlew test
./gradlew :bootstrap:test --tests LayerDependencyArchitectureTest
./gradlew :application:test
./gradlew :bootstrap:bootRun
./gradlew :bootstrap:bootJar
docker build -f deploy/docker/application/Dockerfile -t project-auth-server:local .

Flyway SQL은 infrastructure/src/main/resources/db/migration/에 둡니다.
Kubernetes 환경에서는 이 repo가 별도 migration 이미지를 만들지 않고, Project-Infra에서 공식 flyway/flyway 이미지를 Job으로 실행합니다.


Current Status

Area Status
Clean / Hexagonal 5 modules 구현됨
ArchUnit layer rules 구현됨
Resource Server + Keycloak JWT validation 구현됨
ErrorCode / HTTP status separation 구현됨
Coverage / mutation / property tests JaCoCo coverageGate, PIT gate, jqwik property tests 구성
OpenAPI / Swagger UI 구현됨
Local docker-compose 구성됨
Kubernetes manifests Project-Infra에서 관리

Limitations

  • 사용자 도메인은 아직 작습니다. 현재 핵심 API는 /api/v1/auth/me 중심입니다.
  • GitHub Actions CI는 아직 구성하지 않았고, 로컬 Gradle test / coverageGate / PIT 기준으로 검증합니다.
  • Spring Boot 4.0.3 기반이라 actuator / observability 등 4.x 변경점은 계속 확인 중입니다.

Documentation