사전 클린 아키텍처
패턴

클린 아키텍처

gabury1고친 사람 github-actions[bot]

클린 아키텍처는 업무 규칙을 데이터베이스나 웹 프레임워크 같은 바깥 기술에서 떼어 두는 설계 결정입니다. 코드를 안쪽과 바깥쪽으로 나눕니다. 바깥 코드만 안쪽 코드를 알게 합니다. 그래서 데이터베이스나 프레임워크를 바꿔도 업무 규칙 코드는 손대지 않고 남습니다.

쉽고 빠른 이해

클린 아키텍처는 업무 규칙을 가운데에 두고 데이터베이스와 웹 같은 기술을 바깥에 둡니다. 「배송이 시작된 주문은 취소할 수 없다」 같은 규칙이 가운데에 들어갑니다.

업무 규칙 코드가 데이터베이스 코드를 알고 있으면 저장 기술을 바꿀 때 규칙까지 고쳐야 합니다. 규칙 한 줄을 시험하려 해도 데이터베이스를 띄워야 합니다.

  1. 코드를 안쪽 원과 바깥쪽 원으로 나눕니다
  2. 바깥 코드는 안쪽 코드를 알아도 되지만 안쪽 코드는 바깥을 모릅니다
  3. 안쪽이 바깥 기능을 써야 하면 안쪽이 인터페이스를 정하고 바깥이 그것을 구현합니다

대가는 늘어나는 코드입니다. 원의 경계마다 인터페이스와 데이터를 옮겨 담는 코드가 생깁니다. 업무 규칙이 적고 넣고 꺼내기가 대부분인 프로그램이면 얻는 것보다 이 대가가 큽니다.

상세

이 절은 주문 취소 기능 하나를 예로 듭니다. 업무 규칙이 바깥 기술에 묶이는 문제에서 출발합니다. 그다음 네 원과 원 사이에 걸린 규칙 하나, 이 구조의 대가, 비슷한 구조와의 차이를 차례로 다룹니다.

업무 규칙이 바깥 기술에 묶이는 문제

업무 규칙은 기술과 상관없이 업무가 정한 규칙입니다. 「배송이 시작된 주문은 취소할 수 없다」가 그 예입니다. 데이터베이스를 바꾸든 화면을 바꾸든 이 규칙은 똑같이 지켜져야 합니다.

그런데 이 규칙을 담은 코드가 데이터베이스를 다루는 코드를 직접 들여오는 일이 흔합니다. 규칙을 따지는 메서드가 SQL(Structured Query Language, 구조화 질의 언어) 결과 행을 받아 읽는 식입니다.

이렇게 묶이면 두 가지가 곤란해집니다. 저장 기술을 바꾸면 규칙 코드까지 열어 고쳐야 합니다. 규칙 한 줄을 시험하려 해도 데이터베이스를 띄워야 합니다.

프레임워크도 같은 문제를 만듭니다. 프레임워크는 뼈대를 먼저 깔아 두고 개발자의 코드를 불러 주는 라이브러리 묶음입니다. 규칙 코드가 웹 프레임워크의 요청 객체를 받으면 그 프레임워크 없이는 규칙을 돌릴 수 없습니다.

두 코드가 서로 얼마나 얽혀 있는지를 결합도라고 부릅니다. 클린 아키텍처는 업무 규칙과 바깥 기술 사이의 결합도를 낮추려는 구조입니다.

네 개의 원

클린 아키텍처는 코드를 맡는 일에 따라 네 덩이로 나눕니다. 네 덩이를 과녁처럼 겹친 동심원으로 그리므로 이 문서도 한 덩이를 원 하나라고 부릅니다. 안쪽 원일수록 업무에 가깝고 바깥 원일수록 기술에 가깝습니다.

원 맡는 일 주문 취소에서
엔티티 업무 전체에 걸친 핵심 규칙 배송이 시작된 주문은 취소 못 함
유스케이스 이 애플리케이션이 하는 일 하나의 흐름 주문을 꺼내 취소하고 저장함
인터페이스 어댑터 안쪽과 바깥쪽의 데이터 모양을 바꿈 컨트롤러 · 프레젠터 · 저장소 구현
프레임워크와 드라이버 실제 기술 웹 프레임워크 · 데이터베이스

