오늘은 실무에서 자바 리플렉션(Reflection)에 대한 이해 부족으로 발생했던 버그와 해결 과정을 공유하고, 이를 통해 리플렉션 사용 시 주의해야 할 점에 대해 이야기해보려 합니다.
리플렉션
리플렉션(Reflection)은 자바에서 클래스의 타입을 컴파일 시점이 아닌 런타임 시점에 동적으로 분석하고 조작할 수 있도록 해주는 기능입니다. 글로만 보면 어렵게 느껴질 수 있기에 간단한 예제 코드로 알아보겠습니다.
예제 코드
Hello!
를 출력하는 메서드를 가진 User
클래스입니다.
1
2
3
4
5
6
7
package reflection;
public class User {
public void sayHello() {
System.out.println("Hello!");
}
}
리플렉션 사용 X
일반적으로 자바에서는 아래처럼 컴파일 시점(코드를 작성하는 시점)에 객체를 생성하고, 그 객체가 어떤 메서드를 실행해야 하는지 작성해야 합니다.
1
2
3
4
5
6
7
8
package reflection;
public class Main {
public static void main(String[] args) {
User user = new User();
user.sayHello();
}
}
리플렉션 사용
하지만 리플렉션을 사용하면 아래와 같이 런타임 시점(실행되는 시점)에 객체를 생성하고 원하는 메서드를 사용할 수 있습니다(클래스의 이름이나 메서드명을 런타임에 입력받아 사용할 수 있습니다).
1
2
3
4
5
6
7
8
9
10
11
12
package reflection;
import java.lang.reflect.Method;
public class ReflectionMain {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("reflection.User");
Object o = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod("sayHello");
method.invoke(o);
}
}
그러면 왜 이처럼 번거로운 리플렉션을 사용할까요? 그 이유는, 앞선 예제처럼 클래스 이름이나 메서드명을 문자열로만 알고 있어도, 해당 객체를 만들고 메서드를 실행하는 것이 가능하기 때문입니다.
이런 방식은 다양한 클래스를 공통적인 방식으로 처리하거나, 동적으로 로직을 구성할 때 매우 유용합니다. 예를 들어, @GetMapping
, @PostMapping
같은 어노테이션들도 리플렉션을 통해 해당 메서드나 필드에 접근하고 처리하는 방식으로 동작합니다.
리플렉션에 대한 더 자세한 설명은 다음에 따로 다루겠습니다.
문제 상황
이제 실제 발생했던 버그와 그 문제 해결 과정을 공유해보겠습니다.
사내 프레임워크 개발 중, 특정 상황에서 공통 예외 처리가 의도한 방식으로 동작하지 않는 버그가 발생했습니다. 정상적인 상황이라면, 예외 발생 시 공통 예외 처리가 이를 감지해 예외 메시지를 로그에 남기고, 클라이언트에게 예외 메시지를 담아 에러 응답을 반환해야 했습니다.
하지만 특정 상황에서는 예외가 발생했을 때 공통 예외 처리가 예외를 처리하지만, 로그 메시지와 클라이언트 응답에 빈값이 들어가 의도한 예외 메시지가 전달되지 않는 문제가 발생했습니다.
해당 문제를 파악하고 특정 상황을 찾기 위해 여러 테스트를 진행한 끝에, 리플렉션을 통해 메서드를 호출하는 부분에서 호출한 메서드 내부에서 예외가 발생했을 때 버그가 발생한다는 사실을 확인했습니다.
문제가 발생했던 기능
문제가 발생했던 부분은 다음과 같은 구조로 작성되었습니다.
하나의 공통 컨트롤러에서 클라이언트로부터 클래스 이름, 메서드 이름을 입력받고,
런타임 환경에서 리플렉션을 통해 입력받은 클래스와 메서드를 찾아 동적으로 실행합니다.
이 방식은 여러 클래스를 하나의 API로 묶어 처리할 수 있는 장점이 있어 도입되었습니다.
이 구조 자체는 유연하고 재사용성이 높았지만, 아래의 리플렉션 구조를 제대로 인지하지 못한 상태로 공통 예외 처리에서 메서드 내부에서 발생한 예외의 타입만 처리했기 때문에, 실제 예외 메시지가 무시되고 로그와 응답에 포함되지 않았습니다.
자바는 리플렉션으로 메서드를 호출할 때, 메서드 내부에서 예외가 발생하면 이를
InvocationTargetException
으로 감싸서 던집니다.
해결 방법
이 문제를 해결하기 위해 try-catch
구문으로 리플렉션을 사용한 메서드 호출 부분을 감싸고, 예외가 발생하면 실제 예외만 다시 던지도록 구현하여 문제를 해결했습니다.
Before
1
method.invoke(obj);
After
1
2
3
4
5
try {
method.invoke(obj);
} catch (InvocationTargetException e) {
throw e.getCause();
}
이렇게 변경한 후에는 공통 예외 처리가 예외를 정확히 인식하고, 전체 흐름이 의도한 대로 정상적으로 동작하게 되었습니다.
느낀 점
직접 버그를 경험하고 해결해 보니, 단순히 이론으로만 배웠을 때보다 훨씬 더 잘 이해되고 기억에도 오래 남았습니다.
책이나 강의로 이론으로만 접할 때는 “이런 기능이 있구나” 정도로 가볍게 넘기곤 했는데, 실제로 문제를 마주하고 원인을 분석해 해결하면서 리플렉션의 동작 방식과 예외 처리 구조를 더 명확하게 이해하게 되었습니다.