클린 아키텍처 (Chapter 27~34) - Robert C. Martin


27. ‘크고 작은 모든’ 서비스들

서비스 지향 ‘아키텍처’와 마이크로서비스 ‘아키텍처’는 최근에 큰 인기를 끌고 있다. 그 이유는 다음과 같다.

  • 서비스를 사용하면 상호 결합이 철저하게 분리되는 것처럼 보인다. 나중에 보겠지만, 이는 일부만 맞는 말이다.
  • 서비스를 사용하면 개발과 배포 독립성을 지원하는 것처럼 보인다. 나중에 보겠지만, 이 역시도 일부만 맞는 말이다.

서비스 아키텍처?
먼저 서비스를 사용한다는 것이 본질적으로 아키텍처에 해당하는지에 대해 생각해 보자. 이 개념은 명백히 사실이 아니다.
시스템의 아키텍처는 의존성 규칙을 준수하며 고수준의 정책을 저수준의 세부사항으로부터 분리하는 경계에 의해 정의된다.

단순히 애플리케이션의 행위를 분리할 뿐인 서비스라면 값비싼 함수 호출에 불과하며, 이키텍처 관점에서 꼭 중요하다고 볼 수는 없다.

모든 서비스가 반드시 아키텍처 관점에서 중요해야만 한다는 뜻은 아니다. 기능을 프로세스나 플랫폼에 독립적이 되게끔 서비스들을 생성하면 의존성 규칙 준수 여부와 상관없이 큰 도움이 될 때가 많다. 그러나 서비스 그 자체로는 아키텍처를 정의하지 않는다.

모노리틱 시스템이나 컴포넌트 기반 시스템에서 아키텍처를 정의하는 요소는 바로 의존성 규칙을 따르며 아키텍처 경계를 넘나드는 함수 호출들이다.
반면 시스템의 나머지 많은 함수들은 행위를 서로 분리할 뿐이며, 이키텍처적으로는 전혀 중요하지 않다.

서비스도 마찬가지다. 결국 서비스는 프로세스나 플랫폼 경게를 가로지르는 함수 호출에 지나지 않는다.
아키텍처적으로 중요한 서비스도 있지만, 중요하지 않은 서비스도 존재한다. 이 장에서 관심을 가지는 서비스는 전자다.

서비스의 이점?

  • 결합 분리의 오류
    시스템을 서비스들로 분리함으로써 얻게 되리라 예상되는 큰 이점 하나는 서비스 사이의 결합이 확실히 분리된다는 점이다. 어쨋든 각 서비스는 서로 다른 프로세스에서, 심지어는 서로 다른 프로세서에서 실행된다. 따라서 서비스는 다른 서비스의 변수에 직접 접근할 수 없다. 그리고 모든 서비스의 인터페이스는 반드시 잘 정의되어 있어야 한다.

    이 말에는 어느 정도 일리가 있지만, 꼭 그런 것만은 아니다. 프로세서 내의 또는 네트워크 상의 공유 자원 때문에 결합될 가능성이 여전히 존재한다.
    더욱이 서로 공유하는 데이터에 의해 이들 서비스는 강력하게 결합되어 버린다. 또한 이 서비스들은 이 필드에 담긴 데이터를 해헉하는 방식을 사전에 완벽하게 조율해야 한다.

  • 개발 및 배포 독립성의 오류
    서비스를 사용함에 따라서 예측되는 또 다른 이점은 전담팀이 서비스를 소유하고 운영한다는 점이다. 그래서 데브옵스(Devops) 전략의 일환으로 전담팀에서 각 서비스를 작성하고, 유지보수하며, 운영하는 책임을 질 수 있다. 이러한 개발 및 배포 독립성은 확장 가능한(Scalable)것으로 간주된다. 대규모 엔터프라이즈 시스템을 독립적으로 개발하고 배포 가능한 수십, 수백, 수천 개의 서비스들을 이용하여 만들 수 있다고 믿는다. 시스템의 개발, 유지보수, 운영 또한 비슷한 수의 독립적인 팀 단위로 분할할 수 있다고 여긴다.

    이러한 믿음에도 어느 정도 일리가 있지만, 극히 일부일 뿐이다.
    첫째로, 대규모 엔터프라이즈 시스템은 서비스 기반 시스템 이외에도, 모노리틱 시스템이나 컴포넌트 기반 시스템으로도 구축할 수 있다는 사실은 증명되어 왔다.
    서비스는 확장 가능한 시스템을 구축하는 유일한 선택지가 아니다.

    둘째, ‘결합 분리의 오류’에 따르면 서비스라고 해서 항상 독립적으로 개발하고, 배포하며, 운영할 수 있는 것은 아니다.
    데이터나 행위에서 어느 정도 결합되어 있다면 결합된 정도에 맞게 개발, 배포, 운영을 조정해야만 한다.

결론
서비스는 시스템의 확장성과 개발 가능성 측면에서 유용하지만, 그 자체로는 아키텍처적으로 그리 중요한 요소는 아니다. 시스템의 아키텍처는 시스템 내부에 그어진 경계와 경계를 넘나드는 의존성에 의해 정의된다.



28. 테스트 경계

