티스토리 뷰
✏️ 프로젝트 배경 및 초반 구조
최근 같은 학과 사람들과 함께 Spring Boot + DDD 기반 프로젝트를 진행하고 있습니다.
초기에는 각 도메인별로 패키지를 나누고, 그 안에 Controller, Service, Repository, DTO, Entity 등을 구성하는 전형적인 레이어드 아키텍처로 시작했습니다. 아래는 당시의 디렉터리 구조입니다
├── 📂 common ← 공통 설정/예외/응답 등
│ ├── 📂 base
│ ├── 📂 config
│ ├── 📂 exception
│ ├── 📂 jwt
│ ├── 📂 response
│ └── 📂 status
│
└── 📂 domain ← 비즈니스 영역 중심
├── 📂 application
│ ├── 📂 controller
│ ├── 📂 service
│ ├── 📂 repository
│ ├── 📂 dto
│ └── 📂 entity
│
├── 📂 auth
├── 📂 review
├── 📂 team
└── 📂 user
✏️ 순환 참조 문제 발생
프로젝트가 점차 커지고 도메인 간 데이터 흐름이 복잡해지면서, 서비스 간 의존성이 증가했고 자연스럽게 순환 참조 문제가 발생하기 시작했습니다. 초기에 Application 도메인에서 작업을 하다가 순환 참조 문제가 최초로 발생해서 아래 사진에 보이는 것처럼 논의를 한 후 CommandService / QueryService로 계층을 분리해서 설계하고 있었는데,
User 도메인에서 사용자의 지원 현황을 조회하려고 UserService에서 ApplicationQueryService를 주입하자, 아래와 같은 순환 참조가 발생했습니다.

이렇게 Application 도메인 부분에서 QueryService와 CommandService로 나눠서 개발을 진행을 하고 있었는데 User 도메인 부붕네서 사용자의 지원 현황을 확인하기 위해 UserService에서 ApplicationQueryService를 주입해서 사용하려 했지만,양방향 의존성이 생기면서 다음과 같은 문제가 발생했습니다:
UserService <-> ApplicationQueryService
✏️ 해결 방향 - Facade 패턴 도입하기
그래서 이 문제를 피하기 위해 ApplicationRepository를 직접 호출하도록 변경했으나,
이는 도메인 간 경계를 침범하는 형태였고, 결국 SRP나 DDD 철학과도 어긋나는 방식이었습니다. 이후에는 UserService를 UserCommandService와 UserQueryService로 분리해보기도 했지만, 결국 ApplicationQueryService와 UserQueryService 간의 간접적인 순환 의존이 또다시 발생할 가능성이 있어 근본적인 해결책이 아니었습니다.
class UserService(
private val userTechStackService: UserTechStackService,
private val reviewService: ReviewService,
private val userRepository: UserRepository,
private val applicationRepository: ApplicationRepository, <= ApplicationQueryService를 호출하게 되면 순환 참조 발생
private val userScrappedTeamService: UserScrappedTeamService,
private val userViewedTeamHistoryService: UserViewedTeamHistoryService
)
이처럼 여러 도메인의 서비스를 직접 주입받고,
심지어 다른 도메인의 레포지토리까지 호출하게 되니 유지보수가 매우 어려워졌습니다.
그래서 같은 백엔드 팀원 분과 이 상황에 대해서 논의를 해봤고 사진처럼 Facade를 사용하는 것이 어떻겠냐라는 PR 리뷰를 남겨주셔서 Facade패턴을 도입하게 되었습니다.

✏️ Facade 패턴이란?
Facade 패턴은 복잡한 서브 시스템들을 하나의 인터페이스로 묶어
외부에서는 그 인터페이스(Facade)만 바라보도록 하여 내부 구조의 복잡성을 감추는 디자인 패턴입니다.
Spring Boot 기반의 DDD 구조에서는 단순한 CRUD보다는 도메인 간 상호작용이 복잡한 경우가 많기 때문에,
서비스 조합 로직을 Facade 계층에 위임하는 방식으로 구조를 깔끔하게 만들 수 있습니다.
Controller
↓
OrderFacade
├─ InventoryService
├─ PaymentService
├─ NotificationService
└─ OrderService
이런 구조로 만들면 Controller는 단순히 Facade만 호출하고,
Facade가 내부에서 필요한 서비스들을 호출하여 전체 로직을 처리하게 됩니다.
여기서 단순한 CRUD라는 말이 조금 모호한 것 같아서, 이 부분에 대해서도 팀원과 논의를 해서 정했습니다.


✏️ 코드 예제
Facade패턴을 도입하기로 결정한 후 한 개의 역할마다 Facade를 만들지, 아니면 도메인 별로 하나의 Facade만 만들고 그 안에서 필요한 기능들을 모두 정의할 지 고민이 있었습니다.
도메인 별로 하나의 Facade를 만들게 되면 파일을 관리하기는 쉽겠지만 후에 기능들을 유지보수하거나 수정하는 부분에서 어려움이 있을 것 같아 보여서 팀원분과 논의를 하여 한 개의 역할마다 Facade를 만드는 방향으로 정했습니다.
유저의 프로필 조회하는 부분에서 순환 참조가 있었고 유저의 마이페이지 조회 부분 역시 다른 도메인 서비스를 호출하는 부분이 있어서 이 두 개의 기능들을 Facade로 관리하기로 하였습니다.

