주지하다시피, (Enum 추상 클래스를 상속하는) enum type은 상수 컨테이너로 사용되기 위해 특화되어 있기 때문에 특성상 다른 인터페이스를 구현하거나 Enum을 제외한 다른 클래스를 상속하는 행위가 금지되어 있다. 하지만 프로젝트를 진행하면서, 특정한 몇몇 enum type에 공통적으로 특정한 메소드 기능을 요구하는 일이 생겼다. 현재 이렇게 공통적으로 사용하는 메소드가 몇 개 있는데, 이들 중 이번 포스팅에서 예시로 삼을 것을 하나 골라서 그 시그니처와 기능만을 정리하고 아래 코드에 첨부하였다.
public static <T extends Enum<T>> boolean inEnumValues(Class<T> enumClass, String str) {
// 1. Enum을 상속한 특정 enum type(매개변수명으로는 enumClass)의 상수들 중,
// 특정한 문자열(str)을 상수의 매핑 값으로 삼는 상수가 있다면 true를 반환한다.
// 2. 만약 그런 상수가 없다면 false를 반환한다.
}
즉, 우리는 어떤 매핑 값이 우리 손에 쥐어져 있을 때, 이 값을 매핑 값으로 하는 상수가 특정 enum type에 있는지 여부를 확인하고 싶은 것이다. 근데 여기서 고려하고 싶은 점은, 우리는 이 메소드를 Enum 클래스를 상속하는 모든 enum type에 대하여 사용하고 싶다는 것이다. 물론 각각의 enum type마다 똑같은 형태의 정적 메소드를 생성하고 각 상황에 따라 해당 정적 메소드들을 호출하여 사용해도 결과는 똑같겠지만, 만약에 똑같은 메소드가 10개의 enum type에 대해 필요하다면? 혹은 대규모의 프로젝트의 경우, enum type이 100개가 된다면? 유지보수 측면에서도 그렇고, 코드의 재사용성 및 메모리 점유 등 대부분의 측면에서 유틸리티 메소드 하나를 생성하여 이를 사용하는 것보다 좋지 않은 결과가 초래될 것이다.
이런 점들을 고려해서 위에 제시된 inEnumValues를 구현해 볼 때, 우선 이 메소드를 담은 클래스 혹은 인터페이스를 상속하는 식으로는 문제를 해결할 수 없다는 것을 대번에 알 수 있다. 맨 처음에 말했다시피, enum type은 상속이나 구현에서 제한이 있기 때문이다. 결국 static을 써야 한다는 말이고, 이런 식으로 일관된 static 메소드를 사용하고자 하는 경우 으레 관련지어 생각하게 되는 게 유틸리티 클래스이다. 즉, 이 메소드는 그 형태가 어떠하든 유틸리티 클래스 내에서 운용하는 게 좋겠다는 생각에 이른다. 그 다음으로 볼 건 Class<T>인데, 왜 Enum<T> 대신 Class<T>를 쓰냐면 Class<T>가 리플렉션과 관련한 메소드들을 제공해 주기 때문이다. 더하여, Class<T>는 enum type에서 values() 메소드를 사용하면 반환되는 값을 그대로 얻을 수 있도록 도와주는 getEnumConstants() 메소드를 제공하며, 이 메소드는 추후에 요긴하게 사용될 예정이다. 마지막으로 볼 건 <T extends Enum<T>>으로, 이는 속박된 타입 매개변수라고 부르는 건데, 단순히 메소드 매개변수에서 사용되고 있는 제네릭 타입 매개변수 T를 선언하는 부분이라고 보면 된다. 여기서 T는 반드시 enum type이어야 하므로, Enum<T>를 상속하는 것으로 선언하였다.
나머지 구현에 대한 설명은 완성된 코드를 미리 던져 놓는 것으로 시작하려 한다.
public static <T extends Enum<T>> boolean inEnumValues(Class<T> enumClass, String str) {
try {
Method method = enumClass.getMethod("getValue");
for (T enumConstant : enumClass.getEnumConstants()) {
String value = (String) method.invoke(enumConstant);
if (value.equals(str)) return true;
}
} catch (NoSuchMethodException e) {
throw new RuntimeException(NO_GET_VALUE_FOR_THE_ENUM);
} catch (InvocationTargetException e) {
throw new RuntimeException(CANNOT_INVOKE_GET_VALUE);
} catch (IllegalAccessException e) {
throw new RuntimeException(NOT_HAVE_ACCESS_TO_GET_VALUE);
}
return false;
}
이 메소드는 여러 가지 catch문이 필요한데, 어딘가에 이보다 더 좋은 코드가 있긴 할 듯하다... 어쨌든 여기서는 이 메소드를 다루도록 하겠다. 우선, 앞서 말했다시피 Class<T>는 리플렉션을 돕는 여러 메소드들을 포함하고, 이는 다음과 같은 것들이 있다: 생성자에 대한 getConstructor(), 메소드에 대한 getMethod(), 필드에 대한 getField(). 여기서는 enum type에 공통적으로 정의된 getValue() 메소드를 불러오려고 하므로, getMethod()를 사용했다. 참고로 모든 enum type에 getValue()라는 메소드가 정의될 수 있었던 이유는 필자가 enum type의 모든 매핑 값을 value라는 이름의 변수에 할당하고 이 private 변수에 @Getter를 수식했기 때문이다. 일례로 enum type들 중 하나는 다음과 같이 생겼다.
@Getter
public enum FirstCategory {
CONSTRUCTION("건설"),
DEFENSE("방산"),
DISPLAY("디스플레이"),
ELECTRIC_VEHICLE("전기차"),
ENERGY("에너지"),
ENVIRONMENT("환경"),
FINANCE("금융"),
IT_SERVICE("IT 서비스"),
SECONDARY_BATTERY("2차전지"),
SEMICONDUCTOR("반도체"),
SPACE("우주"),
TELECOMMUNICATION("통신");
private final String value;
FirstCategory(String value) {
this.value = value;
}
}
그리고 여기서 메소드 이름(여기서는 "getValue")과 일치하는 메소드가 없는 경우에 대비한 NoSuchMethodException catch문이 요구된다. 보는 데 헷갈릴 수 있으므로 하나만 더 쓰면, RuntimeException 안에 있는 상수는 필자가 사용자 정의한 문자열이므로 입맛에 맞게 바꾸면 되겠다.
그 다음은 enum type에 대해서 사용할 수 있는 getEnumConstants()를 사용하는 반복 루프인데, 여기서 한 가지 재미있는 사실이 있다. 바로 getEnumConstants()도 까보면 내부적으로 리플렉션을 사용하고 있다는 점이다. 이는 다음과 같이 소스 코드를 확인함으로써 파악할 수 있다. 여기서 getEnumConstantsShared() 내부에서 values() 메소드(우리가 흔히 사용하는 enum type의 그 values() 메소드 맞다)를 getMethod()를 이용해서 찾아내고 있으며, 필자와 마찬가지로 catch문에 NoSuchMethodException이 포함되어 있음도 확인할 수 있다.
public T[] getEnumConstants() {
T[] values = getEnumConstantsShared();
return (values != null) ? values.clone() : null;
}
T[] getEnumConstantsShared() {
T[] constants = enumConstants;
if (constants == null) {
if (!isEnum()) return null;
try {
final Method values = getMethod("values");
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<>() {
public Void run() {
values.setAccessible(true);
return null;
}
});
@SuppressWarnings("unchecked")
T[] temporaryConstants = (T[])values.invoke(null);
enumConstants = constants = temporaryConstants;
}
// These can happen when users concoct enum-like classes
// that don't comply with the enum spec.
catch (InvocationTargetException | NoSuchMethodException |
IllegalAccessException | NullPointerException |
ClassCastException ex) { return null; }
}
return constants;
}
자, 이제 method.invoke를 알아볼 차례다. 대충 보면 감이 오겠지만, 이 매개변수로는 메소드를 촉발할 객체와, 추가적으로 메소드에서 요구하는 아규먼트를 담을 수 있다. 다만 getValue()는 획득자이므로 별도의 매개변수가 없어, 단순히 enumConstant만 매개변수로 넣어 주었다. 그리고 이때 메소드를 해당 객체에 대해 실행할 수 없는 경우(예: 객체에 해당 메소드가 없는 경우)에 대응하는 InvocationTargetException, 메소드가 있기는 하나 접근 권한이 없는 경우에 대응하는 IllegalAccessException 예외 처리가 필요하므로 catch문을 넣어 주었다. 여기까지 성공한다면 value에는 특정한 상수에 대한 매핑 값이 차례로 저장될 것이며, 우리는 이제 이 값과 기존의 매개변수로 주어진 매핑 값을 비교하여 그에 따른 로직을 세우는 일이 가능해진다. 이렇게 우리의 정적 메소드 만들기가 마무리된다.
지금까지 리플렉션으로 Enum에게 공통적으로 요구되는 로직을 어떻게 통합할 수 있는지에 대해 알아 보았는데, 이미 눈치챘겠지만 getMethod() 부분을 getConstructor()로 바꾸는 식으로 리플렉션 로직을 확장하는 일도 가능하다. 그러니 이제는 여러분이 생각하는 로직을 리플렉션을 활용하여 구현하는 일이 조금 더 쉽게 느껴지리라 기대해 본다. 다만 주의할 점이 있다면, 리플렉션은 직렬화 / 역직렬화에 기반한 기술이기 때문에 단순히 메소드를 상속하는 등의 작업보다 속도면에서 뒤처진다. 또한 복잡한 리플렉션은 가독성을 해치는 등의 문제도 있기 때문에, 리플렉션 프로그래밍은 지금처럼 다른 방법을 사용할 수 없거나 컴파일 시점에 클래스를 알 수 없는 경우에 사용하는 것이 가장 좋겠다.
끝!
'웹 개발 (Spring Boot)' 카테고리의 다른 글
| 레이어 아키텍처에서 클린 아키텍처 + DDD로 리팩토링하기 (1) (0) | 2025.12.04 |
|---|---|
| 사이드 프로젝트 팀장으로서의 깨달음 (2) | 2025.06.28 |
| @TestPropertySource를 활용해서 테스트 DB 분리하기 (0) | 2024.09.27 |
| 상수를 관리할 때 중요하게 고려할 점 (3) | 2024.09.17 |
| HttpServletRequestWrapper로 요청 매개변수 설정하기 (0) | 2024.09.10 |