포스트

DDD 멀티모듈 구조 개선: 표현 계층 분리와 Bounded Context의 독립성

I. 문제 상황: Presentation 모듈의 정체성 혼란

1. 기존 구조

프로젝트 초기 구조는 다음과 같았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
connectors-backend/
├── core/
├── setting/
│   ├── cache/
│   ├── database/
│   ├── storage/
│   └── external/
├── presentation/               # ⚠️ 문제의 모듈
│   ├── api/                    # REST API 서버
│   │   └── src/main/java/
│   │       └── com/connectors/presentation/
│   │           ├── ApiApplication.java
│   │           ├── account/    # account context의 Controller
│   │           │   └── AuthenticationController.java
│   │           ├── content/    # content context의 Controller
│   │           │   └── ContentController.java
│   │           └── config/
│   │               ├── SwaggerConfig.java
│   │               ├── SecurityConfig.java
│   │               └── exception/
│   │                   └── HttpExceptionHandler.java
│   └── batch/                  # 배치 서버
└── {context}/
    ├── {context}-domain/
    ├── {context}-app/
    └── {context}-infra/

언뜻 보기엔 깔끔해 보이지만, 실제로 Controller를 작성하면서 심각한 문제를 발견했다.

2. 발생한 문제들

문제 1: Bounded Context 경계가 무너짐

DDD의 핵심 개념 중 하나는 Bounded Context의 명확한 경계다. 각 Context는 자신만의 도메인 모델과 비즈니스 로직을 가지며, 다른 Context와 독립적으로 발전해야 한다.

하지만 presentation:api 모듈 내부에서 accountcontent Context의 Controller가 같은 패키지 구조에 존재하면서, 물리적인 계층 분리는 되어 있지만 논리적인 경계가 모호해졌다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// presentation:api 모듈
package com.connectors.presentation.account;

@RestController
@RequiredArgsConstructor
public class AuthenticationController {
    private final LoginService loginService;        // account-app 모듈
    private final OAuth2UrlService oAuth2UrlService; // account-app 모듈
    
    @PostMapping("/api/v1/auth/login")
    public ResponseEntity<LoginResponse> oAuth2LogIn(
        @RequestBody OAuth2LogInRequest request
    ) {
        LoginContext context = loginService.login(
            request.getProvider(), 
            request.getCode(), 
            request.getState()
        );
        // ... 로그인 처리
    }
}

// 같은 presentation:api 모듈
package com.connectors.presentation.content;

@RestController
@RequiredArgsConstructor
public class ContentController {
    private final ContentService contentService;  // content-app 모듈
    
    @GetMapping("/api/v1/contents/{id}")
    public ContentResponse getContent(@PathVariable Long id) {
        return contentService.getContent(id);
    }
}

모든 Context의 Controller가 presentation:api 모듈 안에 뒤섞여 있다. 이는 표현 계층이 여러 Context의 진입점 역할을 동시에 수행하면서, Context 간 경계가 불분명해지는 결과를 낳았다.

문제 2: 모듈 의존성 관리의 어려움

presentation:api 모듈의 build.gradle을 보면 문제가 더 명확해진다.

1
2
3
4
5
6
7
8
// presentation/api/build.gradle
dependencies {
    implementation project(':account:account-app')
    implementation project(':content:content-app')
    implementation project(':common:common-app')
    
    // 표현 계층에서 여러 Context의 Application Layer를 모두 참조
}

표현 계층 하나가 모든 Context의 Application Layer를 직접 의존한다. 이는 다음과 같은 문제를 야기한다.

  1. 단일 책임 원칙 위배: Presentation 모듈이 “API 서버 실행”과 “모든 Context의 표현 계층 역할”을 동시에 담당
  2. 변경 영향 범위 확대: 특정 Context의 API 변경이 Presentation 모듈 전체에 영향
  3. 테스트 복잡도 증가: 한 Context의 Controller를 테스트하려면 다른 Context의 의존성까지 모두 필요

문제 3: 횡단 관심사의 위치 혼란

