BackEnd/쿠링

[쿠링] 헥사고날 아키텍처를 향하여 (By TDD)

샤아이인 2024. 2. 2.

 

 

이번 글을 통하여 기존의 쿠링의 계층형 아키택처를 Hexagonal Architecture로 리팩토링 해 나가려 한다.

그럼 기존에 어떠한 점이 불편했기에 이러한 선택을 하게 되었을까? 기존의 문제점부터 한번 살펴보자.

 

1. 기존 아키텍처의 문제점

1-1) 데이터 중심의 설계?

우선 가장 첫 문제점은 우리의 앱이 어느순간부터 데이터베이스 중심적으로 설계가 진행되고 있었다는 점이다.

사용자를 위한 애플리케이션이라면, 해당 문제를 해결할 도메인 로직이 중요한데...

정작 이점은 고려하지 못하고 구현된 아키텍처였다.

 

다음 글은 내가 이에 대하여 좀더 설명해 둔 글이기에, 자세한 설명은 다음 글을 읽어봐 주시길!

https://blogshine.tistory.com/688

 

계층형 아키텍처는 왜 데이터베이스 중심의 설계를 유도할까?

1. 계층형 아키텍처가 어때서? 마틴 파울러의 책, PoEAA (Pattern of Enterprise Application Architecture: 엔터프라이즈 애플리케이션 아키텍처 패턴)을 보면 대표적인 3 계층을 소개하는 파트가 있다. 이름도

blogshine.tistory.com

 

1-2) 도대체 어느 패키지에 두어야 할까?

항상 새로운 class를 구현할 때 든 생각은 어디에 이 녀석을 두어야 할까?이다.

어디에 두어야 나 이외의 사람도 원하는 대상을 손쉽게 찾아갈 수 있을까?

어디에 두어야 나 또한 나중에 봐도 손쉽게 찾아갈 수 있을까?

어디에 두어야 우리의 아키텍처에 의존성이 꼬이거나 순환되지 않고 깨끗해질까?

 

이 모든 질문이 항상 너무나도 큰 고민이었으며, 나 또한 명확한 기준이 없었던 것 같다.

이번 Hexagonal Architecture를 통하여 이에 대한 기준점을 만들려 하였다.

적어도 유명한 글들과 사례들이 있으니 적응하기 더 수월하였던것 같다.

 

1-3) 그 유명한 Clean Architecture

유명한 클린아키텍처 책을 읽어본 적이 있어 다양한 좋은 설계기법과, 객체지향적 사고, SOLID 등 좋은 내용을 알고는 있지만 정작 내 프로젝트에는 이를 녹여내지 못하고 있었던 것 같다...

 

지식을 알고만 있으면 뭐 하겠는가.... 내 코드에서 그런 좋은 내용들을 찾아볼 수 없다면...

 

다행히 요즘 Clean Architecture를 위한 여러 설계 방법 중 Hexagonal Architecture가 유행을 이끌고 있기 때문에 부담 없이 적용해 봐야겠다는 생각이 바로 들었다.

Hexagonal Architecture의 경우 주어진 규칙을 따라서 일단 구현하면 자동적으로 Clean Architecture에 가까워진다는 장점이 있기 때문이다!

 

2. ArchUnit을 통한 아키텍처 TDD

맨 처음 시작 전에 든 생각은, 

 

내가 만든 아키텍처가 Hexagonal Architecture 인지 어떻게 검증하지?

 

였다.

 

다행히 주변 지인이 예전에 말해주었던 ArchUnit이 떠올랐으며, 이를 통하여 Hexagonal Architecture를 검증하고 강제화 할 수 있다 생각하게 되었다! 또한 다음 Naver d2의 글이 떠올랐었다.

 

https://d2.naver.com/helloworld/9222129

https://kth990303.tistory.com/455

 

[JUnit5] Archunit 라이브러리를 이용한 아키텍처 테스트

간혹 통합테스트를 작성하다보면 아래와 같은 생각이 들곤 한다. "비즈니스 로직이 의도한대로 작동되는지 테스트는 할 수 있겠어. 하지만 로직 외에 아키텍처 의존성을 테스트할 수는 없을까?"

kth990303.tistory.com

ArchUnit은 간단히 말하면 아키텍처의 의존성 방향이나, 네이밍 룰 등을 검증할 수 있는 테스트 도구이다.

즉, 다음과 같은 경우에 적합하다!

  • 클래스명이 정해둔 컨벤션에 맞는지
  • 아키텍처의 패키지간 의존성이 올바른지 

따라서, 지금의 나에게 매우 적절히 필요한 도구라 바로 도입하게 되었다.

 

또한 이렇게 먼저 큰 규칙을 만든다는 점에서 단순 code level이 아닌, Architecture 수준에서 TDD를 적용해 볼 수 있는 좋은 기회라 생각되었다.

 

우선 User 도메인을 하나 예시로 들면, 다음과 같이 구현해 볼 수 있다.

