[bug] SpringBoot/The dependencies of some of the beans in the application context form a cycle
1. 문제 정의:
서비스 간 순환참조로 인해 스프링 부트 애플리케이션 실행 시 빈 초기화에서 순환 의존성 문제 발생.
The dependencies of some of the beans in the application context form a cycle
┌─────┐
| challengeService defined in file
↑ ↓
| couponService defined in file
└─────┘
도메인 서비스 A와 B 간에 의존성이 존재하며, A는 B를 참조하고 B는 A를 참조하면서 순환참조 문제가 발생. 서비스 A는 ChallengeService를, 서비스 B는 CouponService를 참조하고 있음.
2. 고려사항
서비스 간 의존성 그래프를 분석하여 순환참조를 발생시키는 의존성 구조를 확인. 서비스 간의 의존성을 최소화하면서 문제를 해결할 방법을 고려.
// ChallengeService에서 CouponService를 직접 참조하고 있음
class ChallengeService {
private val couponService: CouponService // 순환참조 문제 발생
// ...
}
// CouponService에서 ChallengeService를 직접 참조하고 있음
class CouponService {
private val challengeService: ChallengeService // 순환참조 문제 발생
// ...
}
3. 고려한 해결방법
3.1. 서비스 분리:
CouponService와 ChallengeService를 더 작은 서비스로 분리하여 각각 독립적으로 동작하도록 설계. 이를 통해 순환참조를 방지하고 서비스 간의 명확한 경계를 설정.
3.2. 이벤트 기반 아키텍처 도입:
이벤트를 통해 서비스 간 통신하도록 아키텍처를 변경. ChallengeService에서 발생한 이벤트를 CouponService에서 수신하고 필요한 작업 수행.
// ChallengeService
class ChallengeService {
private val eventBus: EventBus // 이벤트 버스를 통한 통신
fun processChallengeCompletedEvent(challengeId: String) {
// Challenge 완료 이벤트 발행
eventBus.publish(Event(ChallengeCompletedEvent(challengeId)))
}
}
// CouponService
class CouponService {
fun onChallengeCompletedEvent(event: ChallengeCompletedEvent) {
// Challenge 완료 이벤트 수신 및 처리 로직
issueCoupon(event.challengeId)
}
}
4. 해결방법
서비스 분리 방법 선택. ChallengeService와 CouponService를 별도의 마이크로서비스로 분리하여 순환참조 문제를 해결.
4.1 ChallengeReviewService 도입 후의 ChallengeService
@Service
class ChallengeService(
private val converter : ChallengeConverter,
private val repository: ChallengeRepository,
) : BaseService<ChallengeReq, ChallengeResp, Challenge, String> {
fun findAll(searchDto : ChallengeSearchDto, pageable: Pageable): Page<ChallengeResp> {
// 이전 구현 내용과 동일
}
override fun findById(id: String): ChallengeResp {
// 이전 구현 내용과 동일
}
}
4.2 ChallengeReviewService
@Service
class ChallengeReviewService(
private val repository: ChallengeRepository,
private val couponService: CouponService,
) {
fun review(request: ChallengeReviewReq){
// 도전 내역 리뷰 로직
// ...
// 쿠폰 서비스 호출
couponService.save(CouponReq(/*...*/))
}
}
5. 효과와 결과:
서비스 간의 명확한 분리로 순환참조 문제가 사라지고 애플리케이션 초기화 과정에서 발생한 에러가 해결.
6. 기타 고려 사항:
작은 규모의 프로젝트나 단순한 비즈니스 로직을 가진 프로젝트라서, 이벤트 기반 아키텍처를 도입할 필요성이 낮음.
작은 규모의 시스템에서는 단일 모놀리식 아키텍처나 간단한 마이크로서비스 아키텍처가 충분.