Blog Works Github ↗

믹스인(Mixin)의 정의와 예시: 코틀린 믹스인 구현의 예

Mixin 패턴의 개념과 Kotlin에서 인터페이스 위임으로 구현하는 방법

Contents

정의

위키에 따르면 믹스인(Mixins)은 단위 기능 또는 그 집합을 갖는 클래스를 생성하여 다른 클래스와 혼합하는 “개발 스타일”이다. 다른 클래스가 사용할 함수들을 모아 정의한 뒤 “Has a”(또는 “Can”) 관계를 맺는 어떤 클래스로도 의미가 통한다. “실용주의 프로그래머(The Pragmatic Programmer)” 에서는 클래스보다 기법으로 설명하고 있다.

언어에 따라서 mixin 키워드를 직관적으로 제공하거나, traits, category, protocol extensions 과 같은 용어로 믹스인의 개념을 찾아볼 수 있다. 즉 mixin 스타일을 지원하는 방식과 제약 사항이 다르고, 구현방법 또한 다양하다. 각 언어의 구체적인 명세를 이해하는 것도 중요하겠으나, mixin 자체의 목적과 장점을 이해하고 활용하는 것이 중요하다.

필자는 코프링쟁이다. 하여 이 글에서는 믹스인의 예시를 코틀린으로 작성했다.

목적

1969년 시뮬라67(Simula67)로 “상속”이 처음 등장한 이래, 이른바 “상속세”로 불리우는 여러 문제점들이 공감대를 얻는다. 믹스인은 Flavors 란 언어에서 처음 등장했고 이러한 “상속세”를 피하기 위한 대안 중 하나로 자주 언급되었다. 따라서 주로 언급되는 믹스인을 활용하는 목적은 아래와 같다:

  • 코드 결합도 낮추기
  • 단일 상속 제한 우회
  • 다중 상속 문제 피하기
  • 괴물같은 상속 계층도 피하기
  • 기능 단위 모듈화

믹스인 기법은 이러한 목적 아래 내부 모듈 사이 코드 가시성을 엄격히 다루는 프로젝트나 불특정 클래스 정의를 지원하는 라이브러리에 활용된다.

예시

예시 1. jackson-databind 의 ObjectMapper 믹스인

com.fasterxml.jackson.databind.ObjectMapper는 런타임에 특정 클래스에 대한 직렬화/역직렬화 동작을 변경할 수 있도록 믹스인 기능을 제공한다.

@JsonIgnoreProperties("password")
class User(val username: String, val password: String)

abstract class UserMixin {
    @JsonIgnore
    abstract fun getPassword(): String
}

val mapper = ObjectMapper().apply {
    addMixIn(User::class.java, UserMixin::class.java)
}

이 방식은 라이브러리 내부 클래스를 수정하지 않고, 외부에서 동작만 교체해야 하는 상황에서 유용하게 활용된다.

예시 2. UI 이벤트 처리 상 믹스인 기법 도입

Android 나 Compose 기반 UI에서 클릭 리스너, 스크롤 리스너, 로딩 상태 처리 등 공통 UI 행위를 믹스인 형태로 모듈화할 수 있다.

interface ClickHandler {
    fun onClick() = println("Clicked!")
}

interface LoadingState {
    var isLoading: Boolean
    fun showLoading() { isLoading = true }
    fun hideLoading() { isLoading = false }
}

class MyViewModel : ClickHandler, LoadingState {
    override var isLoading: Boolean = false
}

인터페이스와 디폴트 메서드를 활용하여 상속 트리를 공유하지 않는 ViewModel 들에 대해 행위 중심 기능을 믹스인할 수 있다.

예시 3. Spring의 HandlerInterceptorAdapter 대체

Spring MVC에서 여러 컨트롤러에 공통 행위를 주입하고자 할 때, HandlerInterceptorAdapter나 AOP를 사용하는 대신 믹스인 방식으로 정리할 수 있다.