시스템 컴포넌트인 테스트
테스트는 태생적으로 의존성 규칙을 따른다. 테스트는 세부적이며 구체적인 것으로, 의존성은 항상 테스트 대상이 되는 코드를 향한다. 실제로 테스트는 아키텍처에서 가장 바깥쪽 원으로 생각할 수 있다. 시스템 내부의 어떤 것도 테스트에는 의존하지 않으며, 테스트는 시스템의 컴포넌트를 향해, 항상 원의 안쪽으로 의존한다.

테스트를 고려한 설계
개발자는 종종 테스트가 시스템의 설계 범위 밖에 있다고 여긴다. 이 관점은 치명적이다. 테스트가 시스템의 설계와 잘 통합되지 않으면, 테스트는 깨지기 쉬워지고, 시스템은 뻣뻣해져서 변경하기가 어려워진다.

시스템에 가한 간단한 변경이 대량의 테스트 실패로 이어진다는 사실을 알게 되면, 개발자는 그러한 변경을 하지 않으려 들 것이다.

이 문제를 해결하려면 테스트를 고려해서 설계해야 한다. 소프트웨어 설계의 첫 번째 규칙은 언제나 같다. 변동성이 있는 것에 의존하지 말라. GUI는 변동성이 크다. GUI로 시스템을 조작하는 테스트 스위트는 분명 깨지기 쉽다. 따라서 시스템과 테스트를 설계할 때, GUI를 사용하지 않고 업무 규칙을 테스트할 수 있게 해야 한다.

테스트 API
이 목표를 달성하려면 테스트가 모든 업무 규칙을 검증하는 데 사용할 수 있도록 특화된 API를 만들면 된다. 이러한 API는 보안 제약사항을 무시할 수 있으며, (데이터베이스와 같은) 값 비싼 자원은 건너뛰고, 시스템을 테스트 가능한 특정 상태로 강제하는 강력한 힘을 지녀야만 한다.

테스트 API는 테스트를 애플리케이션으로부터 분리할 목적으로 사용한다. 단순히 테스트를 UI에서 분리하는 것만이 아닌, 테스트 구조를 애플리케이션 구조로부터 결합을 분리하는게 목표다.

구조적 결합
구조적 결합은 테스트 결합 중에서 가장 강하며, 가장 은밀하게 퍼져 나가는 유형이다.
모든 상용 클래스에 테스트 클래스가 각각 존재하고, 또 모든 상용 메서드에 테스트 메서드 집합이 각각 존재하는 테스트 스위트가 있다고 가정해 보자. 이러한 테스트 스위트는 애플리케이션 구조에 강하게 결합되어 있다.

상용 클래스나 메서드 중 하나라도 변경되면 딸려 있는 다수의 테스트가 변경되어야 한다. 결과적으로 테스트는 깨지기 쉬워지고, 이로 인해 상용 코드를 뻣뻣하게 만든다.

결론
테스트는 시스템 외부에 있지 않다. 오히려 시스템의 일부다. 따라서 테스트에서 기대하는 안정성과 회귀의 이점을 얻을 수 있으려면 테스트는 잘 설계 돼야만 한다. 테스트를 시스틈의 일부로 설계하지 않으면 테스트는 깨지기 쉽고 유지보수하기 어려워지는 경향이 있따. 이러한 테스트는 유지보수하기가 너무 힘들기 때문에 결국 방바닥의 휴지처럼 버려지는 최후를 맡는다.



29. 클린 임베디드 아키텍처

더그 슈미트(Doug Schmidt)는 다음과 같이 주장했다.

소프트웨어는 닳지 않지만, 펌웨어와 하드웨어는 낡아 가므로 결국 소프트웨어도 수정해야 한다.

임베디드 시스템을 조금이라도 개발해 봤다면 하드웨어는 계속해서 발전하고 또 개선된다는 사실을 알고 있을 것이다. 그와 동시에 ‘소프트웨어’에는 새로운 기능들이 추가되면서 복잡도가 계속해서 증가한다.

그래서 더그의 주장에 다음과 같이 덧붙이고자 한다.

소프트웨어는 닳지 않지만, 펌웨어와 하드웨어에 대한 의존성을 관리하지 않으면 안으로부터 파괴될 수 있다.

잠재적으로 오래 살아남을 수 있던 임베디드 소프트웨어가 하드웨어 의존성에 오염되는 바람에 짧게 삶을 마감하는 일은 드물지 않다.

앱-티튜드 테스트(App-titude test)
왜 잠재적인 임베디드 소프트웨어는 그렇게도 많이 펌웨어로 변하는가? 엠베디드 코드가 동작하게 만드는 데 대부분의 노력을 집중하고, 오랫동안 유용하게 남도록 구조화하는 데는 그리 신경 쓰지 않기 때문으로 보인다.

켄트 백(Kent Beck)은 소프트웨어를 구축하는 세 가지 활동을 다음과 같이 기술했다. (인용된 문장은 켄트가 한 말이며, 그 옆에 덧붙인 말은 내 해설이다.)

  • “먼저 동작하게 만들어라.” 소프트웨어가 동작하지 않는다면 사업은 망한다.
  • “그리고 올바르게 만들어라.” 코드를 리팩터링해서 당신을 포함한 나머지 사람들이 이해할 수 있게 만들고, 요구가 변경되거나 요구를 더 잘 이해하게 되었을 때 코드를 개선할 수 있게 만들어라.
  • “그리고 빠르게 만들어라.” 코드를 리팩터링해서 ‘요구되는’ 성능을 만족시켜라.

