JAVA/Effective Java

[아이템 65] 리플렉션보다는 인터페이스를 사용하라

꾸준함. 2024. 3. 19. 05:09

리플렉션 기능을 이용하면 프로그램에서 임의의 클래스에 접근할 수 있으며 Class 객체가 주어지면 클래스 정보를 통해 다음과 같은 인스턴스를 가져올 수 있습니다.

  • 생성자
  • 메서드
  • 필드

 

1. 생성자

  • 생성자 시그니처를 가져올 수 있음
  • 생성자 인스턴스를 통해 객체를 생성할 수 있음

 

2. 메서드

  • 메서드 시그니처를 가져올 수 있음
  • 메서드 인스턴스를 통해 메서드를 실행시킬 수 있음

 

3. 필드

  • 필드 타입, 멤버 필드명 등을 가져올 수 있음

 

MyClass.java

 

package com.tistory.jaimemin.effectivejava.ch09.item65;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyClass {
private String name;
private int age;
public void displayInfo() {
System.out.println("이름: " + name + ", 나이: " + age);
}
}
view raw .java hosted with ❤ by GitHub

 

 

ReflectionExample.java

 

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ReflectionExample {
public static void main(String[] args) {
try {
// 클래스 이름을 이용하여 Class 객체 가져오기
Class<?> clazz = Class.forName("com.tistory.jaimemin.effectivejava.ch09.item65.MyClass");
// 생성자 정보 가져오기
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
// 첫 번째 생성자를 사용하여 객체 생성
Object instance = constructors[0].newInstance();
// 메서드 정보 가져오기
Method[] methods = clazz.getDeclaredMethods();
// 메서드 호출 예제
for (Method method : methods) {
if (method.getName().equals("displayInfo")) {
// 매개변수 타입에 맞게 인자 전달
method.invoke(instance);
}
}
// 필드 정보 가져오기
Field[] fields = clazz.getDeclaredFields();
// 필드 값 설정 및 가져오기 예제
for (Field field : fields) {
if (field.getName().equals("name")) {
// 필드에 접근 가능하도록 설정
field.setAccessible(true);
field.set(instance, "jaimemin");
} else if (field.getName().equals("age")) {
// 필드에 접근 가능하도록 설정
field.setAccessible(true);
field.set(instance, 29);
}
}
// displayInfo() 메서드 호출하여 변경된 정보 출력
for (Method method : methods) {
if (method.getName().equals("displayInfo")) {
// 매개변수 타입에 맞게 인자 전달
method.invoke(instance);
}
}
} catch (Exception e) {
log.error("" + e);
}
}
}
view raw .java hosted with ❤ by GitHub

 

리플렉션의 단점

앞서 코드에서 볼 수 있었다시피 리플렉션은 강력한 기능이지만 다음과 같은 단점들이 존재합니다.

  • 컴파일 시점 타입 검사가 주는 이점을 누릴 수 없음
  • 리플렉션을 이용하면 코드가 지저분해지고 장황해짐
  • 성능 저하

 

1. 컴파일 시점 타입 검사가 주는 이점을 누릴 수 없음

 

  • 예외 검사 및 컴파일 에러를 잡아 낼 방법이 없음
  • 프로그램이 리플렉션 기능을 써서 존재하지 않는 혹은 private 메서드를 호출하려고 하면 런타임 오류 발생

 

MyClass에 private 메서드를 추가하면 ReflectionExample 코드를 돌릴 때 IllegalArgumentException 발생

 

 

 

2. 리플렉션을 이용하면 코드가 지저분하고 장황해짐

 

  • 원래라면 instance.displayInfo()만 호출하면 되는데
  • displayInfo() 메서드를 호출하기 전 작성해야 할 코드가 너무 많음
  • 지루한 일이고 가독성 측면에서 좋지 않음

 

3. 성능 저하

 

  • 리플렉션을 통한 메서드 호출은 일반 메서드 호출보다 훨씬 느림
    • 제가 직접 돌려본 결과 별 차이 없었음...

 

