믹스인(Mixin)의 정의와 예시: 코틀린 믹스인 구현의 예
정의
위키에 따르면 믹스인(Mixins)은 단위 기능 또는 그 집합을 갖는 클래스를 생성하여 다른 클래스와 혼합하는 “개발 스타일”이다. 다른 클래스가 사용할 함수들을 모아 정의한 뒤 “Has a”(또는 “Can”) 관계를 맺는 어떤 클래스로도 의미가 통한다. “실용주의 프로그래머(The Pragmatic Programmer)” 에서는 클래스보다 기법으로 설명하고 있다.
언어에 따라서 mixin 키워드를 직관적으로 제공하거나, traits, category, protocol extensions 과 같은 용어로 믹스인의 개념을 찾아볼 수 있다. 즉 mixin 스타일을 지원하는 방식과 제약 사항이 다르고, 구현방법 또한 다양하다. 각 언어의 구체적인 명세를 이해하는 것도 중요하겠으나, mixin 자체의 목적과 장점을 이해하고 활용하는 것이 중요하다.
필자는 코프링쟁이다. 하여 이 글에서는 믹스인의 예시를 코틀린으로 작성했다.
목적
1969년 시뮬라67(Simula67)로 “상속”이 처음 등장한 이래, 이른바 “상속세”로 불리우는 여러 문제점들이 공감대를 얻는다. 믹스인은 Flavors 란 언어에서 처음 등장했고 이러한 “상속세”를 피하기 위한 대안 중 하나로 자주 언급되었다. 따라서 주로 언급되는 믹스인을 활용하는 목적은 아래와 같다:
- 코드 결합도 낮추기
- 단일 상속 제한 우회
- 다중 상속 문제 피하기
- 괴물같은 상속 계층도 피하기
- 기능 단위 모듈화
- …
믹스인 기법은 이러한 목적 아래 내부 모듈 사이 코드 가시성을 엄격히 다루는 프로젝트나 불특정 클래스 정의를 지원하는 라이브러리에 활용된다.
예시
예시 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 개념에 따라 프로젝트 구조가 다양해지면서, 클래스 간 상호 결합 방식을 보다 유연하게 할 필요성이 높아지고 있다. 믹스인 개념은 클래스 사이 결합도를 비교적 낮춰 코드 재사용성을 높이는 기법이므로, 직면하는 몇 문제를 우아하게 풀어내는 방법 중 하나로 보인다. 알아두었다가, 유용하게 사용해보자.