현장에서 지켜본 수많은 임베디드 시스템 소프트웨어는 “동작하게 하라”는 활동만을 염두에 두고 작성된 것처럼 보인다.

이러한 문제들은 임베디드 소프트웨어만 국한되지 않는다. 임베디드가 아닌 대다수의 앱들도 코드를 올바르게 작성해서 유효 수명을 길게 늘리는데는 거의관심 없이, 그저 동작하도록 만들어진다.

앱이 동작하도록 만드는 것을 나는 개발자용 앱-티튜드 테스트(App-titude test)라고 부른다. 프로그래머가 오직 앱이 동작하도록 만드는 일만 신경 쓴다면 자신의 제품과 고용주에게 몹쓸 짓을 하는 것이다. 프로그래밍에는 단순히 앱이 동작하도록 만드는 것보다 중요한 것이 훨씬 많다.

결론
모든 코드가 펌웨어가 되도록 내버려두면 제품이 오래 살아남을 수 없게 된다. 오직 타깃 하드웨어에서만 테스트할 수 있는 제품도 마찬가지다. 클린 임베디드 아키텍처는 제품이 장기간 생명력을 유지하는 데 도움을 준다.



세부사항

30. 데이터베이스는 세부사항이다

아키텍처 관점에서 볼 때 데이터베이스는 엔티티가 아니다. 즉, 데이터베이스는 세부사항이라서 아키텍처의 구성요소 수준으로 끌어올릴 수 없다. 소프트웨어 시스템의 아키텍처와 데이터베이스의 관계를 건물로 비교하면 건물의 아키텍처와 문 손잡이의 관계와 같다.

데이터베이스는 데이터에 접근할 방법을 제공하는 유틸리티다. 아키텍처 관점에서 보면 이러한 유틸리티는 저수준의 세부사항일 뿐이라서 아키텍처와는 관련이 없다. 그리고 뛰어난 아키텍트라면 저수준의 메커니즘이 시스템 아키텍처를 오염시키는 일을 용납하지 않는다.

관계형 데이터베이스
관계형 데이터베이스는 데이터를 저장하고 접근하는 데 탁월한 기술이었다.

하지만 관계형 데이터베이스의 기술이 얼마나 뛰어나든, 유용하든, 아니면 수핮거으로 견고하든, 결국은 그저 기술일 뿐이다. 그리고 이는 관계형 데이터베이스가 세부사항임을 뜻한다.

관계형 테이블은 특정한 형식의 데이터에 접근하는 경우에는 편리하지만, 데이터를 테이블에 행 단위로 배치한다는 자체는 아키텍처적으로 볼 때 전혀 중요하지 않다. 데이터가 테이블 구조를 가진다는 사실은 오직 아키텍처 외부 원에 위치한 최하위 수준의 유틸리티 함수만 알야아 한다.

많은 데이터 접근 프레임워크가 테이블과 행이 객체 형태로 시스템 여기저기에서 돌아다니게 허용하는데, 아키텍처적으로 잘못된 설계다. 이렇게 하면 유스케이스, 업무 규칙, 심지어는 UI조차도 관계형 데이터 구조에 결합되어 버린다.

데이터베이스 시스템은 왜 이렇게 널리 사용되는가?
오라클, MySQL, SQL 서버가 우위를 차지할 수 있던 이유는 무엇일까? 한마디로 답하자면, 바로 ‘디스크’ 때문이었다.

디스크에서 데이터는 원형 트랙에 저장된다. 트랙은 섹터로 분할되고, 각 섹터는 사용하기 편한 크기의 바이트를 저장했는데, 대채로 4K였다. 각 플래터는 대략 수백 개의 트랙으로 구성되었고, 디스크는 십여 개의 플래터로 구성되었다. 디스크에서 특정 바이트를 읽으려면, 먼저 헤드를 적절한 트랙으로 옮기고, 디스크가 돌면서 헤드 위치에 적절한 섹터가 올 때 까지 기다린 후, 해당 섹터에서 4K 모두를 RAM으로 읽어 들여야 한다. 그런 후 해당 RAM 버퍼의 색인을 찾아서 필요한 바이트를 가져왔다. 이 모든 작업에는 밀리초 단위의 시간이 걸렸다.

밀리초가 그리 길지 않다고 느낄 수도 있지만, 대다수의 프로세서에서 한 명령어를 처리하는 주기와 비교해 보면 백만 배나 오래 걸렸다.

이처럼 디스크 때문에 피해갈 수 없는 시간 지연이라는 점을 완화하기 위해, 색인, 캐시, 쿼리 계획 최적화가 필요해졌다. 그리고 데이터를 표현하는 일종의 표준적인 방식도 필요했는데, 이러한 색인, 캐시, 쿼리 계획에서 작업 중인 대상이 어떤 데이터인지 알 수 있어야 했기 때문이다. 시간이 지나면서 이러한 시스템은 뚜렷이 구분되는 두 가지 유형으로 분리되었다. 하나는 파일 시스템이었고, 다른 하나는 관계형 데이터베이스 관리 시스템(RDBMS)이었다.

