-
Notifications
You must be signed in to change notification settings - Fork 3
Port & Adapter Architecture
헥사고널 아키텍처 패턴(Ports and Adapters)은 애플리케이션의 주요 로직을 외부 요소(예: 데이터베이스, 사용자 인터페이스)와 분리하여 개발의 유연성과 유지 보수성을 높이는 것을 목표.
2005년 Alistair Cockburn의해 제안된 아키텍처이며, 전통적인 계층형 아키텍처(Layered Architecture)의 문제점을 보완하고자 등장하였음. 레이어드 아키텍처는 데이터 엑세스 계층에 의존하는 구조인데, 이는 db같은 하위 계층이 강하게 결합되어 db의 변경이나 외부 시스템과의 상호작이 필요할 때 유연하게 대응하기 어려운 문제가 발생, 또한 특정 기술에 의존적는 코드로 작성되고 도메인 로직과 섞이면서 로직을 테스트 하는 것에도 쉽지 않다는 문제가 있음.
따라서 헥사고널 아키텍처는 비즈니스의 로직을 외부 기술 스택에 의존하지 않게 함으로써, 로직의 재사용성과 유지보수을 높이고, 의존성 역전 원칙 (Dependency Inversion Principle)을 통해, 외부 특정 구현에 의존하지 않고 인터페이스에 의존하도록 설계 (상위 모듈 (High-level modules) 은 하위 모듈 (Low-level modules) 에 의존해서는 안 되며, 둘 다 추상화 (abstractions) 에 의존해야 한다).