아래 그림은 표의 네 원을 안쪽부터 겹쳐 놓은 모양입니다. 원마다 주문 취소에서 거기 들어가는 코드를 적었습니다.

flowchart TD
    subgraph F["프레임워크와 드라이버"]
        W["웹 프레임워크"]
        DB[("데이터베이스")]
        subgraph A["인터페이스 어댑터"]
            C["컨트롤러"]
            P["프레젠터"]
            R["저장소 구현"]
            subgraph U["유스케이스"]
                CO["주문 취소"]
                subgraph E["엔티티"]
                    O["주문"]
                end
            end
        end
    end

가장 안쪽 원의 엔티티는 업무 전체에서 통하는 규칙을 담은 객체입니다. 주문 객체가 「배송이 시작됐으면 취소를 거절한다」를 스스로 지킵니다. 이 규칙은 웹 주문이든 전화 주문이든 똑같이 걸립니다.

백엔드 개발자에게는 이 이름이 헷갈릴 수 있습니다. ORM(Object-Relational Mapping, 객체-관계 매핑) 도구가 테이블 한 줄에 맞춰 만드는 클래스도 엔티티라고 부르기 때문입니다. 클린 아키텍처의 엔티티는 테이블과 상관없는 업무 규칙 객체입니다.

두 번째 원의 유스케이스는 애플리케이션이 해 주는 일 하나의 흐름을 담습니다. 「주문 취소」라면 먼저 주문을 꺼냅니다. 엔티티에게 취소를 시킨 뒤 바뀐 주문을 저장합니다. 규칙 자체는 엔티티가 지고 유스케이스는 순서를 정합니다.

세 번째 원의 인터페이스 어댑터는 데이터 모양을 바꿔 주는 코드입니다. 컨트롤러는 들어온 요청을 유스케이스가 받는 모양으로 바꿉니다. 프레젠터는 유스케이스가 낸 결과를 응답 모양으로 바꿉니다.

같은 원의 저장소 구현은 주문을 데이터베이스에 넣고 꺼내는 코드입니다. 유스케이스의 저장 요청을 데이터베이스 호출로 바꿉니다. 유스케이스가 바깥을 모른 채 어떻게 저장을 요청하는지는 아래 「부르는 방향과 의존하는 방향」 절에서 다룹니다.

가장 바깥 원에는 웹 프레임워크, 데이터베이스, 메시지 큐 같은 실제 기술이 들어갑니다. 이 원의 코드는 대개 기술을 안쪽 원에 이어 붙이는 설정 코드뿐입니다. 주문 취소 유스케이스에 어느 저장소 구현을 쓸지 골라 넣어 주는 코드가 그 예입니다.

의존성 규칙

A 의 코드가 B 의 이름을 적어 두고 써야 돌아가면 A 가 B 에 의존한다고 말합니다. 클래스를 들여오거나 함수를 부르거나 타입을 받는 일이 모두 의존입니다. 이 관계를 의존성이라고 부릅니다.

네 원 사이에는 규칙이 하나 있습니다. 소스 코드의 의존성은 안쪽으로만 향해야 한다는 규칙입니다. 이것을 의존성 규칙이라고 부릅니다.

의존성 규칙에 따라 안쪽 원의 코드에는 바깥 원의 이름이 한 번도 나오지 않습니다. 엔티티는 유스케이스를 모르고 유스케이스는 컨트롤러와 데이터베이스를 모릅니다. 거꾸로 바깥 원은 안쪽 원을 마음껏 알아도 됩니다.

그래서 바깥 원을 바꿔도 안쪽은 흔들리지 않습니다. 데이터베이스를 바꾸면 저장소 구현만 고칩니다. 웹 요청 대신 메시지를 받게 바꾸면 컨트롤러 쪽만 고칩니다. 업무 규칙이 든 두 원은 손대지 않습니다.

부르는 방향과 의존하는 방향

