김동형수 개발기

만들면서 배우는 클린 아키텍처 - 03 정리 본문

책 스터디/[완료] 만들면서 배우는 클린아키텍처

만들면서 배우는 클린 아키텍처 - 03 정리

김동형수 2022. 4. 8. 06:02

코드 구성하기

코드를 보는 것만으로도 어떤 아키텍처인지 파악할 수 있다면 좋지 않을까?

 

새 프로젝트에서 괜찮아 보이는 구조를 잡지만, 프로젝트가 계속해서 진행될수록 패키지 구조는 짜임새 없는 엉망진창 코드를 그럴싸하게 보이게 만드는 껍데기 뿐이라는 점을 깨닫게 된다.

뼈를 세게 얻어 맞았다. 효과는 굉장했다.

저자는 BuckPal 예제 코드를 구조화하기 위한 여러 방법을 살펴본다. '송금하기' 유스케이스를 예로 들어서 프로젝트 구조를 구성해보자.


계층으로 구성하기

웹, 도메인, 영속성 계층 각각에 대해 전용 패키지인 web, domain, persistence를 뒀다.

의존성 역전 원칙을 적용하기 위해서 AccountRepository 인터페이스를 domain 패키지에 배치하고 그 구현 클래스는 persistence 패키지에 배치했다.

세 가지 이유로 이 패키지 구조가 최적이 아님을 확인할 수 있다.

1. 애플리케이션 기능조각이나 특성을 구분 짓는 패키지 경계가 없다. 그 예로 사용자 관리기능을 추가한다면 web에 UserController, domain에 User, UserService, UserRepository, persistence에 UserRepositoryImpl 을 추가하게 될 것 이다.

추가적인 구조가 없다면, 서로 연관되지 않은 기능들끼리 예상하지 못한 부수효과(side effect)를 일으킬 수 있는 엉망진창 클래스 묶음으로 변모할 가능성이 크다.

 

이 내용은 동의하기 힘든 것 같다. 보통은 사용자 입력 -> 영속성의 흐름으로 진행되기 때문에 서로 연관되지 않은 기능끼리 부수효과를 일으킨다?에는 크게 동의하지 않는다.
데이터베이스 중심의 설계라도 사용자 입력은 웹 계층부터 영속성 계층까지 목적성 있게 구현하는데 어째서 엉망진창 클래스 묶음처럼 표현하는건지 모르겠다.

 

2. 애플리케이션이 어떤 유스케이스를 제공하는지 파악할 수 없다. AccountService, AccountController가 어떤 유스케이스를 구현했는지 파악할 수 있는가? 추측이나 텍스트 검색으로 어떤 기능을 구현했는지 확인하는 방법밖에 없을 것 같다.

 

동의한다. 현재 LIFIC의 소스코드만 봐도 각 서비스나 컨트롤러에서 이러이러한 것들이 구현되어 있겠구나 추측만 할 뿐이지 명확하게 어떤 책임을 지고 있는지는 알길이 없다.

 

3. 패키지 구조를 통해서 우리가 목표로 하는 아키텍처를 파악할 수 없다.


기능으로 구성하기

'계층형 아키텍처'의 단점 몇가지를 보완하기 위한 기능으로 구성된 아키텍처이다.

기능인 account 패키지로 entity, controller, repository interface, repository implement, service 모든 클래스를 포함하고 있다. 또한 이 구조는 package-private 접근 수준을 이용해 패키지 간의 경계를 강화할 수 있다. 패키지 경계를 package-private 접근 수준과 결합하면 각 기능사이의 불필요한 의존성을 방지할 수 있다.

 

계층형 아키텍처에서 AccountService로 지정한 도메인 서비스 클래스를 책임을 좁히기 위해 SendMoneyService로 변경하면서 '송금하기' 유스케이스를 클래스 명만 보고 찾을 수 있게 되었다. 이를 로버트 마틴이 '소리치는 아키텍처' 명명한 바 있다.

 

