author
postNo
status
thumbnail
description
category
tags
createdAt
updatedAt
시작하며
프로젝트 구조를 이야기할 때 자주 나오는 주제가 있다.
Layered Architecture 와 Hexagonal Architecture 는 무엇이 다를까?둘 다 코드를 역할별로 나누고 복잡도를 낮추기 위한 구조이기 때문에 비슷해 보인다.
하지만 실제로는
무엇을 중심에 두는지, 의존성이 어디를 향하는지, 외부 기술을 어떻게 격리하는지 에서 차이가 꽤 크다.그리고 여기서 한 가지 더 중요한 질문이 있다.
이런 아키텍쳐를 쓰면 순환참조도 자동으로 사라질까?내 생각은
아니다 이다. 좋은 아키텍쳐는 순환참조를 줄이는 데 도움을 주지만, 순환참조를 해결하는 만능 열쇠는 아니다. 결국 순환참조는
의존성의 성격 을 보고 풀어야 한다.이번 글에서는 다음 내용을 정리해보려고 한다.
- Layered Architecture 는 어떻게 구성되는가
- Hexagonal Architecture 는 어떻게 구성되는가
- 둘의 핵심 차이는 무엇인가
- 순환참조는 왜 생기고 어떻게 해결해야 하는가
한 줄로 먼저 정리하면
Layered Architecture는 역할을 층으로 나누는 구조이다.
Hexagonal Architecture는 도메인을 중심에 두고 바깥과의 연결을 Port 와 Adapter 로 감싸는 구조이다.
즉, Layered 는
계층 구조 로 바라보고, Hexagonal 은 핵심 보호 구조 로 바라본다.Layered Architecture 는 어떻게 구성되는가
Layered Architecture 는 가장 익숙한 백엔드 구조이다.
보통 아래와 같은 계층으로 나눈다.
- Presentation Layer
- Application Layer
- Domain Layer
- Infrastructure Layer

위 그림처럼 보통 상위 레이어가 하위 레이어를 호출하는 구조를 가진다.
1. Presentation Layer
사용자의 요청을 직접 받는 계층이다.
- HTTP Controller
- GraphQL Resolver
- Request Validation
- Response Mapping
여기서는 비즈니스 로직을 최대한 적게 두고, 입력과 출력의 형식을 맞추는 역할에 집중하는 것이 좋다.
2. Application Layer
유스케이스를 실행하는 계층이다.
- 트랜잭션 처리
- 여러 도메인 조합
- DTO 변환
- 흐름 제어
실무에서는 흔히
Service 라고 부르는 계층이 여기에 해당한다. 예를 들어
주문 생성, 환불 처리, 회원 탈퇴 같은 유스케이스를 조합하는 역할을 맡는다.3. Domain Layer
핵심 비즈니스 규칙이 위치하는 계층이다.
- Entity
- Value Object
- Domain Service
- Policy
할인 규칙, 주문 가능 여부, 상태 전이 같은 비즈니스 규칙은 가능하면 여기 가까이 두는 편이 좋다.
4. Infrastructure Layer
외부 기술과 연결되는 계층이다.
- DB 접근
- 외부 API 호출
- 메시지 큐
- 메일 발송
- 프레임워크 연동
즉, 애플리케이션을 둘러싼 기술적인 세부 구현이 위치한다.
Layered Architecture 의 장점과 한계
장점은 분명하다.
- 구조가 직관적이다.
- 팀원 대부분이 익숙하다.
- 작은 프로젝트에서 빠르게 개발을 시작하기 좋다.
- 프레임워크와 잘 맞는다.
하지만 시간이 지나면 자주 생기는 문제가 있다.
Service에 비즈니스 로직이 과도하게 몰린다.
- Domain 이 ORM 모델이나 Repository 세부사항에 끌려간다.
- 계층은 나눴지만 핵심 로직이 흩어진다.
즉, 레이어를 나눴다고 해서 자동으로 좋은 구조가 되지는 않는다.
Hexagonal Architecture 는 어떻게 구성되는가
Hexagonal Architecture 는
Ports and Adapters Architecture 라고도 부른다.이 구조의 핵심은 간단하다.
핵심 비즈니스 로직은 외부 기술을 몰라야 한다.DB, 웹 프레임워크, 메시지 큐 같은 것은 바깥에 두고, 안쪽에는 도메인과 유스케이스를 배치한다.

