[AI가 읽을 수 있는 코드베이스 3/5] Standalone App: 도메인 슬라이스 독립 실행

기술 블로그
페이스북링크드인트위터

전체를 올리지 않고 부분을 검증하는 법

백엔드 서비스의 Issue 도메인은 프로덕션에서 Gateway, 인증 서버, 여러 다른 도메인 서비스들과 함께 동작합니다. 하지만 AI 에이전트가 Issue 도메인의 API를 수정할 때, 이 모든 인프라를 기동할 필요가 있을까요?

Hexagonal Architecture의 핵심은 Port/Adapter 패턴을 통한 의존성 역전입니다. 도메인 로직은 Port(인터페이스)에만 의존하고, 구체적인 인프라 연결은 Adapter가 담당합니다. 이 구조가 가능하게 하는 것은 Adapter 교체입니다. 프로덕션의 Adapter를 테스트용 Adapter로 바꿔 끼우면, 도메인 로직은 그대로 두고 실행 환경만 바꿀 수 있습니다.

백엔드 서비스의 issue:standalone-app 모듈이 정확히 이 일을 합니다.


issue 모듈의 전체 구조

먼저 Issue 도메인의 모듈 구조를 보겠습니다.

Kotlin
issue/
├── model/                  ← 순수 도메인 모델 (Kotlin 라이브러리)
├── in-port-internal/       ← 내부 Port 인터페이스
├── service/                ← 비즈니스 로직 (UseCase 구현)
├── api/                    ← REST Controller (외부 API)
├── internal-api/           ← 내부 서비스 간 API
├── internal-api-client/    ← Retrofit2 기반 내부 API 클라이언트
├── internal-api-dto/       ← 내부 API DTO
├── infrastructure/         ← Adapter 구현 (외부 시스템 연결)
├── repository-jdbc/        ← JDBC 기반 데이터 접근
├── web-support/            ← 웹 계층 지원 (Requester 추출 등)
├── exception/              ← 도메인 예외 정의
├── test-fixture/           ← 테스트 픽스처
├── changelog/              ← DB 마이그레이션 (Liquibase)
└── standalone-app/         ← ★ 독립 실행 가능한 애플리케이션
    ├── src/
    │   └── main/kotlin/com/example/issue/standalone/
    │       ├── IssueStandaloneApplication.kt
    │       ├── config/
    │       │   ├── StandaloneSecurityConfig.kt
    │       │   └── StandaloneExceptionHandler.kt
    │       └── seed/
    │           └── IssueDataSeeder.kt
    └── frontend/           ← Vite + React 프론트엔드
        ├── src/
        ├── e2e/            ← Playwright E2E 테스트
        │   ├── tests/
        │   ├── fixtures/
        │   └── recording/  ← 데모 녹화
        └── vite.config.ts

주목할 점은 standalone-app이 Issue 도메인의 다른 모듈들 — api, service, infrastructure, repository-jdbc — 을 조립하되, 프로덕션 환경의 인증/인가 Adapter를 자체 Adapter로 교체한다는 것입니다.

api, service, infrastructure, repository-jdbc는 프로덕션과 동일한 코드입니다. 교체되는 것은 인증/인가 Adapter입니다 — OAuth2와 권한 정책 대신, 헤더 기반 인증과 전체 허용 정책으로 대체합니다. 그래서 standalone에서의 동작 검증은 인증/인가와 외부 연동을 제외한 핵심 비즈니스 로직의 검증으로 유효합니다.


build.gradle.kts: 의존성 조립의 청사진

standalone-appbuild.gradle.kts는 이 조립을 코드로 표현합니다.

Kotlin
dependencies {
    // Issue 도메인 핵심 모듈
    implementation(project(":issue:api"))
    implementation(project(":issue:service"))
    implementation(project(":issue:infrastructure"))
    implementation(project(":issue:repository-jdbc"))

    // issueCode 채번에 필요한 Sequence 도메인
    implementation(project(":sequence:service"))
    implementation(project(":sequence:repository-jdbc"))
}

configurations.all {
    // 전이 의존성으로 유입되는 프로덕션 전용 모듈 차단
    exclude(group = "com.example.permission", module = "protocol")
}

세 가지 설계 결정이 이 선언에 담겨 있습니다. 필요한 도메인만 선택적으로 조립하고(다른 도메인은 포함하지 않음), 크로스 도메인 의존은 최소화하며(issueCode 채번에 필요한 Sequence만 포함), 프로덕션 전용 모듈은 명시적으로 차단합니다(전이 의존성으로 유입될 수 있는 것들).


IssueStandaloneApplication: 최소 Bean 등록

애플리케이션 진입점은 @SpringBootApplication@Import로 세 개의 Standalone 전용 설정 클래스를 명시적으로 등록하는 것이 전부입니다.

  • StandaloneSecurityConfig — 프로덕션 OAuth2 인증을 대체하는 헤더 기반 인증
  • StandaloneExceptionHandler — 프로덕션 공통 예외 핸들러를 대체하는 독립 예외 처리
  • IssueDataSeeder — 기동 시 샘플 데이터를 자동 생성

StandaloneSecurityConfig: OAuth2 → 헤더 기반 인증

Standalone 조립의 핵심은 인증 Adapter의 교체입니다.

Kotlin
@Configuration
@EnableWebSecurity
class StandaloneSecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .csrf { it.disable() }
            .authorizeHttpRequests { it.anyRequest().permitAll() }
            .addFilterBefore(
                StandaloneRequesterAuthFilter(),
                UsernamePasswordAuthenticationFilter::class.java,
            )
        return http.build()
    }
}

프로덕션의 Security 설정과 비교하면 차이가 명확합니다.

