728x90

 

만들면서 배우는 클린 아키텍처 책을 읽고 정리하며 소감을 적는 포스트입니다.

서론


지름길을 방지하기 위해서는 먼저 지름길 자체를 파악해야 한다. 이번에는 잠재적인 지름길에 대한 인식을 높이고 그 영향에 대해 이야기해 보자. 이 정보만 있어도 우발적으로 사용되는 지름길을 인식하고 수정할 수 있다. 또는 정당한 지름길이라면 지름길의 효과를 의식적으로 택할 수도 있다. 어떤 때는 지름길을 먼저 취하고 나중에 고치는 것이 실제로 더 경제적일 수도 있다.

 

왜 지름길은 깨진 창문 같을까?


1969년 심리학자 필립 짐바르도는 나중에 ‘깨진 창문 이론’이라고 알려진 실험을 했다. 이 이론은 삶의 많은 부분에 적용할 수 있다. 코드 작업에 적용될 때의 의미는 다음과 같다.

  • 품질이 떨어진 코드에서 작업할 때 더 낮은 품질의 코드를 추가하기가 쉽다.
  • 코딩 규칙을 많이 어긴 코드에서 작업할 때 또 다른 규칙을 어기기도 쉽다.
  • 지름길을 많이 사용한 코드에서 작업할 때 또 다른 지름길을 추가하기도 쉽다.

이 모든 것을 고려하면 ‘레거시’라고 불리는 많은 코드의 품질이 시간이 가면서 심하게 낮아졌다는 게 그리 놀라운 일은 아니다.

 

깨끗한 상태로 시작할 책임


우리는 모두 깨진 창문 시림에 무의식적으로 영향을 받는다. 그래서 가능한 한 지름길을 거의 쓰지 않고 기술 부채를 지지 않은 채로 프로젝트를 깨끗하게 시작하는 것이 중요하다. 지름길에 몰래 스며드는 순간 깨진 창문과 같아져 버려서 더 많은 지름길을 끌어들이기 때문이다.

 

소프트웨어 프로젝트는 대개 큰 비용이 들고 장기적인 노력을 필요로 하기 때문에 깨진 창문을 막는 것이 소프트웨어 개발자들의 아주 막대한 책임이다. 우리가 프로젝트를 마무리하지 못하고 다른 이들이 프로젝트를 인계받아야 할지도 모른다. 프로젝트를 인계받는 입장에서는 이 코드가 연관성이 전혀 없는 레거시이기 때문에 깨진 창문을 만들어 내기가 더 쉽다.

 

그러나 때로는 지름길을 취하는 것이 더 실용적일 때도 있다. 작업 중인 부분이 프로젝트 전체로 봤을 때 그리 중요하지 않은 부분이거나, 프로토타이핑 작업 중이거나, 경제적인 이유가 있을 수도 있다.

 

이러한 의도적인 지름길에 대해서는 세심하게 잘 기록해둬야 한다. 마이클 나이가드가 제안한 아키텍처 결정 기록(ADRs)의 형태도 괜찮다. 우리는 미래의 우리 혹은 프로젝트를 인계받는 이들에게 빚을 지고 있는 것이다. 만약 팀원 모두가 이 문서에 대해 인지하고 있다면 지름길이 합리적인 이유에 의해 의도적으로 추가됐다는 사실을 알기 때문에 깨진 창문 이론의 영향을 더 줄일 수 있을 것이다.

 

이제 헥사고날 아키텍처에서 고려해 볼 수 있는 지름길들을 몇 가지 이야기해 보자.

 

유스케이스 간 모델 공유하기


4장에서 유스케이스마다 다른 입출력 모델을 가져야 한다고 이야기한 적 있다. 즉, 입력 파라미터의 타입과 반환값의 타입이 달라야 한다는 뜻이다. 아래 그림은 두 개의 유스케이스가 같은 입력 모델을 공유하는 예를 보여준다.

  • 유스케이스 간에 입출력 모델을 공유하게 되면 유스케이스들 사이에 결합이 생긴다.

 

공유로 인한 영향은 SendMoneyUseCase와 RevokeActivityService가 결합된다는 것이다. 공유하고 있는 SendMoneyCommand 클래스가 변경되면 두 유스케이스 모두 영향을 받는다. 단일 책임 원칙에서 이야기하는 ‘변경할 이유’를 공유하는 것이다. 출력 모델을 공유하는 경우에도 마찬가지다.

 