@DisplayName("헥사고갈 아키텍처 검증")
class DependencyRuleTests {

	@DisplayName("User 아키텍처 검증")
	@Test
	void validateUserArchitecture() {
		HexagonalArchitecture.boundedContext("com.kustacks.kuring.user")

			.withDomainLayer("domain")

			.withAdaptersLayer("adapter")
			.incoming("in.web")
			.outgoing("out.persistence")
			.outgoing("out.event")
			.and()

			.withApplicationLayer("application")
			.services("service")
			.incomingPorts("port.in")
			.outgoingPorts("port.out")
			.and()

			.withConfiguration("configuration")
			.check(new ClassFileImporter()
				.importPackages("com.kustacks.kuring.user.."));
	}
}

 

한눈에 보이듯, boundedContext를 시작으로 domain, application, adapter 등을 검사하며, 의존성의 방향을 확인할 수 있었다.

port and adapter 아키텍처라 불리는 만큼 incoming과 outgoing 패키지를 정확하게 지정해 줄 수 있다.

이렇게 의존성의 방향을 지정하고, 존재해야 하는 패키지의 명칭들을 설정해 주면 된다.

 

추가로, static class인 HexagonalArchitecture는 인터넷에서 조금 찾아보니 나와 이를 활용하여 적용해 볼 수 있었다.

 

3. Architecture의 변화

우선 기존 User 도메인의 아키텍처는 다음과 같았다.

User
├── business
├── common
│   └── dto
├── domain
├── facade
└── presentation

 

의존성의 방향이 presentation -> facade -> business -> domain의 방향으로 이루어졌는데 방향성 자체가 큰 문제가 되지는 않았다.

다만 몇 가지 문제점이 있는데...

  1. common같이 공통으로 사용하는 패키지가 갈수록 커지고, 용도를 모르게 되었다.
  2. 이는 맨 위에서 말했듯 DB 중심적인 설계로 나아가고 있었다.

이를 HexagonalArchitecture로 변경하면 다음과 같다.

User
├── adapter
│   ├── in
│   │   └── web
│   │       └── dto
│   └── out
│       ├── event
│       └── persistence
├── application
│   ├── port
│   │   ├── in
│   │   │   └── dto
│   │   └── out
│   │       └── dto
│   └── service
└── domain

이렇게 변경된 쿠링의 HexagonalArchitecture은 어떠한 장점이 생기게 될까? 제가 이번에 만들면서 느낀 3가지는 다음과 같습니다.

  1. 유연성, 유지보수성: 외부 시스템이나 인프라와의 의존성을 낮추어, 구성 요소를 쉽게 교체하거나 업데이트할 수 있게 되었습니다. 그도 그럴 것이 application의 service들은 모두 인터페이스에 해당되는 port에 의존하게 되었습니다. 더 이상 실 구현체가 아니기 때문에 중간에 다른 구현채로 변경되어도 유연하게 대응할 수 있는 장점을 갖게 되었죠! 이는 곧 유지보수성과도 직결된다 생각되더라고요!
  2. 테스트 용이성: 비즈니스 로직을 독립적으로 테스트할 수 있어 품질 향상과 개발 속도 향상에 도움이 됩니다! 인터페이스를 적절하게 사용하였기에 해당 로직의 독립성을 유지할 수 있던 점이 매우 장점이 돼준 것 같습니다.
  3. 팀원과의 협업: 책임이 분리되어 있어, 코드의 이해와 수정이 용이하며, 변화에 빠르게 대응할 수 있습니다. 즉, 흔하게 말하는 SOLID가 모두 지켜지고 있는 좋은 아키텍처 구조입니다. 또한 제2의 멤버가 들어와서 유지보수를 하거나 개편해야 해도 HexagonalArchitecture 자체의 이해도만 있다면 충분히 빠른 적응이 가능하다 생각됩니다.

위 아키텍처를 그림으로 살펴보면 다음과 같습니다.

출처 - https://devocean.sk.com/blog/techBoardDetail.do?ID=165581&boardType=techBlog

 

아마 유일한 단점으로 느껴진 점이 있다면, 팀원 또한 HexagonalArchitecture에 대한 이해도가 있어야 한다는 점? 인 것 같은데...

이거는 뭐 공부하면 되는 거라 크게 문제 될 것은 아니라 생각됩니다.

 

4. 경계 간 모델 매핑 전략은?

4-1) 계층 간 매핑전략

각 계층 간의 모델을 매핑하는 것에 대해서 조금 생각해볼 필요가 있었다.

 

나는 서로 다른 계층간의 매핑(일종의 변환을 통한 데이터 전달 방식의 규칙?)찬성하는 쪽에 해당됐다.

두 계층 간에 매핑을 하지 않는다면, 서로 다른 계층이 동일한 모델을 통해 데이터를 주고받을 탠디 이는 너무 강한 결합이 생기게 되는 주원인이라 생각되었다.

 