리플렉션은 아주 제한된 형태로만 사용해야 단점을 회피할 수 있음

  • 컴파일 시점에 이용할 수 없는 클래스를 사용해야만 하는 프로그램은 비록 컴파일 타임이라도 적절한 인터페이스나 상위 클래스를 이용할 수는 있음
    • 프로그램이 실행 중에 동적으로 클래스를 로드하고 사용해야 하는 경우가 있음
    • 이런 경우에는 컴파일 시에는 해당 클래스에 대한 정보가 없기 때문에 정적인 방식으로는 접근할 수 없음
    • 하지만 리플렉션을 사용하면 실행 중에 해당 클래스를 로드하고 그에 따른 작업을 수행할 수 있음
    • 이때는 적절한 인터페이스나 상위 클래스를 이용하여 코드의 유연성을 유지할 수 있음
    • 리플렉션은 인스턴스 생성에만 이용하고 이렇게 만든 인터페이스나 상위 클래스로 참조해 사용하는 것을 권장

 

리플렉션을 사용했을 때 단점을 보여주는 코드

 

public class ReflectiveInstantiation {
// Reflective instantiation with interface access
public static void main(String[] args) {
// Translate the class name into a Class object
Class<? extends Set<String>> cl = null;
try {
cl = (Class<? extends Set<String>>) // Unchecked cast!
Class.forName(args[0]);
} catch (ClassNotFoundException e) {
fatalError("Class not found.");
}
// Get the constructor
Constructor<? extends Set<String>> cons = null;
try {
cons = cl.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
fatalError("No parameterless constructor");
}
// Instantiate the set
Set<String> s = null;
try {
s = cons.newInstance();
} catch (IllegalAccessException e) {
fatalError("Constructor not accessible");
} catch (InstantiationException e) {
fatalError("Class not instantiable.");
} catch (InvocationTargetException e) {
fatalError("Constructor threw " + e.getCause());
} catch (ClassCastException e) {
fatalError("Class doesn't implement Set");
}
// Exercise the set
s.addAll(Arrays.asList(args).subList(1, args.length));
System.out.println(s);
}
private static void fatalError(String msg) {
System.err.println(msg);
System.exit(1);
}
}
view raw .java hosted with ❤ by GitHub

 

 

코드 부연 설명

  • 런타임에 총 6가지의 예외를 던질 수 있음
    • 리플렉션을 사용하지 않았더라면 컴파일 시점에 체크할 수 있는 예외
    • 모두 Checked Exception

 

  • 클래스명만으로 인스턴스를 생성하기 위해 무려 25줄이나 되는 코드를 작성
    • 리플렉션 사용하지 않았을 경우 생성자 1줄이면 끝

 

  • 리플렉션 예외를 각각 잡는 대신 자바 7 버전부터 도입한 상위 클래스인 ReflectiveOperationException을 잡으면 코드량을 줄일 수 있음
    • ReflectiveOperationExceptionClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException 등을 포함하는 예외 클래스

 

 

리플렉션은 언제 사용해야 할까?

  • 스프링 프레임워크는 많은 기능에서 리플렉션을 활용합니다..
    • 의존성 주입 (Dependency Injection): 스프링은 객체 간의 의존성을 관리하기 위해 리플렉션을 사용
    • Aspect-Oriented Programming (AOP): 스프링에서 AOP를 구현할 때 리플렉션을 사용
    • 프록시 (Proxy) 생성: 스프링은 AOP를 구현할 때 주로 프록시를 사용하며 프록시는 대상 객체를 감싸고 호출을 가로채는데, 이를 위해 리플렉션을 사용하여 프록시를 동적으로 생성
    • 데이터 바인딩: 스프링 MVC에서는 HTTP 요청 파라미터를 자바 객체의 필드에 바인딩할 때 리플렉션을 사용합니다. 즉, 요청 파라미터와 매핑된 객체를 생성하고, 객체의 필드에 값을 설정하기 위해 리플렉션을 사용
    • 컨테이너 초기화 및 구성: 스프링은 XML 또는 어노테이션 기반의 설정을 통해 애플리케이션 컨텍스트를 초기화하고 구성
      • 리플렉션을 사용하여 클래스를 로드하고, 필요한 설정을 적용하며, 빈을 등록

 

  • 컴파일 시점에는 알 수 없는 클래스를 사용하는 프로그램을 작성한다면 리플렉션을 사용해야 함
    • 단, 되도록 객체 생성에만 사용하고, 생성한 객체를 이용할 때는 적절한 인터페이스나 컴파일 시점에 알 수 있는 상위 클래스로 형변환해 사용하는 것을 권장

 

참고

이펙티브 자바

반응형