[프로젝트] 3. 패키지 구조

공부하는 감자·2024년 1월 11일
0

F-Lab 프로젝트

목록 보기
3/11

들어가기 전에

헥사고날 아키텍처를 적용하여 패키지 구조를 정리해봤다.

아키텍처에 관해

아키텍처(Architecture)란?

시스템 구성과 동작원리 그리고 시스템의 구성환경 등을 설명하는 설계도이다.

  • 비즈니스 요구 사항을 만족하는 시스템을 구축하기 위해서 구조를 정의한 것.
  • 소프트웨어 응용 프로그램을 설계하기 위한 모델 또는 패턴을 의미

클린 아키텍처란?

'클린 코드'의 저자 로버트 마틴이 제안한, 의존성에서 벗어나 유지보수 및 확장이 용이하도록 설계된 아키텍처이다.

위 그림은 로버트 마틴의 블로그 The Clean Architecture에서 가져온 것이다.

화살표는 의존성 규칙이 향하는 방향으로, 바깥 쪽의 원이 안쪽의 원으로 향하고 있는 것을 확인할 수 있다.

객체 지향 설계에서도 중요한 것이 바로 '의존성'이다. 이 의존성 때문에 한 곳을 수정하면 다른 곳에도 영향이 가는(수정해야 하는) 상황이 들이닥친다.

예를 들어, A 프로그램에서 MySQL을 사용하고 있었는데 Oracle로 DB를 변경하기로 했다. 이때, DB를 변경하는 것이 내부 로직, 특히 도메인에 영향을 주지 않도록 한 것이 클린 아키텍처이다.

내가 이해한 대로 적었는데 이 설명이 맞는지는 모르겠다. '클린 아키텍처' 서적을 구입한 후 읽어보는 걸 추천한다.

헥사고날 아키텍처(Hexagonal Architecture)란?

  • 사전적 의미로는 "육각형 건축물"을 뜻한다.
  • 레이어 간의 원하지 않는 종속성이나 비즈니스 로직으로 인한 사용자 인터페이스 코드의 오염과 같은 객체지향 소프트웨어 설계의 알려진 구조적 함정을 피하기 위해 Alistair Cockburn에 의해 제안된 소프트웨어 아키텍처이다.
  • "포트 및 어댑터 아키텍처(Ports and Adapters Architecture)"라고도 불린다.

인터페이스나 기반 요소(infrastructure)의 변경에 영향을 받지 않는 핵심 코드를 만들고 이를 견고하게 관리하는 것이 목표이다.

즉, 이 구조의 핵심은 비즈니스 로직이 표현 로직이나 데이터 접근 로직에 의존하지 않는 것이다.

헥사고날 아키텍처의 장점

키워드: 유연성, 테스트 용이성, 유지보수성

  • 아키텍처 확장이 용이하다
  • SOLID 원칙을 쉽게 적용할 수 있다.
  • 모듈 일부를 배포하는 게 용이하다.
  • 테스트를 위해 모듈을 가짜로 바꿀 수 있으므로, 테스트가 더 안정적이고 쉽다.
  • 더 큰 비즈니스적 가치를 갖고 더 오래 지속되는 도메인 모델에 큰 관심을 둔다.
  • 도메인을 먼저 설계하므로 프레임워크, DB 등의 선택을 미룰 수 있다.

헥사고날 아키텍처의 단점

키워드: 구현 복잡성, 초반 개발 시간 증가

  • 포트와 어댑터를 구성하고 관리하는데 약간의 복잡성이 따른다.
  • 아키텍처를 처음 구축할 때 시간과 노력이 더 필요할 수 있다.

헥사고날 아키텍처의 구성

그림 출처: DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together

그림이 조금 어려울 수 있는데, 크게 안쪽 원인 내부(도메인)와 바깥쪽 원인 외부(인프라)로 구분된다.

  • 내부 영역: 순수한 비즈니스 로직을 표현하며 캡슐화된 영역이고 기능적 요구사항에 따라 먼저 설계

  • 외부 영역: 내부 영역에서 기술을 분리하여 구성한 영역이고 내부 영역 설계 이후 설계

핵심 비즈니스 로직은 중앙의 도메인 영역에 위치하며, 입력과 출력을 처리하는 포트와 어댑터를 통해 외부와 소통한다.

포트와 어댑터

포트는 내부 비즈니스 영역을 외부 영역에 노출한 API(인터페이스)이다. 인바운드(Inbound)/아웃바운드(Outbound) 포트로 구분한다.

  • 인바운드 포트: 내부 영역 사용을 위해 노출된 API
  • 아웃바운드 포트: 내부 영역이 외부 영역을 사용하기 위한 API

어댑터는 외부 세계와 포트 간 교환을 조정하고, 인바운드(Inbound)/아웃바운드(Outbound) 포트로 구분한다.

  • 인바운드 어댑터: 외부 애플리케이션/서비스와 내부 비즈니스 영역(인바운드 포트) 간 데이터 교환을 조정
  • 아웃바운드 포트: 내부 비즈니스 영역(아웃바운드 포트)와 외부 애플리케이션/서비스 간 데이터 교환을 조정

