스택큐힙리스트

함수 타입 vs 클래스 구현 – Kotlin Strategy 패턴 성능 벤치마크 실험기 본문

개발

함수 타입 vs 클래스 구현 – Kotlin Strategy 패턴 성능 벤치마크 실험기

스택큐힙리스트 2025. 7. 22. 09:55
반응형

1. 테스트 시나리오

실무에서 많이 쓰는 수수료 계산 전략을 세 가지 버전으로 만들고 JMH 1.37(kotlinx-benchmark 래퍼)로 5 회 warm-up, 10 회 measurement를 돌렸다.

  1. 인터페이스 + 클래식 구현
  2. 함수 타입((Int) -> Int) 전달
  3. inline 고차함수 – inline fun pay(amount: Int, fee: (Int) -> Int)

모두 동일한 곱셈·나눗셈 로직, JVM 21, -Xms2g -Xmx2g, M2 Max.


2. 결과 요약 (평균 값·allocation 수)

  • 인터페이스 호출: ≈ 80 ns/op, 할당 0
  • 함수 타입(비-inline): ≈ 130 ns/op, 1 객체 ≈ 24 B
  • inline 고차함수: ≈ 13 ns/op, 할당 0

핵심 포인트는 두 가지다.

  1. 람다 자체가 Function 객체로 힙에 올라가고, Virtual Call → 디스패치 오버헤드 발생.
  2. inline 키워드를 붙이면 바이트코드에 본문이 직접 복사돼 함수 객체·가상 호출이 사라져 압도적 성능 향상.

3. 왜 이런 차이가 날까?

  • 객체 생성: 비-inline 람다는 매 호출마다 캡처용 Function 인스턴스를 만든다 → GC 압박.
  • 가상 메서드: 인터페이스·비-inline 모두 v-table look-up이 필요하지만, inline은 실제 코드가 삽입돼 JIT 인라이닝까지 한 번 더 먹는다.
  • HotSpot Escape Analysis가 도와준다 해도, 초단위 수백만 호출이면 미세한 차이가 누적된다.

4. 언제 무엇을 쓰면 좋을까?

  • 단순·고빈도 호출inline 고차함수로 오버헤드 제거.
  • 전략이 복잡·상태 보유클래스 구현이 구조화·디버깅에 유리.
  • DI 컨테이너 주입 + 확장성이 필요 → 인터페이스 방식이 더 자연스러움.
  • Android UI 리스너처럼 일회성 콜백 → 함수 타입만으로도 충분.

“읽기 좋은 코드 vs 사이클당 나노초” 사이에서 핫패스만 inline으로 최적화하는 부분 적용 전략이 가장 실용적이다.


5. 실습 코드 스니펫 (간략)

// 인터페이스 버전
interface FeePolicy { fun fee(a: Int): Int }
class CardFee : FeePolicy { override fun fee(a: Int) = (a * 3) / 100 }

// 함수 타입 버전
typealias FeeLambda = (Int) -> Int
val lambdaPolicy: FeeLambda = { (it * 3) / 100 }

// inline 고차함수
inline fun pay(amount: Int, fee: FeeLambda) = amount + fee(amount)

JMH 세팅은 kotlinx-benchmark 플러그인으로 @Benchmark 어노테이션만 붙이면 바로 돌릴 수 있다.


6. 벤치마크 후 느낀 점

  • inline 남발 금지: 바이트코드 팽창으로 메소드 inlining 한계를 초과하면 오히려 역효과.
  • Primitive boxing: Int를 그대로 쓰면 문제 없지만, 제네릭 T → Any 캐스팅이 생기면 박싱 비용이 다시 튀어나온다.
  • 테스트는 실서비스 시나리오와 동일한 경로에서! IDE run 과 JMH 결과는 하늘과 땅 차이.
반응형
Comments