백엔드 면접 용어 사전
설계Circular Dependencies · 필드 주입 · 생성자 주입

순환 의존성

A→B→C→A처럼 의존이 고리를 이루는 상태. 생성자 주입은 기동을 막고 필드 주입은 감춘다.

ServiceA → ServiceB → ServiceC → ServiceA처럼 의존이 고리를 이루는 상태.

무엇이 나쁜가

  • 어디서부터 읽어야 할지 알 수 없다
  • 단위 테스트를 만들 수 없다 (하나를 만들려면 전부가 필요하다)
  • 변경 영향이 고리 전체로 퍼진다
  • 모듈 분리가 불가능해진다

주입 방식이 결과를 가른다

  • 생성자 주입 — 순환이 있으면 기동 자체가 실패한다

    • 설계 결함이 배포 전에 드러난다
  • 필드 주입 — 리플렉션으로 나중에 채우므로 순환이 있어도 통과한다

    • 결함이 조용히 살아남는다

필드 주입이 위험한 진짜 이유가 이것이다. 편해서가 아니라 문제를 감추기 때문이다.

# Spring Boot 2.6부터 순환 참조는 기본 금지다
# 굳이 켜려면 (권장하지 않는다)
spring.main.allow-circular-references=true

끊는 방법

  • ① 고차 함수로 낮추기

    • 객체 의존 대신 필요한 '함수' 만 파라미터로 받는다
    • Function<Integer,Integer> 는 ServiceB 전체보다 훨씬 약한 결합
  • ② 의존을 상위로 올리기

    • A 와 B 를 아는 상위 컴포넌트(Handler·Facade) 가 조립한다
  • ③ 공통 부분을 제3의 모듈로 추출

    • A 와 B 가 함께 쓰는 것을 C 로 빼면 고리가 끊긴다
  • ④ 이벤트로 뒤집기

    • A 가 B 를 직접 부르지 않고 이벤트를 발행한다

완전 제거가 어렵다면

의존 자체를 없앨 수 없는 경우도 있다. 그때는 가능한 한 작게 유지한다 — 클래스 전체가 아니라 메서드 하나만 의존하도록 좁힌다.

면접 함정

  • allow-circular-references=true로 해결 → 문제를 덮는 것이지 푸는 것이 아니다.
  • "필드 주입이 코드가 짧아서 좋다" → 순환을 감추고 테스트에서 교체도 어렵다.

어떻게 발견하나

  • 생성자 주입으로 바꿔 본다 — 기동이 실패하면 순환이 있다

  • 정적 분석 도구 (ArchUnit · jdepend · SonarQube)

  • 모듈로 쪼개 본다 — 컴파일이 안 되면 순환이다

  • ArchUnit 예

    • slices().matching("com.example.(*)..").should().beFreeOfCycles()
    • 테스트로 순환을 CI 에서 막는다

왜 오래된 코드에 잘 생기나

  • 처음엔 A → B 단방향이었다
  • 어느 날 B 에서 A 의 기능이 필요해졌다
  • 가장 쉬운 해결: B 에 A 를 주입한다
  • 고리가 닫힌다

각 단계는 합리적인데 결과는 얽힌 그래프다 그래서 '한 번에 생긴 문제' 가 아니라 '누적된 결정' 이다

계층 규칙으로 예방한다

의존 방향을 미리 정하고 지킨다

  • controller → service → repository

같은 계층끼리(service ↔ service) 의 참조를 금지하면 순환이 구조적으로 생길 수 없다 서비스 간 협력이 필요하면 상위(파사드·오케스트레이터) 로 올린다

이벤트로 뒤집을 때의 대가

A 가 B 를 직접 부르는 대신 이벤트를 발행하면 의존은 끊긴다

다만

  • 흐름 추적이 어려워진다 (누가 이 이벤트를 받는지 코드에서 안 보인다)

  • 트랜잭션 경계가 복잡해진다

  • 순서 보장이 필요하면 더 어렵다

  • 결합을 끊는 대가로 가시성을 잃는다. 남발하지 않는다

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 면접 시나리오·트러블슈팅 — 종합·STAR