전공공부
[Hexagonal Architecture] 1장. 왜 헥사고날 아키텍쳐를 사용하는가? 본문
1. 소프트웨어 아키텍처 검토
헥사고날 아케텍처를 사용하므로써의 이점을 알아야 사용을 하는데 동기부여가 될 것이다. 우선 소프트웨어 아키텍처는 유지 보수가 잘 되어지는 형태로 짜여져야 기술적인 부채를 줄일 수 있다.
그저 동작하는 코드를 짜는 것은 쉽다. 하지만, 소프트웨어 업그레이드가 진행 될 때마다 코드를 다시 유지보수를 진행하고 이때, 아키텍처 구조가 제대로 짜이지 않은 코드는 어색하고 스파게티 코드가 되기 쉽다.
그리고, 대부분의 소프트웨어는 시간이 지날수록 더욱 변경 사항을 추가하고 변경하는 것이 어려워지고 또한, 프론트엔드나 UI가 없이 테스트 케이스를 돌릴 수 있는 코드를 만들기 위해서는 헥사고날 아키텍처가 필요하다고 한다.
2. 헥사고날 아키텍처의 이해
주된 아이디어중 하나는 비지니스 코드를 기술 코드로 부터 분리하여 관리해야 한다는 것이다. 비즈니스 코드를 분리하기 위해서는 첫번째로 도메인 헥사곤을 설계 하여야 한다. 도메인 헥사곤은 POJO의 형태로 만들어진 완전히 의존성을 배재 할 수 있는 형태로 만들어 질 수 있고 이는 Entity와 값 객체로 만들어진다고 한다.
Entity : ID 값을 두고 사용 할 수 있는 추상화된 객체
값 객체 : Entity를 합성하기 위해서 사용되는 상수 컴포넌트
비즈니스 규칙을 사용해서 가공하기 위해 필요한 헥사곤은 애플리케이션 헥사곤이다.
프레임워크 헥사곤은 외부 인터페이스를 제공한다.
2 - 1) 도메인 헥사곤
실 세계 문제를 이해하고 모델링한 객체를 만들어 두는 곳이다. 일반적으로 DTO 또는 VO 또는 Entity로 사용하는 곳으로 생각되어진다.
엔티티
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class Router {
private final RouterType routerType;
private final RouterId routerId;
public Router(RouterType routerType, RouterId routerId) {
this.routerType = routerType;
this.routerId = routerId;
}
public static Predicate<Router> filterRouterByType(RouterType routerType){
return routerType.equals(RouterType.CORE)
? isCore() :
isEdge();
}
private static Predicate<Router> isCore(){
return p -> p.getRouterType() == RouterType.CORE;
}
private static Predicate<Router> isEdge(){
return p -> p.getRouterType() == RouterType.EDGE;
}
public static List<Router> retrieveRouter(List<Router> routers, Predicate<Router> predicate){
return routers.stream()
.filter(predicate)
.collect(Collectors.<Router>toList());
}
public RouterType getRouterType(){
return routerType;
}
@Override
public String toString(){
return "Router{" +
"routerType=" + routerType +
", routerId=" + routerId +
'}';
}
}
public class RouterId {
private String id;
private RouterId(String id){
this.id = id;
}
public static RouterId of(String id){
return new RouterId(id);
}
@Override
public String toString() {
return "RouterId{" +
"id='" + id + '\'' +
'}';
}
}
public enum RouterType {
EDGE,
CORE;
}
값 코드는 엔티티의 예상치 않은 변경을 막는 역할을 한다. Enum과 같이
2 - 2) 애플리케이션 헥사곤
사용자 입력에 따라서 필터링 되는 네트워크 라우터를 생각하고 아래 예제를 보자
UseCase
- 필터링된 라우터 리스트를 보여주는 유스케이스를 보여줌
import dev.davivieira.domain.Router;
import java.util.List;
import java.util.function.Predicate;
public interface RouterViewUseCase {
List<Router> getRouters(Predicate<Router> filter);
}
Input Port
- UseCase는 소프트웨어의 일을 설명하기 위한 인터페이스 일 뿐 실제 구현체는 Input Port가 가지고 있다. 사용자는 비즈니스 로직만을 보고 이해 할 수 있고 실제 구현체를 각 포트에서 만들었다. 아래와 같이 라우팅을 진행한다.
import dev.davivieira.application.ports.output.RouterViewOutputPort;
import dev.davivieira.application.usecases.RouterViewUseCase;
import dev.davivieira.domain.Router;
import static dev.davivieira.domain.Router.retrieveRouter;
import java.util.List;
import java.util.function.Predicate;
public class RouterViewInputPort implements RouterViewUseCase {
private RouterViewOutputPort routerListOutputPort;
public RouterViewInputPort(RouterViewOutputPort routerViewOutputPort) {
this.routerListOutputPort = routerViewOutputPort;
}
@Override
public List<Router> getRouters(Predicate<Router> filter) {
var routers = routerListOutputPort.fetchRouters();
return Router.retrieveRouter(routers, filter);
}
}
OutputPort
- 외부 리소스를 데이터를 가져오는 곳으로 동작을 정의한 곳으로 우리는 특정 DB를 쓰는지 상관하지 않고 출력 어뎁터에 해당 역할을 할당해서 수행한다.
import dev.davivieira.domain.Router;
import java.util.List;
public interface RouterViewOutputPort {
List<Router> fetchRouters();
}
2 - 3) 프레임워크 헥사곤
드라이빙 오퍼레이션
- 소프트웨어의 동작을 요청하는 곳으로 일반적인 MVC 구조에서의 Controller 단이 될 수 있다. 이러한 구축은 상단 Adapter의 API 요청으로 이루어진다. 일반적인 프론트엔드 요청이 아닌 다른 애플리케이션의 요청이 될 수도 있다.
REST , gRPC, stdin 등등...
입력 어뎁터
- 아래 예제는 stdin에서 입력 데이터를 가져오는 방법이지만 Rest 또는 gRPC 통신을 통해서 들어오려면 다른 Adapter를 두면 된다.
import dev.davivieira.application.ports.input.RouterViewInputPort;
import dev.davivieira.application.usecases.RouterViewUseCase;
import dev.davivieira.domain.Router;
import dev.davivieira.domain.RouterType;
import dev.davivieira.framework.adapters.output.file.RouterViewFileAdapter;
import java.util.List;
public class RouterViewCLIAdapter {
RouterViewUseCase routerViewUseCase;
public RouterViewCLIAdapter(){
setAdapters();
}
public List<Router> obtainRelatedRouters(String type) {
return routerViewUseCase.getRouters(
Router.filterRouterByType(RouterType.valueOf(type)));
}
private void setAdapters(){
this.routerViewUseCase = new RouterViewInputPort(RouterViewFileAdapter.getInstance());
}
}
드리븐 오퍼레이션
- 이미 애플리케이션에서 트리거 된 요청을 처리하는 곳으로 생각하자 그리고, 해당 어뎁터의 출력 포트와 일치해야 한다.
출력 어뎁터
- DBMS , File , SMTP 등 외부 프레임워크와 연결되는 곳이다. 이곳은 출력 포트의 구현체로 작동하며 실제 RDBMS 를 쓰다가 No SQL으로 전환시 바뀔 수 있는 부분이다. 또는 어뎁터가 추가되면서 도메인은 그대로 두고 변경이 일어나는 곳이다.
import dev.davivieira.application.ports.output.RouterViewOutputPort;
import dev.davivieira.domain.Router;
import dev.davivieira.domain.RouterId;
import dev.davivieira.domain.RouterType;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class RouterViewFileAdapter implements RouterViewOutputPort {
private static RouterViewFileAdapter instance;
@Override
public List<Router> fetchRouters() {
return readFileAsString();
}
private static List<Router> readFileAsString() {
List<Router> routers = new ArrayList<>();
try (Stream<String> stream = new BufferedReader(
new InputStreamReader(
RouterViewFileAdapter.class.getClassLoader().
getResourceAsStream("routers.txt"))).lines()) {
stream.forEach(line ->{
String[] routerEntry = line.split(";");
var id = routerEntry[0];
var type = routerEntry[1];
Router router = new Router(RouterType.valueOf(type),RouterId.of(id));
routers.add(router);
});
} catch (Exception e){
e.printStackTrace();
}
return routers;
}
private RouterViewFileAdapter() {
}
public static RouterViewFileAdapter getInstance() {
if (instance == null) {
instance = new RouterViewFileAdapter();
}
return instance;
}
}
이렇게 보면 MVC 구조와 달리 변경시 쉽게 변경을 제어 할 수 있을 것 같다. 이전에는 Service 단에서 DB 및 프로토콜 수정이 일어나면 엄청나게 바뀌던 것과는 달리 어뎁터를 제어하면서 바꿀 수 있을 것이다.
확실히 변경에 대해서 유연하고 유지 보수성이 높고 테스트 용이성이 높아졌다.
생각
이미 기존에 운영하던 시스템을 헥사고날로 변경 및 적용하며 책을 읽었기 때문에 이해가 빨랐을지도 모르겠다.
확실히 코드양은 늘었지만 코드단을 PM분들이 이해하기 쉬운 코드로 작성 할 수 있었고 새로운 프레임워크가 붙거나 API가 붙어도 대응이 용이해졌다는 것은 분명한 사실이다.
코드 출저 : (https://github.com/wikibook/dhaj/blob/main/Chapter01)
책 : 만들면서 배우는 헥사고날 아키텍처 설계와 구현
'Study > Spring Boot' 카테고리의 다른 글
[Webflux] Webflux, MVC, Virtual Thread...? (0) | 2024.01.27 |
---|---|
[Spring] Hexagonal Arichtecture - MVC 패턴과 비교하다. (0) | 2024.01.21 |
Mockito를 사용한 단위 테스트에서 발생하는 다양한 이슈 (0) | 2024.01.20 |
[Spring] PSA(Portable Service Abstraction) + DI(Dependency Injection) (0) | 2024.01.19 |
Spring Batch를 이용한 대량의 insert 최적화 (0) | 2023.01.03 |