-
Domain (도메인): 애플리케이션의 핵심 비즈니스 로직을 담당. 도메인 로직은 외부 시스템이나 프레임워크에 의존하지 않음.
-
Ports (포트): 도메인 로직과 외부 시스템 간의 인터페이스를 정의. 포트는 입력(입력 포트)과 출력(출력 포트)으로 나눌 수 있음.
-
Adapters (어댑터): 포트의 인터페이스를 구현하여 도메인 로직을 외부 시스템과 연결. 어댑터는 외부 시스템이 도메인 로직과 상호작용하는 방식을 정의.
- 도메인 중심: 비즈니스 로직은 도메인 계층에 위치하며, 이 계층은 외부 시스템과의 상호작용에서 독립적.
- 포트와 어댑터 계층: 포트는 도메인 로직이 외부 시스템과 상호작용할 수 있는 지점을 정의, 어댑터는 이러한 포트를 구현하여 실제 상호작용을 수행.
- 유연성: 외부 시스템을 변경하더라도 도메인 로직에는 영향을 주지 않음.
- 테스트 용이성: 도메인 로직은 외부 시스템과 분리되어 있기 때문에 독립적으로 테스트할 수 있음.
- 유지보수성: 각 계층이 명확하게 분리되어 있어, 코드의 변경이 다른 부분에 미치는 영향을 최소화할 수 있음.
- 웹 애플리케이션: 웹 UI, API, 데이터베이스 등 다양한 어댑터를 통해 도메인 로직에 접근할 수 있음.
- 마이크로서비스 아키텍처: 각 서비스가 독립적인 도메인 로직을 가지고, 다른 서비스나 외부 시스템과의 통신을 어댑터를 통해 수행.
- 도메인 로직을 정의하고, 이를 중심으로 애플리케이션을 설계.
- 도메인 로직과 상호작용할 수 있는 포트를 정의.
- 포트를 구현하는 어댑터를 작성하여 외부 시스템과의 상호작용을 처리.
- 도메인 로직의 독립성을 유지하면서, 필요에 따라 어댑터를 교체하거나 확장.
육각형 아키텍처는 복잡한 애플리케이션의 구조를 단순화하고, 유연성과 유지 보수성을 높이는 데 도움. 이를 통해 애플리케이션의 각 구성 요소를 독립적으로 개발하고 테스트할 수 있으며, 변화하는 요구 사항에 더 쉽게 대응할 수 있음.
Note
실제 적용 사례 및 개선 과정을 소개합니다
- 헥사고널 아키텍처에서의 도메인은 전통적인 mvc 아키텍처의 도메인의 개념과는 다르다.
- 전통적인 mvc 아키텍처 도메인의 엔티티는 db와 연결되어야 하는 실제 필드값을 의미하며 ORM이 적용된다.
- 하지만 헥사고널 아키텍처의 도메인은 실제 서비스에서 일어나야 하는 action을 정의한다.
- 따라서 헥사고널 아키텍처의 domain(model)에 생성되는 엔티티는 데이터베이스에 연결되는 맵핑 클래스가 아닌 구현 메소드가 된다.
- 겉으로 보기에는 똑같이 필드값을 만들고 빌더 패턴을 활용하여 생성자를 만들지만 이는 비즈니스 로직에서 해결해야 하는 action을 정의 하는 과정이다
- 따라서 application 서비스에서는 해당 액션을 기반으로 비즈니스 로직이 생성된다. 예를 들어 웹툰 추천을 위한 검색이 서비스에서 구현되어야 한다면, 도메인에서는 추천 action (실제로는 method를 작성), 검색 action을 만들고 서비스에서는 해당 action을 조합하여 추천 검색이라는 비즈니스 로직을 구현한다.
- 물론 헥사고널 아키텍처의 domain에도 orm을 통한 db와 통신을 할 수 있는 엔티티로 만드는 것은 가능하지만 그렇게 되는 경우 특정 영속성 레이어에 종속이 되기 때문에 권장되지 않는다.
- 헥사고널 아키텍처는 기존의 mvc 아키텍처와는 다르게 대부분의 통신을 추상화를 통해서 이루어지기 때문에 화면과 db의 영향을 받지 않고 서비스 로직을 만들 수 있다는 장점이 있다.
- 하지만 이러한 추상화는 가독성이 낮아진다는 단점이 있으며, 이러한 이유로 간단하거나 또는 유지 보수를 하기 위한 인원이 자주 교체 되는 서비스에는 헥사고널 아키텍처를 도입 하게 된다면 후임자가 코드를 이해하기 위해서 많은 리소스를 사용해야 한다(Complexity가 높기 때문에 이슈가 발생 했을 때 해결이 복잡할 수 있다).
- 따라서 헥사고널 아키텍처를 도입하기 전에는 서비스 로직이 자주 변경되는지, 유지 보수를 위한 인원이 자주 교체되는지, 간단한 서비스를 구현하기 위한 프로그램인지를 고민하고 도입 하는 것을 추천한다.
- port & adapter 아키텍처는 추가적인 레이어가 많기 때문에 latency issue가 있을 수 있다 (이건 어떻게 측정하지?)
- 여러 화면과 db 연결을 고려한다면 헥사고널 아키텍처는 좋은 선택이 될 수 있다.
src/
└── main/
└── java/
└── org/
└── samsamohoh/
└── webtoonrecommendation/
├── application/
│ ├── model/
│ │ ├── webtoon/
│ │ │ └── Webtoon.java
│ │ └── user/
│ │ └── User.java
│ ├── service/
│ │ ├── webtoon/
│ │ │ ├── WebtoonService.java
│ │ │ ├── WebtoonRecommendationService.java
│ │ │ └── WebtoonSearchService.java
│ │ └── user/
│ │ └── UserService.java
│ └── port/
│ ├── in/
│ │ ├── webtoon/
│ │ │ ├── RecommendWebtoonUseCase.java
│ │ │ └── SearchWebtoonUseCase.java
│ │ └── user/
│ │ └── ManageUserUseCase.java
│ └── out/
│ ├── webtoon/
│ │ ├── WebtoonPersistencePort.java
│ │ └── WebtoonSearchPort.java
│ └── user/
│ └── UserPersistencePort.java
└── adapter/
├── in/
│ └── rest/
│ ├── dto/
│ │ ├── WebtoonDTO.java
│ │ └── UserDTO.java
│ ├── WebtoonController.java
│ └── UserController.java
└── out/
├── persistence/
│ ├── webtoon/
│ │ └── JpaWebtoonPersistenceAdapter.java
│ └── user/
│ └── JpaUserPersistenceAdapter.java
└── search/
└── OpenSearchWebtoonAdapter.java
- Application이 어떤 Action을 수행할 것인가에 대한 정의이며, Application 관점에서 허용할 수 있는 요청 사례(Inbound UseCase) 그리고 외부의 요청이 필요한 사례(Outbound Request)로 나뉘어서 정의를 하는 것이 실제 application port의 역할.
- Application에서 어떤 Action을 수행할 때는 Input, Output을 어떻게 받을지 "메시지"가 정의되어야 한다.
- Adapter의 port는 Application의 Port의 실제 구현체 역할을 담당. 따라서 Adapter의 port는 본인이 In Adapter인지 Out Adapter인지 알 필요도 알 수도 없게 구현. 경우에 따라 Inbound, Outbount로 쓰일 수 있기 때문.
- 현재는 적용되어 있지 않지만 Port & Adapter 아키텍처를 적용하는 경우, gradle multi module이 추천. single module로만 구현되면 추후에 코드 중복이 발생하여 효율이 떨어질 수 있음.
com.example.project
├── ProjectApplication.java // 메인 애플리케이션 클래스
├── adapter // 외부 시스템과의 상호작용을 담당
│ ├── config // 설정 관련 클래스들
│ │ ├── AppConfig.java // 애플리케이션 설정
│ │ ├── SecurityConfig.java // 보안 설정
│ ├── persistence // 데이터베이스와의 상호작용을 담당
│ │ ├── UserPersistenceAdapter.java // 사용자 정보 저장소 어댑터
│ │ ├── DataEntity.java // 데이터 엔티티
│ ├── rdbms // JPA 관련 어댑터
│ │ ├── UserJpaAdapter.java // JPA 사용자 저장소 어댑터
│ ├── searchengine // 검색 엔진과의 상호작용을 담당
│ │ ├── SearchEngineAdapter.java // 검색 엔진 어댑터
│ ├── web // 웹 계층
│ ├── user
│ │ ├── UserController.java // 사용자 컨트롤러
│ ├── content
│ ├── ContentController.java // 콘텐츠 컨트롤러
│ ├── ContentResponse.java // 콘텐츠 응답 DTO
├── application // 비즈니스 로직과 애플리케이션 서비스
│ ├── port // 포트와 관련된 인터페이스
│ │ ├── in
│ │ │ ├── RegisterUserUseCase.java // 사용자 등록 유스케이스
│ │ │ ├── SearchContentUseCase.java // 콘텐츠 검색 유스케이스
│ │ ├── out
│ │ ├── AddDataPort.java // 데이터 추가 포트
│ │ ├── LoadContentPort.java // 콘텐츠 로드 포트
│ ├── service
│ ├── RegisterUserService.java // 사용자 등록 서비스
│ ├── SearchContentService.java // 콘텐츠 검색 서비스
├── common // 공통적으로 사용되는 클래스들
│ ├── ApiResponse.java // API 응답 공통 클래스
│ ├── logs
│ │ ├── LoggingAspect.java // 로깅 어스펙트
│ ├── metrics
│ ├── MetricsConfig.java // 메트릭 설정
├── domain // 도메인 모델
│ ├── UserValidation.java // 사용자 검증
│ ├── SearchableContent.java // 검색 가능한 콘텐츠
com.samsamohoh.webtoonsearch
├── WebtoonSearchApplication.java
├── adapter
│ ├── config
│ │ ├── SearchEngineConfig.java
│ │ ├── SecurityConfig.java
│ ├── persistence
│ │ ├── MemberPersistenceAdapter.java
│ │ ├── RegisterMemberEntity.java
│ │ ├── SearchPersistenceAdapter.java
│ │ ├── SearchWebtoonEntity.java
│ │ ├── SelectPersistenceAdapter.java
│ │ ├── SelectRecordEntity.java
│ ├── rdbms
│ │ ├── MemberPersistenceAdapter.java
│ │ ├── SelectRecordAdapter.java
│ ├── searchengine
│ │ ├── SearchEngineAdapter.java
│ ├── web
│ ├── member
│ │ ├── OAuthController.java
│ │ ├── OAuthResponse.java
│ ├── webtoon
│ ├── SearchWebtoonController.java
│ ├── SearchWebtoonResponse.java
│ ├── SelectRecordController.java
│ ├── SelectRecordRequest.java
├── application
│ ├── port
│ │ ├── in
│ │ │ ├── member
│ │ │ │ ├── RegisterMemberCommand.java
│ │ │ │ ├── RegisterMemberUseCase.java
│ │ │ ├── webtoon
│ │ │ ├── LoadWebtoonQuery.java
│ │ │ ├── SearchWebtoonCommand.java
│ │ │ ├── SearchWebtoonUseCase.java
│ │ │ ├── SelectRecordUseCase.java
│ │ │ ├── SelectWebtoonDTO.java
│ │ │ ├── WebtoonResult.java
│ │ ├── out
│ │ ├── AddRecordPort.java
│ │ ├── LoadWebtoonPort.java
│ │ ├── SaveMemberPort.java
│ ├── service
│ ├── RegisterMemberService.java
│ ├── SearchWebtoonService.java
│ ├── SelectRecordService.java
├── common
│ ├── ApiResponse.java
│ ├── logs
│ │ ├── LoggingAspect.java
│ ├── metrics
│ ├── CustomMetrics.java
│ ├── config
│ ├── MetricsConfig.java
├── domain
│ ├── MemberValidation.java
│ ├── SearchableWebtoon.java
- 🤝 Collaboration
- 💬 Git Commit Convention
- 🌿 Branching Strategy
- 🔀 Pull Request (PR) Guidelines
- 🐋 Docker
- 🎡 Kubernetes
- 🔎 Metrics
- 💊 USE/RED
- 📝 Metrics Design
- 🔥 Prometheus
- 🦖 Grafana
- ⚒️ 실제 구현