헥사고날 아키텍처 도입 후

밀크야살빼자·2024년 4월 4일
0

기존 코드의 구조는 계층형 아키텍처를 따르며 핵심 로직이 도메인과 서비스에 혼합되어 있어 유지보수가 어려웠습니다. 객체 간의 의존성이 너무 강하여 코드를 변경하거나 확장하기 어려워서 새로운 기능을 추가하거나 버그를 수정하는 작업이 매우 어려웠습니다. 또한, 테스트 코드를 작성하면서 코드의 일부를 리팩토링하려고 했지만 의존성이 너무 복잡하여 원하는 대로 코드를 변경하기가 어려웠습니다.

이러한 문제를 해결하기 위해 객체 간의 책임을 명확히 분리하고 유연성을 향상시키기 위해 추상화를 도입하기로 결정했습니다. 기존의 계층형 아키텍처의 인터페이스를 통한 의존성 분리는 일부 도움이 되었지만, 객체 간의 작은 책임 분배에는 적합하지 않았습니다. 따라서 헥사고날 아키텍처를 도입하여 객체의 책임을 명확히 분리하고, 모듈화를 위해 상속 및 인터페이스를 사용했습니다. 헥사고날 아키텍처는 특정 기술의 의존성을 port-adapter를 통해 강제함으로써 모듈 간의 독립성을 유지하고 의존성을 최소화하여 느슨한 결합을 합니다.

헥사고날 아키텍처

헥사고날 아키텍처의 장점

카카오 소셜 로그인은 구글, 네이버 등 다른 소셜 로그인으로 변경 가능성이 있습니다. 계층형 아키텍처에서는 KakaoLoginService 클래스에서 소셜 로그인을 하기위해 외부 API를 호출하는 코드에 대한 의존성을 가질 수 있습니다. 이렇게 되면, core에 kakaoLoginService는 변경 가능성이 있는 외부 기술에 의존하게 됩니다.

예를 들어, 카카오 소셜 로그인에서 네이버 소셜 로그인으로의 의존성 변경이나 JPA가 아닌 다른 Repository로의 변경과 같은 경우, 이러한 변경들은 서비스 내의 코드 수정을 필요로 합니다.

그러나 헥사고날 아키텍처에서는 OutputPort 인터페이스를 통해 외부 시스템과의 의존성을 분리합니다. 이렇게 함으로써, 외부 소셜 로그인 API를 호출하는 로직은 KakaoLoginClient에서 처리되며, Output 인터페이스를 통해 로그인을 수행합니다.

계층형 아키텍처에서는 소셜 로그인 변경시 서비스단을 수정했어야 했지만, 헥사고날 아키텍처에서는 OutputPort 인터페이스를 구현한 새로운 어댑터를 만들어 주입합니다.

이렇게 헥사고날 아키텍처는 외부 시스템과의 연결에서 유연성과 확장성을 제공합니다.

core에 JPA를 의존하여 변경 감지를 사용했습니다.

헥사고날 아키텍처에서는 도메인이 POJO이며, 핵심 로직을 담은 core 모듈 내부에 Java 이외의 의존성을 배제하므로, Spring과 같은 외부 프레임워크에 대한 의존을 core에서 배제하는 것이 원칙입니다. 따라서 영속성과 관련된 부분은 port와 adapter를 통해 분리됩니다. 이렇게 되면 도메인을 비즈니스 로직에만 집중할 수 있는 형태로 만들 수 있는 가장 큰 장점입니다. 따라서 헥사고날에서는 도메인은 더 이상 JPA Entity가 아니므로 변경 감지 기능을 사용할 수 없으며, 대신 OutputPort를 통해 DB에 update 쿼리를 실행해야 합니다.

  • 적용 전

    public class PostService {
    		private final PostRepository postRepository;
    	
    		@Transactional
    		public void updateTitle(Long memberId, UpdatePostTitleRequest updatePostTitleRequest) {
    			Post post = postRepository.findPost(updatePostTitleRequest.getPostId());
    			post.updateTitle(updatePostTitleRerquest.getTitle());
    		} 
    }
    		```
    
  • 적용 후

    @Service
    public class PostService implements PostUseCase {
    		private final UpdatePostPort updatePostPort;
    private final LoadPostPort loadPostPort;
    	
    		@Override
    		@Transactional
    		public void updateTitle(Long memberId, UpdatePostTitleRequest updatePostTitleRequest) {
    			Post post = LoadPostPort.findPost(updatePostTitleRequest.getPostId());
    			updatePostPort.updateTitle(memberId, updatePostTitleRerquest.getTitle());
    		}
    }

도메인과 JPA Entity를 분리에 대한 결정을 내리기 위해 고민했습니다. 이러한 결정을 내리기 위해 두 가지 상황을 고려했습니다.
@Transactional, Spring Event 등의 기능들을 직접 Java로 구현하고 프레임워크 변경을 대비하고, DB를 접근해야 할 때마다 매퍼를 통해 엔티티로 변환하고 접근 후 응답이 필요하다면 도메인으로 변환할지 아니면 스프링의 이점을 챙기고 변경 가능성을 감수할지를 고민했습니다. 결론적으로 변경 가능성을 정확히 예측하기 어려워도 JPA의 Lazy Loading과 Dirty Checking이 제공하는 장점이 상당히 크며, 스프링에서 다른 프레임워크로의 변경 가능성이 낮다고 판단했습니다. 따라서 낮은 변경 가능성을 감수하고 스프링을 사용하는 것이 이 프로젝트에서 적합하다고 판단하여 결정했습니다.

입력 유효성 검증 위치

헥사고날 아키텍처에서는 도메인 객체와 영속성 객체를 분리하기 때문에 입력 유효성 검증을 도메인 로직으로 생각하지 않아 입력에 대한 검증을 도메인 로직이 아닌 application의 port.in의 매개변수인 service의 request 객체에서 수행합니다. 이로써 도메인에서는 입력 값에 대한 유효성 검증을 하지 않으므로, 도메인 생성 시 사용되는 값에 대한 무결성 보장이 보장되지 않습니다.

일반적으로 비즈니스 로직을 수행하기 위해 service에서 필요한 값과 클라이언트에서 받아온 값이 동일합니다. 따라서 controller에서 @RequestBody로 사용하는 파라미터 객체와 Service의 입력 객체를 동일한 객체로 사용한다면 Bean Validation과 @Valid를 사용하여 검증했습니다. 이로써 ArgumentResolver에 의해 검증 로직이 실행되므로 유효성 검증이 Spring에 의존적이게 됩니다.

profile
기록기록기록기록기록

0개의 댓글