유스케이스 간 입출력 모델을 공유하는 것은 유스케이스들이 기능적으로 묶여 있을 때 유효하다. 즉, 특정 요구사항을 공유할 때 괜찮다는 의미다. 이 경우 특정 세부사항을 변경할 경우 실제도 두 유스케이스 모두에 영향을 주고 싶은 것이다.

두 유스케이스가 서로 간에 미치는 영향 없이 독립적으로 진화해야 한다면 입출력 모델을 공유하는 방식은 지름길이 된다. 만약 독립적으로 진화해야 한다면 처음에는 똑같은 입출력 클래스를 복사해야 하더라도 일단 분리해서 시작해야 한다.

 

도메인 엔티티를 입출력 모델로 사용하기


도메인 엔티티인 Account와 인커밍 포트인 SendMoneyUseCase가 있으면 엔티티를 인커밍 포트의 입출력 모델로 사용하고 싶다는 생각이 들지도 모른다.

  • 도메인 엔티티를 유스케이스의 입출력 모델로 사용하면 도메인 엔티티가 유스케이스에 결합된다.

 

인커밍 포트는 도메인 엔티티에 의존성을 가지고 있다. 그 결과, Account 엔티티는 변경할 또 다른 이유가 생겼다.

 

 

Account 엔티티는 인커밍 포트인 SendMoneyUseCase에 의존성이 없으니 인커밍 포트가 어떻게 엔티티를 변경할 이유가 된다는 뜻일까?

현재 Account 엔티티에는 존재하지 않는 정보를 유스케이스가 필요로 한다고 생각해 보자. 이 정보는 최종적으로 Account 엔티티에 저장돼 있어야 하는 것이 아니라 다른 도메인이나 다른 바운디드 컨텍스트에 저장돼야 한다. 그럼에도 불구하고 이미 유스케이스 인터페이스에서 사용할 수 있기 때문에 Account 엔티티에 새로운 필드를 추가하고 싶다는 생각이 든다.

 

간단한 생성이나 업데이트 유스케이스에서는 유스케이스 인터페이스에 도메인 엔티티가 있는 것이 괜찮을지도 모른다. 데이터베이스에 저장해야 하는 바로 그 상태 정보가 엔티티에 있기 때문이다.

 

하지만 유스케이스가 단순히 데이터베이스의 필드 몇 개를 업데이트하는 수준이 아니라 더 복잡한 도메인 로직을 구현해야 한다면, 유스케이스 인터페이스에 대한 전용 입출력 모델을 만들어야 한다. 왜냐하면 유스케이스의 변경이 도메인 엔티티까지 전파되길 바라진 않을 것이기 때문이다.

 

이 지름길이 위험한 이유는 많은 유스케이스가 간단한 생성 또는 업데이트 유스케이스로 시작해서 시간이 지나면서 복잡한 도메인 로직 괴물이 되어간다는 사실 때문이다. 이는 최소 기능 제품으로 시작해서 점점 복잡도를 높여가는 애자일 환경에서 특히 그렇다. 그러므로 처음에는 도메인 엔티티를 입력 모델로 사용했더라도 도메인 모델로부터 독립적인 전용 입력 모델로 교체해야 하는 시점을 잘 파악해야 한다.

 

인커밍 포트 건너뛰기


아웃고잉 포트는 애플리케이션 계층과 아웃고잉 어댑터 사이의 의존성을 역전시키기 위한 필수 요소인 반면, 인커밍 포트는 의존성 역전에 필수적인 요소는 아니다. 인커밍 어댑터가 인커밍 포트 없이 애플리케이션 서비스에 직접 접근하도록 할 수 있다.

  • 인커밍 포트가 없으면 도메인 로직의 진입점이 불분명해진다.

 

인커밍 포트를 제거함으로써 인커밍 어댑터와 애플리케이션 계층 사이의 추상화 계층을 줄였다. 보통 추상화 계층을 줄이는 것은 괜찮게 느껴진다.

 

하지만 인커밍 포트는 애플리케이션 중심에 접근하는 진입점을 정의한다. 이를 제거하면 특정 유스케이스를 구현하기 위해 어떤 서비스 메서드를 호출해야 할지 알아내기 위해 애플리케이션의 내부 동작에 대해 더 잘 알아야 한다. 전용 인커밍 포트를 유지하면 한눈에 진입점을 식별할 수 있다. 이는 새로운 개발자가 코드를 파악할 때 특히 더 도움이 된다.

 

