[내일배움캠프 TIL, Day 16] 순환참조 해결 과정 및 아키텍처 회고

1. 오늘 학습 키워드

  • 순환참조 오류 (Circular Dependency): 빈(Bean) 주입 시 서로가 서로를 참조하며 발생하는 의존성 사이클 이슈.
  • 헥사고날 아키텍처 (Hexagonal Architecture): Port와 Adapter를 이용해 도메인 로직을 외부 환경으로부터 독립시키는 아키텍처.

2. 학습 내용 정리하기

💡 JPA/Spring 순환참조(Circular Dependency)란?

Spring 컨테이너가 빈을 생성할 때, A가 B를 의존하고 B가 다시 A를 의존하여 서로의 주입을 무한히 기다리게 되는 교착 상태를 말한다.
주로 서비스 계층에서 서로의 로직이 필요해 양방향으로 의존성을 가질 때 발생한다.

[해결 방법 4가지 요약]

  1. 구조적 개선: 서비스가 다른 서비스를 참조하는 대신 공통 로직을 담은 제3의 서비스(Facade 패턴 등)를 만드는 등, 구조적으로 의존성 방향을 단방향으로 정리한다.
    cf. 타 도메인 Repository 직접 참조 vs. Service 방식🤔?
    • Repository 직접 참조: 서비스 간의 얽힘이 없어 순환 참조 오류가 발생할 일이 거의 없다. 구현이 가장 빠르고 직관적이다.
      하지만 도메인 서비스가 가진 검증 로직을 무시하고 DB에 접근하게 되어 데이터 정규성이 깨질 위험이 있어, 아키텍처적으로는 가장 결합도가 높은 ‘지양해야 할 방식’으로 꼽히기도 한다.
    • Service 참조 (Port 활용): 도메인 로직을 보호하고 의존성 방향을 제어할 수 있지만, 스프링 빈 주입 시 순환 참조 사이클이 발생할 확률이 높으므로 신중한 구조 설계 가 필요하다.
  2. @Lazy 활용: 주입 시점을 해당 빈이 실제로 사용되는 시점으로 늦춘다. 임시방편으로는 좋으나 근본적인 설계 결함을 감추는 위험이 있다.
  3. 인터페이스 활용 (DIP): 헥사고날 아키텍처처럼 인터페이스(Port)를 중간에 두어 물리적인 직접 참조 고리를 끊어낸다.
  4. 이벤트 기반 아키텍처: 직접 호출 대신 이벤트를 발행(ApplicationEventPublisher)하여 서비스 간의 물리적 의존성을 완전히 제거한다.

🧱 아키텍처의 선택과 집중: 점진적 헥사고날 도입과 회귀?!

  • 문제상황: AreaService/CategoryService → StoreService → AreaService/CategoryService 💥
  • 시도: 도메인 간 직접 결합을 느슨하게 하기 위해 인터페이스(Port)를 두는 헥사고날 아키텍처를 부분 도입하는 것을 시도했다.
    // Area 와 Store
    area/port/AreaStoreCountPort.java       ← Area가 선언
    store/adapter/StoreCountAdapter.java    ← Store가 구현
      
    의존성 방향
    AreaService → AreaStoreCountPort ← StoreCountAdapter → StoreRepository
    
  • 이유 (나홀로 삽질하게 된 배경):
    CategoryAreaStore 순으로 개발을 진행했는데, Store가 생성되면서를 문제를 발견했다.
    • 도메인 논리: StoreCategory/Area를 참조하는 것은 자연스러운 흐름이다. Store 등록에는 카테고리와 운영지역 정보가 필요하기 때문이다.
      반면 CategoryAreaStore가 없어도 독립적으로 존재할 수 있어야 한다. 그런데 CategoryArea에서 삭제/숨김 전 소속된 가게를 확인해하는 과정을 거치다보니 어느새 양방향 참조 구조가 형성된 것이다.
    • 설계적 판단: Store에서는 Category/Area 서비스를 직접 참조하도록 두되, 거꾸로 Category/AreaStore 정보를 알아야 할 때는 인터페이스(Port)를 두어 의존성을 역전시키고자 했다.
      결합도를 낮추고 의존성을 끊어내기 위한 방법을 찾아보았고, 내 수준에서 적용가능한 방식으로 Port/Adapter 패턴으로 헥사고날을 부분 적용해서 Category/AreaStore라는 구체적인 구현체에 휘둘리지 않게 하려는 의도였다.
  • 회귀 결정: 프로젝트의 요구사항 및 완성도 측면(팀 컨벤션 통일), 현재 프로젝트 규모에서의 생산성을 고려하여 전형적인 계층형 아키텍처(Layered Architecture) 로 최종 회귀하게 되었다.🤣

    해결 방법 순환참조 해결 도메인 경계 보호 팀 컨벤션 적합성
    Repository 직접 참조
    Port/Adapter 패턴
    @Lazy 활용
    Facade/Service 분리

3. 학습하며 겪었던 문제점 & 에러

“왜 나만 문제가 있었던걸까?” : 순환참조와 아키텍처의 역설

주말을 지나 월요일이 되면서 PR를 확인하고 팀원들과 대화를 나눠보니, 프로젝트를 진행하며 나만 유독 순환참조(Circular Dependency) 오류로 고전한 것 같았다.
다른 팀원들은 아무 문제 없이 기능을 구현하고 있었는데, 그 원인은 역설적이게도 ‘느슨한 결합’을 위해 도입했던 헥사고날 아키텍처(Port/Adapter) 구조 때문이었다.

💡 정답보다는 합의가 중요하다!

  1. 요구사항 준수의 중요성: 프로젝트 가이드를 뒤늦게 다시 확인하니 “헥사고날은 학습용으로만 참고하고, 본 프로젝트에는 도입하지 않는다” 는 명확한 가이드가 있었다.
  2. 도메인 참조 방식의 차이: 팀원들은 타 도메인을 참조할 일이 없었거나, 또는 Repository를 직접 참조하는 방식으로 구현하고 있었다.
    나 혼자 구조를 다르게 가져가다 보니 빈(Bean) 주입 순서가 꼬이며 사이클이 발생했던 것이다.

튜터님께도 의견을 구하고, 팀원들과도 협의를 거쳐 최종적으로는 요구사항과 컨벤션을 통일하였다. 기껏 고민한 결과물이 조금 아깝긴 했지만 당연한 결론이었다…

🛠️ 아키텍처 선택의 우선순위 재정립

  • 요구사항을 꼼꼼히 읽자: 개발을 시작하기 전, 제약 사항과 요구사항을 꼼꼼히 읽는 것이 불필요한 삽질을 줄이는 가장 빠른 길임을 뼈저리게 느꼈다.
  • 팀 컨벤션이 기술적 우위보다 우선한다: Port/Adapter 패턴이 이론적으로 유연할지라도, 팀 전체가 이해하지 못한 구조는 협업 비용만 높일 뿐이다. 좋은 설계란 팀원 모두가 동의하고 유지보수 할 수 있는 설계임을 배웠다.
  • 적정 기술(Appropriate Technology)의 미학: 현재 규모의 프로젝트에서는 단순한 Repository 직접 참조가 오히려 직관적이고 빠른 생산성을 보장한다. 오버 엔지니어링을 경계해야 하는 이유를 몸소 체험했다.
  • 무엇보다 소통이 제일 중요하다.

4. Todo

  • 프로젝트 마감 제출 전 최종 점검

#내일배움캠프 #단기Java #TIL #SpringBoot #입문프로젝트 #순환참조오류 #삽질기록 #컨벤션통일

Categories:

Updated:

Leave a comment