더 심각한 문제는 횡단 관심사(Cross-cutting Concerns)의 위치였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// presentation:api 모듈
package com.connectors.presentation.config;

@Configuration
public class SecurityConfig {
    // Spring Security 설정
    // JWT 인증 필터 설정
    // CORS 설정
}

@RestControllerAdvice
public class HttpExceptionHandler {
    // 전역 예외 처리
}

@Configuration
public class SwaggerConfig {
    // API 문서화 설정
}

Security, Exception Handling, Swagger 같은 횡단 관심사가 presentation:api 모듈에 섞여 있었다. 이는 다음과 같은 문제를 야기했다.

  1. 재사용 불가: presentation:batch 모듈에서도 Security 설정이 필요하지만, presentation:api에 종속
  2. 책임 혼재: 표현 계층 모듈이 “Controller 관리” + “서버 설정” + “횡단 관심사 관리”를 동시에 담당
  3. 테스트 복잡도: Controller 테스트 시 불필요한 Security, Swagger 설정까지 로딩

문제 4: 계층의 역할 모호성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────────────────────────────────┐
│        Presentation Layer               │  ← 실행 가능한 애플리케이션?
│        (presentation:api)               │  ← 여러 Context의 Controller?
│  - AuthenticationController             │  ← 횡단 관심사 관리?
│  - ContentController                    │  ← 역할이 불명확
│  - SecurityConfig                       │
│  - HttpExceptionHandler                 │
│  - ApiApplication (Main)                │
└──────────────┬──────────────────────────┘
               │ depends on
     ┌─────────┴─────────┐
     │                   │
┌────▼─────┐       ┌─────▼──────┐
│ account  │       │  content   │
│  -app    │       │   -app     │
└──────────┘       └────────────┘

Presentation 모듈이 “API 서버를 실행하는 진입점”인 동시에 “여러 Context의 표현 계층”이자 “횡단 관심사 관리자”라는 세 가지 책임을 동시에 지니게 되었다.

DDD 계층 구조에서 표현 계층은 명확히 “특정 Context의 진입점” 역할을 해야 한다. 하지만 현재 구조는 이를 위배하고 있었다.

3. 근본 원인 분석

이 문제의 근본 원인은 “실행 가능한 애플리케이션”, “표현 계층”, “횡단 관심사”를 모두 동일시했기 때문이다.

DDD에서 표현 계층은:

  • 특정 Bounded Context의 진입점
  • Controller, Request/Response DTO를 포함
  • 해당 Context의 Application Service를 호출

횡단 관심사는:

  • 여러 Context에서 공통으로 사용하는 기능
  • Security, Exception Handling, Logging 등
  • 독립적인 모듈로 분리되어 재사용 가능해야 함

Runnable Application은:

  • 여러 모듈을 조합해 실행 가능한 형태로 만드는 역할
  • Spring Boot Main 클래스
  • 최소한의 서버 설정만 포함

이 세 가지는 명백히 다른 책임을 가지지만, 하나의 모듈에 섞여 있었다.


II. 해결 방안: 표현 계층과 횡단 관심사 분리

1. 개선 전략

문제를 해결하기 위해 다음과 같은 전략을 세웠다.

  1. Bounded Context별 표현 계층 분리: 각 Context에 {context}-api 모듈 추가
  2. 횡단 관심사 독립 모듈화: Security, Swagger 등을 support 모듈로 분리
  3. Runnable 모듈 재정의: presentationrunnable로 변경, 순수한 실행 진입점 역할만 수행
  4. 의존성 방향 재정립: Runnable → Context API → Context App ← Context Domain

