[4주차] 10장 : 아키텍처 경계 강제하기

프로젝트는 시간이 지나면서 계층 간의 경계가 약화되고, 코드는 점점 더 테스트하기 어려워지고, 새로운 기능을 구현하는 데 점점 더 많은 시간이 든다.

아키텍처 내의 경계를 강제하는 방법과 아키텍처 붕괴에 맞서 싸우기 위해 취할 수 있는 몇 가지 조치를 살펴보자.

경계와 의존성

  • 아키텍처의 어디에 경계가 있는가 ? → 각 계층 사이, 안쪽 인접 계층과 바깥쪽 인접 계층 사이에 경계가 있다.

  • 경계를 강제한다는 것은 어떤 의미인가 ? → 의존성이 올바른 방향을 향하도록 강제하는 것을 의미한다.

이번 장에서는 이러한 의존성 규칙을 강제하는 방법들을 알아보고,

(점선 화살표처럼) 잘못된 방향을 가리키는 의존성을 없게 만들고자 한다.

접근 제한자

  • 접근 제한자는 경계를 강제하기 위한 자바에서 제공하는 기본적인 도구이다.

  • public, protected, private, package-private(default) 가 있다.

✔️ package-private

  • 자바 패키지를 통해 클래스들을 응집적인 ‘모듈’로 만들어 준다.

  • 모듈 내에 있는 클래스들은 서로 접근 가능하지만, 패키지 바깥에서는 접근할 수 없다.

  • 모듈의 진입점으로 활용될 클래스들만 골라서 public으로 만들면 의존성이 잘못된 방향을 가리켜서 의존성 규칙을 위반할 위험이 줄어든다.

package-private (o)

  • persistence 패키지에 있는 클래스들은 외부에서 접근할 필요가 없기 때문에 package-private로 만들 수 있다.

  • 영속성 어댑터는 자신이 구현하는 출력 포트를 통해 접근하므로 SendMoneyService를 package-private으로 만들 수 있다.

💡 의존성 주입 메커니즘은 일반적으로 리플렉션을 이용해 클래스를 인스턴스로 만들기 때문에 package-private이더라도 여전히 인스턴스를 만들 수 있다.

💡 이 방법을 스프링에서 사용하려면 클래스패스 스캐닝을 이용해야만 한다. 다른 방법에서는 객체의 인스턴스들을 우리가 직접 생성해야 하기 때문에 public 제한자를 이용해야 한다.

public (+)

  • 나머지 클래스들은 아키텍처의 정의에 의해 public(’+’로 표시)이어야 한다.

  • domain 패키지는 다른 계층에서 접근할 수 있어야 한다.

  • application 계층은 web 어댑터와 persistence 어댑터에서 접근 가능해야 한다.

check

  • package-private 제한자는 몇 개 정도의 클래스로만 이뤄진 작은 모듈에서 가장 효과적이다.

  • 패키지 내의 클래스가 특정 개수를 넘어가기 시작하면 하나의 패키지에 너무 많은 클래스를 포함하는 것이 혼란스러워지게 된다. 이렇게 되면 코드를 쉽게 찾을 수 있도록 (그리고 미적인 욕구를 충족시키기 위해서도) 하위 패키지를 만드는 방법을 선호한다.

  • 하지만 이렇게 하면 자바는 하위 패키지를 다른 패키지로 취급하기 때문에 하위 패키지의 package-private 멤버에 접근할 수 없게 된다. 그래서 하위 패키지의 멤버는 public 으로 만들어서 바깥 세계에 노출시켜야 하기 때문에 우리의 아키텍처에서 의존성 규칙이 깨질 수 있는 환경이 만들어진다.

컴파일 후 체크

  • 클래스에 public 제한자를 쓰면 아키텍처 상의 의존성 방향이 잘못되더라도 컴파일러는 다른 클래스들이 이 클래스를 사용하도록 허용하기 때문에 전혀 도움이 되지 않는다.

  • 컴파일 후 체크(post-compile check)는 의존성 규칙을 위반했는지 확인해준다.

  • 이러한 체크를 도와주는 자바용 도구로 ArchUnit 이 있다.

ArchUnit

도메인 계층에서 바깥쪽의 애플리케이션 계층으로 향하는 의존성이 없다는 것을 체크

class DependencyRuleTests {
}

육각형 아키텍처 내에서 관련된 모든 패키지를 명시할 수 있는 일종의 도메인 특화 언어(DSL)를 만들 수 있고, 패키지 사이의 의존성 방향이 올바른지 자동으로 체크할 수 있다.

class DependencyRuleTest {
}
  • 먼저 바운디드 컨텍스트의 부모 패키지를 지정한다. (단일 바운디드 컨텍스트라면 애플리케이션 전체에 해당한다) .

  • 그런 다음 도메인, 어댑터, 애플리케이션, 설정 계층에 해당하는 하위 패키지들을 지정한다.

  • 마지막에 호출하는 check()는 몇 가지체크를 실행하고 패키지 의존성이 의존성 규칙을 따라 유효하게 설정됐는지 검증한다

