설계의 핵심은 의존성이다.
큰 계획 없이 개발하다 보면 얽히고설킨 스파게티 코드가 나온다. sw는 지속해서 변하고, 정돈되지 않은 코드를 유지보수한다면 개발 시간이 오래 걸릴 것이다. 예를 들어 하나의 기능을 수정하는데 여러 모듈을 건드린다면 당연히 개발비용이 증가한다. 아키텍처가 필요한 이유는 sw를 쉽게 변경할 수 있는 구조를 설계하기 위함이다. 여기서 말하는 설계란 코드를 어떤 클래스, 패키지에 배치할지에 대한 의사결정이다.
위 그림은 마틴 파울러
가 언급한 내용으로, 아키텍처 없는 개발은 초반에는 빠른 생산성(개발 시간)을 가질 수 있지만 개발을 지속할수록 점차 생산성이 떨어지게 된다. 하지만 Good Design을 가진 아키텍처는 시간이 지나도 새로운 기능을 쉽게 반영할 수 있기 때문에 돈과 직결되는 생산성이 비약적으로 좋아진다.
프로젝트엔 여러 기능의 코드가 있다. db에 쿼리를 날리거나, 외부 요청을 받아들일 수 있고, 혹은 도메인 로직을 개발하기도 한다. 이러한 기능들 모두가 중요할까? 중요하다. 하지만 모듈 간 우선순위가 있다. 앞으로 이 기능을 유지보수할 팀을 위해 프로젝트의 핵심 모듈이 외부 인프라를 포함한 다른 모듈에 의존하지 않게 구성하고 싶다.
A가 B에 의존한다는 의미는 B가 변경될 때 A도 함께 변경될 여지가 있다는 의미다. 핵심 로직이 담긴 모듈이 다른 서브 모듈에 의존한다면 어떻게 될까? 아마도 덜 중요한 모듈의 변경 때문에 핵심 로직을 계속해서 수정하고, 수정을 잘못했을까 봐 마음이 떨리기도 할 것이다.
목적이 같은 코드들을 계층으로 그룹화하여 관심사를 분리한 아키텍처
다음으로 프로젝트에서 많이 사용하는 아키텍처 중 하나인 계층형 아키텍처를 살펴본다. 아키텍처 이해가 쉬워 간단한 서비스일 때 쉽게 적용할 수 있으며, 특정 기능을 담당할 코드를 어디에 배치할지 손쉽게 결정할 수 있다.
화면 표현/전환을 담당하는 Presentation Layer → 로직을 담당하는 Application 및 Domain Layer → 데이터 처리를 담당하는 Persistence Layer 흐름으로 구성된다. 이렇게 데이터 인입부터 DB 처리까지 시스템 플로우가 아키텍처에 그대로 적용되어 이해가 쉽다.
예를 들어, 회원 정보를 조회하는 API를 개발한다고 가정해보자. Request를 처리할 Controller는 Presentation 패키지, 회원 정보와 관련된 로직은 Application 패키지에 담는다. 마지막으로 DB로부터 조회할 Entity 및 쿼리는 각각 Domain, Persistence 패키지에 구성한다. 각각의 모듈을 어떤 패키지에 넣을지 굉장히 쉽게 떠올릴 수 있으며 나중에 코드를 어떤 패키지에서 찾을지 떠올릴 때도 유용하다.
package 구조 예시
Presentation Package
- UserController.java
Application Package
- UserUseCase.java
Domain Package
- User.java
infrastructure Package
- UserRepositoryImpl.java
보통 애플리케이션은 도메인이(비즈니스 로직) 핵심인데, Domain Model이 Persistence 계층에 의존할 때 Persistence 영향으로 비즈니스 로직 역시 변경될 수도 있다. 현재 많은 서비스가 MSA 구조로 전환하고 redis, es 등 다양한 데이터 저장소가 나오고 있다. 이렇게 Persistence 계층의 변경이 자주 일어날 수 있는데, Persistence 계층 때문에 핵심 영역인 Domain이 변경되는 건 지양하고 싶다.
이를 해결하고자 'Dependency Inversion Principle' 원리를 적용하기도 한다. Persistence Interface의 위치를 고수준 (비즈니스 계층 영역)으로 위치하고, Persistence Layer의 구현체를 저수준 모듈에 위치함을 말한다. 이로써 도메인 로직이 Persistence Layer의 영향을 받지 않게 구성할 수 있다. 개인적으로 이렇게 변경한 구조는 뒤에 나올 Hexagonal Architecture와 유사한 구조라고 생각한다.
다음으로 말할 내용은 매우 주관적인 생각이다. Layered Architecture의 단점으로 비즈니스 로직이 Persistence 계층에 의존함을 말했다. 만약 사업 초기 비즈니스 규칙이 자주 변하고, Entity 역시 자주 변화는 상황에서 모든 Layer가 도메인 로직에 의존한다면 어떻게 될까? 아마도 유지보수 시 많은 컴포넌트를 뜯어 고쳐야 할 것이다. 물론 도메인 로직이 중요하기 때문에 의존성을 도메인 로직으로 향하지 않게함은 동의한다. 하지만 편의상 사업 초기에 비즈니스 규칙이 자주 바뀔 때는 처음부터 뒤에 나올 클린 아키텍처가 아닌 Layered Architecture가 더 적합할 수도 있다고 생각한다.
핵심은 도메인이다. 도메인 개발이 쉽고, 외부 인프라를 포함한 다른 계층의 영향이 도메인으로 향하는 것을 최소화하는 방향으로 구성한다.
마지막으로 Hexagonal Architecture를 얘기해본다. Hexagonal Architecture는 클린 아키텍처를 구현하는 방법을 '구체화'한 아키텍처다. 결국 두 아키텍처 모두 도메인(업무 규칙)을 제일 우선시하며, Infrastructure → Domain Layer에 영향을 최소화하도록 의존성을 관리한 아키텍처다.
port는 내부 비즈니스 영역을 외부 영역에 노출한 인터페이스, adapter는 외부 서비스와 포트 간 데이터 교환을 담당한다.
다음으로 말할 내용은 개인적인 생각이다. Inbound Adapter(controller)에서 Inbound Port(service interface)를 꼭
사용할 이유는 없다고 생각한다. 서비스가 교체될 때 Inbound Adapter 단에서 변경이 일어나지, 도메인 로직에서 일어나는 건 아니기 때문이다. 하지만, 도메인 로직에서 외부 서비스를 호출할 때는 반드시 Outbound Adapter가 아닌 Outbound Port를 사용해야 한다. Inbound 구조와 달리 Outbound 구조는 추후 변경이 일어날 때 도메인 로직 단에서 변경이 일어날 수 있기 때문이다. 결국 port & adapter 구조를 지키는 것보다 어떻게 하면 도메인 로직을 보호할 수 있을까에 대한 고민이 필요하다고 생각한다.
package 구조 예시
Presentation Package(Inbound Adapter)
- UserController.java
Infrastructure Package(Outbound Adapter)
- UserRepositoryImpl.java
Application Package(Service)
- UserUseCase.java
Domain Package(Domain Logic, Outbound Port)
- UserRule.java
- UserRepository Interface