다만 이렇게 모두 매핑을 해버리면 약간은 보일러플레이트처럼 너무 동일한 느낌이 드는 코드가 많아지게 된 것이 사실이다...

 

따라서 우리 쿠링의 아키텍처에서는

 

쿠링은 표현계층과 애플리케이션 계층 사이에는 '완전 매핑' 전략을 사용하고,

애플리케이션 계층과 영속성 계층 사이에는 '매핑하지 않기' 전략을 사용하여 오버헤드를 줄이려 노력하였다.

 

즉, 이를 그림으로 보면 다음과 같다.

표현계층과 애플리케이션의 앞부분 까지는 전용 모델을 사용하기 때문에 웹 어댑터와 애플리케이션 계층 각각이 자신의 전용 모델을 연산을 수행하는 데 사용하게 된다.

 

당연히 이 방법은 한 계층을 여러 다른 커맨드로 매핑하는 과정이 필요하기 때문에 기존의 방식보다 더 많은 코드가 필요하다.

하지만 이렇게 여러 유스케이스의 요구사항을 함께 다뤄야 하는 매핑에 비해 구현하고 유지보수하기가 훨씬 쉬워진다는 장점이 있다.

 

그럼 왜? 애플리케이션 - 영속성 계층에서는 매핑하지 않기를 선택하였을까?

흔하게 말하는 도메인 Entity의 ORM 애너테이션들이 정말 다 제거되어야 편할까? 진짜 이것조차 순수하게 유지해야 할 만한 이유가 있는 것일까? 나중에 JPA가 아닌 다른 전략을 정말 선택할 확률이 얼마나 될까?

지금까지의 내 경험상 그럴 확률은 거의 0에 가깝다 생각된다.

이 정도면 이 부분은 JPA를 활용하고 더 좋은 생산성을 확보할 수 있다 생각되며, 나는 매핑하지 않기를 선택하게 되었다.

 

4-2) 계층 간 DTO의 네이밍 전략

추가로 항상 dto, request, result 등 난무하던 네이밍 규칙에서 우리 쿠링만의 네이밍 룰을 정하여 이를 따르도록 변경하였다.

그림으로 확인하면 다음과 같은데,

우선 요청은 request로 들어와 이를 표현계층에서 command로 변환한다.

이렇게 변환된 command가 useCase까지 전달되어 사용 및 처리된 후, 결과가 있다면 Dto로 반환된다.

이러한 DTO는 다시 외부로 나가기 전에 Response로 변환되어 나가게 된다.

 

예전에는 표현계층에서 도메인 로직의 계층으로 명령을 전달할 때 동일한 DTO를 사용했다면,

이번에는 이 또한 분리하게 되었다. 이를 통해 도메인 로직을 조금 더 순수하고 변경에 대응할 수 있는 구조가 되었다!

 

5. 끝으로

이번 헥사고날 아키텍처를 적용해보면 거 서의 일주일 넘게 수많은 고민을 한 것 같다...

이게 너무 많아서 글로 쓰면 끝도 없을 거 같은데.... 몇 가지 뽑아보자면

 

5-1) Default 접근 제한자의 힘

일단 대표적으로 접근 제한자에서 default 제한자의 힘을 이번에 많이 느끼게 되었다.

default 제한자가 왜 중요할까? 이는 자바 패키지를 통하여 class들을 응집적인 '모듈'로 만들어주기 때문이다.

의미적으로 같이 사용될 모듈 내부에 있는 class들은 서로 접근이 가능하지만, 패키지 바깥에서는 접근할 수 없다.

따라서 모듈의 진입점으로 활용될 class만 골라서 public으로 만들면 된다.

이러한 방식은 의존성이 잘못된 방향을 가리켜서 의존성 규칙을 위반하게 되는 위험의 정도를 확연하게 줄여주었다!

 

5-2) 유지보수성이 좋은 소프트웨어를 위하여!

금전적, 시간적으로만 생각하면 당장의 편법이나 지름길 방식이 합리적일 수도 있다.

하지만 과연 여기서 끝날까? 뒤에 더 이상 유지보수 할 일이 없을까? 정말 그렇다면 상관없긴 할 것 같은데...

 

당장은 간단한 CRUD 애플리케이션이라면 그냥 대충 해도 될 것 같지만,

조금만 기능이 늘어나다 보면 유지보수 좋은 아키텍처를 선택하는 것은 당연히 좋다. 

 

클린아키텍처 책의 SOLID가 나온 이유가 다 있다 생각된다. 시간이 지나면서 소프트웨어는 계속 변화한다.

우리, 아니 나의 팀은 이에 대처할 준비를 항상 해야만 한다고 생각한다.

 

다만 이런 경우들에 대하여 내가 왜 이 기술을 선택하고, 적용하였는지를 남겨두면 향후 함께하게 될 수 있는 팀원 또한 금방 해당 도메인에 적응할 수 있을 거라 생각된다!

 

 

댓글