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 모듈 내부에서 account와 content 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를 직접 의존한다. 이는 다음과 같은 문제를 야기한다.
- 단일 책임 원칙 위배: Presentation 모듈이 “API 서버 실행”과 “모든 Context의 표현 계층 역할”을 동시에 담당
- 변경 영향 범위 확대: 특정 Context의 API 변경이 Presentation 모듈 전체에 영향
- 테스트 복잡도 증가: 한 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 모듈에 섞여 있었다. 이는 다음과 같은 문제를 야기했다.
- 재사용 불가:
presentation:batch모듈에서도 Security 설정이 필요하지만,presentation:api에 종속 - 책임 혼재: 표현 계층 모듈이 “Controller 관리” + “서버 설정” + “횡단 관심사 관리”를 동시에 담당
- 테스트 복잡도: 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. 개선 전략
문제를 해결하기 위해 다음과 같은 전략을 세웠다.
- Bounded Context별 표현 계층 분리: 각 Context에
{context}-api모듈 추가 - 횡단 관심사 독립 모듈화: Security, Swagger 등을
support모듈로 분리 - Runnable 모듈 재정의:
presentation→runnable로 변경, 순수한 실행 진입점 역할만 수행 - 의존성 방향 재정립: 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/
개선된 구조의 핵심은 세 가지 관심사의 명확한 분리다.
- Runnable 모듈: 애플리케이션 실행과 최소한의 서버 설정
- Support 모듈: 여러 Context에서 공통으로 사용하는 횡단 관심사
- 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:security의JwtProvider,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. 모듈별 책임 분리
| 모듈 | Before | After |
|---|---|---|
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모듈에 위치