순환 참조를 고치기 전에 도메인 경계부터 바로잡기
인터페이스나 의존성 역전으로 순환 참조처럼 보이는 문제를 다루기 전에 모호한 소유권과 개념 경계를 먼저 정리한다.
출처 및 AI 안내: 이 글은 제미니의 개발실무 유튜브를 기반으로 작성되었습니다.
gpt-5.6-sol모델을 사용해 생성·편집했습니다.
주문, 배송, 배송 부가 서비스가 서로 의존하는 것처럼 보이면 순환 참조라고 진단하기 쉽다. 빠른 처방은 흔히 인터페이스다. 화살표 하나를 뒤집어 모듈을 컴파일하고 순환이 사라졌다고 선언한다.
그러나 의존성 그래프만 고치고 더 어려운 문제는 남길 수 있다. 배송 작업이 주문 정보를 입력으로 받으면서 팀이 주문 개념 또는 부가 서비스 개념이라고 부르는 객체를 반환한다면 소유권은 이미 모호하다. 추상화를 하나 더 도입하기 전에 각 작업이 실제로 무엇을 뜻하고 결과를 어느 개념이 소유하는지 물어야 한다.
함수 시그니처를 소유권에 관한 문장으로 읽는다
입력은 비즈니스 흐름의 앞선 개념에서 올 수 있다. 주문이 일어난 뒤 배송을 예약한다면 주문 정보를 배송 쪽에 전달하는 일은 자연스럽다. 개념의 흐름과 코드 의존성 그래프는 관련이 있지만 일대일로 대응하는 그림은 아니다.
출력은 더 주의 깊게 봐야 한다. 배송 예약 작업이라면 보통 배송의 언어로 표현한 결과를 반환해야 한다. 주문 객체를 반환한다면 그 작업이 배송에 속하지 않는 것일 수 있다. 저장 소유권이 다른 배송 부가 서비스 레코드를 반환한다면 하나의 책임 한가운데에 경계를 그은 것일 수 있다.
모든 교차 개념 타입을 금지하는 보편 규칙은 아니다. 입력, 행위, 출력을 하나의 일관된 책임으로 설명할 수 있는지 확인하는 진단 질문이다. 분명한 이유 없이 여러 어휘를 오가는 함수 시그니처는 재사용성을 떨어뜨리고, 패키지 이름만으로 코드의 실제 소유권을 이해하기 어렵게 한다.
인터페이스는 뒤엉킨 개념을 바로잡지 못한다
의미 있는 경계가 있고 구현을 격리할 이유가 있을 때 의존성 역전은 가치가 있다. 하지만 모호한 계약의 의미를 바꾸지는 못한다. 배송 예약 정보를 받고 무관한 개념을 반환하는 인터페이스는 같은 혼란을 더 추상적인 이름 뒤에 보존한다.
그런 상황에서는 인터페이스를 잠시 빼면 설계가 더 잘 보일 수 있다. 먼저 구체적인 흐름을 직관적으로 만든다. 각 작업의 입력과 출력이 그 책임에 맞게 한다. 행위를 이해한 뒤 경계나 구현 변화가 인터페이스를 정당화하면 그때 추출한다.
이 순서는 추상화를 위장막으로 사용하는 일을 막는다. 인터페이스를 거부하려는 것이 아니라 명확한 모델이 인터페이스의 쓸모 있는 위치를 결정하게 하려는 것이다.
개념의 격벽을 레이어와 혼동하지 않는다
어떤 의존성이 “격벽 두 개를 뛰어넘는다”는 표현은 도메인 경계를 위아래로 쌓인 애플리케이션 레이어처럼 가정할 수 있다. 꼭 그래야 하는 것은 아니다. 같은 개념도 다르게 감쌀 수 있다. 부가 서비스가 배송 경계 안에 들어갈 수도 있고, 배송과 부가 서비스가 하나의 더 넓은 격벽을 공유하면서 내부에서 서로 다른 개념으로 남을 수도 있다.
개념도의 화살표는 비즈니스 흐름이나 지식을 설명한다. 모듈 그림의 화살표는 컴파일 시점 의존성을 표현한다. 둘을 같은 그림으로 취급하면 도식을 지나치게 문자 그대로 해석해서만 존재하는 위반을 만들 수 있다.
예를 들어 주문 다음에 배송이 시작된다는 업무 순서는 주문 모듈이 배송 모듈 구현에 직접 의존해야 한다는 뜻이 아니다. 애플리케이션 흐름이 두 책임을 조정할 수도 있고, 한쪽이 명확한 입력 값을 받아 자기 결과를 만들 수도 있다. 반대로 모듈 화살표가 한 방향이라고 해서 비즈니스 개념의 소유권까지 자동으로 정리되는 것도 아니다. 두 그림은 서로 검토하되 같은 규칙으로 읽지 말아야 한다.
모든 명사를 각각의 모듈에 넣으려 하지 말고 소유권부터 그린다. 그다음 어떤 경계를 기술적으로 강제해야 하는지 별도로 정한다. 개념에 이름을 붙이고 모델링하더라도 첫날부터 독립 패키지 경계, 모듈, 의존 방향을 모두 부여할 필요는 없다.
비즈니스가 증명하지 않은 분리는 미룬다
“배송 부가 서비스”라는 이름에는 힌트가 있다. 반복되는 배송이라는 말은 부가 서비스가 여전히 더 큰 배송 개념 안에 속한다는 뜻일 수 있다. 작고, 주요 예약 행위 아래에 숨겨지며, 더 넓은 비즈니스가 직접 조정할 대상이 아니라면 일찍 분리하는 순간 구현 세부사항을 최상위 도메인으로 노출하게 된다.
미래에 커질 것이라는 예상만으로는 경계를 만들 근거가 약하다. 배송을 더 큰 개념으로 시작하고 부가 서비스를 그 안에 둔다. 이후 운영, 규칙, 소유권, 변경 패턴에서 독립적인 생명주기가 드러나면 증거를 가지고 경계를 다시 그릴 수 있다.
더 풍부한 도메인에서는 반대 결론이 맞을 수도 있다. 핵심은 클래스나 테이블이 하나 더 있다는 사실이 아니라 비즈니스 의미로부터 분리를 얻는 것이다. 일부 코드만 본 상태라면 정확한 답은 조건부로 남겨야 한다.
독립 경계를 뒷받침하는 증거는 이름보다 생명주기에서 나온다. 부가 서비스가 배송과 별도로 생성되고, 자기 규칙으로 변경되며, 다른 흐름에서도 직접 사용된다면 별도 개념이 될 가능성이 커진다. 반대로 배송 예약 과정 안에서만 잠깐 만들어지고 배송 없이 존재할 수 없다면 배송 내부의 하위 개념으로 두는 편이 자연스럽다. 테이블이 이미 나뉘어 있거나 클래스가 따로 있다는 사실만으로 어느 쪽도 확정할 수 없다.
의존성을 바꾸기 전에 리뷰의 질문을 바꾼다
순환이 의심될 때는 다음 순서로 모델을 검토한다.
- 비즈니스 흐름을 일상 언어로 말한다.
- 생성되거나 반환되는 각 개념의 소유자를 찾는다.
- 모든 작업의 입력, 행위, 출력이 하나의 일관된 이야기를 만드는지 확인한다.
- 밀접한 개념을 묶은 뒤에 단단한 격벽을 그린다.
- 그다음 모듈 의존성과 인터페이스를 선택한다.
이 과정을 거친 뒤에도 구조를 바꿔야 할 실제 순환 의존성이 남을 수 있다. 반대로 관련 코드에는 순환이 없고 개념 배치만 모호했다는 사실이 드러날 수도 있다. 어느 쪽이든 언어와 소유권을 먼저 바로잡아야 컴파일러만 만족하는 해법이 아니라 다음 유지보수자도 이해할 수 있는 의존성 해법을 만들 수 있다.