그림처럼 Hexagonal Architecture 는 보통 아래 요소로 설명할 수 있다.
- Application
- Domain
- Inbound Port / Inbound Adapter
- Outbound Port / Outbound Adapter
1. Domain
가장 안쪽에 있는 핵심이다.
- Entity
- Value Object
- Domain Rule
- Domain Policy
여기에는 DB, HTTP, MQ, 프레임워크에 대한 코드가 들어오지 않는 것이 이상적이다.
2. Application
유스케이스를 실행한다.
CreateOrderUseCase
CancelOrderUseCase
IssueCouponUseCase
즉, 시스템이 제공하는 기능 단위를 표현한다.
3. Inbound Adapter
바깥에서 안쪽으로 요청을 전달하는 진입점이다.
- REST Controller
- GraphQL Resolver
- Scheduler
- Kafka Consumer
중요한 점은 이들이 바로 도메인 세부 구현을 다루는 것이 아니라, 안쪽의 유스케이스를 호출한다는 점이다.
4. Outbound Port
핵심 로직이 바깥에 요구하는 기능의
계약 이다.예를 들면 이런 인터페이스가 될 수 있다.
핵심은
무엇이 필요하다 를 표현하고, 어떻게 구현하는지 는 말하지 않는 것이다.5. Outbound Adapter
Port 의 실제 구현체이다.
- MySQL Adapter
- Redis Adapter
- External API Adapter
- Message Bus Adapter
즉, Port 를 기술 세부사항으로 연결하는 부분이다.
둘의 가장 큰 차이
둘 다 결국 여러 영역으로 코드를 나누기 때문에 얼핏 보면 비슷하다.
하지만 차이는
경계를 어떻게 자르느냐 와 의존성을 어떻게 통제하느냐 에 있다.Layered Architecture 의 관점
- 컨트롤러
- 서비스
- 리포지토리
처럼
역할 을 기준으로 나눈다.Hexagonal Architecture 의 관점
- 안쪽은 핵심 비즈니스
- 바깥은 연결부
처럼
중심과 외부 를 기준으로 나눈다.그래서 Hexagonal Architecture 는 일반적으로 의존성 역전을 더 강하게 요구한다.
요청 하나가 흐르는 방식 비교
예를 들어
주문 생성 기능이 있다고 해보자.Layered Architecture
직관적이고 빠르게 구현할 수 있다.
하지만 시간이 지나면
OrderService 가 아래 역할을 모두 떠안는 경우가 많다.- 검증
- 비즈니스 규칙
- 외부 API 호출
- 이벤트 발행
- 저장
Hexagonal Architecture
여기서는 유스케이스가 저장이 필요하다는 사실만 알고, 실제 MySQL 구현은 바깥 Adapter 가 담당한다.
이 방식은 테스트 작성과 구현 교체에 유리하다.
순환참조는 왜 생길까
여기서 중요한 포인트가 있다.
Layered Architecture 든 Hexagonal Architecture 든, 구조만 도입한다고 순환참조가 없어지지는 않는다.순환참조는 주로 아래 상황에서 생긴다.
- 도메인 A 가 도메인 B 의 기능을 직접 호출한다.
- 도메인 B 도 다시 도메인 A 를 호출한다.
- 하나의 유스케이스 안에서 책임 경계가 불분명하다.
- 읽기 전용 조회와 비즈니스 행동이 뒤섞인다.
예를 들어 이런 식이다.
처음에는 편해 보이지만, 결국 모듈 초기화 문제, 테스트 어려움, 변경 파급 증가로 이어진다.
아키텍쳐는 순환참조 해결의 만능이 아니다
Layered Architecture 를 써도
OrderService 와 UserService 가 서로를 호출하면 순환참조가 생긴다. Hexagonal Architecture 를 써도
OrderUseCase 와 UserUseCase 가 서로의 Port 를 잘못 물고 들어가면 같은 문제가 생긴다.즉, 문제의 핵심은 아키텍쳐 이름이 아니라
누가 누구를 왜 참조하는가 이다.
그래서 순환참조는
의존성의 종류 에 따라 풀어야 한다.순환참조를 해결하는 방법
1. 하나의 유스케이스라면 상위 Orchestrator 로 올린다
가장 흔한 경우다.
OrderService 가 UserService 를 호출하고, UserService 가 다시 OrderService 를 호출하는 이유는 사실 같은 유스케이스를 서로 나눠서 처리하고 있기 때문인 경우가 많다.이럴 때는 둘 중 하나가 다른 하나를 직접 호출하게 두지 말고, 상위 레벨의 Application Service 또는 UseCase 가 둘을 조합하도록 만드는 편이 낫다.
즉,
동등한 두 서비스가 서로 부르는 구조 를 상위 조정자가 아래를 호출하는 구조 로 바꾼다.이 방식은 Layered Architecture 에서도 유효하고, Hexagonal Architecture 에서도 유효하다.
2. 후속 반응이라면 Domain Event 로 분리한다
어떤 기능은 같은 트랜잭션 안에서 즉시 처리할 필요가 없고, 단지
무언가가 일어났음 을 다른 쪽에 알려주면 된다.예를 들면,
- 주문 완료 후 알림 발송
- 회원 탈퇴 후 쿠폰 정리
- 결제 완료 후 포인트 적립
이런 경우 A 가 B 를 직접 호출하기보다 이벤트를 발행하고, B 가 그 이벤트를 구독하게 만들면 결합도를 낮출 수 있다.
다만 이 방식은
같은 동기 흐름으로 강하게 묶여 있는 작업 에 무조건 쓰면 안 된다. 결국 이벤트는 후속 반응을 분리할 때 유용한 것이지, 모든 의존성을 숨기는 도구는 아니다.
3. 읽기 의존성이라면 Query Service 로 분리한다
많은 순환참조는 사실
행동 이 아니라 조회 때문에 생긴다.예를 들어 주문 도메인이 사용자 이름이나 등급만 읽고 싶은데, 그걸 얻으려고
UserService 전체를 주입하는 식이다.이럴 때는 아래처럼 읽기 전용 조회를 별도 Query 로 분리하는 편이 훨씬 낫다.
즉,
사용자에 대한 모든 기능 이 아니라 필요한 조회 계약 하나 만 의존하게 만든다.4. 진짜 공통 규칙이라면 Shared Policy 로 추출한다
서로 다른 두 도메인이 동일한 규칙을 공유한다면, 그 규칙을 한쪽 도메인 소유로 억지로 두지 말고 공통 정책으로 추출하는 것이 낫다.
예를 들면,
- 권한 판정 규칙
- 금액 계산 정책
- 공통 검증 로직
이런 것은
CommonPolicy, PricingPolicy, PermissionPolicy 처럼 별도 컴포넌트로 뽑아낼 수 있다.단, 여기서도 무분별한
common 폴더는 위험하다. 정말로 여러 도메인이 공유하는 규칙인지 먼저 확인해야 한다.
5. Port 와 Interface 는 수단이지 정답이 아니다
Hexagonal Architecture 를 이야기하면 흔히
인터페이스를 두면 된다 고 말한다. 하지만 인터페이스는 의존성의 방향을 바꾸는 수단일 뿐, 잘못된 책임 분리를 자동으로 고쳐주지는 않는다.
예를 들어 이런 상황은 여전히 문제다.
OrderUseCase가UserPort를 호출한다.
UserUseCase가 다시OrderPort를 호출한다.
형태만 Port 로 바뀌었을 뿐 사실상 순환 의존성은 그대로다.
즉, 인터페이스보다 먼저 봐야 할 것은
이 호출이 정말 필요한가, 상위 조정자가 가져가야 하는가, 이벤트로 분리해야 하는가 이다.6. NestJS 의 forwardRef() 는 마지막 수단이다
NestJS 에서는 DI 초기화 문제를 피하기 위해
forwardRef() 를 사용할 수 있다. 하지만 이건 설계 문제를 해결했다기보다, 컨테이너가 일단 뜨도록 우회하는 경우가 많다.
물론 프레임워크 레벨에서 어쩔 수 없이 써야 하는 경우도 있다.
하지만 비즈니스 서비스끼리의 순환참조를
forwardRef() 로 넘기기 시작하면 보통 구조 문제가 더 커진다.내 기준에서는
forwardRef() 는 해결책이라기보다 경고 신호에 가깝다.실무에서는 어떻게 선택하면 좋을까
개인적으로는 이렇게 정리한다.
- 단순 CRUD 위주라면 Layered Architecture 로 시작해도 충분하다.
- 도메인이 복잡하고 오래 진화해야 한다면 Hexagonal Architecture 가 유리하다.
- 어떤 구조를 쓰든 순환참조는 별도의 설계 문제로 봐야 한다.
즉, 아키텍쳐 선택과 순환참조 해결은 연결되어 있지만 같은 문제는 아니다.
내 생각
실무에서는 둘 중 하나만 순수하게 쓰기보다, Layered Architecture 를 기본으로 가져가되 핵심 유스케이스나 외부 연동 경계에 Hexagonal 관점을 일부 도입하는 경우가 많다.
이 방식이 현실적인 이유는 다음과 같다.
- 팀이 이해하기 쉽다.
- 처음부터 과한 추상화를 만들지 않아도 된다.
- 복잡도가 커지는 지점에만 의존성 역전과 Port/Adapter 를 도입할 수 있다.
그리고 순환참조가 보이기 시작하면 그때는 무조건 기술 트릭부터 찾지 말고 아래 순서로 보는 것이 좋다.
- 이 둘은 사실 하나의 유스케이스인가?
- 이 의존성은 조회인가 행동인가?
- 후속 반응이라면 이벤트로 빼야 하는가?
- 진짜 공통 규칙이라면 별도 정책으로 뽑아야 하는가?
이 질문이 정리되면 대부분의 순환참조는 생각보다 깔끔하게 풀린다.
마무리
Layered Architecture 와 Hexagonal Architecture 는 서로 완전히 대체하는 개념이라기보다, 시스템을 바라보는 다른 렌즈에 가깝다.
- Layered Architecture 는 역할 분리에 강하다.
- Hexagonal Architecture 는 핵심 보호에 강하다.
하지만 둘 다 순환참조를 자동으로 해결해주지는 않는다.
결국 중요한 것은
비즈니스 로직이 어디에 있어야 하는지, 의존성이 왜 필요한지, 그 의존성이 진짜 같은 레벨에서 발생해야 하는지 를 계속 점검하는 것이다.좋은 아키텍쳐의 목적은 멋진 다이어그램이 아니라, 시간이 지나도 안전하게 바꿀 수 있는 구조를 만드는 것이라고 생각한다.