본문 바로가기
Language/Kotlin

InvocationTargetException (리플렉션을 통해 함수를 동적으로 실행할 시 예외 처리에 관한 글)

by 시니성 2024. 6. 26.

오늘은 Java와 Kotlin에서 리플렉션을 사용할 때 자주 마주치는 InvocationTargetException에 대해 이야기해보겠습니다. 이 예외를 효과적으로 처리하는 방법도 함께 알아볼게요.

InvocationTargetException이란?

InvocationTargetException은 Java의 리플렉션 API를 사용해 메서드를 동적으로 호출할 때 발생할 수 있는 예외입니다. 호출된 메서드 내부에서 발생한 예외를 감싸서 전달하는 역할을 하죠.

주요 특징:

  • 원인 예외를 감싸서 전달: 발생한 예외를 그대로 감싸서 전달합니다.
  • 원본 예외 접근 가능: getCause()getTargetException() 메서드를 사용해 원본 예외에 접근할 수 있습니다.
  • 주로 발생하는 메서드: Method.invoke()Constructor.newInstance() 같은 리플렉션 메서드에서 주로 발생합니다.

예시 상황: 결제 시스템

간단한 결제 시스템을 예로 들어보겠습니다. 다양한 결제 방식을 지원하는 PaymentController가 있고, 이를 동적으로 호출하는 라우터를 구현했습니다.

class PaymentController {
    fun processCardPayment(amount: Int) {
        if (amount <= 0) {
            throw PaymentException("카드", "유효하지 않은 결제 금액입니다.")
        }
        // 결제 처리 로직
    }
}

class PaymentException(val paymentType: String, override val message: String) : Exception()

fun invokePaymentMethod(methodName: String, amount: Int) {
    val controller = PaymentController()
    val method = controller.javaClass.getMethod(methodName, Int::class.java)
    try {
        method.invoke(controller, amount)
    } catch (e: InvocationTargetException) {
        // 여기서 InvocationTargetException이 발생!
        println("결제 처리 중 오류 발생: ${e.cause?.message}")
    }
}

위 예시에서 invokePaymentMethod는 리플렉션을 사용해 PaymentController의 메서드를 동적으로 호출합니다.

문제 상황

다음 코드를 실행하면 PaymentException이 발생하지만, 이는 InvocationTargetException으로 래핑되어 전달됩니다. 그래서 우리가 원하는 대로 예외를 처리하기 어려워집니다.

fun main() {
    invokePaymentMethod("processCardPayment", -100)
}

해결 방법

InvocationTargetException을 적절히 처리하기 위해 다음과 같은 방법을 사용할 수 있습니다:

1. 원인 예외 추출하기

fun invokePaymentMethod(methodName: String, amount: Int) {
    val controller = PaymentController()
    val method = controller.javaClass.getMethod(methodName, Int::class.java)
    try {
        method.invoke(controller, amount)
    } catch (e: InvocationTargetException) {
        val cause = e.cause
        when (cause) {
            is PaymentException -> println("결제 오류: [${cause.paymentType}] ${cause.message}")
            else -> println("알 수 없는 오류 발생: ${cause?.message}")
        }
    }
}

2. 예외 전파하기

때로는 원본 예외를 그대로 전파하고 싶을 때가 있습니다. 이 경우 다음과 같이 처리할 수 있습니다:

fun invokePaymentMethod(methodName: String, amount: Int) {
    val controller = PaymentController()
    val method = controller.javaClass.getMethod(methodName, Int::class.java)
    try {
        method.invoke(controller, amount)
    } catch (e: InvocationTargetException) {
        throw e.cause ?: e
    }
}

fun main() {
    try {
        invokePaymentMethod("processCardPayment", -100)
    } catch (e: PaymentException) {
        println("결제 오류 발생: [${e.paymentType}] ${e.message}")
    } catch (e: Exception) {
        println("기타 오류 발생: ${e.message}")
    }
}

이렇게 하면 원본 PaymentException을 직접 처리할 수 있게 됩니다.

결론

리플렉션을 사용할 때 InvocationTargetException은 피할 수 없는 부분입니다. 하지만 이를 적절히 처리하면 더 견고하고 예측 가능한 코드를 작성할 수 있습니다. 특히 프레임워크나 라이브러리를 개발할 때 이러한 예외 처리는 매우 중요합니다.

리플렉션은 강력한 도구이지만, 동시에 주의 깊게 다뤄야 하는 양날의 검과 같습니다. InvocationTargetException을 올바르게 처리함으로써, 리플렉션의 유연성을 최대한 활용하면서도 안정적인 코드를 유지할 수 있습니다.