헥사고날 아키텍처를 적용하고

공병주(Chris)·2023년 10월 3일
1

해당 글은 헥사고날을 소개하는 글이 아니며, 도서-만들면서 배우는 클린 아키텍처에 기반해 헥사고ㅎ날 아키텍처를 적용한 후의 개인적인 생각에 대한 글입니다. 반대되는 의견은 언제든 환영입니다! 이런 이야기를 더 많이 나눌 수 있으면 좋겠습니다.

in-adapter는 Spring을 사용했습니다.

1. 헥사고날의 가장 큰 장점 : 계층형 아키텍처의 DB 중심 설계에서 벗어남

사실, DB 중심 설계의 원인은 계층형 아키텍처와 ORM을 사용하는 것입니다. 보통, 해당 방식에서는 대부분 Domain을 JPA Entity로 사용합니다. 해당 방식에서는, 도메인 로직에서는 필요하지 않은 연관관계이지만, DB 연관관계에 의해 도메인에서도 JPA의 @ManyToOne, @OneToOne 어노테이션과 함께 연관 관계를 맺는 경우가 많습니다. 이것이 가장 큰 계층형 아키텍처 + ORM의 단점이죠.

헥사고날의 가장 큰 장점 중 하나는 헥사고날 아키텍처에서 도메인은 POJO이며, DB 연관관계를 위한 불필요한 의존이나 필드가 존재하지 않는다는 것입니다. 도메인에는 JPA 연관관계 편의 메서드 등이 존재하지 않고, 도메인을 비지니스 로직에만 집중하는 형태로 가져갈 수 있습니다.

2. port-adapter 패턴에 대해

아래의 사항은 in port-adapter가 아닌 out port-adapter에만 해당하는 사항입니다.

사실, 기존의 계층형 아키텍처에서도 특정 기술에 대한 의존성을 interface를 통해 분리 가능합니다. 하지만, 인터페이스로 분리하지 않은 후, 강결합 된 부분의 변경으로 인한 큰 사이드 이펙트를 맞이하는 경우를 많이 봄

헥사고날 아키텍처는 특정 기술의 의존성을 port-adapter를 통해 강제합니다. 이는 다르게 말해, 느슨한 결합을 강제하는 것이라 할 수 있습니다.

변경 가능성이 낮은 기술은?

변경 가능성이 상당히 낮은 특정 기술에 대해 port-adapter 적용하는 것이 불필요하다고 생각할 수 있습니다.

하지만, 변경 가능성이라는 것은 완벽하게 예상 불가합니다.

그리고, 좋은 설계라함은, 테스트하기 어려운 외부 시스템 통신 등 어려운 부분도 모킹 라이브러리 없이 쉽게 테스트할 수 있어야 한다고 생각. 테스트를 잘 하기 위한 프로덕션 코드를 짜자는 것이 아니라, 좋은 코드와 설계는 테스트하기도 쉽다라는 관점입니다.

3. in port-adapter에 대해

헥사고날에서 in-adapter와 service 사이에 in-port가 존재합니다. 저는 이 in-port에 존재에 대한 의구심을 가졌습니다.

우선, in-adapter가 core를 의존하기 때문에 in-adapter 변경에 의한 core의 변경은 없을 것이라 생각합니다. 만약, in-adapter의 변경에 따라 core가 변경된다면 이는 좋지 않다고 생각합니다. core가 갑이고 in-adapter가 을이라고 생각합니다. 따라서, core는 변경하지 않고 변경은 in-adapter로 제한해야함

따라서, in adapter와 core 사이에 port를 두는게 큰 의미가 있는지에 대한 의문. port-adapter의 개수만 늘어나는 것일 수도 있고 영역을 나누는 의미 밖에 없다고 생각합니다.

4. core 내부에 Spring 의존성을 준 이유

헥사고날 아키텍처는 core 내부에 java 이외의 의존성을 배제합니다. 따라서, core의 Spring 의존 배제가 원칙입니다.

하지만, 아래와 같은 이유로 core 내부에 Spring의 의존이 존재하도록 결정했습니다.

