|

코틀린 디자인 패턴 (13) – Chain of Responsibility 패턴으로 조건 분기 줄이기

코틀린 Chain of Responsibility 패턴 대표 이미지
책임 연쇄 패턴의 핵심은 큰 분기문을 처리기 사슬로 나누는 데 있다

코틀린 Chain of Responsibility 패턴은 조건 분기가 점점 길어질 때 특히 의미가 커집니다. 처음에는 if 문 몇 개로 시작하지만, 검증 규칙과 예외 조건이 늘어나면 곧 읽기 어려운 분기 더미가 만들어집니다. 이때 책임 연쇄 패턴은 요청을 여러 처리기가 순서대로 판단하게 만드는 구조로 문제를 다시 정리합니다.

이번 글에서는 책임 연쇄 패턴을 단순 정의로 끝내지 않고, 검증 파이프라인 예시를 통해 왜 if/else 폭발을 줄이는 데 도움이 되는지 자세히 설명하겠습니다.

책임 연쇄 패턴 핵심 요약 카드
책임 연쇄 패턴은 조건 분기를 한곳에 몰아넣지 않고 처리기 사슬로 나눠서 읽기 쉽게 만든다

왜 이 패턴이 필요할까

서비스 코드가 길어지는 이유 중 하나는 검증 책임이 한 메서드 안에 몰리기 때문입니다. 형식 검사, 권한 검사, 정책 검사, 상태 검사, 저장 전 확인이 한 덩어리로 붙으면 코드는 금방 길어지고, 새 규칙을 추가할 때마다 분기가 더 커집니다.

fun validate(request: OrderRequest) {
    if (request.userId.isBlank()) error("userId required")
    if (request.amount <= 0) error("amount must be positive")
    if (!permissionService.canOrder(request.userId)) error("permission denied")
    if (request.amount > 1_000_000) error("limit exceeded")
    if (!inventoryService.isAvailable(request.itemId)) error("out of stock")
}

처음에는 단순해 보여도 규칙이 늘수록 이 함수는 금방 무거워집니다. 책임 연쇄 패턴은 이런 검사를 여러 처리기로 쪼개서 순서대로 통과시키는 구조로 바꿉니다.


가장 쉬운 정의: 요청을 처리기 사슬에 흘려보낸다

책임 연쇄 패턴은 요청을 하나의 객체가 독점 처리하지 않고, 여러 처리기가 차례대로 판단할 기회를 갖게 만드는 방식입니다. 어떤 처리기는 직접 처리하고 끝낼 수 있고, 어떤 처리기는 다음 처리기로 넘길 수 있습니다. 그래서 구조의 핵심은 “누가 처리할지 미리 단단히 고정하지 않는다”는 점입니다.

검증 요청이 체인을 따라 이동하는 흐름 그림
요청은 처리기 하나에 몰리지 않고 체인을 따라 흘러가며 각 단계에서 검사된다

코틀린 코드로 보면 어떻게 생길까

data class OrderRequest(
    val userId: String,
    val itemId: String,
    val amount: Int,
)

interface OrderValidator {
    fun validate(request: OrderRequest)
}

class UserIdValidator : OrderValidator {
    override fun validate(request: OrderRequest) {
        require(request.userId.isNotBlank()) { "userId required" }
    }
}

class AmountValidator : OrderValidator {
    override fun validate(request: OrderRequest) {
        require(request.amount > 0) { "amount must be positive" }
    }
}

class CompositeValidator(
    private val validators: List<OrderValidator>,
) : OrderValidator {
    override fun validate(request: OrderRequest) {
        validators.forEach { it.validate(request) }
    }
}

이 코드는 전통적인 next 포인터 체인보다, 코틀린답게 리스트 기반 체인으로 풀어낸 예시입니다. 중요한 점은 검증 규칙이 각각 독립된 객체가 되고, 클라이언트는 전체 체인만 바라본다는 점입니다.


이 구조가 if/else보다 나은 이유

  1. 규칙 하나를 수정해도 전체 거대한 함수가 덜 흔들린다
  2. 검사 순서를 바꾸거나 일부만 조합하기 쉽다
  3. 처리기 단위 테스트가 쉬워진다
  4. 새 규칙을 추가할 때 기존 분기문을 크게 건드리지 않아도 된다

특히 검증 규칙이 계속 늘어나는 도메인에서는 이 차이가 큽니다. 분기문은 시간이 갈수록 수정 비용이 늘어나지만, 체인 구조는 작은 처리기 단위로 조합을 바꾸는 쪽이 더 쉽습니다.


하지만 언제나 정답은 아니다

책임 연쇄 패턴을 쓰면 구조가 깔끔해지는 대신, 처리 흐름이 파일 여러 개로 흩어질 수 있습니다. 그래서 간단한 검증 두세 개 정도밖에 없는데도 무조건 체인으로 쪼개면 오히려 읽기가 더 어려워질 수 있습니다.

  • 규칙이 매우 적고 거의 바뀌지 않으면 단순 함수가 더 낫다
  • 체인이 너무 길어지면 흐름 추적이 어려워진다
  • 처리 순서 의존성이 강하면 체인 재조합이 위험할 수 있다

즉 중요한 것은 패턴 자체보다, 조건 분기와 규칙 추가가 정말 자주 일어나는 문제인가를 먼저 보는 일입니다.


Strategy나 Decorator와는 무엇이 다를까

Strategy는 여러 알고리즘 중 하나를 바꿔 끼우는 데 가깝고, Decorator는 기능을 감싸서 덧붙이는 데 가깝습니다. 반면 Chain of Responsibility는 여러 처리기가 순서대로 요청을 만지거나 판단하게 만드는 흐름 구조에 더 가깝습니다.

즉 “하나를 선택해서 쓰는가”, “기능을 감싸 확장하는가”, “여러 단계를 순서대로 통과시키는가”를 구분하면 세 패턴이 훨씬 덜 헷갈립니다.


코틀린에서는 어떻게 더 단순해질까

코틀린에서는 전통적인 next handler 필드를 엄격하게 구현하지 않아도, 리스트와 고차 함수만으로 체인 감각을 꽤 자연스럽게 만들 수 있습니다. 그래서 책임 연쇄 패턴을 반드시 클래식한 UML 형태로만 구현해야 한다고 생각할 필요는 없습니다.

오히려 코틀린에서는 작은 validator 객체 목록, sealed class 기반 결과 모델, 함수 체인으로 더 간결하게 표현하는 쪽이 실용적인 경우도 많습니다.


마무리

코틀린 디자인 패턴 13편의 핵심은 이렇습니다. Chain of Responsibility 패턴은 조건 분기를 감추는 마법이 아니라, 요청 처리 책임을 여러 작은 처리기로 나누고 순서대로 흘려보내는 구조입니다.

그래서 이 패턴은 if/else가 무조건 나쁘다는 뜻이 아니라, 검증 규칙과 처리 단계가 계속 늘어나는 상황에서 구조를 덜 무겁게 만드는 선택지로 이해하는 편이 가장 실용적입니다.

함께보면 좋은 글