Architecture / / 2024. 4. 20. 18:38

Clean Architecture(클린 아키텍처) 공부 - 육각형 아키텍처, 엔티티, 유스케이스, 프레임워크, 드라이버, 관심사 분리(SoC), 험블 객체 패턴, 프레젠터, 뷰, 뷰모델, 아키텍처

 

저자는 육각형 아키텍처(Hexagonal Architecture), DCI(Data, Context and Interaction) 그리고 BCE(Boundary-Control-Entity)를 소개하면서 이들 아키텍처가 시스템으로 하여금 다음과 같은 특징을 지니도록 만든다고 한다.

 

관심사 분리

  • 프레임워크 독립성 - 프레임워크의 존재 여부에 의존하지 않는다. 프레임워크를 도구로 사용하고, 이것이 지닌 제약사항으로 시스템을 욱여 넣도록 강제하지 않는다.
  • 테스트 용이성 - 외부 요소 없이도 테스트가 가능하다.
  • UI 독립성 - 시스템의 일부분을 변경하지 않아도 UI를 쉽게 변경할 수 있다.
  • DB 독립성 - 다른 데이터 베이스로 바꾸어도 지장이 없다.
  • 모든 외부 에이전시에 대한 독립성.

이러한 관심사 분리에 대해 좀 더 찾아봤다.

 

 

Separation of Concerns (SoC) - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org

 

특히 내가 관심 가는 분야는 바로 응용 분야였다. 아키텍처, 설계, 코딩, 테스트, 디버깅,  유지 및 보수 쪽에 대해 자세하게 설명을 해놓아서 한번 가져와 보았다.

 

아키텍처:

  • 계층 아키텍처: 프레젠테이션, 비즈니스 로직, 데이터 접근과 같은 계층으로 관심사를 분리.
  • 마이크로서비스 아키텍처: 시스템을 독립적인 서비스로 분해하며, 각 서비스는 특정 관심사를 담당.
  • 서비스 지향 아키텍처 (SOA): 시스템을 느슨하게 결합되고 상호 운영 가능한 서비스로 구성.

설계:

  • 모듈화: 시스템을 더 작고 관리하기 쉬운 모듈로 분할하며, 각 모듈은 특정 관심사를 다룬다.
  • 명확한 인터페이스: 컴포넌트 간의 명확한 인터페이스를 정의하여 구현 세부 사항을 캡슐화하고 느슨한 결합을 촉진.
  • 도메인 주도 설계 (DDD): 핵심 도메인을 식별하고 모델링하며, 경계 컨텍스트(bounded context) 내에 도메인 로직을 캡슐화.

코딩:

  • 단일 책임 원칙 (SRP): 함수, 클래스 및 모듈이 단일 책임 또는 관심사를 갖도록 한다.
  • 캡슐화: 관련 로직과 데이터를 함수, 클래스 또는 모듈 내에 캡슐화하면서 관련 없는 관심사는 별도로 유지.
  • 명확한 명명: 함수, 변수 및 클래스의 용도와 책임을 전달하기 위해 명확하고 설명적인 이름을 사용.

테스트:

  • 단위 테스트: 각 관심사를 독립적으로 검증하는 집중된 단위 테스트 작성.
  • 통합 테스트: 모듈 또는 컴포넌트 간의 상호 작용을 테스트하여 예상대로 함께 작동하는지 확인.
  • 테스트 격리: 테스트 내에서 관심사를 분리하여 문제를 식별하고 진단하는 작업을 용이하게 한다.

디버깅:

  • 격리: 코드 내에서 관심사를 분리하여 버그 또는 문제의 근본 원인을 좁힌다.
  • 추적 및 로깅: 로깅 및 추적 관심사를 비즈니스 로직과 분리하여 디버깅 용이하게 한다.
  • 오류 처리: 핵심 기능과 별도로 오류 및 예외를 처리하여 오류 진단 및 해결 향상.

