TroubleShooting & Study/Architecture & Design Pattern

[Architecture] Layered Architecture 탈출기 (feat. Hexagonal, Test Code)

DH_0518 2024. 7. 18. 18:11

 개발을 시작하며 기본적인 MVC 패턴과 Layered 아키텍처를 공부한 이후 줄곧 Layered로 백엔드 패키지 구조를 설계했었다.
 
 내가 지금까지 진행했던 프로젝트들은 대부분 데드라인이 있어서 일정 기간 안에 빠르게 끝내야 했고, 내 실력도 많이 부족했기에 테스트코드나 패키지 구조, 고도화 등은 생각조차 하지 못했다.
 
 그러다 최근에 여유가 좀 생기면서 실제로 서비스를 운영해보고 싶다는 생각이 들었고, 좀 더 견고한 서버를 운영하기 위해 테스트코드 작성에 관심이 생겼다.
 열심히 여기저기 구글링을 해보고 깃허브도 뒤져가며 테스트 코드를 작성하려 했지만, 내가 진행하던 프로젝트에서는 테스트 코드가 볼륨이 너무 크고 코드 작성 자체가 너무 어려웠다. 아주 작은 기능 하나를 테스트하려면, 서비스 하나에 묶여있는 모든 의존성을 다 불러와서 테스트를 진행해야 했다.
 난 분명 작은 로직 하나를 테스트 하고싶었는데 매번 rdb 연결해서 엔티티 저장과 삭제를 반복해줘야 했고, 당장 내가 테스트하고 싶은 로직보다 준비 작업에서 시간이 더 많이 소요되었다. 테스트 실패의 원인은 대부분이 rdb에 제대로 값이 저장되거나 삭제되지 않아서 발생했다.
 
 무언가 잘못되었다는 생각이 들었다. 방법을 찾아야 했기에 인프런에서 김우근님의 테스트 코드 강의를 듣기도 하고, 개발바닥 고수분들께 질문을 하기도 했다.
 감사하게도 김우근님의 인프런 강의와 개발바닥 고수분(재형님)께 많은 도움을 받아서 내가 테스트 코드 작성에 실패한 이유를 알 수 있었고, 구조적으로 문제가 있다는 것을 알게 되었다.
 결과적으로 나는 Layered 아키텍처를 탈출하기로 했다. 이제 그 이유를 알아보자
 
 
 
 
 
 
 
 

Layered Architecture의 문제점

 

기존의 패키지 구조

 

무서울 정도로 많은 하위 모듈에 의존하고있는 거대한 Service 계층. 테스트 할 생각에 벌써 머리가 지끈거린다

 
 
먼저 왜 테스트 코드 작성을 공부하다가 레이어드 아키텍처를 탈출하게 되었는지 설명하기 위해, 레이어드 아키텍처의 문제점을 파악해 보자

  • 완벽하지 못한 의존성 분리
    • 레이어드 아키텍처에서도 'Spring Event'나 'Facade Pattern'등을 사용해서 충분히 도메인 간 의존성을 분리할 수 있다. 하지만 의존성 분리는 도메인간 의존성만을 말하는 것일까?
    • 그렇지 않다. 계층 간 의존성도 생각을 해줘야 한다. 하지만 Controller, Service, Repository 계층으로 나뉘어진 레이어드 아키텍처에서는 의존성 방향이 Controller -> Service -> Repository 형태이기 때문에, Repository에서 변화가 생기면 그 영향이 Service와 Controller 모두에게 전파될 수 있다. 따라서 이러한 문제를 해결해야 한다
  • JPA에 의존하는 도메인 엔티티
    • 도메인이란 우리가 해결하고자 하는 유사한 문제들의 집합이다. 즉, 우리가 집중해야 하는 것은 도메인이다. 그런 도메인이 특정 DB에 의존하는 것이 옳은 것일까?
    • 그리고 편리함을 위해서 JPA를 사용하지만, 만약 DB를 바꿔야 할 때가 온다면 그에 맞춰서 코드를 다시 작성해야 하는데 괜찮은 걸까? 
    • 또한 도메인 엔티티와 영속성 엔티티(JPA Entity)를 분리하지 않으면, 그를 사용하는 Service 역시 JPA에 의존하게 되므로 의존성을 분리할 수 없게 된다
  • 유닛 테스트 작성의 어려움
    • 위에서 말한 것처럼 각 계층과 프레임워크의 의존성이 존재하므로, 작은 단위로 테스트를 하고 싶어도 묶여있는 모든 의존성을 주입해야 한다.
    • 구글의 테스트 3 분류에 따르면 유닛 테스트가 80%를 차지해야 하는데 계층 간 의존성 분리가 되지 않으면 이를 지키기 힘들다. 따라서 테스트 코드가 커지고, 느려지고, 작성이 어려워진다

 
 
 
 
 
 
 

Haxagonal Architecture

 
 
 
위에서 나열한 문제들은 DIP(Dependency Inversion Principle, 의존성 역전 원칙)를 적용해서 해결할 수 있다. 

  • Dependency Inversion Principle
    • 객체에서 어떤 Class를 참조해서 사용해야 하는 상황이 생긴다면, 그 Class를 직접 참조하는 것이 아니라, 그 대상의 상위 요소(Abstract Class / Interface)로 참조해야 한다
    • 즉, 상위 모듈이 하위 모듈의 구현에 의존하지 않게끔 설계를 해야 한다
    • 하위 모듈은 외부에서 호출될 코드가 작성된 모듈이고, 상위 모듈은 하위 모듈을 호출하는 모듈을 말한다

 여기서 '상위 모듈이 하위 모듈의 구현에 의존하지 않게끔 설계를 해야 한다'만 기억하면 된다!!
 그리고 상위 모듈과 하위 모듈이 헷갈릴 수 있는데, 레이어드 아키텍처에서는 외부(Client, DB)에서 호출될 코드가 작성된 'Controller, Repository'가 하위 모듈이고, 그 하위 모듈에 작성된 코드들을 호출하는 'Service'가 상위 모듈이 된다
 
 그렇다면 DIP를 만족하려면 어떻게 설계를 해야 할까? 그 정답은 헥사고날 아키텍처에서 찾아볼 수 있다. 헥사고날 아키텍처를 보면서 레이어드 아키텍처를 어떻게 변화시킬 수 있는지 확인해 보자
 
 
 

