리플렉션의 핵심 개념
프로그램 실행 중에 클래스의 구조를 탐색하고 조작할 수 있는 기능을 리플렉션(Reflection)이라 한다. 실행 시점에 클래스의 메서드, 필드, 생성자 등을 동적으로 분석하거나 호출할 수 있으며, 이를 통해 범용적인 유틸리티를 구축할 수 있다.
리플렉션의 대표적인 활용 분야는 다음과 같다:
- 런타임에 클래스의 메타데이터를 분석
- 어떤 객체에 대해서도 동작하는 범용 메서드(toString 등) 작성
- 제네릭 배열 조작 로직 구현
- Method 객체를 통한 동적 메서드 호출
리플렉션은 강력하지만 복잡도가 높아 프레임워크나 도구 개발자가 주로 사용하며, 일반 애플리케이션 개발에서는 필요한 경우에만 제한적으로 활용하는 것이 바람직하다.
Class 객체 이해하기
자바에서 클래스 정보는 Class 객체에 담긴다. 이 객체는 클래스 로더에 의해 메모리에 적재될 때 생성되며, 해당 클래스의 모든 메타정보(클래스명, 상위 클래스, 구현 인터페이스, 멤버 등)를 내포하고 있다.
Class 객체를 얻는 대표적인 세 가지 방법:
- 인스턴스에서 획득:
객체참조.getClass() - 클래스 리터럴 사용:
클래스명.class - 동적 로딩:
Class.forName("전체_경로_클래스명")← 가장 유연한 방식
public class ClassAcquisitionDemo {
public static void main(String[] args) throws Exception {
demonstrateAcquisition();
}
public static void demonstrateAcquisition() throws Exception {
// 방법 1: 인스턴스로부터 Class 객체 획득
Employee emp = new Employee();
Class<?> clazz1 = emp.getClass();
System.out.println("방법1 해시코드: " + clazz1.hashCode());
// 방법 2: 클래스 리터럴 사용
Class<?> clazz2 = Employee.class;
System.out.println("방법2 해시코드: " + clazz2.hashCode());
// 방법 3: 동적 로딩 (완전한 클래스 경로 지정)
Class<?> clazz3 = Class.forName("com.example.reflection.Employee");
System.out.println("방법3 해시코드: " + clazz3.hashCode());
}
}
리플렉션을 통한 클래스 정보 탐색
Class 객체가 제공하는 핵심 메서드를 활용해 클래스의 다양한 정보를 추출할 수 있다.
| 메서드 | 설명 |
|---|---|
getName() | 전체 클래스명(패키지 포함) |
getPackage() | 패키지 정보 |
getSuperclass() | 직접 상위 클래스 |
getInterfaces() | 구현한 인터페이스 배열 |
getConstructors() | public 생성자 목록 |
getDeclaredMethods() | 선언된 모든 메서드(접근 제한자 무관) |
getDeclaredFields() | 선언된 모든 필드(접근 제한자 무관) |
newInstance() | 기본 생성자로 인스턴스 생성 |
public class ClassInspector {
public static void main(String[] args) throws Exception {
inspectClassStructure();
manipulateConstructors();
invokeMethodsDynamically();
accessPrivateFields();
}
// 클래스 기본 정보 조회
public static void inspectClassStructure() throws Exception {
Class<?> targetClass = Class.forName("com.example.reflection.Employee");
System.out.println("클래스명: " + targetClass.getName());
System.out.println("패키지: " + targetClass.getPackage().getName());
System.out.println("상위래스: " + targetClass.getSuperclass().getName());
System.out.println("간단명: " + targetClass.getSimpleName());
Class<?>[] implementedInterfaces = targetClass.getInterfaces();
System.out.println("구현 인터페이스: " + java.util.Arrays.toString(implementedInterfaces));
}
// 생성자 활용 및 객체 생성
public static void manipulateConstructors() throws Exception {
Class<?> targetClass = Class.forName("com.example.reflection.Employee");
// 기본 생성자로 인스턴스화
java.lang.reflect.Constructor<?> defaultCtor = targetClass.getConstructor();
Object instance1 = defaultCtor.newInstance();
System.out.println("기본 생성자: " + instance1);
// 매개변수 있는 생성자로 인스턴스화
java.lang.reflect.Constructor<?> paramCtor = targetClass.getConstructor(String.class, int.class);
Object instance2 = paramCtor.newInstance("철수", 28);
System.out.println("매개변수 생성자: " + instance2);
}
// 메서드 동적 호출
public static void invokeMethodsDynamically() throws Exception {
Class<?> targetClass = Class.forName("com.example.reflection.Employee");
Object targetObj = targetClass.newInstance();
// public 메서드 호출
java.lang.reflect.Method workMethod = targetClass.getMethod("work");
workMethod.invoke(targetObj);
// 매개변수 있는 메서드 호출
java.lang.reflect.Method setTaskMethod = targetClass.getMethod("setTask", String.class);
setTaskMethod.invoke(targetObj, "프로젝트 기획");
// 반환값 있는 메서드 호출
java.lang.reflect.Method getInfoMethod = targetClass.getMethod("getInfo");
Object result = getInfoMethod.invoke(targetObj);
System.out.println("반환값: " + result);
// private 메서드 접근
java.lang.reflect.Method hiddenMethod = targetClass.getDeclaredMethod("internalProcess");
hiddenMethod.setAccessible(true);
hiddenMethod.invoke(targetObj);
// static 메서드 호출
java.lang.reflect.Method staticMethod = targetClass.getMethod("companyPolicy");
staticMethod.invoke(null);
}
// 필드 값 조작
public static void accessPrivateFields() throws Exception {
Class<?> targetClass = Class.forName("com.example.reflection.Employee");
Object employee = targetClass.newInstance();
// private 필드 접근
java.lang.reflect.Field nameField = targetClass.getDeclaredField("fullName");
nameField.setAccessible(true);
// 필드 값 설정
nameField.set(employee, "이영희");
// 필드 값 읽기
String retrievedName = (String) nameField.get(employee);
System.out.println("조회된 이름: " + retrievedName);
}
}
범용 메서드 호출 유틸리티
리플렉션을 활용하면 임의의 객체에 대해 메서드명과 매개변수 타입만 알면 동적으로 호출할 수 있는 범용 유틸리티를 만들 수 있다.
public class DynamicInvoker {
/**
* 지정된 객체의 메서드를 동적으로 호출
*
* @param target 메서드를 호출할 대상 객체
* @param methodName 호출할 메서드 이름
* @param paramTypes 매개변수 타입 배열
* @param arguments 실제 전달할 인자들
* @return 메서드 실행 결과
*/
public static Object executeMethod(Object target, String methodName,
Class<?>[] paramTypes, Object... arguments) throws Exception {
Class<?> clazz = target.getClass();
java.lang.reflect.Method method = clazz.getMethod(methodName, paramTypes);
return method.invoke(target, arguments);
}
// 활용 예시
public static void main(String[] args) throws Exception {
java.util.Properties config = new java.util.Properties();
// Properties의 setProperty 메서드를 동적으로 호출
executeMethod(config, "setProperty",
new Class[]{String.class, String.class},
"database.url", "jdbc:mysql://localhost:3306/mydb");
System.out.println(config);
}
}
디자인 패턴: 동적 팩토리
리플렉션을 결합하면 기존 팩토리 패턴의 한계를 극복할 수 있다. 새로운 제품 클래스를 추가할 때 팩토리 클래스를 수정하지 않고, 외부 설정만으로 동적으로 객체를 생성하는 방식이다.
USB 장치를 예로 들어, 새로운 주변기기가 추가되어도 팩토리 코드는 변경 없이 설정 파일만 수정하면 된다.
// 공통 인터페이스
public interface Peripheral {
void activate();
}
// 구현체들
public class OpticalMouse implements Peripheral {
public void activate() {
System.out.println("광학 마우스 연결됨, DPI: 1600");
}
}
public class MechanicalKeyboard implements Peripheral {
public void activate() {
System.out.println("기계식 키보드 연결됨, 청축 스위치");
}
}
// 동적 팩토리
public class PeripheralFactory {
public static Peripheral createPeripheral(String classPath) {
try {
Class<?> clazz = Class.forName(classPath);
return (Peripheral) clazz.newInstance();
} catch (Exception e) {
throw new RuntimeException("주변기기 생성 실패: " + e.getMessage());
}
}
}
// 설정 기반 클라이언트
public class PeripheralClient {
public static void main(String[] args) throws Exception {
java.util.Properties deviceConfig = new java.util.Properties();
java.io.FileInputStream configStream = new java.io.FileInputStream("devices.properties");
deviceConfig.load(configStream);
configStream.close();
java.util.Scanner scanner = new java.util.Scanner(System.in);
System.out.print("장치 번호 입력: ");
String selection = scanner.nextLine();
String deviceClass = deviceConfig.getProperty(selection);
Peripheral device = PeripheralFactory.createPeripheral(deviceClass);
device.activate();
}
}
devices.properties 파일 내용:
mouse1=com.example.factory.OpticalMouse keyboard1=com.example.factory.MechanicalKeyboard
싱글턴 패턴의 다양한 구현
싱글턴은 클래스의 인스턴스를 하나만 생성하도록 보장하는 패턴이다. 구현 방식에 따라 초기화 시점과 스레드 안전성이 달라진다.
즉시 초기화(Eager Initialization)
클래스 로딩 시점에 인스턴스를 생성하므로 스레드 안전하지만, 불필요한 자원 소비가 발생할 수 있다.
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
지연 초기화 + 이중 검증 잠금(Double-Checked Locking)
실제 필요한 시점에 생성하며, 동기화 블록을 최소화해 성능을 개선한다.
public class OptimizedLazySingleton {
private volatile static OptimizedLazySingleton instance;
private OptimizedLazySingleton() {}
public static OptimizedLazySingleton getInstance() {
if (instance == null) {
synchronized (OptimizedLazySingleton.class) {
if (instance == null) {
instance = new OptimizedLazySingleton();
}
}
}
return instance;
}
}
정적 내부 클래스 활용
가장 권장되는 방식으로, 클래스 로딩 메커니즘을 활용해 지연 초기화와 스레드 안전성을 동시에 달성한다.
public class HolderSingleton {
private HolderSingleton() {}
private static class InstanceHolder {
private static final HolderSingleton INSTANCE = new HolderSingleton();
}
public static HolderSingleton getInstance() {
return InstanceHolder.INSTANCE;
}
}
열거형(Enum) 타입
열거형은 미리 정의된 상수 집합을 나타내는 특수한 참조 타입이다. enum 키워드로 선언하며, 값의 안전성을 컴파일 타임에 보장한다.
열거형의 내부 구현 특성:
java.lang.Enum을 암묵적으로 상속받는 final 클래스- 각 상수는 해당 열거 타입의
static final인스턴스 - 생성자는 private으로 제한됨
public enum StatusCode {
PENDING(102, "처리 대기중"),
SUCCESS(200, "정상 처리됨"),
REDIRECT(302, "다른 주소로 이동"),
NOT_FOUND(404, "리소스 없음"),
ERROR(500, "서버 내부 오류");
private final int code;
private final String description;
StatusCode(int code, String description) {
this.code = code;
this.description = description;
}
public int getCode() { return code; }
public String getDescription() { return description; }
}
// switch 문과의 활용
public class EnumSwitchDemo {
public static void main(String[] args) {
StatusCode current = StatusCode.SUCCESS;
switch (current) {
case PENDING:
System.out.println("잠시만 기다려주세요...");
break;
case SUCCESS:
System.out.println("요청이 완료되었습니다: " + current.getDescription());
break;
case NOT_FOUND:
System.out.println("찾을 수 없습니다.");
break;
default:
System.out.println("기타 상태: " + current.getCode());
}
}
}
어노테이션(Annotation)과 리플렉션
어노테이션은 코드에 부가적인 메타데이터를 부여하는 표기법이다. @interface로 직접 정의할 수 있으며, 리플렉션을 통해 런타임에 해당 정보를 추출할 수 있다.
메타 어노테이션
| 어노테이션 | 역할 |
|---|---|
@Retention | 어노테이션 유지 범위 지정 (SOURCE, CLASS, RUNTIME) |
@Target | 적용 가능한 대상 제한 (METHOD, FIELD, CLASS 등) |
@Documented | JavaDoc 문서에 포함 여부 |
커스텀 어노테이션 정의 및 활용
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface ServiceMetadata {
String serviceName();
int timeout() default 30;
boolean requiresAuth() default true;
}
리플렉션을 통한 어노테이션 정보 추출:
public class AnnotationProcessor {
public static void main(String[] args) throws Exception {
Class<?> serviceClass = Class.forName("com.example.service.OrderService");
// 메서드 획득
java.lang.reflect.Method processMethod = serviceClass.getMethod("processOrder", String.class);
// 어노테이션 존재 여부 확인
if (processMethod.isAnnotationPresent(ServiceMetadata.class)) {
ServiceMetadata metadata = processMethod.getAnnotation(ServiceMetadata.class);
System.out.println("서비스명: " + metadata.serviceName());
System.out.println("타임아웃: " + metadata.timeout() + "초");
System.out.println("인증 필요: " + metadata.requiresAuth());
// 어노테이션 값을 메서드 호출에 활용
Object serviceInstance = serviceClass.newInstance();
if (metadata.requiresAuth()) {
System.out.println("인증 확인 후 실행...");
}
processMethod.invoke(serviceInstance, "ORD-2024-001");
}
}
}
복합 어노테이션 예시
public @interface ValidationRule {
// 다양한 속성 타입 지원
String fieldName();
int minLength() default 0;
int maxLength() default Integer.MAX_VALUE;
Class<? extends Exception> exceptionType() default IllegalArgumentException.class;
Season applicableSeason(); // 열거형 타입
Deprecated legacyMarker() default @Deprecated(since = "1.0"); // 어노테이션 타입
}
어노테이션은 프레임워크에서 설정 대신 코드 기반 메타데이터를 표현하는 핵심 메커니즘으로, Spring의 @Autowired, JPA의 @Entity 등이 대표적인 활용 사례이다.