Featured image of post [Clean Architecture] 27. 경계: 선 긋기와 플러그인 아키텍처

[Clean Architecture] 27. 경계: 선 긋기와 플러그인 아키텍처

아키텍처에서 경계를 긋는 방법과 시점을 다룹니다. 비즈니스 규칙과 세부사항 사이에 경계를 설정하여 플러그인 아키텍처를 구현하는 방법을 설명합니다.

아키텍처에서 **경계(Boundary)**란 관심사를 분리하는 선이다. 경계의 한쪽에는 비즈니스 규칙이, 다른 쪽에는 세부사항이 있다. 경계를 제대로 그으면 플러그인 아키텍처가 된다.

경계란?

“경계는 소프트웨어 요소를 분리하고, 한쪽이 다른 쪽을 모르게 한다.” — Robert C. Martin

flowchart LR
    subgraph Left [한쪽]
        A[비즈니스 규칙]
    end
    
    BOUNDARY[경계선]
    
    subgraph Right [다른 쪽]
        B[세부사항]
    end
    
    A --- BOUNDARY --- B

어디에 경계를 긋는가?

관련 없는 것들 사이에 경계를 긋는다:

경계의 한쪽경계의 다른 쪽
비즈니스 규칙GUI
비즈니스 규칙데이터베이스
비즈니스 규칙프레임워크
비즈니스 규칙외부 서비스
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 경계를 긋기 전: 모든 것이 섞여있음
public class OrderService {
    public void createOrder(Request request) {
        // GUI에 대한 지식
        String json = parseJsonFromHttpRequest(request);
        
        // 비즈니스 규칙
        Order order = new Order(json);
        order.validate();
        
        // DB에 대한 지식
        Connection conn = DriverManager.getConnection("...");
        PreparedStatement stmt = conn.prepareStatement("INSERT...");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 경계를 긋고 난 후: 깔끔하게 분리됨
public class CreateOrderUseCase {
    private final OrderRepository repository;  // 인터페이스
    
    public void execute(OrderRequest request) {
        // 비즈니스 규칙만 알고 있음
        Order order = new Order(request);
        order.validate();
        repository.save(order);
        // DB가 MySQL인지, 파일인지 모름
    }
}

FitNesse 사례

마틴은 자신이 개발한 FitNesse 프로젝트 경험을 공유한다.

조기 결정의 유혹

flowchart LR
    subgraph Temptation [유혹]
        T1[MySQL이 필요할 것 같아]
        T2[지금 당장 선택하자]
        T3[나중에 바꾸기 어려워]
    end

초기에 MySQL을 선택하고 싶었지만, 결정을 미뤘다. 데이터베이스 선택 없이 개발을 진행했다.

경계를 먼저 그음

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// FitNesse의 데이터 접근 인터페이스
public interface WikiPagePersistence {
    WikiPage load(String pageName);
    void save(WikiPage page);
    List<WikiPage> findAll();
}

// 비즈니스 규칙은 이 인터페이스만 알면 됨
public class WikiPageService {
    private final WikiPagePersistence persistence;
    
    public WikiPage getPage(String name) {
        return persistence.load(name);
    }
}

결과

진행 단계상태
초기 개발파일 시스템으로 구현
나중에MySQL 옵션 추가
최종사용자가 선택
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 파일 시스템 구현
public class FileSystemWikiPagePersistence implements WikiPagePersistence {
    public WikiPage load(String pageName) {
        // 파일에서 읽기
        return readFromFile(pageName);
    }
}

// MySQL 구현 (나중에 추가)
public class MySqlWikiPagePersistence implements WikiPagePersistence {
    public WikiPage load(String pageName) {
        // MySQL에서 읽기
        return readFromDatabase(pageName);
    }
}

“놀랍게도 MySQL이 필요 없었다. 파일 시스템만으로 충분했다. 우리는 결정을 미루는 데 성공했고, 그 덕분에 불필요한 복잡성을 피할 수 있었다.”

플러그인 아키텍처

경계를 제대로 그으면 플러그인 아키텍처가 된다.

flowchart TB
    subgraph Core [비즈니스 코어 - 중심]
        BR[Business Rules]
        UC[Use Cases]
        INTF[Interfaces]
    end
    
    subgraph Plugins [플러그인들 - 외곽]
        DB[(Database)]
        GUI[GUI]
        FW[Framework]
        EXT[외부 서비스]
    end
    
    DB -->|구현| INTF
    GUI --> UC
    FW --> BR
    EXT -->|구현| INTF

플러그인 아키텍처의 특징

특징설명
코어비즈니스 규칙, 유스케이스, 인터페이스
플러그인DB, GUI, 프레임워크, 외부 서비스
의존성 방향플러그인 → 코어
교체 가능성플러그인은 언제든 교체 가능

코드로 보는 플러그인 아키텍처

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 코어: 인터페이스 정의
public interface PaymentGateway {
    PaymentResult process(Payment payment);
}

// 플러그인 1: Stripe
public class StripeGateway implements PaymentGateway {
    public PaymentResult process(Payment payment) {
        // Stripe API 호출
        return stripeApi.charge(payment);
    }
}

// 플러그인 2: PayPal
public class PayPalGateway implements PaymentGateway {
    public PaymentResult process(Payment payment) {
        // PayPal API 호출
        return paypalApi.charge(payment);
    }
}

// 플러그인 3: 테스트용 Mock
public class MockPaymentGateway implements PaymentGateway {
    public PaymentResult process(Payment payment) {
        return PaymentResult.success();
    }
}

// 비즈니스 규칙: 플러그인을 모름
public class PaymentService {
    private final PaymentGateway gateway;
    
    public void pay(Order order) {
        Payment payment = createPayment(order);
        PaymentResult result = gateway.process(payment);
        // Stripe인지 PayPal인지 모르고 처리
    }
}

경계 긋기 규칙

규칙 1: 비즈니스 규칙이 UI를 모르게

flowchart LR
    UI[UI] --> BR[비즈니스 규칙]
    BR -.->|모름| UI
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 나쁜 예: 비즈니스 규칙이 UI를 암
public class OrderService {
    public void createOrder(HttpServletRequest request) {  // UI 기술!
        String json = request.getParameter("order");
        // ...
    }
}

// 좋은 예: UI를 모름
public class CreateOrderUseCase {
    public void execute(CreateOrderRequest request) {  // 순수 DTO
        // ...
    }
}

규칙 2: 비즈니스 규칙이 DB를 모르게

flowchart LR
    DB[(Database)] --> BR[비즈니스 규칙]
    BR -.->|모름| DB
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 나쁜 예: 비즈니스 규칙이 DB를 암
public class OrderService {
    public void save(Order order) {
        Connection conn = DriverManager.getConnection("jdbc:mysql://...");
        // ...
    }
}

// 좋은 예: DB를 모름
public class OrderService {
    private final OrderRepository repository;  // 인터페이스
    
    public void save(Order order) {
        repository.save(order);  // 구체적인 DB 기술 모름
    }
}

규칙 3: 의존성은 비즈니스 규칙을 향하게

flowchart TB
    subgraph Outer [바깥쪽]
        UI[UI]
        DB[(DB)]
        FW[Framework]
    end
    
    subgraph Inner [안쪽]
        BR[Business Rules]
    end
    
    UI --> BR
    DB --> BR
    FW --> BR

입력과 출력은 중요치 않다

마틴은 흥미로운 관점을 제시한다:

“입력과 출력은 중요치 않다. 비즈니스 규칙이 중요하다.”

flowchart LR
    INPUT[입력
Web, CLI, API] --> CORE[비즈니스 규칙
핵심] CORE --> OUTPUT[출력
DB, File, API]

우리는 종종 시스템을 입력/출력 관점에서 생각하지만, 실제로 가치를 창출하는 것은 비즈니스 규칙이다.

관점초점
전통적입력 → 처리 → 출력
클린 아키텍처비즈니스 규칙이 중심

경계를 늦게 그으면?

경계를 늦게 그으면 다음과 같은 문제가 발생한다:

flowchart TB
    LATE[경계를 늦게 그음]
    
    P1[코드 결합도 증가]
    P2[테스트 어려움]
    P3[변경 비용 증가]
    P4[기술 종속]
    
    LATE --> P1 --> P2 --> P3 --> P4
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 경계 없이 시작한 코드
public class OrderController {
    public void createOrder(HttpServletRequest req) {
        // UI, 비즈니스, DB가 모두 섞임
        String name = req.getParameter("name");
        Order order = new Order(name);
        order.setTotal(order.calculateTotal());
        
        Connection conn = DriverManager.getConnection("...");
        PreparedStatement stmt = conn.prepareStatement(
            "INSERT INTO orders VALUES (?, ?)");
        stmt.setString(1, order.getName());
        stmt.setDouble(2, order.getTotal());
        stmt.execute();
        
        // 이 코드를 분리하려면? 재작성 수준의 노력 필요
    }
}

핵심 요약

항목내용
경계의 정의관심사를 분리하는 선
경계의 위치비즈니스 규칙과 세부사항 사이
플러그인 아키텍처코어는 중심, 세부사항은 플러그인
의존성 방향세부사항 → 비즈니스 규칙
핵심 이점세부사항 교체 가능

“경계를 제대로 그으면, 나중에 세부사항을 바꿀 수 있다. 경계가 없으면 갇히게 된다.” — Robert C. Martin