파일 시스템은 문서(Document)기반이다. 파일 시스템은 문서 전체를 자연스럽고 편리하게 저장하는 방법을 제공한다. 일련의 문서를 이름을 기준으로 저장하거나 조회할 때는 잘 동작하지만, 내용을 기준으로 검색할 때는 그리 크게 도움되지 않는다.

데이터베이스 시스템은 내용 기반이다. 데이터베이스 시스템은 내용을 기반으로 레코드를 자연스럽고 편리하게 찾는 방법을 제공한다. 레코드가 서로 공유하는 일부 내용에 기반해서 다수의 레코드를 연관 짓는 데 매우 탁월하다. 하지만 안타깝게도 정형화되지 않은 문서를 저장하고 검색하는 데는 대체로 부적합하다.

이들 두 시스템은 데이터를 디스크에 체계화해서, 각 시스템에 특화된 방식으로 접근해야 할 때 가능한 한 효율적으로 데이터를 저장하고 검색할 수 있도록 한다. 각 시스템은 데이터를 색인하고 배치하는 고유한 전략을 활용한다. 데이터를 빠르게 조작할 수 있도록 결국에는 관련있는 데이터를 RAM으로 가져온다.

디스크가 없다면 어떻게 될까?
디스크는 RAM으로 대체되고 있다. 디스크가 모두 사라진다면, 그래서 모든 데이터가 RAM에 저장된다면 데이터를 어떻게 체게화할 것인가? 데이터를 테이블 구조로 만들어 SQL을 이용해 접근할 것인가? 파일 구주로 만들어 디렉터리를 통해 접근할 것인가?

당연히 아니다. 이 데이터들을 연결 리스트, 트리, 해시 테이블, 스택, 큐 혹은 여타 무수히 맣ㄴ은 데이터 구조로 체계화할 것이며, 데이터에 접근할 때는 포인터나 참조를 사용할 것이다. 이것이 프로그래머가 하는 일이기 때문이다.

당신은 이미 이렇게 일하고 있다는 사실을 알아챌 것이다. 데이터가 데이터베이스나 파일 시스템에 있더라도, RAM으로 읽은 후에는 다루기 편리한 형태로 그 구조를 변경한다. 리스트, 집합, 스택, 큐, 트리 등 입맛에 맞는 임의의 구조로 말이다.

세부사항
데이터베이스가 세부사항이라고 말하는 이유는 바로 이러한 현실 때문이다. 데이터베이스는 그저 메커니즘에 불과하며, 디스크 표면과 RAM사이에서 데이터를 이리저리 옮길 때 사용할 뿐이다. 실제로 데이터베이스는 비트를 담는 거대한 그릇이며, 데이터를 장기적으로 저장하는 공간에 지나지 않는다.

따라서 아키텍처 과넞ㅁ에서 본다면 회전식 자기 디스크에 데이터가 있기만 한다면, 데이터가 어떤 형태인지는 절대로 신경 써서는 안된다.

하지만 성능은?
데이터 저장소에서 데이터를 빠르게 넣고 뺄 수 있어야 하는 것은 맞지만, 이는 저수준의 관심사다. 이 관심사는 저수준의 데이터 접근 메커니즘 단에서 다룰 수 있다. 성능은 시스템의 전반적인 아키텍처와는 아무런 관련이 없다.

결론
체계화된 데이터 구조와 데이터 모델은 아키텍처적으로 중요하다. 반면, 그저 데이터를 회전식 자기 디스크 표면에서 이리저리 옮길 뿐인 기술과 시스템은 아키텍처적으로 중요치 않다. 데이터를 테이블 구조로 만들고 SQL로만 접근하도록 하는 관계형 데이터베이스 시스템은 전자보다는 후자와 훨씬 관련이 깊다. 데이터는 중요하다. 데이터벵시ㅡ는 세부사항이다.



31. 웹은 세부사항이다

1960년대 이래로 우리 업계는 일련의 반복되는 진동을 겪어왔고, 현재 우베은 그저 이러한 진동의 맨 끝에 있을 뿐이다. 이 진동은 모든 연산 능력을 중앙 서버에 두는 방식과 모든 연산 능력을 단말에 두는 방식 사이에서 끊임없이 움직여 왔다.

웹이 유명세를 탄 이래 지난 십여 년 사이에도 우리는 이러한 진동을 수차례 목격했다. 처음에는 모든 연산 능력이 서버 밤에 위치할 것이고 브라우저는 멍청해질 거라고 생각했다. 그러다가 브라우저에 애플릿(applet)을 추가하기 시작했다. 하지만 그러다가 다시 이 방식이 내키지 않았기에 동적인 내용은 다시 서버로 이동시켰다. 하지만 그러다가 다시 이 방식이 마음에 들지 않아서 웹 2.0을 고안했고, Ajax와 자바스크립트를 이용해서 처리 과정의 많은 부분을 다시 브라우저로 옮겼다. 지금은 거대한 애플리케이션 전부를 브라우저에서 실행되도록 작성할 수 잇는 수준까지 다다랐다. 그리고 이제 우리 모두는 노드(Node.js)를 이용해 자바스크립트를 다시 서버로 이동시키는 방식에 열광해 있다.

