JUN0.DEV
JUN0.DEV

순환 참조 문제와 @Lazy 적용의 한계

Published on
  • avatarJunyoung Yang

카카오테크캠퍼스에서 선물하기 API를 구현하던 중, 상품과 옵션 로직을 나누는 과정에서 서비스 간 순환 참조가 발생했다. 기능 자체는 단순해 보였다. 옵션을 만들 때 상품이 존재하는지 확인해야 했고, 상품 서비스에서도 옵션 정보를 다루는 로직이 필요했다.

처음에는 필요한 서비스를 서로 주입하면 된다고 생각했다. 하지만 애플리케이션을 실행하자 빈 생성 단계에서 순환 참조 에러가 발생했다. 이 글은 그 문제를 @Lazy로 우회하는 데서 멈추지 않고, 왜 순환 참조가 생겼는지 다시 본 기록이다.

처음 보인 문제

구조는 아래처럼 얽혀 있었다.

OptionService는 옵션을 생성할 때 상품 존재 여부를 확인해야 해서 ProductService를 참조했다. 반대로 ProductService도 상품 상세나 옵션 관련 로직 때문에 OptionService를 참조하고 있었다.

각각 따로 보면 이상해 보이지 않았지만, 전체 의존 방향을 보면 두 서비스가 서로를 알아야만 동작하는 구조였다. Spring은 애플리케이션 시작 시점에 두 빈을 만들 수 없었고, 순환 참조 에러가 발생했다.

처음 시도한 해결

처음에는 @Lazy를 적용해 빈 생성 시점을 미루는 방식으로 문제를 피했다.

public OptionService(
        OptionRepository optionRepository,
        @Lazy ProductService productService
) {
    this.optionRepository = optionRepository;
    this.productService = productService;
}

이 방식은 실행 에러를 피하는 데는 효과가 있었다. 실제 객체 대신 프록시를 먼저 주입하고, 필요한 시점에 의존 객체를 가져오게 만들기 때문이다.

하지만 문제가 사라진 것은 아니었다. 의존성이 늦게 연결될 뿐, ProductServiceOptionService가 서로를 알아야 한다는 구조는 그대로 남아 있었다. 즉, 런타임 에러를 숨긴 것이지 설계를 정리한 것은 아니었다.

원인을 확인한 과정

멘토링 과정에서 받은 피드백은 단순했다. 순환 참조가 생겼다는 것은 두 클래스가 너무 깊게 엮였다는 신호라는 것이었다.

그래서 아래 기준으로 다시 봤다.

  • 정말 서비스끼리 서로 의존해야 하는지
  • 서비스 의존 없이 Repository 의존만으로 충분한 부분은 없는지
  • 상품과 옵션을 조합하는 책임이 별도 레이어에 있어야 하는지
  • 도메인 로직과 조회 편의 로직이 한 서비스에 섞인 것은 아닌지

이렇게 보니 문제는 단순히 Spring 설정이 아니었다. 상품과 옵션의 책임 경계가 흐려진 상태에서 필요한 메서드를 서비스끼리 직접 호출하다 보니 의존 방향이 얽힌 것이었다.

해결 방안

먼저 서로 호출하지 않아도 되는 로직을 분리했다. 옵션 생성 과정에서 상품 존재 여부만 확인하면 되는 부분은 필요한 최소 의존만 갖도록 정리했다.

그리고 두 서비스를 조합해야 하는 흐름은 특정 서비스에 억지로 몰아넣기보다 상위 조합 레이어를 두는 방식을 검토했다. 예를 들어 상품과 옵션을 함께 처리하는 유스케이스가 많아진다면 ProductOptionFacade 같은 파사드가 더 자연스러울 수 있다.

핵심은 양방향 의존을 그대로 둔 채 @Lazy만 적용하지 않는 것이었다. 임시로 실행은 되더라도, 이후 기능이 늘어날수록 같은 문제가 반복될 가능성이 컸다.

마무리

이 문제를 겪은 뒤에는 @Lazy를 문제 해결 도구로 먼저 떠올리기보다, 의존 방향이 왜 얽혔는지를 먼저 확인한다.

특히 서비스 계층에서는 아래 기준을 남겼다.

  • 순환 참조는 설정 문제가 아니라 설계 신호로 본다.
  • 서비스끼리 서로 호출하기 전에 책임 경계를 먼저 확인한다.
  • 조합 로직이 커지면 파사드나 유스케이스 레이어를 고려한다.
  • 임시 우회가 필요한 상황과 구조 개선이 필요한 상황을 구분한다.

결국 이 작업에서 중요한 것은 @Lazy를 알게 된 것이 아니라, 순환 참조를 보고 설계를 다시 볼 수 있게 된 점이었다.