interface LoggingHandler {
    fun logRequest(uri: String) = println("Request URI: $uri")
}

@RestController
class MyController : LoggingHandler {

    @GetMapping("/hello")
    fun hello(request: HttpServletRequest): String {
        logRequest(request.requestURI)
        return "Hello"
    }
}

단순한 로깅, 메트릭 수집, 인증 체크 등 반복되는 처리에 대해 믹스인 스타일로 재사용 가능하며, AOP 없이도 명시적이고 가독성 좋은 구조를 만들 수 있다.

고급 구현 예시

예시 4. Kotlin 위임을 활용한 믹스인 스타일 구성

코틀린의 위임 기능을 이용해 믹스인 스타일을 구성할 수 있다.

interface ClickHandler {
    fun onClick()
}

class DefaultClickHandler : ClickHandler {
    override fun onClick() {
        println("Clicked from default handler!")
    }
}

interface LoadingState {
    var isLoading: Boolean
    fun showLoading()
    fun hideLoading()
}

class DefaultLoadingState : LoadingState {
    override var isLoading: Boolean = false

    override fun showLoading() {
        isLoading = true
        println("Loading started")
    }

    override fun hideLoading() {
        isLoading = false
        println("Loading ended")
    }
}

class MyViewModel(
    private val clickHandler: ClickHandler = DefaultClickHandler(),
    private val loadingState: LoadingState = DefaultLoadingState()
) : ClickHandler by clickHandler, LoadingState by loadingState {

    fun loadData() {
        showLoading()
        println("Loading data...")
        hideLoading()
    }
}
fun main() {
    val vm = MyViewModel()
    vm.onClick()
    vm.loadData()
}

예시 5. benoitaverty 식 위임 믹스인

benoitaverty 란 도메인으로 운영하는 이 블로그 글의 아이디어도 도움이 된다. 위임을 활용과 더불어 상태를 갖는 객체에 대한 팩토리 메소드와 호출 연산자를 구현하여 믹스인을 표현한다.

import java.time.Instant

data class TimestampedEvent(
    val timestamp: Instant,
    val event: String
)

interface Auditable {
    fun auditEvent(event: String)
    fun getLatestEvents(n: Int): List<TimestampedEvent>

    companion object {
        private class Holder : Auditable {
            private val events = mutableListOf<TimestampedEvent>()
            override fun auditEvent(event: String) {
                events.add(TimestampedEvent(Instant.now(), event))
            }
            override fun getLatestEvents(n: Int): List<TimestampedEvent> {
                return events.sortedByDescending(TimestampedEvent::timestamp).takeLast(n)
            }
        }
        
        operator fun invoke(): Auditable = Holder()
    }
}

class BankAccount: Auditable by Auditable() {
    private var balance = 0
    fun deposit(amount: Int) {
        auditEvent("deposit $amount")
        balance += amount
    }

    fun withdraw(amount: Int) {
        auditEvent("withdraw $amount")
        balance -= amount 
    }
    
    fun getBalance() = balance
}

fun main() {
    val myAccount = BankAccount()
    
    // This function will call deposit and withdraw many times but we don't know exactly when and how
    giveToComplexSystem(myAccount)
    
    // We can query the balance of the account
    myAccount.getBalance()
    
    // Thanks to the mixin, we can also know the operations that have been performed on the account.
    myAccount.getLatestEvents(10)
}

결론

벌써 예전이 된 DDD, MSA 개념에 따라 프로젝트 구조가 다양해지면서, 클래스 간 상호 결합 방식을 보다 유연하게 할 필요성이 높아지고 있다. 믹스인 개념은 클래스 사이 결합도를 비교적 낮춰 코드 재사용성을 높이는 기법이므로, 직면하는 몇 문제를 우아하게 풀어내는 방법 중 하나로 보인다. 알아두었다가, 유용하게 사용해보자.