Architecture?

Dev·2022년 12월 31일
1

1. 아키텍처가 필요한 이유

설계의 핵심은 의존성이다.

큰 계획 없이 개발하다 보면 얽히고설킨 스파게티 코드가 나온다. sw는 지속해서 변하고, 정돈되지 않은 코드를 유지보수한다면 개발 시간이 오래 걸릴 것이다. 예를 들어 하나의 기능을 수정하는데 여러 모듈을 건드린다면 당연히 개발비용이 증가한다. 아키텍처가 필요한 이유는 sw를 쉽게 변경할 수 있는 구조를 설계하기 위함이다. 여기서 말하는 설계란 코드를 어떤 클래스, 패키지에 배치할지에 대한 의사결정이다.

위 그림은 마틴 파울러가 언급한 내용으로, 아키텍처 없는 개발은 초반에는 빠른 생산성(개발 시간)을 가질 수 있지만 개발을 지속할수록 점차 생산성이 떨어지게 된다. 하지만 Good Design을 가진 아키텍처는 시간이 지나도 새로운 기능을 쉽게 반영할 수 있기 때문에 돈과 직결되는 생산성이 비약적으로 좋아진다.

프로젝트엔 여러 기능의 코드가 있다. db에 쿼리를 날리거나, 외부 요청을 받아들일 수 있고, 혹은 도메인 로직을 개발하기도 한다. 이러한 기능들 모두가 중요할까? 중요하다. 하지만 모듈 간 우선순위가 있다. 앞으로 이 기능을 유지보수할 팀을 위해 프로젝트의 핵심 모듈이 외부 인프라를 포함한 다른 모듈에 의존하지 않게 구성하고 싶다.

A가 B에 의존한다는 의미는 B가 변경될 때 A도 함께 변경될 여지가 있다는 의미다. 핵심 로직이 담긴 모듈이 다른 서브 모듈에 의존한다면 어떻게 될까? 아마도 덜 중요한 모듈의 변경 때문에 핵심 로직을 계속해서 수정하고, 수정을 잘못했을까 봐 마음이 떨리기도 할 것이다.

2. Layered Architecture

목적이 같은 코드들을 계층으로 그룹화하여 관심사를 분리한 아키텍처

다음으로 프로젝트에서 많이 사용하는 아키텍처 중 하나인 계층형 아키텍처를 살펴본다. 아키텍처 이해가 쉬워 간단한 서비스일 때 쉽게 적용할 수 있으며, 특정 기능을 담당할 코드를 어디에 배치할지 손쉽게 결정할 수 있다.

화면 표현/전환을 담당하는 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가 더 적합할 수도 있다고 생각한다.

3. Hexagonal Architecture란

핵심은 도메인이다. 도메인 개발이 쉽고, 외부 인프라를 포함한 다른 계층의 영향이 도메인으로 향하는 것을 최소화하는 방향으로 구성한다.

마지막으로 Hexagonal Architecture를 얘기해본다. Hexagonal Architecture는 클린 아키텍처를 구현하는 방법을 '구체화'한 아키텍처다. 결국 두 아키텍처 모두 도메인(업무 규칙)을 제일 우선시하며, Infrastructure → Domain Layer에 영향을 최소화하도록 의존성을 관리한 아키텍처다.

배경

  • 최근 다양한 데이터 저장소가 등장하고, 기존 서비스들도 MSA 구조로 전환하면서 하나의 서비스는 많은 외부 서비스를 호출하고 있다. 따라서 외부 서비스의 변경은 이전보다 빈번해졌다. 이때 도메인이 외부 인프라와 통신하는 로직에 의존한다면, 외부 서비스의 변경이 일어날 때마다 도메인을 수정해야 한다.
  • Hexagonal Architecture는 도메인을 보호하기 위해 도메인을 표현하는 내부 영역과 외부 서비스를 표현하는 외부 영역을 철저히 분리한다. 예를 들어 데이터베이스가 변경되거나, API를 교체하는 등 기술적인 세부 사항이 변경되더라도 도메인 로직 수정 없이 인프라 코드만 구현하면 된다.
  • 근데 왜 다른 모듈보다 도메인을 우선시할까? 비즈니스 로직은 결국 시스템의 '규칙'이자 '시스템이 존재하는 이유'다. 따라서 시스템의 데이터가 어떻게 저장되고, 수정될지를 도메인이 결정한다. 당연히 프로젝트의 근간이자 핵심은 외부의 변화로부터 멀어져야 한다.