여기서 문제가 하나 생깁니다. 주문 취소 유스케이스는 주문을 저장해야 합니다. 저장은 바깥 원의 일인데 유스케이스는 바깥을 몰라야 합니다.

이 문제는 인터페이스로 풉니다. 인터페이스는 메서드 이름과 모양만 정해 두고 몸통은 비워 둔 약속입니다. 부르는 쪽은 그 약속만 알면 되고 뒤에 어떤 코드가 있는지 몰라도 됩니다.

유스케이스 원이 「주문을 찾고 저장해 주는 것」이라는 인터페이스를 직접 정합니다. 바깥의 저장소 구현이 그 인터페이스를 구현합니다. 아래 세 코드 조각을 안쪽 원부터 차례로 봅니다.

첫 조각은 가장 안쪽의 엔티티 원입니다. 취소 규칙이 주문 객체 안에 있습니다.

Java
// 엔티티 원
class Order {
  private Status status;

  void cancel() {
    if (status == Status.SHIPPED)
      throw new CannotCancel();
    status = Status.CANCELED;
  }
}

이 클래스에는 데이터베이스도 요청도 나오지 않습니다. 상태가 배송됨(SHIPPED)이면 예외를 던지고 아니면 취소됨(CANCELED)으로 바꿉니다.

다음은 유스케이스 원입니다. 저장 인터페이스와 그것을 쓰는 유스케이스가 함께 있습니다.

Java
// 유스케이스 원
interface OrderRepository {
  Order findById(long id);
  void save(Order order);
}

class CancelOrder {
  private final OrderRepository orders;

  CancelOrder(OrderRepository orders) {
    this.orders = orders;
  }

  void run(long id) {
    Order order = orders.findById(id);
    order.cancel();
    orders.save(order);
  }
}

CancelOrder 는 OrderRepository 라는 인터페이스만 압니다. 그 뒤에 어떤 데이터베이스가 있는지는 모릅니다. 시험할 때는 메모리에 주문을 들고 있는 가짜 구현을 넣어 데이터베이스 없이 돌립니다.

마지막은 바깥의 인터페이스 어댑터 원입니다. 저장소 구현이 안쪽의 인터페이스를 구현합니다.

Java
// 인터페이스 어댑터 원
class SqlOrderRepository
    implements OrderRepository {

  public Order findById(long id) {
    OrderRow row = db.selectOrder(id);
    return row.toOrder();
  }

  public void save(Order order) {
    db.updateOrder(OrderRow.from(order));
  }
}

OrderRow 는 테이블 한 줄의 모양을 본뜬 클래스입니다. 이 클래스는 어댑터 원 안에만 있습니다. toOrder 와 from 이 테이블 모양과 업무 객체 모양을 서로 바꿔 줍니다. db 는 데이터베이스에 질의를 보내는 바깥 원의 객체입니다.

아래 그림은 세 조각이 서로를 어느 방향으로 아는지 보입니다. 코드를 원별로 묶어 바깥 원을 위에, 안쪽 원을 아래에 두었습니다. 화살표는 부르는 방향이 아니라 의존하는 방향입니다.

flowchart TD
    subgraph A["인터페이스 어댑터"]
        C["컨트롤러"]
        S["SqlOrderRepository · 저장소 구현"]
    end
    subgraph U["유스케이스"]
        CO["CancelOrder · 주문 취소"]
        I["OrderRepository · 저장 인터페이스"]
    end
    subgraph E["엔티티"]
        O["Order · 주문"]
    end
    C --> CO
    S -->|"구현한다"| I
    S --> O
    CO --> I
    CO --> O

모든 화살표가 위의 원에서 아래의 원으로, 곧 바깥에서 안쪽으로 내려갑니다. 아래에서 위로 올라가는 화살표는 하나도 없습니다.

실행될 때 부르는 방향은 다릅니다. 유스케이스가 save 를 부르면 바깥의 SqlOrderRepository 코드가 돕니다. 부르는 방향은 안에서 밖으로 나가지만 의존하는 방향은 밖에서 안으로 향합니다.