항목 프로덕션 Standalone
인증 방식 OAuth2 Resource Server (JWT 검증) 헤더 기반 (X-Flex-Scope, X-Flex-Actor)
인가 규칙 엔드포인트별 권한 검사 permitAll() — 모든 요청 허용
외부 의존성 인증 서버, Gateway 없음
CSRF 활성화 비활성화 (API 테스트 편의)

이 설계에서 핵심은 교체 가능한 경계를 어디에 두느냐입니다. X-Flex-ScopeX-Flex-Actor 헤더를 읽어 Requester 객체를 만들고 SecurityContext에 넣는 것이 전부이지만, Service 계층이 받는 Requester 객체는 프로덕션과 동일합니다. 인증의 복잡성은 Filter에 격리되어 있고, Service 이하의 비즈니스 로직은 Requester라는 추상화만 알면 됩니다. Adapter를 교체해도 비즈니스 로직은 변하지 않습니다.


IssueDataSeeder: 기동 시 샘플 데이터 자동 생성

Standalone 앱이 기동되면 IssueDataSeeder가 자동으로 샘플 데이터를 생성합니다.

Kotlin
@Configuration
class IssueDataSeeder(
    private val issueApplicationUseCase: IssueApplicationUseCase,
) : ApplicationRunner {

    override fun run(args: ApplicationArguments) {
        // 중복 방지: 이미 데이터가 있으면 건너뜀
        if (issueApplicationUseCase.listIssues(tenant).isNotEmpty()) return

        // UseCase를 통해 샘플 이슈 생성 (도메인 검증 로직을 거침)
        val issue = issueApplicationUseCase.createIssue(...)
        // 다양한 상태의 데이터 확보를 위해 상태 변경도 수행
        issueApplicationUseCase.updateIssueStatus(issue.id, IssueStatus.DONE)
    }
}

주목할 점은 Seeder가 Repository를 직접 호출하지 않고 UseCase를 통해 데이터를 생성한다는 것입니다. 이는 시드 데이터가 도메인의 검증 로직과 비즈니스 규칙을 모두 거쳐서 생성된다는 뜻입니다. issueCode 채번, 상태 전이 검증, 이벤트 발행 — 프로덕션과 동일한 경로를 탑니다.


TestContainer 연동과 데이터베이스

bootRun 태스크에는 useContainer("mysql", "flow") 한 줄이 있습니다. flex의 TestContainers Gradle 플러그인이 제공하는 함수로, 이 한 줄로 MySQL TestContainer가 기동되고 JDBC URL이 자동으로 주입됩니다. 개발자의 로컬에 MySQL을 설치할 필요가 없습니다.


Swagger UI + Vite + React 프론트엔드

Standalone 앱은 API 문서화와 프론트엔드까지 포함합니다.

Swagger UI는 springdoc-openapi에 의해 자동으로 생성됩니다. Issue 도메인의 모든 API가 /swagger-ui/index.html에서 인터랙티브하게 테스트 가능합니다.

프론트엔드는 Vite + React로 구성되어 있으며, Gradle 빌드에 통합됩니다.

프론트엔드 빌드는 Gradle 태스크로 통합되어, processResources 시점에 React 빌드 결과물이 src/main/resources/static에 복사됩니다. CI 환경에서는 건너뛰어 빌드 시간을 절약합니다.

이 구조가 만드는 것은 완전히 자기 완결적인(self-contained) 개발 환경입니다.

./gradlew :issue:standalone-app:bootRun 한 줄이면, MySQL이 뜨고, 스키마가 적용되고, 샘플 데이터가 생성되고, Swagger UI와 React 프론트엔드가 서빙됩니다. AI 에이전트든 사람이든, Issue 도메인의 핵심 비즈니스 로직과 API 시나리오를 즉시 검증할 수 있습니다.


왜 Standalone이 AI 시대에 중요한가

Standalone App은 단순히 편리한 개발 도구가 아닙니다. AI 코딩 에이전트와의 협업에서 세 가지 구조적 장점을 제공합니다.

1. 빠른 피드백 루프

전체 시스템을 올리지 않고 도메인만 기동하므로, 빌드와 기동이 빠릅니다. 에이전트의 코드 수정 → 빌드 → 기동 → 검증 루프가 짧아집니다.

2. 격리된 검증

다른 도메인의 상태나 외부 서비스의 가용성에 영향받지 않습니다. 에이전트가 Issue 도메인만 수정했다면, Issue standalone에서의 검증만으로 충분합니다.

3. Acceptance 증명의 기반

4화에서 자세히 다루겠지만, standalone 환경 위에 E2E 테스트와 데모 녹화가 올라갑니다. AI가 만든 PR에 "standalone 환경에서 E2E 통과"라는 증거를 자동으로 첨부할 수 있습니다.

본편 5화의 타임머신(시간 축 교체), Rewrite Host(공간 축 교체)와 같은 사고방식입니다. Standalone App은 구조 축의 교체 — 전체 시스템 대신 도메인 슬라이스만 조립해서 검증합니다.


다음 화 — Acceptance 증명이 리뷰를 바꾼다. standalone 환경 위에 올라가는 E2E 테스트, 데모 녹화, 그리고 이것이 AI가 만든 PR의 Code Review를 어떻게 바꾸는지 실제 Gradle 태스크와 Playwright 설정으로 분석합니다. (2축 프레임워크의 A축: 독립 실행 가능성)

🚀플렉스팀 채용페이지 바로가기☕flex Private Talk 신청하기
글이 마음에 드셨나요?
공유하기
페이스북링크드인트위터
flex가 궁금하다면? 지금 무료로 체험해 보세요
flex가 궁금하다면? 지금 무료체험하기
  • 아티클
    2020. 5. 25
    근태관리, 유연근무제, 그리고 코로나 시대
    코로나, 뉴노멀, 유연근무제, 그리고 근태관리