외부에서 요청해야 동작하는 포트와 어댑터를 주요소(primary)라고 하며, 포트와 어댑터에 따라 주포트 혹은 주어댑터라고 부른다.

애플리케이션이 호출하면 동작하는 포트와 어댑터를 부요소(seconddary)라고 하며, 부포트 또는 부어댑터라고 부른다.

포트와 어댑터 아키텍처 구성

아키텍처를 따르다 보면 자연스럽게 한 형태를 갖추게 된다.

  • application 패키지
    • 주로 유스케이스(UseCase)가 작성된 클래스를 포함한다.
    • 이 계층엔 업무 로직이 거의 없다.
    • domain의 여러 업무 로직을 조합하는 역할을 한다.
  • domain 패키지
    • 주로 업무 로직을 포함하는 클래스들이 들어선다.
  • infrastructure 패키지
    • 기반 요소, 즉 다른 서비스를 사용하는 어댑터를 포함한다.
    • 이곳에 포함되는 어댑터는 부어앱터로, Kafka나 Redis, MySQL 또는 다른 서비스의 API를 사용하는 구현체가 포함되는 패키지다.
  • interface 패키지
    • 클라이언트와 약속한 통신 방식의 어댑터를 포함한다.
    • 이곳에 포함되는 어댑터는 주어댑터로, MVC의 컨트롤러나 RPC 서비스 등이다.

나는 패키지 구조를 만들 때 interface 패키지를 따로 만들지 않고, infrastructure 패키지에 주 어댑터를 함께 넣도록 했다.

설계 시 고려할 점.

  1. 주어댑터는 애플리케이션을 일방적으로 알고 있다.

    • 컨트롤러(주어댑터)에서 request를 그대로 서비스(애플리케이션)의 파라미터로 넘기면, requestDto가 수정되면 서비스도 같이 수정되어야 된다.
    • 따라서, request를 서비스 계층에서 사용하는 Dto로 변경한 후 넘긴다.
  2. 부어댑터는 부포트를 준수해야 한다.

    • 다른 서비스를 호출하는 HTTP 인터페이스(부어댑터)가 있을 때, 연동한 서비스의 DTO를 바로 반환하게 되면(외부 서비스의 DTO를 반환) 외부 서비스에서 DTO의 규격을 변경하면 애플리케이션 영역에까지 영향을 미치게 된다.
    • 따라서 애플리케이션 계층에서 사용하는 DTO로 변경한 후 반환해야 한다.
  3. 인터페이스 자체를 어느 한 쪽에 치우치게 설계하지 말고, 도메인 관점에서 도메인이 필요로 하는 인터페이스를 설계해야 한다.

    • 위에서 언급한 주고받는 데이터의 형태뿐만이 아니라, Repository의 인터페이스가 MyBatis나 JPA에 의존하지 않도록 신경써야 한다.

패키지 구조

아래의 참고 사이트와 GitHub에서 많은 도움을 받았다.

계층 별 생성

먼저, 찾아보니 다들 Package by Component가 좋은 구조라고 이야기했다. 도메인 별로 패키지를 분리하고, 그 안에서 기능 별로 다시 패키지를 묶어주는 구조라고 이해했다.

실제로 실무에서도 이 구조대로 사용하고 있기도 하다.

그런데, 헥사고날 아키텍처는 각 계층, 특히 도메인 계층을 외부 계층과 분리하는 것이 목표인 구조이다.

즉, 계층 별로 분리하여 한 눈에 볼 수 있어야 한다고 생각했다. 따라서 크게 3개 패키지를 만들어봤다.

  • application: 애플리케이션 계층
  • domain: 도메인 계층
  • infrastructure: 외부 계층
    • 원래 interface 계층과 분리되어 있지만, 고민하다가 뭉뚱그려서 '외부 계층'으로 묶었다.
    • 원으로 보면 둘 다 바깥쪽 영역에 포함되므로 같이 엮어봐도 괜찮겠다고 생각했다.
    • 그런데 개발하다보면 패키지 구조가 복잡해질까? 그건 좀 고민된다.

애플리케이션 패키지

애플리케이션 계층은 도메인 계층과 외부 계층 사이에 위치하며, 각 계층의 의존성을 덜어내기 위해 존재한다.

도메인 계층과 외부 계층은 포트라는 인터페이스를 통해 서로 요청을 주고 받는데, 이 포트라는 인터페이스가 속한 계층이 바로 이 애플리케이션 계층이다.

따라서 위와 같이 외부 계층에서 요청이 들어오는 input 포트와 도메인 계층이 요청을 하는 output 포트로 다시 나누어, 그 안에 인터페이스를 배치했다.

