🥕 이번 당근 클론 프로젝트에서는 기존에 자주 사용하던 MVC 구조가 아닌 헥사고날 아키텍처를 적용해보려 한다. 따라서 먼저 배경 지식을 습득후 간단한 예제 코드를 통해 헥사고날 아키텍처에 대해 익숙해지려 한다.
헥사고날 아키텍처를 적용하려는 이유
이전의 협업 프로젝트에서 이런 일이 있었다.
여러 회의를 거쳐 프로젝트의 방향성을 정하고, 데이터를 추출한 뒤, 백엔드 팀은 데이터베이스 구조를 모두 설계하고 엔티티에 맞게 비즈니스 로직을 작성하여 4~5 개의 MVP 기능을 구현한 상태였다.
이후 PM과의 회의를 통해 새로운 기능을 추가하기 위해, 데이터베이스 테이블 구조의 변경이 필요하다는 것을 인지했다. 따라서 데이터베이스 테이블 구조를 다소 변경하게 되었다. 여기서 문제가 발생했다. 데이터는 거의 변함없이 테이블 구조만 변경하는 작업임에도 불구하고, 애플리케이션 전반에 걸친 소스코드 수정이 일어났다.
그땐 잘 몰랐지만 지금 다시 생각해보면, 비즈니스 로직이 엔티티와 아주 강하게 결합되어 있고, 의존하고 있었기 때문이었던 것 같다. 물론 비즈니스 로직에서 사용하는 데이터가 변경된다면, 애플리케이션 전반에 걸친 소스코드 수정이 당연히 필요하다고 생각하지만, 간단하게 테이블 구조를 변경하고 엔티티가 조금 수정되는 작업이 애플리케이션 전체에 영향을 끼친다는 것은 뭔가 크게 잘못되었다고 생각한다.
실제로 이러한 상황이 두어번 반복되자 요구사항을 만족하기 위한 간단한 데이터베이스 변경 작업에도 민감하게 반응하게 되었고, 비판적으로 생각하게 되었다. 우리가 설계를 잘못한 건 아닌가 라는 생각은 하지 않고 말이다.
사실 왜 애플리케이션 전체가 엔티티에 의존하게 되었는지는 클래스간의 의존성 다이어그램을 그려보면 쉽게 파악할 수 있다.
이번 프로젝트를 시작하기 앞서 위의 문제점을 다시 상기시켜 서칭해본 결과 도메인과 비즈니스 로직을 인터페이스를 사용하여 외부와 분리시키는 헥사고날 아키텍처를 알게되었다. 이론으로는 잘 와닿지 않으니 간단한 MVC 구조의 API 서버를 헥사고날 아키텍처로 마이그레이션 해보려 한다.
MVC 구조와 헥사고날 아키텍처 구조
헥사고날 아키텍처 구조의 빠른 이해를 위해 스프링부트 + MVC패턴으로 하나의 기능만 가지는 API서버를 헥사고날 아키텍처로 마이그레이션 해보려 한다.
상품을 등록하는 프로세스를 MVC 구조로 설계하면 아래 그림과 같다.
위의 그림 처럼 MVC 구조에서는 핵심 로직에 해당하는 Service 레이어와 각 계층의 클래스들이 서로 의존하는 꼴로 구성된다. 즉 각 계층에서 발생하는 변경이 다른 계층에 까지 전파될 수 있는 구조인 것이다.
// Controller는 자신이 호출하는 서비스가 무엇인지 알게된다.
// Service는 Product라는 엔티티와 강하게 의존한다.
@RestController
@RequiredArgsConstructor
public class ProductController {
public final ProductService productService
//...
}
@Service
public class ProductService {
public final ProductRepository productRepository
//...
}
@Repository
public interface ProductRepository implements JpaRepository<Product, Long> {
}
이렇게 MVC구조의 단점은 비즈니스 로직이 외부 요소들과 강하게 의존하고 있어, 변경에 취약하다.
헥사고날 아키텍처는 도메인을 외부요소와 분리시키고 인터페이스를 통해 연결하여, 외부 요소들이 도메인을 의존하는 구조를 가진다. 그렇다고 구조가 막 엄청나게 달라지진 않고 비즈니스 로직이 UseCase/Output과 같은 인터페이스를 의존하고 외부 요소들이 이를 구현하는 형태로 변경된다.
그림을 보면 알 수 있듯이 이제 비즈니스로직은 UseCase/Output Port라는 인터페이스 테두리가 생겼다.
이제 비즈니스 로직은 외부 요소를 전혀 신경쓰지 않고, 자기가 수행해야 할 로직을 UseCase와 Output Port 인터페이스를 통해 구현할 것이다. 그리고 외부 요소들은 로직 처리를 위해 UseCase 와 Output Port와 의존한다.
먼저 클래스 다이어그램을 살펴보면 아래와 같다.
이제 각 클래스의 역할과 코드를 살펴보자.
Domain - Product
- 비즈니스 로직에서 사용되는 애플리케이션의 도메인 영역이다.
- 도메인 내에서만 의존성이 생겨야 한다.
public class Product {
private final Long id;
private final String name;
private final String description;
}
Service(Application) - ProductService
- 도메인을 사용하여 비즈니스 로직을 처리하는 영역이다.
UseCase
를 구현하며OutputPort
를 사용한다.- 두 종류의 인터페이스와 의존하며 외부 영역과의 의존성을 완전히 끊는다.
@RequiredArgsConstructor
public class ProductService implements CreateProductUseCase, GetProductUseCase {
private final ProductOutputPort productOutputPort;
@Override
public Product createProduct(Product product) {
return productOutputPort.saveProduct(product);
}
@Override
public Product getProductById(Long id) {
return productOutputPort.getProductById(id).orElseThrow(() ->
new ProductNotFoundException("Product not found with id=" + id)
);
}
}
Primary / Driving Adapter - ProductRestAdapter
- Controller에 해당하는 클래스이다.
- 각 UseCase 인터페이스를 통해 사용자 요청을 처리한다.
@RestController
@RequiredArgsConstructor
public class ProductRestAdapter extends ProductEntity {
private final CreateProductUseCase createProductUseCase;
private final GetProductUseCase getProductUseCase;
@PostMapping("/products")
public ResponseEntity<ProductCreateResponse> createProduct(
@RequestBody @Valid ProductCreateRequest productCreateRequest) {...}
@GetMapping("/products/{id}")
public ResponseEntity<Object> getProduct(@PathVariable Long id) {...}
}
Secondary / Driven Adapter - ProductPersistenceAdapter
- Repository와 이를 사용하는 객체에 해당하는 클래스이다.
- OutputPort 를 구현하며 Service(Application)에 의해 런타임에 호출되어 동작한다.
@RequiredArgsConstructor
public class ProductPersistenceAdapter implements ProductOutputPort {
private final ProductRepository productRepository;
@Override
public Product saveProduct(Product product) {...}
@Override
public Optional<Product> getProductById(Long id) {...}
}
다음으로는 디렉터리 구조를 설명하고, 이를 멀티 모듈로 구성해 보려 한다.
'중고 거래 플랫폼 API 서버 개발' 카테고리의 다른 글
MySQL Full-Text Search를 통한 쿼리 개선 (0) | 2024.05.07 |
---|---|
헥사고날 아키텍처 Kafka + Event 적용 (0) | 2024.05.02 |
프로젝트 아키텍처와 의존성 방향 다이어그램 (0) | 2024.02.24 |
헥사고날 아키텍처: 싱글 모듈 → 멀티 모듈 (1) | 2024.02.09 |