2. 개선된 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
connectors-backend/
├── core/
├── setting/
├── support/                    # ✅ 횡단 관심사 분리
│   ├── security/               # Spring Security, JWT, OAuth2
│   └── swagger/                # API 문서화 설정
├── runnable/                   # ✅ 실행 가능한 애플리케이션 모듈
│   ├── api-server/             # REST API 서버
│   │   └── src/main/java/
│   │       └── com/connectors/runnable/api/
│   │           ├── ApiApplication.java       # Main 진입점
│   │           └── config/                    # 서버 설정만 관리
│   │               ├── SwaggerConfig.java     # Swagger 문서 조합
│   │               ├── WebConfig.java         # CORS 등 Web 설정
│   │               └── exception/
│   │                   └── HttpExceptionHandler.java  # 전역 예외 처리
│   └── batch-server/           # 배치 서버
└── {context}/                  # Bounded Context
    ├── {context}-api/          # ✅ 표현 계층 (Presentation Layer)
    │   └── src/main/java/
    │       └── com/connectors/{context}/api/
    │           ├── api/        # API 인터페이스 (Swagger 문서화)
    │           ├── controller/ # Controller 구현체
    │           ├── request/    # Request DTO
    │           ├── response/   # Response DTO
    │           └── config/     # Context별 API 그룹 설정
    ├── {context}-domain/
    ├── {context}-app/ㅔ
    └── {context}-infra/

개선된 구조의 핵심은 세 가지 관심사의 명확한 분리다.

  1. Runnable 모듈: 애플리케이션 실행과 최소한의 서버 설정
  2. Support 모듈: 여러 Context에서 공통으로 사용하는 횡단 관심사
  3. Context API 모듈: 각 Context의 독립적인 표현 계층

3. 계층별 책임 재정의

3-1. Support 모듈 - 횡단 관심사 독립화

가장 먼저 진행한 작업은 횡단 관심사를 독립 모듈로 분리하는 것이었다.

Support: Security 모듈

1
2
3
4
5
6
7
8
9
10
11
12
13
// support/security/build.gradle
dependencies {
    implementation project(':core')
    
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    
    // JWT 의존성
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'
    compileOnly 'io.jsonwebtoken:jjwt-api:0.11.2'
}

Security 모듈은 다음 기능을 제공한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
support/security/
├── config/
│   └── WebSecurityConfig.java           # Spring Security 설정
├── jwt/
│   ├── JwtProvider.java                 # JWT 토큰 생성
│   ├── JwtResolver.java                 # JWT 토큰 파싱
│   ├── JwtHeadersProvider.java          # JWT 쿠키 헤더 생성
│   └── filter/
│       ├── JwtAuthenticationFilter.java # JWT 인증 필터
│       └── SecurityExceptionHandlerFilter.java
├── handler/
│   ├── AuthenticationFailureHandler.java
│   └── AuthorizationFailureEntryPoint.java
├── annotation/
│   ├── CurrentUserId.java               # 사용자 ID 파라미터 바인딩
│   └── RequireAuthority.java            # 권한 체크 AOP
└── dto/
    ├── AuthToken.java                   # 토큰 DTO
    └── UserPrincipal.java               # 인증 사용자 정보
  • 재사용성: runnable:api-server, runnable:batch-server 모두에서 Security 설정 공유 가능
  • 독립적 테스트: Security 로직을 독립적으로 테스트 가능
  • 명확한 책임 분리: 인증/인가 관련 로직만 포함

3-2. Runnable 모듈 (runnable:api-server)

Swagger 설정 - Context별 API 문서 조합

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// runnable/api-server/config/SwaggerConfig.java
@SecurityScheme(
    name = "AccessToken Authentication",
    type = SecuritySchemeType.APIKEY,
    in = SecuritySchemeIn.COOKIE,
    paramName = "access-token"
)
@Configuration
public class SwaggerConfig {
    @Bean
    public OpenAPI openAPI() {
        Info info = new Info()
                .title("Connectors API")
                .description("Connectors API document");

        Server localServer = new Server()
                .description("Local server")
                .url("http://localhost:8080");

        Server devServer = new Server()
                .description("Dev server")
                .url("https://dev-api.connectforme.com");

        return new OpenAPI()
                .info(info)
                .servers(List.of(localServer, devServer))
                .security(List.of(
                        new SecurityRequirement()
                            .addList("AccessToken Authentication")
                ));
    }
}

Runnable 모듈의 SwaggerConfig는 구동되는 어플리케이션에서 보여줄 API 문서의 메타 정보만 정의한다. 실제 각 Context의 API 그룹은 각 Context의 API 모듈에서 정의한다.