Spring에서 다른 framework로 변경될 여지가 거의 없다고 생각

위에서 변경 가능성이라는 것은 예상 불가라고 했습니다. 하지만, Spring은 우리에게 많은 생산성과 편리성의 이점을 줍니다. 따라서, 아래 2개의 trade-off를 고려해보았습니다.

  • @Transactional, Spring Event 등의 해당 기능들을 모두 java로 직접 구현하고 framework의 변경을 대비
  • Spring의 변경 가능성을 감수하고 Spring이 주는 이점을 채택

결론적으로, Spring의 변경 가능성을 고려해보았을 때, 낮은 변경 가능성을 감수하고 Spring을 사용하는 것이 좋다고 생각했습니다.

5. 입력 유효성 검증의 위치

헥사고날에서는 입력 유효성 검증을 도메인 로직으로 생각하지 않고, 아래 처럼 입력 유효성 검증을 core 내부 in-port의 매개변수(Service의 Request) 객체에서 진행합니다.

package dandi.dandi.member.application.port.in; // service의 in port

public class LocationUpdateCommand extends SelfValidating<LocationUpdateCommand> {

    private static final String INVALID_LOCATION_EXCEPTION_MESSAGE =
            "위도, 경도 값이 존재하지 않거나 범위가 잘못되었고 지역 문자열이 존재하지 않습니다.";

    @NotNull
    @DecimalMin(value = "-90.0")
    @DecimalMax(value = "90.0")
    private final Double latitude;

    @NotNull
    @DecimalMin(value = "-180.0")
    @DecimalMax(value = "180.0")
    private final Double longitude;

    @NotBlank
    private final String district;

    public LocationUpdateCommand(Double latitude, Double longitude, String district) {
        this.latitude = latitude;
        this.longitude = longitude;
        this.district = district;
        this.validateSelf(INVALID_LOCATION_EXCEPTION_MESSAGE);
    }
		
		// getter
}

도메인의 무결성

위 방식대로라면, 도메인에서는 입력 값에 대한 유효성 검증을 하지 않기 때문에, 도메인 생성시 사용되는 값에 대한 무결성 보장이 되지 않는다고 생각합니다.

위와 같이 service의 입력 객체에서 유효성 검증 로직을 진행하는 이유는, 도메인 내부에 입력 유효성 로직이 들어간다면, 도메인 로직에 대한 집중도가 떨어진다는 이유때문입니다.

하지만, 이는 별도로 선언한 입력 유효성 검증 객체를 도메인 내부에서 사용하면 된다고 생각합니다.

유효성 검증 여부

또한, 다른 문제가 생길 수도 있습니다. 보통, 비즈니스 로직을 수행하기 위해 Service에서 필요한 값과 Client에서 받아온 값이 동일합니다. 따라서, Controller의 @RequestBody 로 사용하는 파라미터 객체와 Service의 입력 객체를 동일한 하나의 객체를 사용한다면 아래와 같은 입력 객체의 형태가 나올 것입니다. 여기에 검증 로직을 추가할테죠.

package dandi.dandi.member.application.port.in; // service의 in port

public class LocationUpdateCommand {

    private static final String INVALID_LOCATION_EXCEPTION_MESSAGE =
            "위도, 경도 값이 존재하지 않거나 범위가 잘못되었고 지역 문자열이 존재하지 않습니다.";

    @NotNull
    @DecimalMin(value = "-90.0")
    @DecimalMax(value = "90.0")
    private final Double latitude;

    @NotNull
    @DecimalMin(value = "-180.0")
    @DecimalMax(value = "180.0")
    private final Double longitude;

    @NotBlank
    private final String district;

    public LocationUpdateCommand() {
    }
		
		// getter
}

위 Service 객체는 core 내부의 in-port에 선언했습니다. 하지만, 해당 객체를 Spring의 @RequestBody로 사용하고 @Valid를 통해 검증하도록 한다면, 유효성 검증 여부는 Spring에 의존적입니다. 해당 방식에서는, ArgumentResolver에 의해서 검증 로직이 실행되기 때문이죠.

0개의 댓글