티스토리 뷰

2. 헥사고날(Hexagonal) 아키텍처로 전환하기

어쩌면 작은 스타트업에서는 적합하지 않을 수 있겠지만, 일반적인 계층형 아키텍처를 헥사고날 아키텍처로 전환하는 작업을 하면서 깨지는 테스트를 어떻게 보수하고 헥사고날 아키텍처에 익숙해지는 경험을 하고. 어떤 장점이 있는지 느껴보고자 하였다.

📅 일정

22.11.22 ~ 22.11.25 

🎯 목적

미니 커머스를 헥사고날 아키텍처로 전환하여 헥사고날 아키텍처를 경험하는 것을 목적으로 한다..

📢 요구사항

  • 추가적인 피쳐는 없다.
  • 현재 계층형 아키텍처에서 헥사고날 아키텍처로 전환하는 작업을 진행한다.

 헥사고날(Hexagonal) 아키텍처 

헥사고날 아키텍처는 포트와 어댑터(ports-and-adapters) 아키텍처로도 알려져 있는데, 데이터베이스나 UI가 없이 비즈니스 로직 만으로 동작할 수 있도록 구성하는 아키텍처를 의미한다. 이렇게 구성함으로써 테스트가 쉬워지고 의존관계가 명확해 변경 시에 변경 지점을 찾기 쉬어진다는 장점이 있다.

 

결국 헥사고날 아키텍처는 비즈니스 코드와 특정 기술에 종속적인 코드를 분리하고 서로를 느슨하게 결합하여 변경이 쉽도록 만들어주는 아키텍처이다.

 패키지 구조

계층형 아키텍처의 패키지 구조

이전 글 ATDD로 미니커머스 만들기를 통해 사이드 프로젝트의 패키지 구조이다. 각자마다 구성하는 방식이 조금씩은 다르지만 일반적인 계층형 아키텍처의 패키지 구조를 가지고 있다. 보고 바로 이해할 수 있을 정도로 단순한 구조로 쉽게 이해할 수 있다. 하지만 문제가 있다.

 

첫 번째는 기능을 구분하기 힘들다는 것이다. 현재 프로젝트에는 상품 생성, 수정, 삭제, 조회의 기능을 가지고 있다. 아직은 그 기능의 개수가 적어 Controller, Service, Repository 내에서 해당 기능을 찾는 것이 어렵지 않을 수 있지만 그 개수가 많고 복잡해지면 어떤 기능이 어떤 클래스에 존재하는지 찾기 힘들어진다.

 

두 번째는 테스트가 힘들다는 것이다. 위 에서 각각의 기능이 한 클래스에 모여있어 기능을 찾기 힘들다는 내용을 얘기했다. 한 클래스에 너무 많은 기능이 모여있다는 것은 하나의 클래스가 가지는 책임이 너무 많아 SRP(Single Responsibility Prinsiple)가 지켜지지 않는다는 의미이고 이렇게 너무 많은 책임을 하나의 클래스가 가지고 있게 되면 의존관계가 복잡해져 의존하는 객체의 수가 늘어나 단위 테스트를 하기 위해 목킹해야 하는 의존 클래스도 많아져 테스트가 힘들어진다.

 

헥사고날 아키텍처를 패키지로 표현하기

크게 adapter , application, domain 으로 구분하고 adapter에는 비즈니스 로직가 관계없는 외부 요소의 구현체를 위치시키고 application 에는 Port와 Service를 두고 In Comming Port인 UseCase와 Out Going Port인 Port를 가지고 있다. 또한 각 서비스의 이름에 그 피쳐를 드러낼 수 있도록 하여 어떤 기능이 어떤 클래스에 있는지 패키지 구조만 보고도 예상할 수 있게 되었다.

https://reflectoring.io/spring-hexagonal/

 

위 패키지 구조가 표현하고 있는 아키텍처의 구조도이다. adapter 패키지에 WebAdapter와 Persistence Adapter를 확인할 수 있고. application/port 패키지에 내부 비즈니스 로직에 접근하는 In&Out Port를 확인할 수 있다. 또한 domain 패키지에 위치한 Item 이 위 그림에 Entity에 해당한다고 할 수 있다. 물론 위 프로젝트에 Item은 JPA에 종속적이기 때문에 JPA Entity용 Item과 내부 비즈니스 로직을 담당하는 Item을 구분해야 한다고 말할 수 있지만. 이는 생략했다.

 

 In Comming Port (UseCase)  

Web Adapter인 Controller와 Service를 연결해주기 위한 In coming Port를 알아보도록 하자. 위 프로젝트에서는 In coming Port의 네이밍을 OOOUseCase로 하였다. UseCase는 컨트롤러가 서비스에 접근하기 위한 인터페이스로 이전에는 직접 Service에 의존하던 것을 UseCase를 통해 접근하도록 변경하였다. 이는 객체지향 프로그래밍 설계 5원칙 (SOLID)에 의존성 역전 원칙(Dependecy Inversion Principle) 를 활용해 서로의 의존관계를 인터페이스를 통함으로 느슨한 결합을 만들어냈다.

 

 Out Going Port (Port) 

Persistence Adapter 와 비즈니스 로직을 연결해주기 위한 Out Going Port는 패키지 내에서 application/port/out 에 위치한 OOPort라는 이름을 가진 인터페이스들을 의미한다. In coming Port의 경우 외부에서 도메인을 호출하는 형태를 띠고 있었다면 Out Going Port는 내부에서 외부를 호출하기 위한 포트라고 생각하면 좋다. Out Going Port 역시 DIP를 활용하여 느슨한 결합을 만들고 있다.

 

Out Going Port에서는 ISP도 확인할 수 있다. 코드를 통해 확인해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RequiredArgsConstructor
@Component
public class ItemPersistenceAdapter implements CreateItemPort, ShowOneItemPort, ModifyItemPort,
    DeleteItemPort {
 
  private final ItemRepository itemRepository;
 
  @Override
  public Item saveItem(Item item) {
    return itemRepository.save(item);
  }
 
  @Override
  public Item modifyItem(Long itemId, ModifyItemDto modifyItemDto) {
    Item modifiedItem = itemRepository.findById(itemId).orElseThrow(RuntimeException::new);
    ItemModifyParams params = modifyItemDto.toDto();
    modifiedItem.modify(params);
    return modifiedItem;
  }
 
//...
}
 
 
cs

 

Persistence Adpater 에서는 각각의 Port를 구현하고 있다. 덕분에 각각의 서비스에서는 필요한 기능에만 접근할 수 있게 해 실수를 방지할 수 있다.

 

전체 구조

In Comming Port, Service, Out Going Port의 관계를 그리면 위와 같다. 웹 어댑터인 컨트롤러는 UseCase를 통해 Service에 접근하고 서비스에서 데이터베이스에 접근하기 위해 Out Going Port를 통해 Persistence Adapter를 접근한다. 이렇게 어댑터와 비즈니스 로직을 분리하고 그 과정에서 DIP, SRP, ISP 등 객체지향 설계 원칙을 적극적으로 활용하고 있다.

 

덕분에 가시성 좋은 패키지 구조, 책임이 확실한 클래스, 각 어댑터와의 느슨한 결합, 꼬이지 않은 의존관계의 코드를 자연스럽게 작성 가능한 아키텍처를 구현해 냈다.

 

 

 

💡 추후에는 해당 의존관계를 컴파일러를 통해 방지할 수 있도록 모듈을 통해 구분하는 방법을 추가해 나갈 필요가 있어보인다.

🔗 링크

👉 다음 시리즈

  • 미니 커머스 클라우드에 무식하게 배포해보기

📌 참조

댓글