유지 관리 및 발전:

  • 변경 격리: 특정 관심사로 변경을 국한화하여 시스템 수정 및 유지 관리 용이하게 한다.
  • 버전 관리 및 의존성 관리: 의존성과 버전을 관리하여 한 관심사의 변경이 다른 관심사에 영향을 미치지 않도록 한다.
  • 리팩토링: 관심사 분리 및 유지 관리 향상을 위해 코드 리팩토링 수행.

여기서 SRP(Single Responsibility Principle)과의 차이점도 있는데 아주 명쾌하게 설명해 주었다.

  • SRP (Single Responsibility Principle): 하나의 컴포넌트는 변경이 필요하다는 것을 결정하는 역할에 의해만 변경될 수 있는 한 가지 이유만 있어야 한다. 콘웨이 법칙에 따르면, 컴포넌트는 해당 컴포넌트를 사용하는 역할에서만 변경 필요성이 결정됩니다. 따라서 책임을 역할별로 수직적으로 분할해야 한다.
  • SoC (Separation of Concerns): 기능을 구현하려면 인프라, 애플리케이션 및 도메인 계층 로직의 조합에 의존해야 한다. SoC 원칙은 기능의 "기술적" 구현 세부 정보를 구분하도록 촉구합니다. 이를 위해 계층화된 아키텍처(수평)를 구현할 수 있다.

 

의존성 규칙

위의 그림에서 내부의 원 안으로 들어갈수록 고수준의 소프트웨어가 된다. 바깥쪽은 메커니즘 안쪽은 정책이다. 여기서 중요하게 설명하는 것이 있다.

소스 코드 의존성은 반드시 안쪽으로,
고수준의 정책을 향해야 한다.

 

  • 내부의 원 : 외부 원에 선언된 어떤 것에 대해서도 언급해서는 안된다
  • 외부의 원 : 내부의 원에서 외부 원에 선언된 데이터 형식은 절대 사용해서는 안된다. 만약 그것이 프레임워크가 생성한 것은 더더욱 사용해서는 안 된다.

 

Entity : 전사적인 핵심 업무 규칙을 캡슐화

  • 일반적으로 엔티티는 가장 일반적이며 고수준인 규칙을 캡슐화한다. 외부의 무언가가 변경되더라도 엔티티는 변경될 가능성이 지극히 낮다.
  • 단순하게 만약 투두 앱을 만든다고 하면 JavaScript코드로 구현하면 다음과 같다.
class Todo {
  constructor(text) {
    this.text = text;
    this._isComplete = false;
  }

  markComplete() {
    this._isComplete = true;
  }

  getStatus() {
    return this._isComplete ? 'Complete' : 'Incomplete';
  }
}

module.exports = Todo;

 

UseCase : 어플리케이션에 특화된 업무규칙을 포함

  • 유스케이스는 엔티티로 들어오고 나가는 데이터 흐름을 조정한다. 엔티티가 자신의 핵심 업무 규칙을 사용해서 유스케이스의 목적을 달성하도록 이끈다. 

 

Interface Adaptor : 데이터의 변환을 맡은 임무

  • 데이터를 유스케이스와 엔티티에게 가장 편리한 형식에서 DB같은 외부 에이전세에게 편리한 형식으로 변환.
  • Presenter, View, Controller등등이 속한다.
  • SQL기반 DB를 사용하면, SQL은 이 계층을 벗어나면 안된다.

이 때 프레임워크나 DB는 모두 세부사항이 위치하는 곳이다. 이러한 것들을 모두 외부에 위치시켜서 피해를 최소화 해야 한다. 또한 원이 4개로만 이루어질 필요도 없다. 하지만 어떤 경우에도 의존성 규칙은 적용되어야 한다. 소스 코드 의존성은 항상 안 쪽을 향해야 한다. 안 쪽으로 향할 수록 점점 추상화 되고 캐슐화 된다.

 

경계 횡단하기

 

제어 흐름이나, 소스 코드 의존성 모두 원 안쪽으로 방향성을 향하고 있는 것을 알 수 있다. 만약 방향이 반대가 되어야 하는 경우 대체로 의존성 역전 원칙을 사용하여 해결한다. 만약 유스케이스에서 프레젠터를 호출한다면? 직접 호출해버리면 의존성 규칙을 위배하기에, 인터페이스를 호출하도록 하고, 외부 원인 프레젠터가 그 인터페이스를 구현하도록 만든다.

 

