기술 서적 정리/만들면서 배우는 헥사고날 아키텍처
01 왜 헥사고날 아키텍처인가
wampy
2023. 5. 1. 15:00
헥사고날 아키텍처 이해
- 애플리케이션이 UI나 데이터베이스 없이 동작하도록 만드다?
- 비즈니스 코드를 기술코드로부터 분리
- 비즈니스 코드가 어디에 존재해야 하는지
- 기술 문제로부터 격리되고 보호돼야 하는 위치가 어디인지
- 도메인 헥사곤 생성
- 도메인 헥사곤?
- 소프트웨어가 해결하기를 원하는 핵시미 문제를 설명하는 요소를 결합한다.
- 엔티티, 값 객체
- 엔티티 (Entity) : 식별자(identiy)를 할당할 수 있는 것
- 값 객체(Value Object) : 엔티티들을 합성하기 위해 사용하는 불변 컴포넌트
- 애플리케이션 헥사곤?
- 도메인 헥사곤에서 나오는 비즈니스 규칙을 사용, 처리하고 조정
- 비즈니스 측면과 기술 측면 사이에 있으며
- 양쪽과 상호작용하는 중개자 역할을 함
- 포트와 유스케이스를 이용
- 프레임워크 헥사곤
- 외부 인터페이스를 제공
- 애플리케이션 기능의 노출 방법을 결정할 수 있는 곳
- REST 엔드포인트 정의...... 데이터베이스... 메시지브로커.... 메커니즘 정의....
- 어댑터를 통해 기술 결정을 구체화함
도메인 헥사곤
- 실 세계 문제를 이해하고 모델링하는 활동을 나타냄
- e.g 네트워크 및 토폴로지 인벤토리를 생성하는 프로젝트...
- 목적은 네트워크를 구성하는 모든 리소스에 대한 포괄적인 뷰 제공..
- 도메인 헥사곤을 사용해 이러한 네트워크 및 토폴로지 요소를 코드로 식별 및 분류하고
- 연관시키는 데 필요한 지식을 모델링
엔티티
- 객체를 특징짓는 것 : 연속성, 정체성
- 객체의 수명주기 및 변경 가능한 특성과 관련 있다
- 연속성과 정체성을 통한 라우터의 특징 분석
- 상태가 활성 상태인지 비활성 상태인지 정의
- 다른 라우터나 네트워크 장비와 갖는 관계를 설명하는 일부 속성 할당 -> 수명주기를 가짐
- 모든 라우터는 인벤토리에서 고유해야함 -> 식별자를 가져야 함
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 enum RouterType {
EDGE, CORE;
}
애플리케이션 헥사곤
- 애플리케이션 특화 작업을 추상적으로 처리하는 곳
- 도메인 비즈니스 규칙에 기반한 소프트웨어 사용자의 의도와 기능을 표현
- ex. 같은 타입의 라우터들을 조회하는 방법?
- 데이터 처리가 필요
- 라우터 타입을 조회하기 위해 사용자 입력을 받아야 함 -> 입력 검증 필요 -> 비즈니스 규칙 사용
- 이 모튼 작업은 유스케이스에서 그룹화 할 수 있다
유스케이스, 입력포트, 출력포트는 애플리케이션에 포함되어 있음
유스케이스
- 도메인 제약사항을 지원하기 위해 시스템의 동작을
- 소프트웨어 영역 내에 존재하는 애플리케이션 특화 오퍼레이션을 통해 나타냄
- 엔티티 및 다른 유스케이스와 직접 상호작용하고 그것들을 유연한 컴포넌트로 만들 수 있음
- 소프트웨어가 할 수 있는 것을 표현하는 인터페이스로 정의된 추상화로 나타낸다
public interface RouterViewUseCase {
// 필터링된 라우터 리스트를 가져오는 오퍼레이션을 제공
List<Router> getRouters(Predicate<Router> filter); // 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);
}
}
출력 포트
- 유스케이스가 목표를 달성하기 위해 외부 리소스에서 데이터를 가져온다
- 유스케이스나 입력 포트가 오퍼레이션을 수행하기 위해 어떤 종류의 데이터를 외부에서 가져와야 하는지를
- 기술에 구애받지 않고 설명하는 인터페이스로 표현
public interface RouterViewOutputPort {
List<Router> fetchRouters();
}
프레임워크 헥사곤
- 소프트웨어와 통신하는 기술을 구현하는 영역
- 드라이버 관점 - 입력 어댑터
- 드리븐 관점 - 출력 어댑터
드라이빙 오퍼레이션과 입력 어댑터
- 드라이빙 오퍼레이션
- 소프트웨어 동작을 요청
- 외부 엔티티가 시스템과 상호작용하고 외부 엔티티의 요청을 내 도메인 애플리케이션으로 변환하는 방법으로 정의
- 외부 엔티티들이 시스템의 동작을 유도
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());
}
}
드라이빙 오퍼레이션과 출력 어댑터
- 외부에서 소프트웨어 요구사항을 충족시키는 데 필요한 데이터를 가져옴
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;
}
}