찾아봤을 땐 보통 input 포트로 들어오는 걸 usecase라고 불렀다.

도메인 패키지

제일 중요한 패키지다. 핵심은 외부에 의존하지 않을 것. 특히 프레임워크에 종속되지 않도록 신경써야 한다.

라이브러리는 사용해도 된다고 해서 lombok 라이브러리만 우선 사용 중이다.

도메인 모델이 있는 것은 당연하고, 저 서비스 패키지는 위치를 많이 고민했다.

MemberService는 input 포트인 ~MemberUseCase 인터페이스의 구현체이다. 내부적으로는 합성(Composition)으로 외부 포트를 사용 중이기도 하다.

즉, 외부에서 들어온 요청을 처리하고 DB 작업이 필요한 경우 외부에 요청하기도 하는 역할을 수행한다.

그래서 '도메인에 영향을 주지 않고, 외부 계층과 내부 계층을 연결시켜주는' 역할이라고 생각해서 애플리케이션 계층에 넣으려고 했었다.

그런데 생각해보니, 여기서 처리하는 로직이 바로 비즈니스 로직이다.

  • 회원가입을 시켜주거나
  • 회원탈퇴를 시켜주거나
  • 회원정보를 보여주거나

또한 외부로 노출되는 것이 애플리케이션 계층의 포트이고, 그 안의 구현체는 감춰둬야 맞다.

따라서, 도메인 계층에 위치하는 것이 옳다고 생각하고 여기에 뒀다.

외부 패키지

제일 고민이 많은 패키지다. 앞서 말했듯, 사용자(클라이언트)에서 요청하는 부분은 interface 패키지로 따로 빼도 괜찮았다.

그런데 참고한 코드들에서도 두 계층을 합쳐뒀었고, input/output으로 같이 보는게 직관적이지 않을까 해서 합쳐보았다.

지금 생각해보면 기능이 많아질 수록 복잡해질 거 같아서, interface로 빼는게 나았을 거란 생각도 든다.

아무튼, 크게 어댑터와 설정 패키지로 나뉜다. config 패키지를 이곳에 둔 까닭은 스프링 빈 등록 등을 처리하는 설정 정보이기 때문에, 외부 종속성에 든다고 판단했기 때문이다.

어댑터 패키지는 다시 input 패키지와 output 패키지로 나뉜다.

input은 외부에서 들어오는 요청을 처리하는 패키지로, 나는 API 서버를 구축할 것이므로 Rest 폴더 아래에 주어댑터를 넣었다.

output은 외부로 나가는 요청을 처리하는 패키지로, 당연하게도 DB와 관련된 클래스과 부어댑터가 있다.

input과 output 안에는 mapper 패키지가 있는데, 여기서 말하는 mapper는 MVC 패턴에서 많이 봤던 DB와 매핑시켜주는 매퍼가 아니다.

외부 계층과 내부 계층(도메인)에서 사용하는 DTO는 각 계층에 맞게 변환시켜 주어야 한다.

즉, 외부 계층의 DTO를 내부 계층에서 사용하는 도메인 모델로 변경하고, 다시 내부 계층에서 사용하는 도메인 모델을 외부 계층의 DTO로 변경하여 각 계층의 의존성을 줄이기 위한 매퍼 클래스이다.

현재 구현체는 따로 만들지 않았는데, 많이 사용한다는 mapstruct 혹은 modelmapper 라이브러리를 사용할 생각이다.

아니면 경험삼아 스스로 만들어봐도 괜찮다고 생각한다.

수정

멘토링을 받으며 패키지 구조를 변경한 사유와 결과를 기입할 것이다.

Reference

프로젝트 구조 참고

https://github.com/rbailen/Hexagonal-Architecture

Hexagonal Architecture With Spring Boot | Code With Arho

Package by Component with Clean Modules in Java

Package by Layer vs Package by Feature

Package by Component and Architecturally-aligned Testing

헥사고날 아키텍처

헥사고날(Hexagonal) 아키텍처 in 메쉬코리아 :: MESH KOREA | VROONG 테크 블로그

지속 가능한 소프트웨어 설계 패턴: 포트와 어댑터 아키텍처 적용하기

헥사고날 아키텍처(Hexagonal Architecture) : 유연하고 확장 가능한 소프트웨어 디자인 🌟 feat. Java Example - 오픈소스컨설팅 테크블로그 %

지속 가능한 소프트웨어 설계 패턴: DDD + Hexagonal Architecture

주니어 개발자의 클린 아키텍처 맛보기 | 우아한형제들 기술블로그

멀티모듈 설계 이야기 with Spring, Gradle | 우아한형제들 기술블로그

Spring Boot Kotlin Multi Module로 구성해보는 헥사고날 아키텍처 | 우아한형제들 기술블로그

profile
책을 읽거나 강의를 들으며 공부한 내용을 정리합니다. 가끔 개발하는데 있었던 이슈도 올립니다.

0개의 댓글

관련 채용 정보