끝없이 반복하는 추
물론 이처럼 반복되는 진동이 웹으로부터 시작되었다고 보는 일은 옳지 않다.
앞으로도 우리는 연산 능력을 어디에 둘지 알 수 없을 것이다. 연산 능력을 중앙에 집중하는 방식과 분산하는 방식 사이에서 우리는 끊임없이 움직인다.

IT 역사 전체로 시야를 넓히면 웹은 아무것도 바꾸지 않았다. 우베은 우리가 발버둥치면서 생기는 여느 수많은 진동 중 하나에 불과하다. 이 진동은 우리가 태어나기 전에도 있어 왔고, 우리가 은퇴한 뒤에도 지속될 것이다.
아키텍트로서 우리는 멀리 내다봐야 한다. 이 진동은 그저 핵심 업무 규칙의 중심에서 밀어내고 싶은 단기적인 문제일 뿐이다.

요약
요약하면 다음과 같다. GUI는 세부사항이다. 웹은 GUI다. 따라서 우베은 세부사항이다. 그리고 아키텍트라면 이러한 세부사항을 핵심 업무 로직에서 분리된 경계 바깥에 두어야 한다.

웹이라면 예외가 될 수도 있찌 않을까?
그 근거로 웹과 같은 GUI는 너무 특이하고 다채롭기 때문에 장치 독립적인 아키텍처를 추구하는 일이 터무니없다고 말할 수도 있다. 자바스크립트의 유효성 검증이나 드래그-앤-드롭 방식의 AJAX호출, 그리고 웹 페이지에 넣을 수 있는 다른 무수한 위젯과 가젯으로 인한 복잡함을 생각해 볼 때, 웹에서 장치 독립성은 비현실적이라고 주장하기 쉽다.

이 주장이 어느 정도는 옳다. 애플리케이션과 GUI의 상호작용은 ‘빈번하며’, 또한 이러한 상호작용 방식도 사용 중인 GUI 종류에 따라 차이가 매우 크다. 브라우저와 웹 애플리케이션이 함께 추는 춤은 데스크톱 GUI와 데스크톱 애플리케이션이 함께 추는 춤과는 차이가 난다. 이 같은 춤을 추상화하려는 시도는 유닉스로부터 장치를 추상화했던 것과는 달리 성공할 가능성이 없어 보인다.

하지만 UI와 애플리케이션 사이에는 추상화가 가능한 또 다른 경계가 존재한다.

완전한 입력 데이터와 그에 따른 출력 데이터는 데이터 구조로 만들어서 유스케이스를 실행하는 처리 과정의 입력 값과 출력 값으로 사용할 수 있다. 이 방식을 따르면 각 유스케이스가 장치 독립적인 방식으로 UI라는 입출력 장치를 동작시킨다고 간주할 수 있다.

결론
이러한 종류의 추상화는 만들기 쉽지 않고, 제대로 만들려면 수차례의 반복 과정을 거쳐야 할 것이다. 하지만 가능하다. 그리고 세상은 마케팅 귀재로 가득하기 때문에 이러한 추상화가 꼭 필요할 때가 많다고 주장하기는 어렵지 않다.



32. 프레임워크는 세부사항이다

프레임워크는 상당한 인기를 끌고 있다. 일반적으로 말하자면 좋은 현상이다. 하지만 아무리 해도 프레임워크는 아키텍처가 될 수 없다.

위험 요인
고려해야 할 위험 요인들은 다음과 같다.

  • 프레임워크의 아키텍처는 그다지 깔끔하지 않은 경우가 많다. 프레임워크는 의존성 규칙을 위반하는 경향이 있다. 업무 객체를 만들 때, 프레임워크 제작자는 자신의 코드를 상속할 것을 요구한다. 당신만의 고유한 엔티티를 만들 때 말이다. 프레임워크 제작자는 자신의 프레임워크가 당신의 가장 안쪽 원과 결합되기를 원한다. 하지만 프레임워크가 한 번 안으로 들어가버리면 다시는 원 밖으로 나오지 않을 것이다.

해결책
프레임워크를 사용할 수는 있다. 다만 프레임워크와 결합해서는 안 된다. 프레임워크는 아키텍처의 바깥쪽 원에 속하는 세부사항으로 취급하라. 프레임워크가 아키텍처의 안쪽 원으로 들어오지 못하게 하라.

업무 객체를 만들 때 프레임워크가 자신의 기반 클래스로부터 파생하기를 요구한다면, 거절하라! 대신 Proxy를 만들고, 업무 규칙에 플러그인할 수 있는 컴포넌트에 이들 Proxy를 위치시켜라.

프레임워크가 핵심 코드 안으로 들어오지 못하게 하라. 대신 핵심 코드에 플러그인할 수 있는 컴포넌트에 프레임워크를 통합하고, 의존성 규칙을 준수하라.

스프링은 훌륭한 의존성 주입 프레임워크. 아마도 의존성을 연결할 때 스프링의 오토-와이어링(auto-wiring)기능을 사용할 것이다. 이 방법도 괜찮지만, @autowired 어노테이션이 업무 객체 도처에 산재해서는 안 된다. 업무 객체는 절대로 스플링에 대해 알아서는 안 된다.

