이 글은 육각형 아키텍처를 갓 접해보고 개발해본 입장에서 작성하기에, Controller-Service-Repository로 이루어진 전통적인 계층형 아키텍처에 익숙해진 사람들과 DDD를 모르는 사람에게 추천합니다❗
만들면서 배우는 클린 아키텍처(Get Your Dirty Hands On Clean Architecture의 번역본)의 1장과 2장을 통해 계층형 아키텍처(Layered Architecture)가 가진 단점은 무엇인지, 육각형 아키텍처(Hexagonal Architecture)는 어떻게 그 단점을 보완할 수 있는지 소개합니다.
계층형 아키텍처를 사용해서 웹 어플리케이션을 구현하면 대체로 그림과 같은 구조를 만듭니다. 웹 계층은 Controller에, 도메인은 Service에, 영속성은 Repository에 매치하고.. 순서대로 의존하는 클래스 구조를 만들고.. 새로운 기능을 구현할 때는 반대로 영속성 - 도메인 - 컨트롤러 순으로 만들고 .. 와 같은 순서를 생각할 수 있습니다.
책을 읽으면서 가져야 할 가장 중요한 원칙은, "계층형 아키텍처가 잘못 된 패턴이고 육각형 아키텍처가 옳은 패턴이다"가 아니라는 것입니다.
잘 구축된 계층형 아키텍처 기반의 웹 애플리케이션은 높은 유지보수성과 확장성을 가질 수 있습니다. 현존하는 아주 많은, 대부분의 웹 애플리케이션 서버는 계층형 아키텍처로 구성되어 있습니다.
그러면 DDD(Domain-Driven-Development)에서는 왜 계층형 아키텍처 대신 육각형 아키텍처를 택한 걸까요?
는 아래에서 ....
위 그림에서도 볼 수 있듯이, 의존성은 항상 하위 계층으로 향해 있고 마지막에는 Persistence(영속성)가 위치합니다.
그래서 우리는 새로운 유스케이스를 개발할 때 습관적으로 Repository를 구현하고, Repository를 필드로 가지는 Service를 구현하고, 마지막으로 Controller를 구현합니다.
비즈니스 관점에서는 Domain을 포함한 비즈니스 로직을 최우선으로 구현해야 합니다. 하지만 계층형 아키텍처에서는 의존성의 끝이 영속성을 가리키기 때문에 도메인을 최우선 순위로 둔다는 건 쉽지 않은 문제입니다.
또, ORM Framework(JPA, Hibernate)를 사용하면 Domain 로직이 ORM과 강하게 결합됩니다. Framework나 Database와 관련된 의존성 없이 순수한 비즈니스 로직을 가져야 할 Entity 클래스는 @GeneratedValue
, @ManyToOne
과 같은 어노테이션과 함께 다른 계층과 연관된 많은 제약사항들을 가지게 되고, 자연스럽게 영속성 계층에 위치하게 됩니다.
이것을 Domain 계층에서 그대로 사용하는 것은 도메인 계층과 영속성 계층을 강하게 결합시키는 행동이라고도 볼 수 있습니다.
짧고 간편한 구현을 할 수 있지만 구조의 균형을 깨뜨리기 쉬운 지름길을 택하기 쉬워집니다. 최하단 계층은 아무것도 의존할 필요가 없다는 점 때문에 시간이 지날수록 헬퍼, 유틸리티의 형태를 한 클래스들이 증가하기 쉽습니다. 결국 헬퍼, 유틸리티, 리포지토리가 합쳐져 영속성 계층은 점점 비대해집니다.
단순한 CRUD만 수행하는 서비스 메서드가 굳이 필요할까?라는 생각은 모두에게나 한번쯤 들 만한 의문이자 유혹입니다.
서비스가 컨트롤러의 명령을 단순히 전달하지 않고, 리포지토리를 접근해서 바로 데이터를 저장하고, 찾아올 수 있다면 불필요한 중간 과정을 건너뛸 수 있을 것 같습니다. 아래 코드처럼요.
public class UserController {
private final UserRepository userRepository;
@GetMapping("/user/{id}")
public String userInfo(@PathVariable Long id) {
User user = userRepository.findById(id)
.orElseThrows(IllegalArgumentException.class);
// another logic
}
}
하지만 이러한 방식은 도메인 로직이 웹 계층에 구현되어 책임의 구분이 불분명해집니다. 위의 코드에서도 찾으려는 id의 유저가 존재하지 않을 경우에 예외를 반환하는데, 이러한 비즈니스 로직 코드가 웹 계층까지 침범한 것을 볼 수 있어요.
웹 계층이 도메인 로직 뿐 아니라 영속성 계층에도 의존성을 가져 테스트 하기에도 어려워집니다. 생성해야 하는 Mock이 Service도 있고, Repository도 있다면 의존 관계를 쉽게 이해하기 힘들 수 있습니다.
테스트 구성 환경이 어렵고 복잡하다는 것은 테스트를 짜기 싫은 마음으로 이어지고, 포기한다는 다른 유혹으로 인도할 수 있습니다.
이는 연쇄적으로 유스케이스가 분산될 수 있다는 문제점을 야기합니다. 유스케이스가 간단하다는 이유로 도메인 계층을 생략하면 비즈니스 로직이 웹 계층에도 존재할 수 있고, 영속성 계층이 비대해졌다면 영속성 계층에도 비즈니스 로직이 존재할 수 있습니다.
마지막으로, 위에서 언급했듯 계층 간 의존성의 방향 때문에 영속성이 구현되어 있지 않으면 도메인을 구현할 수 없고, 웹 계층도 구현할 수 없습니다. 구현의 순서가 강제되기 때문에 협업하기에도 꽤 불편한 구조가 될 수 있습니다.
계층형 아키텍처는 올바르게 구축하고 제약사항을 적용한다면 좋은 유지보수성과 확장성을 가질 수 있지만, 이것을 엄격하게 강제하지 않으면 금방 깨지기 쉽습니다. 품질을 보장하기에는 어렵고 큰 관리 리소스를 소모합니다.
이미 계층형 아키텍처를 사용하고 있는데 그럼 지금 당장 이주해야 할까요? 답은 정해져 있지 않습니다. 현재 구조에서 유지보수와 확장하는 데 소모되는 비용, 다른 아키텍처로 구조를 완전히 변경하면서 발생할 수 있는 위험성과 비용을 계산하고, 고민하는 걸 추천합니다.
1장에서 마지막으로 강조하는 것은, 어떤 아키텍처를 선택하던 이 글에서 언급한 함정과 위험성을 염두에 두면 더 안전하고 유용한 솔루션을 만들기에 도움이 될 것이라는 점 입니다. 😄
다음 글에서는 육각형 아키텍처이자 포트와 어댑터 아키텍처로 불리는 구조에 대해 소개하고 장단점을 알아봅니다.
영속성의 구현체를 RDBMS에서 DocumentDB로 변경할 때, HTTP API만 제공하던 인터페이스를 RPC도 추가해야할 때 구현을 어떻게 변경해야 할지 고민해보면 좋을 것 같습니다.
출처: 만들면서 배우는 클린 아키텍처
👉 지속 가능한 소프트웨어 설계 패턴: 포트와 어댑터 아키텍처 적용하기 📚
https://engineering.linecorp.com/ko/blog/port-and-adapter-architecture/