Clean Architecture 정리 [번역]
in Programming on Programming
출처 : https://pusher.com/tutorials/clean-architecture-introduction
서문
나는 컴퓨터 관련 서적이 너무 빨리 시대에 뒤쳐지기 때문에 사지 않는다. 게다가 온라인에 모든 정보가 올라와있다. 하지만 1년전, 나는 로버트 마틴의 '클린코드'
책을 읽기 시작했다. 그 책은 나의 소프트웨어 개발 방법을 정말 많이 향상 시켰기 때문에 그의 다른 책 '클린 아키텍처'
가 나오자마자 구매하였다.
클린 코드처럼 클린 아키텍처는 어떤 언어든 시대를 초월한 원리로 채워져 있다. 온라인에서 검색해보면 저자와 다른 의견을 가진 사람을 발견 할 수 있을 것이다. 하지만 여기서 비평하는 것은 내 일이 아니며 그는 50년을 프로그래밍 해왔지만 나는 그러지 않다.
이 책의 내용이 조금 어려워서 최대한 이해 할 수 있도록 중요한 개념을 설명하겠다. 아직 소프트웨어 설계를 공부중이니 비판적인 시간으로 글을 읽어줬으면 한다.
클린 아키텍쳐란?
아키텍쳐는 프로젝트 전체의 설계를 말한다. 클래스, 파일, 컴포넌트, 모듈등을 구성하고 코드들이 어떻게 서로 연관되는 설계하는 것이다. 아키텍쳐는 핵심 기능의 장소와 유저, 디비와 어떻게 상호작용하는지 정의한다.
클린 아키첵쳐는 프로젝트가 커질 수록 이해하고 변화하기 쉽도록 프로젝트를 구성하는 것을 말한다. 이건 우연히 되는 것이 아니라 계획이 필요하다.
클린 아키텍쳐의 특성
유지보수가 쉬운 큰 프로젝트를 구축하는 비결 : 파일이나 클래스를 다른 컴포넌트와 독립적으로 변경 할 수 있도록 분리하는 것이다.
위 이미지에서 가위를 칼로 바꾸려고 한다면 어떻게 해야 하는가? 주변 물체들과 연결되어 있는 끈을 풀고 칼에 다시 묶어 줘야한다. 이게 칼에게는 영향이 없을지 몰라도 만약 펜이 “잠깐, 나 가위가 필요해!” 라고 말한다면 어떻게 할까? 그래서 이제 펜과 테이프가 동작을 하지 않아서 수정해야하는데, 이건 다시 그들에 묶여있는 다른 물체들에 영향을 준다. 정말 엄청진창이다.
비교해보아라
이제 가위를 칼로 교체하려면 어떻게 해야할까? 가위에 연결되어 있는 선을 빼내고 칼에 묶인 새 끈만 추가 해주면 된다. 아주 쉽다. 포스트잇에 끈이 묶여 있지 않아서 신경 쓰지 않아도 된다.
두번째 사진의 아키텍쳐가 분명히 수정하기 더 쉬웠다. 포스트잇을 자주 바꿀 필요가 없는 한, 이 시스템은 유지보수가 매우 쉬울 것이다. 이와 같은 개념이 소프트웨어의 유지보수를 쉽게 해주는 아키텍쳐다.
내부 원은 앱의 도메인 계층이다. 여기가 바로 비즈니스 로직을 넣는 곳이다. ‘비즈니스’라고 해서 회사를 뜻하지 않는다. 어플의 핵심 기능을 말한다. 예를 들어 번역 앱은 번역을 하고 온라인 쇼핑몰은 판매할 상품을 가지고 있따. 이런 비즈니스 로직은 앱의 본질이 자주 바뀌지 않기 때문에 상당히안정적이다.
바깥 원은 인프라스트럭쳐이다. UI, 디비, api, 프레임워크 같은 것들이 포함된다. 이런 것들은 도메인 보다 자주 변화될 가능성이 높다. 예를 들어, 대출 계산 식보다는 대출 버튼 UI가 더 변경하기 쉽다.
도메인과 인프라스트럭쳐는 서로에 대해 아무것도 알지 못하도록 설정한다. 즉, UI와 디비는 비즈니스 규칙에 따라 달라지지만, 비즈니스 규칙은 UI나 디비에 따라 달라지지 않는다. 이것은 플러그인 아키텍쳐가 된다. UI가 웹인지 모바일인지 데스크탑인지 중요하지 않다. 데이터를 SQL이나 NoSQL이나 클라우드나 상관없다. 도메인은 상관없이 인프라스트럭쳐를 쉽게 바꿀 수 있게 한다.
용어 정의
위의 원은 아래와 같이 더 다듬을 수 있다.
여기서 도메인 계층은 Entities와 Use cases로 세분화 되며, Adapters 계층은 도메인과 인프라 계층 사이의 경례를 형성한다. 각 용어가 좀 헷갈릴 수 있으니 개별적으로 살펴보자.
Entity
Entity
는 앱의 핵심 기능에 중요한 비즈니스 규칙의 집합이다. OOP (객체 지향 프로그래밍) 에서 Entity에 대한 규칙은 한 클래스의 메서드로 함꼐 그룹화 될 것이다. 응용 프로그램이 없을지라도 규칙은 존재한다. 예를 들어, 컴퓨터로 계산한건지 종이로 쓴건지랑 상관없이 대출에 10% 이자를 부과하는 것은 은행이 가질 수 있는 규칙이다. 아래는 Entity
에 대한 예시이다.
Entity
는 다른 계층을 알 수 없다. 어떤 것에도 의존하지 않는다.
Use cases
Use case
는 특정 상황에 대한 비즈니스 규칙이다. 이것은 앱의 동작을 결정한다. 다음은 책에서 나온 사례이다.
새 대출에 대한 정보 가져오기
입력 : 이름, 주소, 생일 등...
출력 : 같은 정보 + 신용 점수
규칙 :
1. 이름 확인
2. 주소 확인
3. 신용 점수 가져오기
4. 만약 신용 점수가 500보다 낮으면 거부
5. 높으면 대출 평가
Use case
는 바로 아래 Entity
계층에는 의존적이지만 그 위의 계층에 대해서는 아무것도 모른다. 이것이 아이폰에서 접속햇는지 데크스탑인지 상관하지 않는다. 또한 DB가 어떤것인지도 상관하지 않는다.
이 계층은 인터페이스를 정의하거나 외부 계층에서 사용 할 수 있도록 추상 클래스를 가지고 있다.
Adapters
Interface Adapter
라고도 불리는 Adapter
는 domain
과 infrastructure
사이의 변환기다. 예를들어 UI에서 입력받은 정보를 가져와 use case
와 entity
에 맞춰 repackage
를 해준다. 그런 다음 use case
와 entity
로 부터 받아온 데이터를 UI에 표시하거나 DB에 저장하기 위해 repackage
를 해준다.
Infrastructure
이 레이어는 UI, DB, 프레임워크, 장치
등 모든 I/O 구성 요소
가 이동하는 곳이다. 가장 변화가 많이 일어난다. 변화 가능성이 높기 때문에, domain
계층들로부터 가능한 멀리 떨어져있다. 이렇게 분리되어 있기 때문에 쉽게 변경과 다른 컴포넌트로 교체가 가능하다.
클린 아키텍처 구현 원칙
아래의 5가지 원칙은 SOLID 로 줄여서 기억하자. 클래스 수준의 원칙이지만 컴포넌트에도 적용된다.
단일 책임 원칙 (SRP, Single Responsibility Principle)
SRP는 한 클래스는 한가지 일만 가져야 한다고 말한다. 클래스 안에 많은 메소드를 가지고 있을 수 있지만 한가지 일을 위해서 존재해야 한다. 그 클래스에 변경될 이유 한가지만 있어야 한다. 예를 들어, 재무과에서 클래스를 변경해야 할 이유가 있고 인사과가 다른 방식으로 클래스를 변경해야 할 이유가 있다면 그 클래스는 2개로 쪼개져야 한다. 한 클래스에 한가지 변경 이유만 있어야 한다.
개방 폐쇄 원칙 (OCP, Open Closed Principle)
OCP는 확장(exteions)에 대해서는 열려(open) 있어야 하고 수정(modification)에 대해서는 닫혀(close) 있어야한다. 클래스 또는 컴포넌트는 기능을 추가 할 수 있어야 하지만 기존 기능을 수정할 필요는 없다. 어떻게? SRP대로 모든 클래스는 한가지 책임만 있어야하며 덜 안정적인 클래스가 변경되어야 할 때 영향받지 않도록 인터페이스 뒤에 안정적인 클래스를 숨긴다.
대체 원칙 (LSP, Liskov Substitution Principle)
SOLID 원칙을 위해 L이 필요했지만 Substitution(대치)만 기억해라. 이 원칙은 하위 클래스가 상위 클래스에 영향을 미치지 않고 대체될 수 있음을 의미한다. 추상 클래스나 인터페이스를 구현함으로써 이루어질 수 있다. 예를 들어, JAVA 에서는 ArrayList아 LinkedList 둘 다 List 인터페이스를 구현하여 서로 대체할 수 있다. 이 원칙이 아키텍처 차원에서 적용되면 Mysql은 도메인 변경 없이 MongoDB로 대체될 수 있다.
인터페이스 분리 원칙 (ISP, Interface Segregation Principle)
ISP는 말한다. 인터페이스 사용을 클래스 분리를 위한 다른 클래스로 부터 이것을 사용하는
ISP는 인터페이스를 사용하여 클래스를 사용하는 다른 클래스와 분리하는 것을 말한다. 인터페이스는 종속 클래스가 필요로 하는 메소드의 하위 집합만을 노출한다. 그렇게 해서 다른 메소드가 변경될 때 종속 클래스에 영향을 주지 않는다.
종속성 뒤집기 원칙 (DIP, Dependency Inversion Principle)
이것은 덜 안정적인 클래스가 더 안정적인 클래스에 종속되야 하는 것을 말한다, 반대가 되어서는 안된다. 안정적인 클래스가 불안정적인 클래스를 종속하면 불안정적인 클래스가 변경될 때 마다 안정적인 클래스에도 영향을 미친다. 그래서 의존성의 방향이 뒤집어 져야 한다. 어떻게? 추상 클래스와 인터페이스 뒤에 안정적 클래스를 숨기자.
class StableCalss {
void myMethod(VolatileClass param) {
param.doSomething();
}
}
위의 예시를 보면 StableCalss가 VolatileClass 이름을 가지고 호출을 하고 있다. 여기서 VolatileClass가 변경된다면 StableClass도 변경될 수 있다.
class StableClass {
interface StableClassInterface {
void doSomething();
}
void myMethod(StableClassInterface param) {
param.doSomething();
}
}
class VolatileClass implements StableClass.StableClassInterface {
@Override
public void doSomething() {
}
}
위의 코드는 종속성을 뒤집은 예시이다. StableClass 내부에 인터페이스를 작성하여 VolatileClass 이름을 모른 상태로 인터페이스를 호출한다. VolatileClass는 StableClass 내부 인터페이스를 상속하여 작성한다. 즉, StableClass는 VolatileClass를 모르는 상태로 작성되었다.
재사용 / 배포 동등성 원칙 (REP, Reuse/Release Equivalence Principle)
REP는 컴포넌트 수준의 원칙이다. Reuse는 재사용 가능한 클래스 또는 모듈의 그룹을 말한다. Release는 버전 번호로 배포하는 것을 말한다. 이 원칙은 뭘 배포하던 응집력 있는 유닛으로 재사용되어야 한다고 말한다. 상관 없는 클래스들의 무작위 그룹이면 안된다.
공통 닫힘 원칙 (CCP, Common Closure Principle)
CCP는 컴포넌트 수준의 원칙이다. 구성 요소는 같은 이유로 동시에 변하는 클래스의 집합이어야 한다. 만약 변경 이유가 다르거나 변경되는 비율이 다르다면 분할해야 한다. 기본적으로 단일 책임 원칙과 같다.
공통 재사용 원칙 (CRP, Common Reuse Principle)
CRP는 컴포넌트 수준의 원칙이다. 필요 없는 클래스가 있는 컴포넌트에 의존해서는 안된다고 한다. 이러한 컴포넌트는 사용자가 사용하지 않는 클래스에 의존할 필요가 없도록 분할해야 한다. 이것은 기본적으로 인터페이스 분리 원칙과 같다.
위 세가지 원칙은 (REP, CCP, CRP) 서로 긴장하고 있다. 너무 많이 갈라지거나 너무 많이 그룹화는 둘다 문제가 발생한다. 상황에 따라 균형을 맞춰야 한다.
비순환 종속성 원칙 (ADP, Acyclic Dependency Principle)
ADP는 프로젝트에서 어떠한 의존성 고리를 가지고 있으면 안되는 것을 의미한다. 예를 들어 A가 B를 의존하고 B가 C를 의존하고 C가 A를 의존하고 있다면 이것은 종속성 사이클이 생기게 된다.
이러한 사이클이 생기면 시스템을 변경하려고 할 때 큰 문제가 발생한다. 이 순환을 깨기 위해서는 의존성 뒤집기 원칙을 통해 요소들 사이에 인터페이스를 추가하는 것이다. 만약 다른 개인 혹은 팀이 각각 컴포넌트에 대한 책임이 있다면 컴포넌트는 각각의 버전 번호로 개별적으로 공개되어야 한다. 그런 식으로 한 컴포넌트의 변화가 다른 팀에게 즉시 영향을 줄 필요는 없다.
안정적인 종속성 원칙 (SDP, Stable Dependency Principle)
이 원칙은 종속성의 방향이 안정적이어야 한다고 말한다. 즉, 안정성이 떨어지는 컴포넌트는 보다 안정적인 컴포넌트에 의존해야 한다. 이것은 변화의 영향을 최소화한다. 일부 컴포넌트는 휘발성을 갖도록 되어 있다. 그것은 괜찮지만 안정적인 컴포넌트가 휘발성 컴포넌트를 의존해서는 안된다.
안정적 추상화 원리 (SAP, Stable Abstraction Principle)
SAP는 컴포넌트가 안정적일수록 추상적이어야 한다. 즉, 추상 클래스를 더 많이 포함해야 한다는 것이다. 추상 클래스는 연장이 더 쉬우므로 안정적인 컴포넌트들이 융통성 없어지는 것을 방지한다.
최종 정리
위의 원칙들은 클린 아키텍처 책의 주요 원리를 요약한 내용들이다. 여기에 추가적으로 중요한 요점을 설명하겠다.
테스트
나는 항상 UI Test를 하며 힘든 시간을 보냈다. GUI를 거치는 테스트를 하지만 UI를 바꾸자마자 Test가 중단된다. 그래서 결국 Test를 지우고 말았다. 하지만 나는 Adapter Layer에서 Presenter Object를 만들어야 한다는 것을 배웠다. Presenter는 비즈니스 규칙의 출력을 가져와 UI에 맞춰 모든 형식을 지정한다. 그런 다음 UI는 Presenter가 제공한 데이터를 표시하는 것 외에는 아무 것도 하지 않는다. 이렇게 되면 UI와 독립적으로 Presenter 코드를 Test 할 수 있다.
비즈니스 규칙을 테스트할 특수 API를 만드십시오. 이것은 어플리케이션 구조가 변경될 때마다 Test가 중단되지 않도록 인터페이스 어댑터와 분리해야 합니다.
Use case별로 Component 분리
도메인과 인프라 레이어(수평 구조) 안에서 어플리케이션이 가지고 있는 Use case별로 컴포넌트 구성을 수직으로 구분할 수 있다.
층이 나눠진 케익 조각 처럼 각 조각은 Use case이고 조각 안에 층은 컴포넌트이다.
예를 들어, 비디오 사이트에서 유저가 비디오를 보는 use case가 있다고 치자. 그러면 우리는 ViwerUseCase Component, viwerPresenter Component, ViwerView Component…. 이렇게 만들어 갈 것이다. 추가로 동영상 업로드에 대한 use case가 있으면 PublisherUseCase Component, PublisherPresenter Component, PublisherView Componet…등이 있을 것이다. 이렇게 수평적인 Usecase 내부를 수직적으로 개별 Component들이 생성된다.
어플리케이션이 배포되면, 각 컴포넌트들은 합리적인 방법으로 그룹화 될 수 있다.
레이어 분할 집행
아무리 완벽한 아키텍처를 구성하여도 새로운 개발자가 와서 의존성을 주입하기 시작하면 모든 것이 끝난다. 이것을 방지하는 가장 좋은 방법은 컴파일러를 활용하여 아키텍처를 보호하는 것이다. 예를 들어, Java에서는 클래스 패키지를 비공개로 설정하여 해당 클래스에 대해 알면 안 되는 모듈로부터 숨길 수 있다.
필요한 만큼만 복잡성 추가
처음부터 시스템을 과도하게 설계하지 마라. 필요한 만큼의 아키텍처만 사용하라. 그러나 아키텍처 내에서 향후 컴포넌트를 쉽게 분리할 수 있는 경계를 유지해라.
예를 들어, 먼저 단일 응용프로그램인 것을 외부에 배치할 수 있지만 내부에서 적절한 경계를 유지한다. 나중에 이 모듈을 개별 모듈로 분리할 수 있고 서비스로 배치할 수 있다. 레이어 경계를 유지하는 한, 배치 방식을 조정할 수 있다. 이런 식으로 사용되지 않을 수도 있는 불필요한 복잡성을 만들지 마라.
세부사항
프로젝트를 시작할 때는 업무 규칙을 먼저 작성해야 한다. 다른 것은 모두 세부사항이다. DB, OS, web API, framework… 다 세부사항이다. 가능한 오래 세부사항에 대해서 고민해라. 그러면 필요로 할때 현명하게 선택 할 수 있다. 도메인 계층은 인프라에 대해서 아무것도 모르기 때문에 초기 개발에는 중요하지 않다. DB를 선택할 때가 되면 DB를 결정해서 어댑터 코드를 입력해 연결해라. UI를 사용할 준비가 되었으면 UI 어댑터 코드를 입력하고 연결해라.
마지막 조언
Entity 개체를 외부 레이어에 전달할 데이터 구조로 사용하지 마라. 이를 위해 별도의 데이터 모델을 만들어라. 당신 프로젝트의 최상위 조직은 이 프로젝트가 무엇에 관한 것인지 사람들에게 분명하게 알려야 한다. 이것을 소리지르는 아키텍처라고 말한다. 이제 이 레슨을 실천하자. 실천을 해야만 진정으로 배울 수 있다.
연습 : 종속성 그래프 만들기
현재 프로젝트 중 하나를 열고 종이에 종속성 그래프를 만들어라. 프로젝트의 모든 구성 요소 또는 클래스에 대한 상자를 그려라. 그럼 각 클래스가 어떤 클래스에 의존하는지 알아보라. 종속성을 화살표로 그려라.
이제 질문을 던져보자.
- 비즈니스 규칙은 어디에 있는가?
- 비즈니스 규칙은 다른 것에 의존하는가?
- 다른 데이터베이스를 사용해야 하는 경우 영향을 받는 클래스 또는 구성 요소 숫자는?
- 종속성 순환이 발생하는가?
- 플러그인 아키텍처를 생성하려면 어떤 리팩토링을 수행해야 하는가?
결론
클린 아키텍처 책의 핵심은 플러그인 아키텍처를 생성해야 한다는 것이다. 같은 이유로 동시에 변경될 수 있는 클래스는 구성 요소로 그룹화 해야 한다. 비즈니스 규칙 컴포넌트는 보다 안정적이어야 하며 UI, DB, Web, Framework, 다른 세부사항을 처리하는 휘발성 높은 인프라 컴포넌트를 전혀 알지 못해야 한다. 컴포넌트 계층 사이의 경계는 데이터를 변환하고 종속성이 보다 안정적으로 유지되도록 인터페이스 어댑터를 사용하여 유지된다.