결론
가급적이면 프레임워크를 가능한 한 오랫동안 아키텍처 경계 너머에 두자. 아마 젖소를 사지 않고도 우유를 얻는 방법을 찾을 수 있을 것이다.



34. 빠져 있는 장

계층 기반 패키지
아마도 가장 단순한 첫 번째 설계 방식은 전통적인 수평 계층형 아키텍처다. 기술적인 관점에서 해당 코드가 하는 일에 기반해 그 코드를 분할한다.
흔히 우리는 이 방식을 ‘계층 기반 패키지’라고 부른다.

clean_archit044

그림의 UML 클래스 다이어그램에서 계층 기반 패키지가 어떤 모습인지 볼 수 있다.

이 전형적인 계층형 아키텍처에는 웹, ‘업무 규칙’, 영속성 코드를 위해 계층이 각각 하나씩 존재한다. 다시 말해 코드는 계층이라는 얇은 수평 조각으로 나뉘며, 각 계층은 유사한 종류의 것들을 묶는 도구로 사용된다. ‘엄격한 계층형 아키텍처’의 경우 계층은 반드시 바로 아래 계층에만 의존해야 한다. 자바의 경우 계층은 주로 패키지로 구현된다.

  • OrdersController: 웹 컨트롤러이며, 웹 기반 요청을 처리한다. Spring MVC컨트롤러 등이 여기 해당한다.
  • OrdersService: 주문 관련 ‘업무 규칙’을 정의하는 인터페이스
  • OrdersServiceImpl: OrdersService의 구현체
  • OrdersRepository: 영구 저장된 주문 정보에 접근하는 방법을 정의하는 인터페이스
  • JdbcOrdersRepository: OrderRepository 인터페이스의 구현체

이 아키텍처는 엄청난 복잡함을 겪지 않고도 무언가를 작동시켜 주는 아주 빠른 방법이다. 문제는, 마틴이 지적했듯이 소프트웨어가 커지고 복잡해지기 시작하면, 머지 않아 큰 그릇 세 개만으로는 모든 코드를 담기엔 부족하다는 사실을 깨닫고, 더 잘게 모듈화해야 할지를 고민하게 될 것이다.


기능 기반 패키지
코드를 조직화하는 또 다른 선택지로 ‘기능 기반 패키지’ 구조도 있다. 이는 서로 연관된 기능, 도메인 개념, 또는 Aggregate Root에 기반하여 수직의 얇은 조각으로 코드를 나누는 방식이다.

clean_archit045

그림에서 보듯이 등장하는 인터페이스와 클래스는 이전과 같지만, 모두가 (세 개가 아닌) 단 하나의 패키지에 속하게 된다. 이는 ‘계층 기반 패키지’를 아주 간단히 리팩터링한 형태지만, 이제 코드의 상위 수준 구조가 업무 도메인에 대해 무언가를 알려주게 된다.

또 다른 이점으로, ‘주문 조회하기’ 유스케이스가 변경될 경우 변경해야 할 코드를 모두 찾는 작업이 더 쉬워질 수 있다. 변경해야 할 코드가 여러 군데 퍼져 있지 않고 모두 한 패키지에 담겨 있기 때문이다.

나는 소프트웨어 개발팀이 수평적 계층화(계층 기반 패키지)의 문제를 깨닫고, 수직적 계층화(기능 기반 패키지)로 전환하는 걸 자주 목격했다. 내가 보기에 두 접근법은 모두 차선책이다. 이 책을 지금까지 읽었다면, 분명 이보다 훨씬 잘할 수 있을 거라고 생각할 것이다.


포트와 어댑터
엉클 밥에 따르면, ‘포트와 어댑터(Ports and Adapters)’혹은 ‘육각형 아키텍처(Hexagonal Architecture)’, ‘경계, 컨트롤러, 엔티티(BCE)’등의 방식으로 접근하는 이유는 업무/도메인에 초점을 둔 코드가 프레임워크나 데이터베이스 같은 기술적인 세부 구현과 독립적이며 분리된 아키텍처를 만들기 위해서다.

clean_archit046

요약하자면 그림에서 제시하는 것처럼, 그런 코드 베이스는 ‘내부(도메인)’와 ‘외부(인프라)’로 구성됨을 흔히 볼 수 있다. ‘내부’영역은 도메인 개념을 모두 포함하는 반면, ‘외부’영역은 외부 세계 (예를 들며 UI, 데이터베이스, 서드파티 통합)와의 상호작용을 포함한다. 여기서 주요 규칙은 바로 ‘외부’가 ‘내부’에 의존하며, 절대 그 반대로는 안 된다는 점이다

clean_archit047

그림은 ‘주문 조회하기’ 유스케이스를 이 방식으로 구현한 모습이다.

여기에서 com.mycompany.myapp.domain 패키지가 ‘내부’이며, 나머지 패키지는 모두 ‘외부’다. 의존성이 ‘내부’를 향해 흐르는 모습에 주목하라. 이전 다이어그램의 OrderRepository가 Orders라는 간단한 이름으로 바뀌었음을 눈치챘을 것이다. 이는 도메인 주도 설계라는 세계관에서 비롯된 명ㅁ여법으로, 도메인 주도 설계에서는 ‘내부’에 존재하는 모든 것의 이름은 반드시 ‘유비쿼터스 도메인 언어(Ubiquitous domain language)’관점에서 기술하라고 조언한다. 바꿔 말하면, 도메인에 대해 논의할 때 우리는 ‘주문’에 대해 말하는 것이지, ‘주문 레파지토리’에 대해 말하는 것이 아니다.