이렇게 인터페이스를 안쪽이 가져 의존 방향을 뒤집는 원칙을 의존성 역전 원칙이라고 부릅니다. 클린 아키텍처는 원과 원이 맞닿는 곳마다 이 원칙을 씁니다.

유스케이스가 결과를 내보낼 때도 같은 방법을 씁니다. 유스케이스 원이 「결과를 받아 줄 것」이라는 인터페이스를 정합니다. 프레젠터가 그 인터페이스를 구현해 결과를 응답 모양으로 바꿉니다.

누가 CancelOrder 에 SqlOrderRepository 를 넣어 주는지도 정해야 합니다. 이 일은 가장 바깥 원의 설정 코드가 맡습니다. 필요한 객체를 밖에서 만들어 넣어 주는 방식을 의존성 주입이라고 부릅니다.

원의 경계를 넘는 데이터

원과 원이 맞닿는 곳을 경계라고 부릅니다. 경계를 넘는 데이터는 안쪽 원이 쓰기 편한 모양이어야 합니다.

OrderRow 같은 테이블 행 객체를 유스케이스로 그대로 올려 보내면 의존성 규칙이 깨집니다. 유스케이스 코드에 바깥 원의 타입 이름이 나오기 때문입니다. 웹 프레임워크의 요청 객체를 안으로 넘겨도 마찬가지입니다.

반대로 앞 코드의 findById 가 엔티티 Order 를 돌려주는 것은 규칙에 어긋나지 않습니다. 저장소 구현이 안쪽 타입 Order 를 아는 것은 바깥이 안을 아는 방향이기 때문입니다. 의존성 규칙이 막는 것은 바깥 타입이 안으로 들어오는 쪽입니다.

그래서 바깥의 데이터를 안으로 넘길 때는 필드만 담은 단순한 객체로 옮겨 담습니다. 계층이나 경계 사이에서 데이터를 나르려고 만든 이런 객체가 DTO(Data Transfer Object, 데이터 전송 객체)입니다.

주문 취소에서는 두 곳에서 이런 옮겨 담기가 일어납니다. 컨트롤러는 웹 요청 객체에서 주문 번호만 꺼내 유스케이스에 넘깁니다. 지금은 값이 번호 하나라 long 으로 충분합니다. 넘길 값이 여럿이면 그 값만 담은 입력 객체를 따로 만듭니다.

결과가 나가는 쪽에도 이런 객체를 씁니다. 유스케이스는 프레젠터에 결과를 넘길 때 주문 번호와 바뀐 상태만 담은 결과 객체를 만듭니다. 프레젠터는 그 객체를 받아 응답 모양으로 바꿉니다.

네 원은 정해진 수가 아니다

원이 꼭 넷일 필요는 없습니다. 업무 규칙이 단순하면 엔티티와 유스케이스를 한 원으로 합치기도 합니다. 큰 시스템이면 원을 더 잘게 나누기도 합니다.

바뀌지 않는 것은 의존성 규칙 하나입니다. 원을 몇 개로 나누든 소스 코드의 의존성은 안쪽으로만 향합니다.

클린 아키텍처가 치르는 대가

업무 규칙을 떼어 낸 대신 코드 양과 거쳐 가는 단계가 늘어납니다. 그 대가는 셋입니다.

첫째는 경계마다 생기는 코드입니다. 저장 하나에도 인터페이스, 구현, 행 객체, 변환 메서드가 따로 생깁니다. 필드 하나를 더하면 엔티티, 행 객체, DTO, 변환 코드를 차례로 고칩니다.

둘째는 읽기가 한 번 더 꺾인다는 점입니다. 유스케이스에서 save 를 따라가면 몸통 없는 인터페이스가 나옵니다. 실제로 도는 코드를 보려면 그 인터페이스를 구현한 클래스를 다시 찾아야 합니다.

셋째는 프레임워크가 주는 편의를 일부 내려놓는다는 점입니다. 업무 객체 클래스에 프레임워크의 어노테이션을 붙여 바로 저장하는 지름길을 쓰지 않습니다. 그 어노테이션이 엔티티 원에 바깥 이름을 들여오기 때문입니다.