Hexagonal Architecture

 
위의 그림은 헥사고날 아키텍처이다. 이렇게만 보면 레이어드 아키텍처를 어떻게 변경시켜야 할지 감이 잘 안 올 것이다. 그렇다면 다음 그림을 보자
 

 
 Controller -> Service -> Repository 계층별로, 세로로 내려오던 레이어드 아키텍처를 옆으로 눕히니까 헥사고날 아키텍처가 완성되었다!!
 그림처럼 레이어드 아키텍처의 각 계층들 사이에 Port라는 Interface를 두고, 각 계층이 Port를 호출하게 함으로써 각 계층의 의존성을 끊어버릴 수 있는 것이다.

Hexagonal Architecture가 적용된 패키지 구조

 
 
 
 
 
 
 

Service Layer의 추상화

 
 
 
 그런데 여기서 한 가지 더 깊게 생각해 볼 부분이 있다. 정말 서비스 레이어가 추상화되어야 하는 것일까?
이 문제에 대해 테스트 코드 강의자이신 김우근님과, 내가 도움을 받았던 재형님 두 분 다 같은 의견을 말씀하셨다. 또한 나도 헥사고날을 처음 공부할 때 두 분과 같은 의견을 가졌었기에 말해보고자 한다

  • Controller -> Service 의존성
    • 우리가 Repository와 Service의 의존성을 끊는 이유를 생각해 보자.

      레이어드 아키텍처에서 Service가 Repository에 의존하고 있고, 따라서 Repository에서 사용하는 DB가 바뀐다면 그 영향이 우리가 집중해야 하는 핵심 비즈니스를 다루는 Service까지 전파될 것이기에, 이를 막고자 의존성을 제거한 것이다

    • 그렇다면 Controller는 어떤가? 이미 레이어드 아키텍처에서도 의존성 관계는 외부 -> 내부로 되어있기에 의존성 방향을 바꿀 필요는 없다.

      그럼에도 불구하고 Interface를 통해서 접근하는 이유는, Service의 변경사항이 Controller에 영향을 미치지 않기 위해서와 Service를 교체해야 하는 상황이 발생했을 때 Controller 코드를 직접 바꾸는 경우를 방지하기 위해서다. 실제로 Controller를 테스트할 때는, In Port를 두고 중간에서 실제 Service를 FakeService로 바꿔서 테스트하는 것이 훨씬 수월하다

 
 
 
그럼에도 불구하고 왜 서비스 레이어의 추상화를 의심하는 걸까?

  1. Service가 교체될 일은 테스트를 제외하고는 거의 없다
  2. Controller 테스트를 꼭 해야 할 필요가 없다

다음은 김우근님의 테스트 코드 강의의 일부분이다.

 
사진의 설명처럼 웹 어댑터의 역할 대부분을 Spring Framework가 해결할 수 있다.  따라서 테스트를 통해 검증할 수 있는 부분은 유스케이스를 호출하는 부분만이 남았는데, '굳이 UseCase 호출을 테스트하는 Controller 테스트 코드는 필요 없다'라는 의견이다.
 

그림에 따르면 서비스 레이어의 추상화는 '쓸모없는 구역'에 속하지 않을까 싶다.. 물론 이건 정답이 없기에 이건 옳고 저건 그르다! 라는 주장은 아니다.  그냥 헥사고날 아키텍처의 실용성에 대해 화두를 던져주고 싶어서 추가한 내용이기에, 다들 한 번쯤 생각해 보고 자신이 생각한 대로 나아갔으면 좋겠다.
 
 
 
그럼 마지막으로 최종 패키지 구조를 보고 글을 마무리하겠다 
 
 
 
 
 
 
 

최종 패키지 구조

 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

도메인 모델은 순수해야 해!!!!!!!!!!!
 
 
 
 
 
 
 
 
 
 
 
 
 
Reference

Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트 강의 | 김우근 - 인프런

김우근 | Spring에 테스트를 넣는 방법을 알려드립니다! 더 나아가 자연스러운 테스트를 할 수 있게 스프링 설계를 변경하는 방법을 배웁니다., 프로젝트 설계를 발전시키는 테스트의 본질을 짚

www.inflearn.com

[Clean Architecture] SOLID원칙 06. 의존성 주입과 의존성 역전원칙(DI, DIP)

Dependency(의존성) 객체지향 프로그래밍에서 Dependency, 의존성은 서로 다른 객체 사이에 의존 관계가 있다는 것을 말한다. 즉, 의존하는 객체가 수정되면 다른 객체도 영향을 받는다는 것이다. 예를

clamp-coding.tistory.com

💠 완벽하게 이해하는 DIP (의존 역전 원칙)

의존 역전 원칙 - DIP (Dependency Inversion Principle) DIP 원칙이란 객체에서 어떤 Class를 참조해서 사용해야하는 상황이 생긴다면, 그 Class를 직접 참조하는 것이 아니라 그 대상의 상위 요소(추상 클래스

inpa.tistory.com