개발공부/주저리

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

siotMan 2025. 5. 29. 23:25

정의

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

 

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

 

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

목적

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

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

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

예시

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