이러한 경계를 횡단하는 데이터는 다양한 모습을 띌 수 있다. DTO(Data Transfer Object), 구조체, 인자를 사용해서 데이터 전달 등등 다양하게 전달할 수 있다. 핵심은 이것이다. 격리되어 있는 간단한 데이터 구조가 경계를 가로질러 전달된다. 경계를 가로질러 데이터를 전달할 때, 항상 내부의 원에서 사용하기에 가장 편리한 형태를 가져야 한다.

 

시나리오 예시

 

 

시나리오를 정리하면 다음과 같다.

 

  1. WS(Web Server)는 컨트롤러에게 데이터를 모아 전달한다, 이를 평범한 자바 객체(단순하게 말해 인스턴스, 혹은 Object)로 묶고서 UseCaseInteractor로 전달.
  2. UseCaseInteractor는 이를 분석, Entity의 제어 및 이것이 사용할 데이터를 Data Access Interface에서 불러온다.
  3. Entity가 완성된다.
  4. 이후 UseCaseInteractor는 OutputData를 구현한다. 이를 OutputBoundary를 통해 Presenter로 전달한다.
  5. Presenter는 OutputData를 화면에 출력할 수 있게 데이터를 가공한다.
  6. 이후 ViewModel과 View를 통해 화면에 출력한다.

 

험블 객체 패턴

단순하게 말해, 테스트하기 쉬운 행위와 그렇지 않은 것들(이를 험블객체로 분리한다)을 분리해서 정리하는 것이다. 여기서 View는 험블 객체이다. 즉 테스트하기 어렵다는 것이다. 왜냐하면 데이터를 GUI(Graphic User Interface)로 옮기기만 할 뿐이다. 

 

프레젠터는 테스트하기 쉽다. 프레젠터는 Application, Server 혹은 그 외 요소로부터 데이터를 받아 화면에 표현할 수 잇는 포맷으로 만들기 때문이다. 즉, 복잡하거나 자주 변경되는 레이어를 가능한 험블(Humble)하게 유지하고 이 부분에 대한 단위 테스트 작성 노력을 최소화하는 것이 좋다. 단위 테스트 작성에 드는 노력 대비 효과가 적기 때문. 대신 비즈니스 로직 레이어에 집중하여 상세한 테스트를 진행하는 것이 좋다.

 

데이터베이스 게이트웨이와 데이터 매퍼(ORM)

유스케이스 인터랙터와 데이터베이스 사이에는 데이터베이스 게이트웨이가 위치한다. 다형적 인터페이스로, CRUD와 관련된 모든 메서드를 포함한다. 이 때 유스케이스 계층은 SQL을 허용하지 않는다, 즉 필요한 메서드를 제공하는 게이트웨이 인터페이스를 호출한다. 

 

이 구현체는 험블 객체다 - 너무 데이터와 가까워서 그러지 않을까 하는 생각이 든다 - 반면에 인터랙터는 업무 규칙을 캡슐화하기에, 테스트하기 쉬운데 게이트웨이 같은 것들은 Mock, Stub, Faker같은 것들로 대체가 가능하기 때문이다.

 

그렇다면 ORM(Prisma, SQLAlchemy, JPA 등등)은 어디에 속할까? 저자는 이러한 명칭부터 잘못 되었다고 본다. 결국 데이터 매퍼라고 불러야 한다고 하며, 그 이유에 대해 RDB에서 가져온 데이터를 데이터 구조에 맞게 담아주기 때문이라고 한다. 결국 Mapping을 해주는 것이라고 보는 듯 하다.

 

아무튼 ORM의 경우에는 실제로 데이터베이스 계층과 가깝고, 저자도 데이터베이스 계층이라고 본다.

 

서비스 리스너

여기서도 험블 객체 패턴을 발견할 수 있다. 결국 데이터가 횡당되면서 다양한 변화가 이루어지고 포맷이 변경되기도 한다. 즉, 데이터의 변화에 따라 테스트하기 어려울 수도 쉬울 수도 있다.

728x90
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유