잘못된 의존성을 바로잡는 데 컴파일 후 체크가 큰 도움이 되긴 하지만, 패키지 이름에 오타를 내면 테스트가 어떤 클래스도 찾지 못하기 때문에 의존성 규칙 위반 사례를 발견하지 못한다. 컴파일 후 체크는 언제나 코드와 함께 유지보수 해야 한다.

빌드 아티팩트

지금까지 코드 상에서 아키텍쳐 경계를 구분하는 유일한 도구는 패키지였다. 모든 코드가 같은 모놀리식 빌드 아티팩트(monolithic build artifact)의 일부였던 셈이다.

빌드 아티팩트

  • 빌드 아티팩트는 빌드 프로세스의 결과물이다.

  • 빌드 도구에는 메이븐(Maven)과 그레이들(Gradle) 등이 있다.

  • 빌드 도구의 주요한 기능 중 하나는 의존성 해결(dependency resoluntion)이다.

  • 빌드 도구는 어떤 코드베이스를 빌드 아티팩트로 변환하기 위해 코드베이스가 의존하고 있는 모든 아티팩트가 사용 가능한지 확인한다.

빌드 아티팩트 with 의존성

  • 이를 활용해서 모듈과 아키텍처의 계층 간의 의존성을 강제할 수 있다.

  • 각 모듈 혹은 계층에 대해 전용 코드베이스와 빌드 아티팩트로 분리된 빌드 모듈(JAR 파일)을 만들 수 있다.

  • 각 모듈의 빌드 스크립트에서는 아키텍처에서 허용하는 의존성만 지정한다.

  • 클래스들이 클래스패스에 존재하지도 않아 컴파일 에러가 발생하기 때문에 개발자들은 더이상 실수로 잘못된 의존성을 만들 수 없다.

💡 잘못된 의존성을 막기 위해 아키텍처를 여러 개의 빌드 아티팩트로 만든다.

맨 왼쪽

  • 설정, 어댑터, 애플리케이션 계층의 빌드 아티팩로 이뤄진 기본적인 3개의 모듈 빌드 방식이 있다.

  • 설정 모듈은 어댑터 모듈에 접근할 수 있고, 어댑터 모듈은 애플리케이션 모듈에 접근할 수 있다. 설정 모듈은 암시적이고 전이적인 의존성 떄문에 애플리케이션 모듈에도 접근할 수 있다.

  • 어댑터 모듈은 영속성 어댑터뿐만 아니라 웹 어댑터도 포함하고 있다.

두번째

  • 하나의 어댑터 모듈을 여러 개의 빌드 모듈로 쪼개서 어댑터당 하나의 모듈이 되게 할 수도 있다.

세번째

  • 의존성 역전 원칙을 적용해서 포트 인터페이스만 포함하는 API 모듈을 분리해서 빼낼 수 있다.

  • 어댑터 모듈과 애플리케이션 모듈은 API 모듈에 접근할 수 있지만, 그 반대는 불가능하다.

  • 어댑터는 더이상 엔티티와 서비스에 직접 접근할 수 없고 포트를 통해서 접근해야 한다.

네번째

  • API 모듈을 인커밍 포트와 아웃고잉 포트 각각만 가지고 있는 두 개의 모듈로 쪼갤 수 있다.

  • 애플리케이션 모듈을 서비스만 가지고 있는 모듈과 도메인 엔티티만 가지고 있는 모듈로 더 쪼갤 수도 있다.

    • 엔티티가 서비스에 접근할 수 없어지고, 도메인 빌드 아티팩트에 대한 의존성을 간단하게 선언하는 것만으로도(다른 유스케이스, 다른 서비스를 가진) 다른 애플리케이션이 같은 도메인 엔티티를 사용할 수 있게 된다.

모듈을 더 세분화할수록, 모듈 간 의존성을 더 잘 제어할 수 있게 되지만 더 작게 분리할수록 모듈 간에 매핑을 더 많이 수행해야 한다.

빌드 모듈로 아키텍처 경계를 구분했을 때 장점

  1. 빌드 도구는 순환 의존성을 허용하지 않는다. 순환 의존성은 하나의 모듈에서 일어나는 변경이 잠재적으로 순환 고리에 포함된 다른 모든 모듈을 변경하게 만들며, 단일 책임 원칙을 위배하기 때문에 좋지 않다.

  2. 두 번째로, 빌드 모듈 방식에서는 다른 모듈을 고려하지 않고 특정 모듈의 코드를 격리한 채로 변경할 수 있다.

  3. 모듈 간 의존성이 빌드 스크립트에 분명하게 선언돼 있기 때문에 새로 의존성을 추가하는 일은 의식적인 행동이 된다.

하지만 이런 장점에는 빌드 스크립트를 유지보수하는 비용 수반하기 때문에 아키텍처를 여러 개의 빌드 모듈로 나누기 전에 아키텍처가 어느 정도는 안정된 상태여야 한다.

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

소프트웨어 아키텍처는 아키텍처 요소 간의 의존성을 관리하는 게 전부다.

아키텍처를 잘 유지해나가고 싶다면 의존성이 올바른 방향을 가리키고 있는지 지속적으로 확인해야 한다.

아키텍처 경계를 강제하고 시간이 지나도 유지보수하기 좋은 코드를 만들기 위해 세 가지 접근 방식 모두를 함께 조합해서 사용할 수 있다.

DISCUSSION

Last updated