Port & Adapter

port는 내부 비즈니스 영역을 외부 영역에 노출한 인터페이스, adapter는 외부 서비스와 포트 간 데이터 교환을 담당한다.

Inbound 구조

  • Inbound Adapter : 외부에서 들어온 요청을 처리하며 Inbound Port를 호출함으로 도메인 로직에 접근할 수 있다. ex) Controller
  • Inbound Port : 도메인 로직 사용을 위해 노출한 포트로 Inbound Adapter가 Inbound Port를 통해 도메인 로직을 호출한다. ex) Service Interface

Outbound 구조

  • Outbound Port : 도메인 로직에서 외부 영역에 있는 Outbound Adapter를 호출할 때 사용한다. Outbound Port는 Outbound Adapter의 추상화로, 도메인 로직에서 외부 서비스 호출 시 Outbound Adapter에 직접 의존하는 걸 막아준다. 즉, 도메인 로직은 Outbound Adapter를 몰라도된다. Domain은 Outbound Port만 알면된다. 이러한 방향으로 개발한다면 , 외부 인프라에 의존하는 도메인이 줄어들어 도메인을 외부로부터 보호할 수 있다. ex) Repository Interface
  • Outbound Adapter : outbound port의 구현체로, 외부 서비스와 연계하여 내부 비즈니스 영역과 외부 서비스 간 데이터 교환을 담당한다. ex) RepositoyImpl

다음으로 말할 내용은 개인적인 생각이다. Inbound Adapter(controller)에서 Inbound Port(service interface)를 사용할 이유는 없다고 생각한다. 서비스가 교체될 때 Inbound Adapter 단에서 변경이 일어나지, 도메인 로직에서 일어나는 건 아니기 때문이다. 하지만, 도메인 로직에서 외부 서비스를 호출할 때는 반드시 Outbound Adapter가 아닌 Outbound Port를 사용해야 한다. Inbound 구조와 달리 Outbound 구조는 추후 변경이 일어날 때 도메인 로직 단에서 변경이 일어날 수 있기 때문이다. 결국 port & adapter 구조를 지키는 것보다 어떻게 하면 도메인 로직을 보호할 수 있을까에 대한 고민이 필요하다고 생각한다.

Hexagonal Architecture 시나리오

  1. 그림의 왼쪽 초록색 영역의 WebServer에서 요청이 들어오면Controller(Outbound Adapter)가 요청 로직을 처리하고 Service Interface(Outbound Port)를 통해 Service(Application Layer)로 전환한다.
  2. Service에서 도메인 로직을 위해 필요한 작업(ex domain entity 변환)을 수행 후, Domain Layer로 전환한다.
  3. 도메인 로직을 처리하던 중 외부 서비스와 통신이 필요하다면 Repository Interface(Outbound Port)을 통해 요청한다.
  4. RepositoyImpl(Outbound Adapter)가 실제로 외부 API와 통신하며 결괏값을 도메인 로직 쪽으로 전달한다.
  5. 그림의 오른쪽 상단에 SMS Server → Mailing Server로 외부 인프라를 교체한다고 가정해보자. 우리는 Outbound Port를 사용했기 때문에 도메인 영역에 영향 없이 Outbound 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

4. 결론

  • 먼저 살펴본 것은 아키텍처가 필요한 이유다. 코드를 특정 기준에 따라 분류한다면, 나중에 기능을 파악할 때 코드들이 어디 있는지 빠르게 파악할 수 있어 유지보수가 쉬울 것이다. 여기서 더 나아가 의존성의 중요성을 살펴봤었다. 결국 비즈니스는 계속 변하고, 이에 맞춰 서비스도 빠른 배포가 필요할 것이다. 이렇게 변화에 대응하기 위해 의존성을 고민하며 SW를 설계해야 됨을 깨달았다.
  • 이후 아키텍처를 구분하는 방식에 대해 알아봤다. 플로우를 쉽게 파악하기 위한 'Layered Architecture', 혹은 각각의 기능마다 패키지를 구성하는 방식, 혹은 도메인 로직을 보호하기 위한 'Clean Architecture'를 적용하는 사례가 있었다. 특정 아키텍처가 항상 좋음이 아닌 각각의 아키텍처마다의 장단점이 있고 상황에 맞게 적용이 필요함을 느꼈다.

참고 자료

profile
성장하는 개발자가 되고싶어요

0개의 댓글