컴포넌트 기반 패키지
계층형 아키텍처를 좋지 않은 아키텍처로 여겨야 하는 이유를 몇 가지 들었지만, 이게 전부는 아니다. 계층형 아키텍처의 목적은 기능이 같은 코드끼리 서로 분리하는 것이다. 웹 관련 코드는 업무 로직으로부터 분리하고, 업무 로직은 다시 데이터 접근으로부터 분리한다. UML 믈래스 다이어그램에서 봤듯이, 구현 관점에서 보면 각 계층은 일반적으로 자바 패키지에 해당한다. 코드의 관점에서 OrdersController가 OrdersService 인터페이스에 의존하려면 OrdersService 인터페이스는 반드시 public으로 선언되어야 하는데, 두 인터페이스는 서로 다른 패키지에 속하기 때문이다. 마찬가지로 OrdersRepository 인터페이스도 public이어야만 repository 패키지 외부에 있는 OrdersServiceImpl 클래스에서 접근할 수 있다.

엄격한 계층형 아키텍처에서는 의존성 화살표는 항상 아래를 향해야 하며, 각 계층은 반드시 바로 아래 계층에만 의존해야 한다. 이런 방식으로 멋지고 깔끔한 비순환 의존성 그래프를 만들 수 있을 거라 생각할 수도 있지만, 정말로 코드 베이스의 요소들이 서로 의존할 때는 몇 가지 규칙을 반드시 지켜야 한다.
그런데 여기에는 큰 문제가 있다. 속임수를 써서 몇몇 의존성을 의도치 않은 방식으로 추가하더라도, 보기에는 여전히 좋은 비순환 의존성 그래프가 생성된다는 사실이다.

예를 들어 팀에서 신규 인력을 고용하여 주문과 관련된 또 다른 유스케이스를 구현하라고 지시했다고 가정해 보자. 이 사람은 신입인 탓에 이 유스케이스를 가능한 한 빨리 구현해서 깊은 인상을 남기고 싶어한다. 그 결과 만들어진 UMl 다이어그램은 그림과 같다.

clean_archit048

의존성 화살표는 여전히 아래를 향하지만, 이제 몇몇 유스케이스에서는 OrdersController가 OrdersService를 우회하고 있다. 이러한 조직화는 계층가 인접한 계층을 건너뛰는 일이 허용되기 때문에 흔히 완화된 계층형 아키텍처라고 부른다. 경우에 따라 이는 의도된 겨로가이기도 한데, CQRS패턴을 지키려고 시도하는 경우다.

새로운 유스케이스가 동작은 하겠지만 우리가 기대하는 형태로 구현되지는 않았다. 여기에서 우리에게 필요한 것은 지침(아키텍처 원칙)으로, “웹 컨트롤러는 절대로 레파지토리에 직접 접근해서는 안 된다”와 같은 원칙이 필요하다. 물론 문제는 강제성이다.
훨씬 적은 수의 팀만이 빌드 시 정적 분석 도구를 사용해서 아키텍처적인 위반 사항이 없는지를 검사하여 자동으로 강제한다고 답했다.

‘컴포넌트 기반 패키지’를 도입해야 하는 이유는 바로 이 때문이다. 이 접근법은 지금까지 우리가 본 모든 것들을 혼합한 것으로, 큰 단위(coarse-grained)의 단일 컴포넌트와 관련된 모든 책임을 하나의 자바 패키지로 묶는 데 주안점을 둔다. 이 접근법은 서비스 중심적인 시각으로 소프트웨어 시스템을 바라보며, 마이크로서비스 아키텍처가 가진 시각과도 동일하다. 포트와 어댑터에서 웹을 그저 또 다른 전달 메커니즘으로 취급하는 것과 마찬가지로, 컴포넌트 기반 패키지에서도 사용자 인터페이스를 큰 단위의 컴포넌트로부터 분리해서 유지한다.

clean_archit049

그림에서 ‘주문 조회하기’ 유스케이스가 어떤 모습인지 보여준다. 본질적으로 이 접근법에선느 ‘업무 로직’과 영속성 관련 코드를 하나로 묶는데, 이 묶음을 나는 ‘컴포넌트’라고 부른다.
엉클 밥은 이 책 앞부분에서 ‘컴포넌트’에 대한 정의를 아래와 같이 제시했다.

컴포넌트는 배포 단위다. 컴포넌트는 시스템의 구성 요소로, 배포할 수 있는 가장 작은 단위다. 자바의 경우 jar파일이 컴포넌트다.

컴포넌트에 대한 내 정의는 약간 다르다.
“컴포넌트는 멋지고 깔끔한 인터페이스로 감싸진 연관된 기능들의 묶음으로, 애플리케이션과 같은 실행 환경 내부에 존재한다.”