인커밍 포트를 유지해야 하는 또 다른 이유는 아키텍처를 쉽게 강제할 수 있기 때문이다. 10장에서 소개한 아키텍처를 강제하는 옵션들을 이용하면 인커밍 어댑터가 애플리케이션 서비스가 아닌 인커밍 포트만 호출하게 할 수 있다. 그럼 애플리케이션 계층에 대한 모든 진입점을 정의하는 것이 아주 의식적인 결정이 된다. 인커밍 어댑터에서 호출할 의도가 없던 서비스 메서드를 실수로 호출하는 일이 절대 발생할 수 없다.

 

애플리케이션 규모가 작거나 인커밍 어댑터가 하나밖에 없어서 모든 제어 흐름을 인커밍 포트의 도움 없이 단숨에 파악할 수 있다면 인커밍 포트가 없는 것이 편하다. 그러나 애플리케이션의 규모가 이후로도 작게 유지된다고 확신할 수 있을까?

 

애플리케이션 서비스 건너뛰기


어떤 유스케이스에서는 애플리케이션 계층을 통째로 건너뛰고 싶을 수도 있다.

  • 애플리케이션 서비스가 없으면 도메인 로직을 둘 곳이 없다.

그림에서 아웃고잉 어댑터에 있는 AccountPersistenceAdapter 클래스는 직접 인커밍 포트를 구현해서 일반적으로 인커밍 포트를 구현하는 애플리케이션 서비스를 대체한다.

 

간단한 CRUD 유스케이스에서는 보통 애플리케이션 서비스가 도메인 로직 없이 생성, 업데이트, 삭제 요청을 그대로 영속성 어댑터에 전달하기 때문에 정말 구미가 당기는 방법이다. 그대로 전달하는 대신 영속성 어댑터가 직접 유스케이스를 구현하게 할 수 있다.

 

하지만 이 방법은 인커밍 어댑터와 아웃고잉 어댑터 사이에 모델을 공유해야 한다. 이 경우에 공유해야 하는 모델이 Account 도메인 엔티티이므로 앞에서 이야기한 도메인 모델을 입력 모델로 사용하는 케이스가 되는 것이다.

 

나아가 애플리케이션 코어에 유스케이스라고 할 만한 것이 없어진다. 만약 시간이 지남에 따라 CRUD 유스케이스가 점점 복잡해지면 도메인 로직을 그대로 아웃고잉 어댑터에 추가하고 싶은 생각이 들 것이다. 이미 유스케이스가 어댑터에 있으니 말이다. 이렇게 되면 도메인 로직이 흩어져서 도메인 로직을 찾거나 유지보수하기가 어려워진다.

 

결국 단순히 전달만 하는 보일러플레이트 코드가 가득한 서비스가 많아지는 것을 방지하기 위해 간단한 CRUD 케이스에서는 애플리케이션 서비스를 건너뛰기로 결정할 수도 있다. 하지만 유스케이스가 엔티티를 단순히 생성, 업데이트, 삭제하는 것보다 더 많은 일을 하게 되면 애플리케이션 서비스를 만든다는 명확한 가이드라인을 팀에 정해둬야 한다.

 

요약


경제적인 관점에서 지름길이 합리적일 때도 있다. 이번 장에서는 지름길을 사용할지 여부를 결정하는 데 도움이 되도록 지름길을 사용한 결과에 대한 식견을 제공했다.

 

간단한 CRUD 유스케이스에 대해서는 전체 아키텍처를 구현하는 것이 지나치게 느껴지기 때문에 지름길의 유혹을 느낄 수 있다. 하지만 모든 애플리케이션은 처음에는 작게 시작하기 때문에, 유스케이스가 단순한 CRUD 상태에서 벗어나는 시점이 언제인지에 대해 팀이 합의하는 것이 매우 중요하다. 합의를 이루고 난 후에야 팀은 지름길을 장기적으로 더 유지보수하기 좋은 아키텍처로 대체할 수 있다.

 

어떤 경우든 아키텍처에 대해, 그리고 왜 특정 지름길을 선택했는가에 대한 기록을 남겨서 나중에 우리 자신 또는 프로젝트를 인계받는 이들이 이 결정에 대해 다시 평가할 수 있게 하자.

 

 

728x90

+ Recent posts