30장. 데이터베이스는 세부사항이다.
- 아키텍처 관점에서 볼 때 데이터베이스는 엔티티가 아닌 세부사항이라서 아키텍처 구성요소 수준으로 끌어올릴 수 없다.
- 애플리케이션 내부 데이터의 구조는 시스템 아키텍처에서 대단히 중요하지만, 데이터베이스는 데이터 모델이 아니다.
- 데이터베이스는 일개 소프트웨어이며 데이터에 접근할 방법을 제공하는 유틸리티임
- 아키텍처 관점에서 유틸리티는 저수준의 세부사항일 뿐이라서 아키텍처와 관련이 없다.
관계형 데이터베이스
- 관계형 데이터베이스가 뛰어나든, 유용하든 결국 기술일 뿐이고 이는 관계형 데이터베이스가 세부사항임을 뜻한다.
- 데이터를 행 뒨으로 배치한다는 자체는 아키텍처에서 중요하지 않으며 유스케이스는 알아서도, 관여해서도 안된다.
- 데이터가 테이블 구조를 가진다는 사실은 아키텍처 외부 원에 위치한 최하위 수준의 유틸리티 함수만 알아야 함
하지만 성능은?
- 성능은 당연히 아키텍처적인 관심사이지만, 데이터 저장소의 측면에서 성능은 완전히 캡슐화하여 업무 규칙과는 분리할 수 있는 관심사다.
- 데이터를 빠르게 넣고 뺄수 있어야 하는 것은 맞지만, 이는 저수준의 관심사다.
- 이 관심사는 저수준의 데이터 접근 메커니즘 단에서 다룰 수 없음
- 성능은 시스템의 전반적인 아키텍처와 아무런 관련이 없다.
결론
- 체계화된 데이터 구조와 데이터 모델은 아키텍처적으로 중요하다.
- 반면, 데이터를 저장하는 기술과 시스템은 아키텍처적으로 중요하지 않다.
- 데이터를 테이블 구조로 만들고 SQL로만 접근하도록 하는 관계형 데이터베이스 시스템은 전자보다는 후자와 횔씬 관련이 깊다.
- 데이터는 중요하지만, 데이터베이스는 세부사항이다.
31장. 웹은 세부사항이다.
끝없이 반복하는 추
- IT 역사 전체로 시야를 넓히면 웹은 아무것도 바꾸지 않았다.
- 웹은 우리가 발버둥치면서 생기는 여느 수많은 진동 중 하나에 불과하다.
- 이 진동은 우리가 태어나기 전에도 있어 왔고, 우리가 은퇴한 뒤에도 지속될 것임
- 아키텍처로서 이 진동은 그저 핵심 업무 규칙의 중심에서 밀어내고 싶은 단기적인 문제일 뿐이다.
요약
- 웹은 입출력 장치이자 GUI이며 GUI는 세부사항 이므로 핵심 업무 로직에서 분리된 경계 바깥에 두어야 한다.
- UI와 애플리케이션 사이에는 추상화가 가능한 또 다른 경계가 존재한다.
- 업무 로직은 다수의 유스케이스로 구성되며. 각 유스케이스는 사용자를 대신해서 일부 함수를 수행하는 것으로 볼 수 있다.
- 각 유스케이스는 입력 데이터, 수행할 처리 과정, 출력 데이터를 기반으로 기술할 수 있다.
결론
- 추상화는 만들기 쉽지 않고, 제대로 만들려면 수차례의 반복 과정을 거처야 할 것이다.
- 세상은 마케팅 귀재로 가득하기에 이러한 추상화가 꼭 필요할 때가 많다고 주장하기는 어렵지 않다.
32장. 프레임워크는 세부사항이다.
혼인 관계의 비대칭성
- 개발자는 프레임워크를 위해 큰 헌신을 해야하지만, 프레임워크 제작자는 개발자를 위해 아무런 헌신을 하지 않는다.
- 프레임워크를 사용해야할 경우, 개발자는 프레임워크 제작자가 제공하는 문서를 꼼꼼히 읽는다.
- 이 문서에는 우리가 만든 어플리케이션과 프레임워크를 어떻게 통합할 수 있을지 조언한다.
- 대개의 경우 이들은 프레임워크를 중심에 두고 우리의 아키텍처는 그 바깥을 감싸야 한다고 말한다.
- 프레임워크 제작자는 개발자의 어플리케이션이 가능하면 프레임워크에 공고하게 결합될 것을 강하게 역설한다.
- 프레임워크 제작자는 프레임워크에 대해 절대적인 제어권을 쥐고 있기에 어플리케이션이 프레임워크와 결합되기를 바란다.
- 모든 위험과 부담은 오롯이 개발자가 담당할 뿐, 프레임워크 제작자가 감수하는 건 아무것도 없다.
위험요인
- 프레임워크의 아키텍처는 의존성 규칙을 위반하는 경향이 있다.
- 업무 객체를 만들 때, 프레임워크 제작자는 엔티티에 자신의 코드를 상속할 것을 요구한다.
- 즉 자신의 프레임워크가 당신의 가장 안쪽 원과 결합하기를 원한다.
- 하지만 프레임워크가 한번 안으로 들어가버리면 다시는 원 밖으로 나오지 않을 것이다.
- 프레임워크는 애플리케이션의 초기 기능을 만드는 데는 도움이 될 것이다.
- 제품이 성숙해지면서 프레임워크가 제공하는 기능과 틀을 벗어나게 될 것이다.
- 시간이 지나면서 프레임워크와 싸우고 있는 자신을 발견하게 될 것이다.
- 새롭고 더 나은 프레임워크가 등장해서 갈아타고 싶을 수도 있다.
해결책
- 프레임워크를 사용할 수는 있지만, 프레임워크와 적당한 거리를 두고 결합해서는 안된다.
- 프레임워크는 아키텍처의 바깥쪽 원에 속하는 세부사항으로 취급하고, 원 안쪽으로 들어오지 못하도록 하라.
- 프레임워크가 자신의 기반 클래스로 부터 파생하기를 요구한다면 프록시(proxy)를 만들고, 업무 규칙에 플러그인 할 수 있는 컴포넌트 위치 시켜라
- 핵심 코드에 플러그인 할 수 있는 컴포넌트에 프레임워크를 통합하고, 의존성 규칙을 준수하라
- 스프링은 훌륭한 의존성 주입 프레임워크이지만, 업무 객체는 @Autowired를 포함해 절대로 스프링에 대해서 알아서는 안된다.
34장. 빠져있는 장
계층 기반의 패키지
- 가장 단순하고 전통적인 수평 계층형 아키텍처다.
- 기술적인 관점에서 해당 코드가 하는 일에 기반해 그 코드를 분할하며 이를 ‘계층 기반 패키지’라고 부른다.
- 전형적인 계층형 아키텍처는 웹, 업무 규칙, 영속성 코드를 위해 계층이 하나씩 존재한다.
- 코드는 계층이라는 얇은 수평 조각으로 나뉘며, 각 계층은 유사한 종류의 것들을 묶는 도구로 사용된다.
- ‘엄격한 계층형 아키텍처’의 경우 계층은 반드시 아래 계층에만 의존해야 한다.
- 자바의 경우 계층은 주로 패키지로 구현되며 계층 사이의 의존성은 모두 아래를 향한다.
- OrdersController : 웹 컨트롤러이며, 웹 기반 요청을 처리한다. Spring MVC 컨트롤러 등이 여기 해당
- OrdersService : 주문 관련 ’업무 규칙’을 정의하는 인터페이스
- OrdersServiceImpl : OrdersService의 구현체
- OrdersRepository : 영구 저장된 주문 정보에 접근하는 방법을 정의하는 인터페이스
- JdbcOrderRepository : OrdersRepository 구현체
- 이 아키텍처는 엄청난 복잡함을 겪지 않고 무언가를 작동시켜 주는 아주 빠른 방법이다.
- 하지만, 소프트웨어가 커지고 복잡해지기 시작하면 문제가 발생한다.
- 3계층만으로 모든 코드를 담기엔 부족하다는 사실을 깨닫고, 더 잘게 모듈화해야 할지를 고민하게 될 것이다.
- 계층형 아키텍처는 도메인에 대해 아무것도 말해주지 않는더.
- 전혀 다른 도메인이라도 코드를 만들고 보면, 웹, 서비스, 리포지터리로 구성된 모습이 기분 나쁠 정도로 비슷하게 보인다.
기능 기반 패키지
- ‘기능 기반 패키지’는 서로 연관된 기능, 도메인 개념, 또는 Aggregate Root에 기반하여 수직의 얇은 조각으로 코드를 나누는 방식이다.
- 전형적으로 모든 타입이 하나의 자바 패키지에 속하며, 패키지 이름은 그 안에 담긴 개념을 반영해 짓는다.
- 등장하는 인터페이스와 클래스는 이전과 같지만, 모두가 단 하나의 패키지에 속한다.
- 이는 ‘계층 기반 패키지’를 간단히 리팩터링한 형태지만, 이제 코드의 상위 수준 구조가 업무 도메인에 대해 무언가 알려준다.
- 변경해야할 코드가 한 패키지에 있으므로 주문 관련 유스케이스가 변경될 경우 변경할 코드를 찾는 작업이 쉬워진다.
- 하지만 이 방법 역시 차선책일 뿐이다.
포트와 어댑터
- ‘포트와 어댑터’ 혹은 ‘육각형 아키텍처’, ‘경계, 컨트롤러, 엔티티’등의 방식으로 접근한다.
- 그 이유는 업무/도메인에 초점을 둔 코드가 프레임워크나 데이터베이스 같은 기술적인 세부 구현과 독립적이며 분리된 아키텍처를 만들기 위해서다.
- 코드 베이스는 ‘내부(도메인)’와 ‘외부(인프라)’로 구성됨을 흔히 볼 수 있다.
- ‘내부’ 영역은 도메인 개념을 모두 포함하는 반면, ‘외부’ 영역은 외부 세계와의 상호작용을 포함한다.
- 여기서 주요 규칙은 ‘외부’가 ‘내부’에 의존하며, 절대 그 반대로는 안 된다는 점이다.
- 위 그림에서 domain 패키지가 ‘내부’이며, 나머지 패키지는 모두 ‘외부’로 의존성이 ‘내부’를 향해 흐른다.
- 이전 다이어그램의 OrdersRepository가 Orders라는 간단한 이름으로 바뀌었는데, 이는 도메인 주도 설계 세계관에서 비롯된 명명법이다.
- 도메인 주도 설계에서는 ‘내부’에 존재하는 모든 것의 이름을 반드시 ‘유비쿼터스 도메인 언어’ 관점에서 기술하라고 조언한다.
- 도메인에 대해 논의할 때 우리는 ‘주문’에 대해 말하는 것이지, ‘주문 리포지토리’에 대해 말하는 것이 아님
컴포넌트 기반 패키지
- 계층형 아키텍처의 목적은 기능이 같은 코드끼리 서로 분리하는 것으로 웹은 업무 로직으로 부터 분리하고, 업무 로직은 데이터 접근으로부터 분리한다.
- 엄격한 계층형 아키텍처에서는 의존성 화살표는 항상 아래를 향해야 하며, 각 계층은 반드시 아래 계층에만 의존해야 한다.
- 하지만 의도치 않은 방식으로 추가하더라도, 잘못되었지만 보기에는 여전히 좋은 비순환 의존성 그래프가 생성된다.
- 예를 들어 OrdersController에서 OrdersRespository를 주입하여 코드가 동작하도록 만든 결과는 아래와 같다.
- 의존성 화살표는 여전히 아래를 향하지만, OrdersController가 OrdersService를 우회하고 있다.
- 이러한 조직화는 인접한 계층들을 건너뛰는 일이 허용되기에 흔히 ‘완화된 계층형 아키텍처’라고 부른다.
- CQRS 패턴을 지키려고 하는 경우 이는 의도된 결과이기도 한데, 이외의 경우는 업무 로직을 우회하는 일은 바람직하지 못하다.
- 여기서 필요한 것은 아키텍처 원칙으로 “웹 컨트롤러는 절대로 레포지토리에 직접 접근해서는 안된다”와 같은 원칙이 필요하다.
- 많은 팀이 “우리는 훌륭한 규율, 코드리뷰를 통해서 이 원칙을 강제합니다. 우리는 개발자를 믿습니다.”라며 쉽게 말한다.
- 하지만 자금이 바닥나거나 납기가 다가오면 무슨 일이 벌어날지를 우리는 잘 알고 있다.
- 훨씬 적은 수의 팀만이 빌드 시 정적 분석 도구를 사용해서 아키텍처적인 위반 사항이 없는지를 자동으로 강제한다고 답했다.
- 이는 다소 조잡하지만, 팀 차원에서 정의한 아키텍처 원칙을 위반하는 항목을 알려주고, 위반 시 빌드가 실패하므로 효과가 있다.
- 하지만 두 접근법 모두 오류가 있을 수 있으며, 그 결과를 알게 되는 주기가 필요 이상으로 길다는 문제다.
- 개인적으로는 가능하면 컴파일러를 사용해서 아키텍처를 강제하는 방식을 선호한다.
- “컴포넌트 기반 패키지”는 이것들을 혼합한 것으로, 큰 단위의 단일 컴포넌트와 관련된 모든 책임을 하나의 자바 패키지로 묶는다.
- 이 접근법은 서비스 중심적은 시각으로 소프트웨어 시스템을 바라보며, 마이크로서비스 아키텍처가 가진 시각과도 동일하다.
- 포트와 어댑터에서 웹을 그저 또 다른 전달 메커니즘으로 취금하는 것과 마찬가지로, 컴포넌트 기반 패키지에서도 사용자 인터페이스를 큰 단위의 컴포넌트로부터 분리해서 유지한다.
- 이 접근법에서는 ‘업무 로직’과 영속성 관련 코드를 하나로 묶는데, 이 묶음을 ‘컴포넌트’라고 부르며 앞선 정의와는 다르다.
- 앞부분 : 컴포넌트는 배포 단위다. 컴포넌트는 시스템의 구성 요소로, 배포할 수 있는 작은 단위다.
- 여기 : 컴포넌트는 멋지고 깔끔한 인터페이스로 감싸진 연관된 기능들의 묶음으로, 애플리케이션과 같은 실행 환경 내부에 존재한다.
- 컴포넌트 기반 패키지 접근법의 주된 이점은 주문과 관련된 무언가를 코딩할 때, 오직 한 곳만 둘러보면 된다는 점이다.
- 이 컴포넌트 내부에서 관심사의 분리는 여전히 유효하며, 업무 로직은 데이터 영속성과 분리되어 있다.
- 하지만 이는 컴포넌트 구현과 관련된 세부사항으로 사용자는 알 필요가 없다.
- 모노리틱 어플리케이션에서 컴포넌트를 잘 정의하면, 마이크로서비스 아키텍처로 가기 위한 발판으로 삼을 수 있다.
구현 세부사항엔 항상 문제가 있다.
- 자바에서 public 접근 지시자를 지나칠 정도로 방만하게 사용하는 모습을 자주 본다.
- 모든 타입에서 public 지시자를 사용한다는 건 사용하는 프로그래밍 언어가 제공하는 캡슐화 관련 이점을 활용하지 않겠다는 뜻이다.
- 이로 인해 누군가가 구현체의 인스턴스를 직접 생성하는 일을 막을 수 없으니, 아키텍처 스타일을 위반하게 될 것이다.
조직화 vs 캡슐화
- 만약 모든 타입을 public으로 지정한다면, 패키지는 단순히 조직화를 위한 매커니즘으로 전략하여 캡슐화를 위한 메커니즘이 될 수 없다.
- public 타입을 코드 어디에도 사용할 수 있다면 패키지를 사용하는 데 이점이 거의 없다.
- 이는 사실 상 패키지를 사용하지 않는 것과 같다.
- 패키지를 무시해버리면 최종적으로 어떤 아키텍처 스타일로 만들려고 하는지 아무런 의미가 없어진다.
- public을 과용하면 앞에서 제시한 4가지 아키텍처 접근법은 본질적으로 완전히 같아진다.
- 위 그림의 각 타입 사이의 화살표를 보면 모두 동일한 방향을 가르킨다.
- 개념적으로 이 접근법들은 매우 다르지만, 구문적으로는 완전히 똑같다.
- 이처럼 모든 타입을 public으로 선언하면, 실제로 갖게되는 것은 수평적 계층형 아키텍처를 표현하는 4가지 방식에 지나지 않음
- 접근 지시자를 적절하게 사용하면, 타입을 패키지로 배치하는 방식에 따라서 각 타입에 접근할 수 있는 정도가 실제로 크게 달라질 수 있다.
- 만약 다이어그램에서 패키지 구조를 다시 살려서 더 제한적인 접근 지시자를 사용할 수 있는 타입을 흐리게 표시하면 인상적으로 변한다.
- 여기에서 설명한 내용은 모노리틱 어플리케이션에 대한 것으로, 모든 코드가 단 하나의 소스 코드 트리에 존재하는 경우다.
- 이러한 애플리케이션을 구축 중이라면, 아키텍처 원칙을 강제할 때 반드시 컴파일러에 의지할 것을 권장한다.
- ‘계층 기반 패키지’ 접근법
- OrdersService와 OrdersRepository 인터페이스는 외부 패키지의 클래스로부터 자신이 속한 패키지 내부로 들어오는 의존성이 존재한다.
- 그러므로 public으로 선언되어야 한다.
- 반면 구현체 클래스는 package protected로 선언할 수 있다.
- 이들 클래스는 누구도 알 필요가 없는 구현 세부사항이다.
- ‘기능 기반 패키지’ 접근법
- OrdersController가 패키지로 들어올 수 있는 유일한 통로를 제공하므로 나머지는 모두 protected로 지정할 수 있다.
- 가장 주의할 점은, 이 패키지 밖의 코드에서는 컨트롤러를 통하지 않으면 주문 관련 정보에 접근할 수 없다.
- 따라서 이는 바람직할 때도 있고 아닐 때도 있다.
- ‘포트와 어댑터’ 접근법
- OrdersService와 Orders 인터페이스는 외부로부터 들어오는 의존성을 가지므로 public으로 지정해야한다.
- 이 경우에도 구현 클래스는 package protected로 지정하며, 런타임에 의존성을 주입할 수 있다.
- ‘컴포넌트 기반 패키지’ 접근법
- 컨트롤러에서 OrdersComponent 인터페이스로 향하는 의존성을 가진다.
- 그 이외의 모든 패키지는 protected로 지정할 수 있다.
- 이제 이 패키지 외부의 코드에서는 OrdersRepository 인터페이스나 구현체를 직접 사용할 수 있는 방법이 전혀 없다.
- 따라서 컴파일러의 도움을 받아 ‘컴포넌트 기반 패키지’ 아키텍처의 접근법을 강제할 수 있다.
다른 결합 분리 모드
- 프로그래밍 언어가 제공하는 방법 외에도 소스 코드 의존성을 분리하는 방법은 존재할 수 있다.
- 예를 들어 자바에는 OSGi 모듈 프레임워크나 자바 9에서 제공하는 JPMS가 있다.
- 다른 선택지로는 소스 코드 수준에서 의존성을 분리하는 방법도 있는데, 정확히는 서로 다른 소스 코드 트리로 분리하는 방법이다.
- 포트와 어댑터를 예로 들자면 다음과 같은 소스 코드 트리를 만들 수 있다.
- 업무와 도메인용 소스 코드(기술이나 프레임워크와 독립적인 모든 것) : OrdersService, OrdersServiceImpl, Orders
- 웹용 소스 코드 : OrdersController
- 데이터 영속석용 소스 코드 : OrdersRepository
- 마지막 두 소스 코드 트리는 업무와 도메인 코드에 대해 컴파일 시점에 의존성을 가진다.
- 반대로 업무와 도메인 코드 자체는 웹이나 데이터 영속성 코드에 대해서는 아무것도 알지 못한다.
- 구현 관점에서 이렇게 분리하려면 빌드 도구를 사용해서 모듈이나 프로젝트가 서로 분리되도록 구성해야 한다.
- 하지만 이는 너무 이상적인 해결책이다. 현실에서 소스 코드를 이처럼 나누다 보면 성능, 복잡성, 유지보수 문제가 생긴다.
- 포트와 어댑터 접근법을 적용할 때는 이보다 간단한 방법을 사용하기도 하는데, 단순히 소스 트리를 두 개만 만드는 것이다.
- 도메인 코드(내부)
- 인프라 코드(외부)
- 이 다이어 그램에서 인프라는 도메인에 대해 컴파일 시점의 의존성을 가진다.
- 이 접근법은 소스 코드를 조직화할 때 효과가 있겠지만, 잠재적으로 절충해야 할 부분이 있음을 알고 있어야만 한다.
- 인프라를 모아두는 것은 특정 인프라 코드가 도메인 코드를 통하지 않고 애플리케이션의 다른 영역을 직접 호출할 수 있다는 뜻이다.
- 예를 들어 웹 컨트롤러가 데이터베이스 리포지토리에 있는 코드를 직접 호출할 수 있음
결론: 빠져있는 조언
- 이 장은 최적의 설계를 꾀했더라도, 구현 전략에 얽힌 복잡함을 고려하지 않으면 설계가 순식간에 망가질 수도 있음을 강조한다.
- 설계를 어떻게 해야만 원하는 코드 구조로 매핑할 수 있을지, 그 코드를 어떻게 조직화할지, 런타임과 컴파일타임에 어떤 결합 분리 모드를 적용할지를 고민해야 한다.
- 가능한 선택사항을 열어두되, 실용적으로 행해야 한다.
- 팀의 규모, 기술 수준, 해결책의 복잡성을 제약(일정과 예산)과 동시에 고려 해야한다.
- 또한 선택된 아키텍처 스타일을 강제하는 데 컴파일러의 도움을 받을 수 있을지 고민해야 한다.
- 데이터 모델과 같은 다른 영역에 결합되지 않도록 주의해라. 구현 세부사항에는 항상 문제가 있는 법이다.
모든 내용은 [클린 아키텍처] 서적의 정리한 내용이며, 망나니 개발자님의 블로그에서 정리 방법을 참고했습니다.
클린 아키텍처: 소프트웨어 구조와 설계의 원칙 | 로버트 C. 마틴 - 교보문고
클린 아키텍처: 소프트웨어 구조와 설계의 원칙 | 살아있는 전설이 들려주는 실용적인 소프트웨어 아키텍처 원칙 소프트웨어 아키텍처의 보편 원칙을 적용하면 소프트웨어 수명 전반에서 개발
product.kyobobook.co.kr
[개발서적] 클린 아키텍처 6부 세부사항 - 내용 정리 및 요약
이번에는 로버트 C 마틴의 클린 아키텍처를 읽은 내용을 정리해보도록 하겠습니다. 개인적인 설명은 기울임으로 표시해두었으니, 읽으면서 참고하시면 될 것 같습니다. 30장. 데이터베이스는 세
mangkyu.tistory.com
'독서 > 클린 아키텍처' 카테고리의 다른 글
클린 아키텍처 5장. 아키텍처 - 2 (3) | 2024.07.24 |
---|---|
클린 아키텍처 5부. 아키텍처 - 1 (8) | 2024.07.24 |
클린 아키텍처 4부. 컴포넌트 원칙 (0) | 2024.07.21 |
클린 아키텍처 3부. 설계 원칙 (0) | 2024.07.19 |
클린 아키텍처 2부. 벽돌부터 시작하기: 프로그래밍 패러다임 (2) | 2024.07.19 |
댓글