스택큐힙리스트

Interpreter 패턴: 미니 DSL로 “규칙을 읽는” 코드 만들기 본문

개발

Interpreter 패턴: 미니 DSL로 “규칙을 읽는” 코드 만들기

스택큐힙리스트 2025. 8. 8. 15:02
반응형

복잡한 if-else 정글 대신, 사람이 읽는 문장 같은 규칙을 그대로 코드가 해석(interpret)해 실행하면 얼마나 깔끔할까요?
Interpreter 패턴은 아주 단순한 문법(Grammar)을 클래스 구조로 표현하고, 그 트리를 순회하며 의미를 평가하는 방식입니다. 검색 필터, 권한 규칙, 프로모션 조건, 피처 플래그 같은 반복적으로 해석해야 하는 도메인 문장에 제격이에요.


언제 쓰나? (한눈에)

  • 도메인 규칙을 문장처럼 표현하고 싶을 때 (ex. “가격 > 1만 AND 태그=세일”).
  • 같은 규칙을 여러 데이터에 반복 적용해야 할 때.
  • 문법이 작고 안정적일 때(중요!). 커지면 파서/컴파일러 영역이라 다른 접근이 낫습니다.

큰 그림: 구성 요소

  • Expression 계층: 규칙을 트리로 표현하는 인터페이스/클래스들
    • TerminalExpression : 숫자·문자·단일 속성 비교 같은 원자 규칙
    • NonTerminalExpression : AND/OR/NOT 같은 조합 규칙
  • Context: 해석 대상(예: 상품, 사용자, 요청 등)
  • interpret(context): 트리를 따라 내려가며 참/거짓 또는 값을 계산

실무에선 Composite로 트리를 만들고, 필요하면 Visitor로 최적화·로깅·출력 같은 부가 작업을 붙입니다.


Kotlin 미니 예제 – 상품 필터 DSL

“가격이 1만 원 초과이고, 태그에 ‘세일’ 또는 ‘신상’이 포함” 규칙을 해석하는 예제입니다.

// --- 도메인 ---
data class Product(val price: Int, val tags: Set<String>)

// --- Expression ---
fun interface Expr { fun interpret(p: Product): Boolean }

// Terminal
class PriceGt(private val min: Int) : Expr {
    override fun interpret(p: Product) = p.price > min
}
class HasTag(private val tag: String) : Expr {
    override fun interpret(p: Product) = tag in p.tags
}

// NonTerminal
class And(private val left: Expr, private val right: Expr) : Expr {
    override fun interpret(p: Product) = left.interpret(p) && right.interpret(p)
}
class Or(private val left: Expr, private val right: Expr) : Expr {
    override fun interpret(p: Product) = left.interpret(p) || right.interpret(p)
}
class Not(private val expr: Expr) : Expr {
    override fun interpret(p: Product) = !expr.interpret(p)
}

// --- DSL 헬퍼 ---
infix fun Expr.and(other: Expr) = And(this, other)
infix fun Expr.or(other: Expr)  = Or(this, other)
fun not(e: Expr) = Not(e)

// --- 규칙 트리 (Grammar를 코드로) ---
val rule: Expr =
    PriceGt(10_000) and (HasTag("세일") or HasTag("신상"))

// --- 사용 ---
val a = Product(price = 9000,  tags = setOf("세일"))
val b = Product(price = 12000, tags = setOf("여름", "신상"))

println(rule.interpret(a)) // false (가격 조건 탈락)
println(rule.interpret(b)) // true

핵심은 규칙을 데이터처럼 조립한다는 점이에요. 이제 “세일은 아니지만 ‘특가’ 태그면 OK” 같은 요구도 트리만 바꾸면 바로 반영됩니다.


실전 팁

  1. 문법을 욕심내지 말 것: 비교·AND/OR/NOT 정도로 시작하세요. 커지면 파싱·성능이 급격히 어려워집니다.
  2. 캐시/메모이제이션: 같은 규칙을 많은 데이터에 적용하면 서브트리 결과 캐시가 체감 성능을 올려줍니다.
  3. 빌더/DSL로 표현력 업그레이드: 위처럼 infix/헬퍼 함수로 “읽히는” 코드를 만드세요.
  4. 검증 단계 추가: 배포 전 규칙 트리를 정적 검사(불가능한 조합, 항상 true 등)해 장애를 줄이세요.
  5. 한계를 인정하기: 문법이 커지기 시작하면 파서(ANTLR), 스펙/룰 엔진(SPEL, MVEL, Drools), Kotlin DSL, 혹은 Query 언어로 전환을 고려하는 게 장기적으로 이득입니다.

장단점 정리

  • 장점
    • 규칙을 데이터처럼 조립 → 변경·테스트 용이
    • 도메인 담당자와 공유 가능한 표현(의사 언어)
  • 단점
    • 문법이 커지면 성능·가독성·테스트 비용 폭증
    • 트리 생성(파싱)까지 자체 구현하면 유지보수 난도↑

한 줄 요약

Interpreter 패턴은 “작은 문법을 가진 규칙 엔진”에 최적—트리로 규칙을 만들고 해석해, 변화 많은 비즈니스 룰을 안전하게 운용할 수 있습니다.

반응형
Comments