api-server 모듈 내 전역 예외 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// runnable/api-server/config/exception/HttpExceptionHandler.java
@Slf4j
@RestControllerAdvice
public class HttpExceptionHandler {
    @ExceptionHandler(BadRequestException.class)
    public ResponseEntity<?> handleBadRequestException(BadRequestException e) {
        log.error("[" + e.getErrorCode() + "] : " + e.getErrorLog());
        return ExceptionResponse.toResponseEntity(e, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(UnauthorizedException.class)
    public ResponseEntity<?> handleUnauthorizedException(UnauthorizedException e) {
        log.error("[" + e.getErrorCode() + "] : " + e.getErrorLog());
        return ExceptionResponse.toResponseEntity(e, HttpStatus.UNAUTHORIZED);
    }
    
    // ... 기타 예외 처리
}

모듈 의존 관계

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// runnable/api-server/build.gradle
plugins {
    id 'org.springframework.boot'
}

dependencies {
    implementation project(':core')
    
    // Context별 API 모듈만 의존 (Application Layer 직접 의존 제거)
    implementation project(':account:account-api')
    implementation project(':content:content-api')
    implementation project(':common:common-api')
    
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13'
}

bootJar { enabled = true }
jar { enabled = false }
  • Application Layer({context}-app)를 직접 의존하지 않음
  • Context별 API 모듈만 참조하여 의존성 방향 명확화
  • Controller 코드는 단 한 줄도 포함하지 않음
  • 전역 예외 처리와 Swagger 메타 정보만 관리

3-3. Context별 표현 계층 ({context}-api)

Controller 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// account/account-api/controller/AuthenticationController.java
package com.connectors.account.api.controller;

@RestController
@RequiredArgsConstructor
public class AuthenticationController implements AuthenticationApi {
    private final JwtProperties jwtProperties;
    private final JwtProvider jwtProvider;
    private final JwtHeadersProvider jwtHeadersProvider;
    
    private final LoginSessionService loginSessionService;  // account-app
    private final OAuth2UrlService oAuth2UrlService;        // account-app
    private final LoginService loginService;                // account-app
    
    @Override
    public ResponseEntity<OAuth2LoginUrl> getOAuth2LogInUrl(
        OAuth2UrlRequest request
    ) {
        return ResponseEntity.ok(oAuth2UrlService.generateLoginUrl(
            request.getProvider(), 
            request.getCodeVerifier(), 
            request.getRedirectPath(), 
            request.getRedirectUri()
        ));
    }

    @Override
    public ResponseEntity<LoginResponse> oAuth2LogIn(
        OAuth2LogInRequest request
    ) {
        LoginContext context = loginService.login(
            request.getProvider(), 
            request.getCode(), 
            request.getState()
        );
        
        AuthToken refreshToken = jwtProvider.generateRefreshToken(
            context.memberId().id()
        );
        
        loginSessionService.create(
            context.memberId(), 
            refreshToken.token(), 
            refreshToken.expiresAt()
        );

        AuthToken accessToken = jwtProvider.generateAccessToken(
            context.memberId().id(),
            context.nickname(),
            AuthorityMapper.toAuthorities(context.authorities())
        );

        return ResponseEntity.ok()
                .headers(jwtHeadersProvider.createHeaders(
                    List.of(refreshToken, accessToken)
                ))
                .body(LoginResponse.fromContext(context));
    }
}
  • Controller는 support:securityJwtProvider, JwtHeadersProvider를 사용
  • 횡단 관심사를 별도 모듈에서 주입받아 사용
  • 자신의 Context(account-app)의 Service만 호출

Context별 Swagger API 그룹 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// account/account-api/config/AccountApiGroupConfig.java
@Configuration
public class AccountApiGroupConfig {
    @Bean
    public GroupedOpenApi accountApi() {
        return GroupedOpenApi.builder()
                .group("01-Account(계정)")
                .packagesToScan("com.connectors.account.api")
                .build();
    }
}

// content/content-api/config/ContentApiGroupConfig.java
@Configuration
public class ContentApiGroupConfig {
    @Bean
    public GroupedOpenApi contentApi() {
        return GroupedOpenApi.builder()
                .group("02-Content(콘텐츠)")
                .packagesToScan("com.connectors.content.api")
                .build();
    }
}

각 Context는 자신의 API 그룹을 독립적으로 정의한다. Runnable 모듈의 SwaggerConfig는 이를 자동으로 수집하여 하나의 문서로 통합한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// account/account-api/build.gradle
dependencies {
    implementation project(':core')
    implementation project(':support:security')  // 횡단 관심사 의존
    
    // 자신의 Application Layer만 의존
    implementation project(':account:account-app')
    
    // Infrastructure Layer는 Runtime에만 필요
    runtimeOnly project(':account:account-infra')
    
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13'
}
  • 해당 Context의 Application Service만 의존
  • 횡단 관심사는 support 모듈에서 주입
  • Swagger API 그룹을 독립적으로 정의
  • Request/Response DTO를 통한 명확한 계약 정의

III. 구조 개선 효과

1. Bounded Context 경계 분리

개선 전과 후의 의존성 구조를 비교해보자.

Before: 경계가 모호한 구조

1
2
3
4
5
6
7
8
9
10
         presentation:api (Controller, Security, Swagger 등 구현 책임)
          ↓            ↓
┌───────────────┬───────────────┐
│ account-app   │ content-app   │
└───────────────┴───────────────┘

문제:
- Presentation 모듈이 모든 Context를 참조하며, Controller 등 기능 구현 담당
- Context 경계를 무시하고, 각 Context의 기능들을 직접 호출해 조합 가능
- 횡단 관심사가 Presentation에 혼재

After: 경계가 명확한 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
       runnable:api-server
        ↓   (단순 참조)   ↓
┌───────────────┬───────────────┐
│ account-api   │ content-api   │ --→ support:security
└───────┬───────┴───────┬───────┘
        │               │
        ↓               ↓
┌───────────────┬───────────────┐
│ account-app   │ content-app   │
└───────────────┴───────────────┘
   
결과:
- 각 Context의 표현 계층이 독립적으로 Controller 등 기능 구현
- 횡단 관심사가 독립 모듈로 분리됨
- Context는 자신의 Application Service만 호출

2. 모듈별 책임 분리

모듈BeforeAfter
presentation:api① API 서버 실행
② 모든 Context의 Controller
③ 횡단 관심사 관리
(삭제)
runnable:api-server(없음)① API 서버 실행
② 전역 예외 처리
③ Swagger 메타 정보만 관리
support:security(없음)① 인증/인가 로직
② JWT 처리
③ Security 필터
{context}-api(없음)① 해당 Context의 HTTP 진입점
② Request/Response 변환
③ API 그룹 정의

개선 효과:

  • Security 로직을 support:security 모듈 하나에서 관리
  • 모든 api 모듈에서 동일한 Security 설정 재사용 가능
  • 더 이상 context 간 기능들을 혼용해서 활용할 수 있는 포인트가 존재하지 않음

4. 테스트 격리 개선

Before: 모듈이 혼재된 상태로 테스트 진행

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// presentation/api 모듈 테스트
@SpringBootTest
class AuthControllerTest {
    @Autowired
    private MockMvc mockMvc;
    
    // 모든 Context의 의존성이 필요
    @MockBean
    private AuthService authService;
    @MockBean
    private ContentService contentService;  // 불필요한 의존성
    @MockBean
    private MemberService memberService;
    
    // Security, Swagger 설정까지 모두 로딩
    
    @Test
    void loginTest() { ... }
}

After: 모듈별 테스트 보장

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// account/account-api 모듈 테스트
@WebMvcTest(AuthenticationController.class)
class AuthenticationControllerTest {
    @Autowired
    private MockMvc mockMvc;
    
    // 자신의 Context만 Mock
    @MockBean
    private LoginService loginService;
    @MockBean
    private OAuth2UrlService oAuth2UrlService;
    @MockBean
    private JwtProvider jwtProvider;
    
    @Test
    void loginTest() { ... }
}

5. Swagger 문서 구조 개선

Before: 모든 API가 한 곳에

1
2
3
4
5
6
7
Swagger UI
├── API 문서
    └── All APIs (모든 Context의 API가 뒤섞임)
        ├── POST /api/v1/auth/login
        ├── GET /api/v1/contents/{id}
        ├── POST /api/v1/members/nickname
        └── ...

After: Context별 그룹 분리

1
2
3
4
5
6
7
8
9
10
11
Swagger UI
├── 01-Account(계정)
│   ├── POST /api/v1/auth/login
│   ├── POST /api/v1/auth/logout
│   └── GET /api/v1/auth/refresh
├── 02-Content(콘텐츠)
│   ├── GET /api/v1/contents/{id}
│   ├── POST /api/v1/contents
│   └── PUT /api/v1/contents/{id}
└── 03-Common(공통)
    └── ...

개선 효과:

  • Context별로 API 문서가 그룹화되어 가독성 향상
  • 각 Context API 모듈에서 독립적으로 API 그룹 정의
  • API 문서를 확인하는 사람들이 Context를 자연스럽게 학습할 수 있음

IV. 결과

1. Context 간 의존성 제어

모듈 레벨에서 Context 간 의존성을 제어하고자 하는 것이 프로젝트의 메인 컨셉이었지만, 표현 계층을 잘못 활용하면서 오히려 Context가 이리저리 뒤섞이게 되었다. 이번 개선을 통해 보다 안전하고 명확하게 활용이 가능할 것 같다.

1
2
3
4
5
6
7
8
9
10
# Before: 패키지만 분리
presentation/
├── account/AuthController.java
└── content/ContentController.java
→ 같은 모듈 내부이므로 서로 참조 가능

# After: 모듈로 분리
account/account-api/controller/AuthenticationController.java
content/content-api/controller/ContentController.java
→ 다른 모듈이므로 build.gradle에 명시하지 않으면 참조 불가능

2. 횡단 관심사의 분리

Security, Logging, Exception Handling 같은 횡단 관심사는 여러 Context에서 공통으로 사용된다. 이를 특정 Context나 Presentation 모듈에 포함시키면 재사용이 불가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ❌ 잘못된 구조: Presentation에 Security 포함
presentation/api/
├── config/SecurityConfig.java
└── controller/AuthController.java

→ presentation:batch에서 Security 재사용 불가
→ account-api에서 JWT 처리 불가

# ✅ 올바른 구조: 독립 모듈로 분리
support/security/
├── config/WebSecurityConfig.java
├── jwt/JwtProvider.java
└── filter/JwtAuthenticationFilter.java

→ account-api, content-api에서도 JWT 기능 활용

DDD는 도메인을 중심에 두지만, 횡단 관심사를 무시하지 않는다. 오히려 횡단 관심사를 명확히 분리함으로써 도메인 로직에 집중할 수 있게 한다.

3. Runnable Application은 조합의 역할만

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// runnable/api-server/build.gradle
...

dependencies {
    implementation project(':core')
    
    implementation project(':account:account-api')
    implementation project(':content:content-api')
    implementation project(':common:common-api')
    
    ...
}

...

단순히 필요한 모듈들을 스캔하여 조합할 뿐, 비즈니스 로직이나 Controller를 직접 포함하지 않는다.

Runnable 모듈에 포함된 기능들:

  • ApiApplication.java: Main 진입점
  • HttpExceptionHandler.java: 각 Context에서 발생되는 예외 전역 처리
  • SwaggerConfig.java: API 문서 메타 정보 (각 Context의 그룹 조합)

Runnable 모듈에 포함되지 않는 기능들:

  • Controller: 각 Context의 -api 모듈에 위치
  • Security 설정: support:security 모듈에 위치
  • Application Service: 각 Context의 -app 모듈에 위치

V. 참고

  1. 최범균 - 도메인 주도 개발 시작하기
  2. Alistair Cockburn - Hexagonal Architecture
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.