컴포넌트 기반 패키지 접근법의 주된 이점은 주문과 관련된 무언가를 코딩해야 할 때 오직 한 곳, 즉 OrdersComponent만 둘러보면 된다는 점이다. 이 컴포넌트 내부에서 관심사의 분리는 여전히 유효하며, 따라서 업무 로직은 데이터 영속성과 분리되어 있다. 하지만 이는 컴포넌트 구현과 관련된 세부사항으로, 사용자는 알 필요가 없다. 이는 마이크로서비스나 서비스 지향 아키텍처를 적용했을 때 얻는 이점과도 유사하다. 즉, 주문 처리와 관련된 모든 것들을 캡슐화하는 별도의 OrdersService가 존재한다. 큰 차이는 결합 분리 모드에 있다. 모노리틱 애플리케이션에서 컴포넌트를 잘 정의하면 마이크로서비스 아키텍처로 가기 위한 발판으로 삼을 수 있다.

구현 세부사항엔 핫아 문제가 있다
표면상으로는 이 네 가지 접근법이 코드를 조직화하는 완전히 서로 다른 방식처럼 보이며, 따라서 서로 다른 아키텍처 스타일로 여길 수도 있다. 하지만 세부사항을 잘못 구현하면 이러한 견해도 아주 빠르게 흐트러지기 시작한다.
모든 타입에서 public 지시자를 사용한다는 건 사용하는 프로그래밍 언어가 제공하는 캡슐화 관련 이점을 활용하지 않겠다는 뜻이다. 이로 인해 누군가가 구체적인 구현 클래스의 인스턴스를 직접 생성하는 코드를 작성하는 일을 절대 막을 수 없으니, 결국 당신이 지향하는 아키텍처 스타일을 위반하게 될 것이다.

조직화 VS 캡슐화
만약 자바 애플리케이션에서 모든 타입을 public으로 지정한다면, 패키지는 단순히 조직화를 위한 메커니즘(폴더와 같이 무언가를 묶는 방식)으로 전랴갛여 캡슐화를 위한 메커니즘이 될 수 없다. public타입을 코드 베이스 어디에서도 사용할 수 있다면 패키지를 사용하는 데 따른 이점이 거의 없다. 따라서 사실상 패키지를 사용하지 않는 것과 같다.

패키지를 무시해 버리면 (캡슐화나 은닉을 하는 데 아무런 도움도 되지 않으므로) 최종적으로 어떤 아키텍처 스타일로 만들려고 하는지는 아무런 의미가 없어진다. public 지시자를 과용하면 이 장의 앞에서 제시한 네 가지 아키텍처 접근법은 본질적으로 완전히 같아진다.

clean_archit051

그림에서 각 타입 사이의 화살표를 유심히 살펴보라. 채택하려는 아키텍처 접근법과 아무런 관계 없이 화살표들이 모두 동일한 방향을 가리킨다. 개념적으로 이 접근법들은 매우 다르지만, 구문적으로는 완전히 똑같다. 이처럼 모든 타입을 public으로 선언한다면, 우리가 실제로 갖게 되는 것은 수평적 계층형 아키텍처를 표현하는 네 가지 방식에 지나지 않는다.

왼쪽부터 하나씩 살펴보자. 먼저 ‘계층 기반 패키지’ 접근법에서 OrdersService와 OrdersRepository 인터페이스는 외부 패키지의 클래스로부터 자신이 속한 패키지 내부로 들어오는 의존성이 존재하므로 public으로 선언되어야 한다. 반면 구현체 클래스(OrdersServiceImpl과 JdbcOrdersRepository)는 더 제한적으로 선언할 수 있다(패키지 protected). 이들 클래스는 누구도 알 필요가 없는 구현 세부사항이다.

두 번째, ‘기능 기반 패키지’ 접근법에서는 OrdersController가 패키지로 들어올 수 있는 유일한 통로를 제공하므로 나머지는 모두 패키지 protected로 지정할 수 있다. 이 방식에서 가장 주의할 점은, 이 패키지 밖의 코드에서는 컨트롤럴를 통하지 않으면 주문 관련 정보에 접근할 수 없다는 사실이다. 이는 바람직할 때도 있고 아닐 때도 있다.

세 번째, ‘포트와 어댑터’ 접근법의 경우, OrdersService와 Orders인터페이스는 외부로부터 들어오는 의존성을 가지므로 public을 지정해야 한다. 이 경우에도 구현 클래스는 패키지 protected로 지정하며, 런타임에 의존성을 주입할 수 있다.

마지막으로 ‘컴포넌트 기반 패키지’ 접근법에서는 컨틀롤러에서 OrdersComponent 인터페이스로 향하는 의존성을 가지며, 그 외의 모든 것은 패키지 protected로 지정할 수 있다.

public 타입이 적으면 적을수록 필요한 의존성의 수도 적어진다.

결론:빠져 있는 조언
이 장은 최적의 설계를 꾀했더라도, 구현 전략에 얽힌 복잡함을 고려하지 않으면 설계가 순식간에 망가질 수도 있따는 사실을 강조하는 데 그 목적이 있다. 설계를 어떻게 해야만 원하는 코드 구조로 매핑할 수 있을지, 그 코드를 어떻게 조직화할지, 런타임과 컴파일타임에 어떤 결합 분리 모드를 적용할지 고민하라.





Reference

  • 클린 아키텍처 (로버트 C. 마틴)

Tag: [ book  clean-architecture  service  ]