작성한 코드들은 다음과 같습니다.
@GetMapping("/me")
@Operation(summary = "유저 마이페이지 조회")
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "유저 마이페이지 조회 성공", content = [Content(mediaType = "application/json", schema = Schema(implementation = UserMyPageResponse::class))])
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "유저가 존재하지 않는 경우",content = [Content()])
fun getUserPage(
@AuthenticationPrincipal userId: Long
): ResponseEntity<ApiResponse<UserMyPageResponse>> {
val response = userPageFacade.getUserPage(userId)
return ApiResponse.success(SuccessStatus.GET_USER_MY_PAGE_SUCCESS, response)
}
@GetMapping("/me/profile")
@Operation(summary = "내 프로필 조회")
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "유저프로필 조회 성공", content = [Content(mediaType = "application/json", schema = Schema(implementation = UserProfileResponse::class))])
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "유저가 존재하지 않는 경우",content = [Content()])
fun getUserProfile(
@AuthenticationPrincipal userId: Long
): ResponseEntity<ApiResponse<UserProfileResponse>> {
val response = userProfileFacade.getUserProfile(userId)
return ApiResponse.success(SuccessStatus.GET_USER_PROFILE_SUCCESS, response)
}
@Service
class UserPageFacade(
private val userService: UserService,
private val applicationService: ApplicationService,
private val userScrappedTeamService: UserScrappedTeamService,
private val userViewedTeamHistoryService: UserViewedTeamHistoryService
) {
fun getUserPage(userId: Long): UserMyPageResponse {
val user = userService.findUserByUserId(userId)
val applicationCount = applicationService.getUserApplicationCount(user.userId)
val scrapCount = userScrappedTeamService.getUserScrapCount(user)
val recentViewCount = userViewedTeamHistoryService.getUserRecentViewCount(user)
val activityCounts = UserActivityCounts(applicationCount, scrapCount, recentViewCount)
return UserMyPageResponse.from(user, activityCounts)
}
}
@Service
class UserProfileFacade(
private val userService: UserService,
private val userTechStackService: UserTechStackService,
private val reviewService: ReviewService
) {
fun getUserProfile(userId: Long): UserProfileResponse {
val user = userService.findUserByUserId(userId)
val techStack = userTechStackService.findUserTechStackListByUser(user)
val reviewDetails = reviewService.getUserReviewDetails(user)
return UserProfileResponse.from(user, techStack, reviewDetails)
}
}
✏️ Facade 패턴의 장단점 + 나의 느낌
장점
- 서비스 간 복잡한 호출 로직을 한 곳에 모을 수 있어 가독성이 좋아짐
- Controller가 깔끔해짐 – 각종 서비스 호출을 Facade로 위임
- 서비스 간 의존성을 줄일 수 있음, 순환 참조 방지에 효과적
- 테스트 작성 용이 – 복잡한 시나리오를 Facade 단위에서 테스트 가능
단점
- Facade가 커질 위험이 있음 – 결국 또 하나의 거대 서비스가 될 수 있음
- 로직이 중복될 수 있음 – Facade와 Service 사이에서 책임이 애매해질 경우
- 지나친 추상화는 오히려 구조를 더 복잡하게 만들 수 있음
확실히 단순히 CommandService / QueryService로 나누는 것보다 더 구조적인 해결책이었던 것 같습니다.
예전에 NestJS 기반 프로젝트에서도 비슷한 상황이 있었는데, 그때는 결국 레포지토리를 직접 호출해서 문제를 우회했습니다.
이번엔 그보다 더 견고하고 의미 있는 방식으로 해결했다는 점에서 만족감이 큽니다.
자바에서는 인터페이스 기반 구현체를 통해 이런 문제를 어느 정도 방지할 수 있었지만,
Kotlin에서는 인터페이스 기반 서비스를 잘 사용하지 않다 보니 이런 구조적 고민이 더 자주 필요하다는 점도 새삼 느꼈습니다.
✏️ 마무리
Spring Boot에서 DDD 구조를 사용하다 보면,
도메인 간 호출이 얽히고 서비스 의존도가 높아지는 순간이 발생할 수 있습니다.
그럴 때 Facade 패턴은 깔끔한 해결책이 될 수 있을 것 같아요.
각 도메인의 책임은 그대로 유지하면서, 조합 로직만 한 곳으로 모아둠으로써
구조를 더 명확히 하고 유지보수성도 높일 수 있습니다.
'Back-End' 카테고리의 다른 글
| [UMC/시니어 미션 #1] (0) | 2025.09.17 |
|---|---|
| [Spring/스프링] - 스프링 부트(Kotlin) Github Actions 자동 배포 (0) | 2025.08.29 |
| [Spring/스프링] - Spring Boot에서의 MVC 흐름 (4) | 2025.08.21 |
| [Spring/스프링] - JPA에서 N+1 문제와 해결 방법 (2) | 2025.08.20 |
| [Spring/스프링] - Spring Boot에서 Entity에 Setter를 사용하면 안 되는 이유 (2) | 2025.08.19 |
- Total
- Today
- Yesterday
- C++ Stack
- HTML5
- 자료구조
- DP
- 우선순위 큐
- 알고리즘 공부
- Do it!
- 유클리드 호제법
- html
- 반복문
- 백준 풀이
- java
- 백준
- 에라토스테네스의 체
- js
- C++
- DFS
- 이분 매칭
- 자바
- 투 포인터
- 자바스크립트
- c++ string
- 카운팅 정렬
- 유니온 파인드
- 스프링 부트 crud 게시판 구현
- 스택
- 세그먼트 트리
- CSS
- BFS
- 알고리즘
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
