Reflection API
리플렉션은 힙 영역에 로드된(런타임) Class 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성할 수 있도록 지원하고, 인스턴스의 필드와 메서드를 접근 제어자와 상관 없이 사용할 수 있도록 지원하는 API이다.
- 로드된 클래스 : JVM의 클래스 로더에서 클래스 파일에 대한 로딩을 완료한 후, 해당 클래스의 정보를 담은 Class 타입의 객체를 생성하여 메모리의 힙 영역에 저장해 둔 것(new 키워드를 통해 만드는 객체와는 다른 것)
- 자바에는 동적으로 객체를 생성하는 기술이 없었으나 동적으로 인스턴스를 생성하는 Reflection으로 그 역할을 대신하게 된다.
- Reflection으로 객체의 형은 알고 있지만 형변환을 알 수 없는 상태에서 객체의 메서드를 호출할 수 있다.
Reflection API 사용법
.class로 가져오기
- 인스턴스가 존재하지 않고, 컴파일된 클래스 파일만 있다면 리터럴로 Class 객체를 곧바로 얻을 수 있다.
- 가장 심플하게 Class 객체를 가져오는 방법
public static void main(String[] args) {
// 클래스 리터럴(*.class)로 얻기
Class<? extends String> cls2 = String.class;
System.out.println(cls2); // class java.lang.String
}
인스턴스(Object).getClass()로 가져오기(리터럴로 얻기)
- 모든 클래스의 최상위 클래스인 Object 클래스에서 제공하는 getClass() 메서드를 통해 가져온다.
- 단 해당 클래스가 인스턴스화 된 상태여야 사용할 수 있는 제약이 있다.
public static void main(String[] args) {
// 스트링 클래스 인스턴스화
String str = new String("Class클래스 테스트");
// getClass() 메서드로 얻기
Class<? extends String> cls = str.getClass();
System.out.println(cls); // class java.lang.String
}
Class.forName("클래스명") 으로 가져오기(동적 로딩)
- 리터럴 방식과 같이 컴파일된 클래스 파일이 있다면 클래스 파일이 있다면 클래스 이름만으로 Class 객체를 반환 받을 수 있다
- 가장 메모리를 절약하며 동적으로 로딩할 수 있기 때문에 성능이 좋다.
- 클래스 도메인을 상세히 적어야 한다, 클래스 파일 경로에 오타가 없는지 꼭 확인할 것(대소문자 실수 등) 만일 Class 객체를 찾지 못한다면 ClassNotFoundExcetoption을 발생시키기 때문에 예외처리가 강제 된다.
- 보통 다른 클래스 파일을 불러올 때 컴파일 시 JVM의 method Area에 클래스 파일이 같이 바인딩 되지만 동적 로딩의 경우 컴파일에 바인딩 되지않고 런터임때 불러오기 때문에 동적 로딩이라고 부른다(컴파일 타임에 체크할 수 없기 때문에 클래스 유무가 확인되지 않아 예외처리를 해줘야 한다).
public static void main(String[] args) {
try {
// 도메인.클래스명으로 얻기
Class<?> cls3 = Class.forName("java.lang.String");
System.out.println(cls3); // class java.lang.String
} catch (ClassNotFoundException e) {}
}
로드된 클래스 가져오기
- 위 3가지 방법으로 가져온 Class 타입의 인스턴스는 모두 동일하다.
- Class 타입을 통해 클래스의 인스턴스를 생성할 수 있고, 인스턴스의 필드와 메서드를 접근 제어자와 상관 없이 사용할 수 있게 된 것이다.
public class Member {
private String name;
protected int age;
public String hobby;
public Member() {
}
public Member(String name, int age, String hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
}
public void speak(String message) {
System.out.println(message);
}
private void secret() {
System.out.println("비밀번호는 1234입니다.");
}
@Override
public String toString() {
return "Member{" +
"name='" + name + '\'' +
", age=" + age +
", hobby='" + hobby + '\'' +
'}';
}
}
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Class<Member> memberClass = Member.class;
System.out.println(System.identityHashCode(memberClass));
Member member = new Member("제이온", 23, "다라쓰 개발");
Class<? extends Member> memberClass2 = member.getClass();
System.out.println(System.identityHashCode(memberClass2));
Class<?> memberClass3 = Class.forName("{패키지명}.Member");
System.out.println(System.identityHashCode(memberClass3));
}
}
// 실행 결과
1740000325
1740000325
인스턴스 생성
- getConstructor(): 기본 생성자를 통한 인스턴스 생성
- newInstance(): 인스턴스 동적 생성
public class Main {
public static void main(String[] args) throws Exception {
// Member의 모든 생성자 출력
Member member = new Member();
Class<? extends Member> memberClass = member.getClass();
Arrays.stream(memberClass.getConstructors()).forEach(System.out::println);
// Member의 기본 생성자를 통한 인스턴스 생성
Constructor<? extends Member> constructor = memberClass.getConstructor();
Member member2 = constructor.newInstance();
System.out.println("member2 = " + member2);
// Member의 다른 생성자를 통한 인스턴스 생성
Constructor<? extends Member> fullConstructor =
memberClass.getConstructor(String.class, int.class, String.class);
Member member3 = fullConstructor.newInstance("제이온", 23, "다라쓰 개발");
System.out.println("member3 = " + member3);
}
}
// 실행 결과
public Member()
public Member(java.lang.String,int,java.lang.String)
member2 = Member{name='null', age=0, hobby='null'}
member3 = Member{name='제이온', age=23, hobby='다라쓰 개발'}
생성한 인스턴스의 필드와 메서드 접근
- getDeclaredFields(): 클래스의 인스턴스 변수 모두 가져오기
- 필드.get(): 필드 값 반환
- 필드.set(): 필드 값 수정
- 주의: private 접근제어자 필드에 접근할 때는 setAccessible()의 인자를 true로 넘겨줘야 한다.
setAccessible(true)
- getDeclaredMethod(): 메서드를 가져올 수 있다.
- 주의: 메서드의 이름과 파라미터 타입을 인자로 넘겨줘야하며, private 접근제어자 메서드에 접근할 때는 setAccessible()의 인자를 true로 넘겨줘야 한다.
memberClass.getDeclaredMethod("speak", String.class);
invoke() 메서드를 통해 Reflection API를 얻어온 메서드를 호출할 수 있다.
public class Main {
public static void main(String[] args) throws Exception {
Member member = new Member("제이온", 23, "다라쓰 개발");
Class<? extends Member> memberClass = member.getClass();
// 필드 접근
Field[] fields = memberClass.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
System.out.println(field.get(member));
}
fields[0].set(member, "제이온2");
System.out.println(member);
// 메소드 접근
Method speakMethod = memberClass.getDeclaredMethod("speak", String.class);
speakMethod.invoke(member, "리플렉션 테스트");
Method secretMethod = memberClass.getDeclaredMethod("secret");
secretMethod.setAccessible(true);
secretMethod.invoke(member);
}
}
// 실행 결과
제이온
23
다라쓰 개발
Member{name='제이온2', age=23, hobby='다라쓰 개발'}
리플렉션 테스트
비밀번호는 1234입니다.
장단점
- 장점
- 런타임 시점에 클래스의 인스턴스를 생성하고 접근 제어자와 관계 없이 필드와 메서드에 접근하여 필요한 작업을 수행할 수 있는 유연성을 가지고 있다.
- 단점
- 캡슐화를 저해한다.
- 런타임 시점에 인스턴스를 생성하므로 컴파일 시점에서 해당 타입을 체크할 수 없다.
- 런타임 시점에 인스턴스를 생성하므로 구체적인 동작 흐름을 파악하기 어렵다.
- 단순히 필드 및 메서드를 접근할 때보다 Reflection을 사용하여 접근할 때 성능이 느리다(단 모든 상황에서 느리지는 않다).
캡슐화를 저해하므로 꼭 필요한 상황에서만 사용하는 것이 좋다.
'항해 99 > Spring' 카테고리의 다른 글
Web Game 코드 설계 정리 (0) | 2024.04.23 |
---|---|
프로젝트 코드 분석 (0) | 2024.04.23 |
POJO (0) | 2024.04.22 |
Spring boot 모니터링 with Prometheus, Grafana (1) | 2024.04.20 |
낙관적 락 & 비관적 락 (0) | 2024.04.19 |