그러나 기능에 의한 패키징 방식은 계층에 의한 패키징 방식보다 아키텍처의 가시성을 훨씬 더 떨어뜨린다. 어댑터를 나타내는 패키지명이 없고 인커밍 포트, 아웃고잉 포트를 확인할 수 없다. 모든 클래스가 같은 패키지내에 있다보니 다음과 같은 문제점이 생길 수 있다. SendMoneyService에서 AccountRepository만 알고있으면 되는데, package-private 접근 수준 때문에 AccountRepositoryImpl까지 접근할 수 있어서 실수로 도메인에서 영속성 계층에 의존성이 생길 수 있다.


아키텍처적으로 표현력 있는 패키기 구조

핵사고날 아키텍처에서 구조적으로 핵심적인 요소는 엔티티, 유스케이스, 인커밍/아웃고잉 포트, 인커밍/아웃고잉 어댑터 이다.

최상위에는 Account와 관련된 유스케이스 모듈임을 나타내는 패키지 account가 위치한다. 그 하위로는 domain, application, adapter가 있다.

 

domain 패키지는 도메인 모델이 속해있다.

application 패키지는 도메인 모델을 둘러싼 서비스 계층을 포함한다.

adapter 패키지는 애플리케이션의 인커밍/아웃고잉 포트에 대한 구현을 제공하는 어댑터를 포함한다.

 

이 패키지 구조는 용도와 구조가 명확하기 때문에 새로운 작업에 대한 위치 지정이 명확하다고 생각한다.

 

이 패키지 구조는 '아키텍처-코드 갭'을 효과적으로 다룰 수 있는 강력한 요소이다. 만약 패키지 구조가 아키텍처를 반영할 수 없다면 시간이 지남에 따라 코드는 점점 목표하던 아키텍처로부터 멀어지게 될 것이다.

 

그런데 패키지가 많다는 것은 모든 것을 public으로 만들어서 패키지 간의 접근을 허용해야 한다는 것을 의미하는게 아닐까?

적어도 어댑터 패키지에 대해서는 그렇지 않다. 어댑터 -> 애플리케이션 -> 외부 방향으로 흘러가기 때문에 애플리케이션에서 어댑터 클래스로 향하는 우발적인 의존성은 있을 수 없다.

 

하지만 application, domain 패키지 내에 일부패키지는 public으로 지정해야 한다. 의도적으로 어댑터에서 애플리케이션에 접근할 경우  해당 포트들은 public으로 되어야 한다.

 

데이터베이스 타입이 키-밸류 -> SQL 으로 변경되어야 한다면, 이 패키지 구조에서는 아웃고잉 포트만 변경작업을 하면 된다.

 

DDD(도메인 주도 개발)을 적용하기 용이하다.


의존성 주입의 역할

애플리케이션 계층이 인커밍/아웃고잉 어댑터에 의존성을 갖지 않게 하는 것이 클린 아키텍처의 본질이다.

 

의존성 주입을 사용하게 되면 의존성 역전 원칙을 지키기 위한 인터페이스만 알고 있으면 되기 때문에 실제 구현 클래스에 대해서 모르고 있어도 된다.


유지보수 가능한 소프트웨어를 만드는 데 어떻게 도움이 될까?

프로젝트를 개발/유지보수하는 시간이 길어지더라도 코드를 보는 것 만으로 어떤 아키텍처인지 파악이 가능하도록 패키지 구조를 구성하면 좋다. 

계층으로 구성된 패키지구조보다는 기능으로 구성된 패키지구조 그 보다 추상적인 아키텍처를 패키지 구조로 녹여서 표현한 클린 아키텍처가 좋다.

패키지 구조만으로 용도/구조 파악이 가능하니 개발 / 유지보수할 때 클래스 추가할 위치 탐색을 직관적으로 할 수 있고 패키지 구조 + 클래스 명으로 어떤 기능을 하는 클래스인지 파악이 가능하다. 이 말은 결국 유지보수의 비용절감으로 이어질 수 있다는 말과 같다.

Comments