경계마다 붙는 코드는 업무 규칙이 크든 작든 똑같이 생깁니다. 규칙이 적고 넣고 꺼내기가 대부분이면 얻는 것이 대가보다 작습니다. 규칙이 크고 오래 사는 프로그램이면 얻는 것이 커집니다. 붙는 바깥 기술이 자주 바뀔 때도 그렇습니다.

레이어드 · 헥사고날과 갈리는 점

레이어드는 코드를 표현, 업무, 영속 계층으로 위아래로 쌓습니다. 의존성은 아래로 흘러 업무 계층이 영속 계층을 압니다. 클린 아키텍처는 이 방향을 뒤집어 저장 쪽이 업무 쪽을 알게 합니다.

헥사고날은 업무 규칙을 한가운데 두고 바깥 기술과 만나는 곳마다 포트와 어댑터를 둡니다. 포트는 안쪽이 정한 인터페이스입니다. 어댑터는 그 인터페이스를 기술에 잇는 구현입니다. 그래서 헥사고날을 포트와 어댑터라고도 부릅니다.

클린 아키텍처는 헥사고날과 어니언 아키텍처 같은 앞선 구조들을 한 그림으로 묶은 것입니다. 셋 다 업무 규칙이 가운데에 있고 의존성이 안쪽을 향합니다. 클린 아키텍처는 가운데를 엔티티와 유스케이스 두 원으로 한 번 더 가른다는 점이 다릅니다.

구조 업무 규칙을 두는 곳 의존성의 방향
레이어드 세 계층 중 가운데 업무 계층 표현 → 업무 → 영속
헥사고날 한가운데 한 덩이 어댑터 → 포트 → 업무 규칙
클린 아키텍처 엔티티와 유스케이스 두 원 바깥 원 → 안쪽 원

이 구조들은 모두 시스템을 어떤 모양으로 지을지 정하는 결정입니다. 이 결정을 소프트웨어 아키텍처라고 부릅니다.

모듈러 모놀리스는 시스템을 한 덩이로 배포하는 구조입니다. 대신 안은 업무 단위 모듈로 나눕니다. 이 구조에서는 모듈 하나하나를 클린 아키텍처로 짓기도 합니다.

관련 항목

클린 아키텍처를 이루는 원

엔티티 · 유스케이스 · 인터페이스 어댑터 · 프레임워크와 드라이버 · 컨트롤러 · 프레젠터

클린 아키텍처가 따르는 설계 원칙

의존성 규칙 · 의존성 역전 원칙 · SOLID · 관심사 분리 · 결합도 · 응집도 · 의존성 · 의존성 주입 · 인터페이스 · 추상화

클린 아키텍처가 뿌리를 둔 아키텍처 양식

헥사고날 · 포트와 어댑터 · 어니언 아키텍처 · 스크리밍 아키텍처 · DCI · BCE

클린 아키텍처와 맞세워지는 아키텍처 양식

레이어드 · 수직 슬라이스 아키텍처 · MVC · 트랜잭션 스크립트

클린 아키텍처가 속하는 상위 분류

소프트웨어 아키텍처 · 아키텍처 양식 · 아키텍처 패턴 · 디자인 패턴

원의 경계를 넘을 때 쓰는 수단

DTO · 매퍼 · 리포지토리 패턴 · ORM · 값 객체

업무 규칙을 모델링하는 방법론

도메인 주도 설계 · 도메인 모델 · 빈약한 도메인 모델 · 애그리거트 · 비즈니스 로직

클린 아키텍처가 바깥 원에 두는 기술

데이터베이스 · 프레임워크 · 웹 프레임워크 · 메시지 큐

클린 아키텍처를 안에 품는 시스템 구성

모놀리스 · 모듈러 모놀리스 · 마이크로서비스 · 모듈

클린 아키텍처에서 자주 생기는 문제

과잉 설계 · 누수 추상화 · 순환 의존성 · 계층 위반 · 싱크홀 안티패턴

안쪽 원을 떼어 시험하는 방법

단위 테스트 · 테스트 더블 · 목 객체 · 통합 테스트

다른 이름: clean architecture · Clean Architecture · 클린 아키텍쳐