Reflection
Reflection이란?
- 컴파일 타임이 아닌 런타임에 클래스, 메서드, 필드 등의 정보를 조회하고 검사할 수 있도록 하는 자바의 기
Reflection의 주요 기능
- 클래스 정보(클래스 이름, 메서드, 필드 등)을 런타임에 알 수 있음
- 런타임에 해당 클래스의 객체 생성 가능
- 런타임에 메서드 호출하능
- 런타임에 필드에 접근하고 수정할 수 있음
예를 들어 JPA 기술을 쓰면, DB에서 값을 찾아오고 그 값으로 객체를 만들어 주는데 이때 리플렉션으로 private 변수에 직접 접근하여 값을 할당합니다.
객체와 Json 사이의 직렬화/역직렬화를 도와주는 Jackson 라이브러리도 리플렉션이라는 기능을 활용합니다.
Reflection 활용 예제
아래와 같은 Calculator 클래스를 만들고, 리플렉션의 여러 기능들을 적용해보겠습니다.
class Calculator {
private String name;
public Calculator() {
}
// 생성자는 퍼블릭으로 만들어야 리플렉션에서 전역적으로 접근할 수 있다.
public Calculator(String name) {
this.name = name;
}
public void sayHello() {
System.out.println("my name is " + this.name);
}
}
인스턴스.getClass()로 클래스 타입 가져오기
Calculator calculator = new Calculator("Samsung");
Class<?> myClass = calculator.getClass();
System.out.println("myClass: " + myClass);
getClass() 메서드를 통해 인스턴스의 클래스 타입을 가져올 수 있습니다.
<?>는 제네릭 표현으로 어떤 클래스 타입도 가능하다는 의미입니다.
실행 결과:
클래스.getConstructor() 메소드로 인스턴스 생성하기
Calculator cal1 = (Calculator)myClass.getConstructor(String.class).newInstance("Samsung");
Calculator cal2 = Calculator.class.getConstructor(String.class).newInstance("Casio");
메서드 실행하기
System.out.println("line 31");
Method cal1Method = myClass.getMethod("sayHello");
cal1Method.invoke(cal1); // 메소드를 실행할 때는 객체가 있어야한다. (어떤 객체에서 메소드를 실행해야하는지)
Method cal2Method = Calculator.class.getMethod("sayHello");
cal2Method.invoke(cal2);
getMethod("메소드명") 메소드와 invoke(인스턴스) 메소드를 통해서 클래스의 메소드를 가져오고 실행할 수 있습니다.
필드에 접근하여 값 조회, 변경하기
System.out.println("바꾸기 전");
cal1.sayHello();
Field nameField = Calculator.class.getDeclaredField("name");
nameField.setAccessible(true); // 내가 접근할 수 있도록 true로 만들기
System.out.println("get()으로 값 조회: " + nameField.get(cal1));
nameField.set(cal1, "Casio");
System.out.println("바꾼 후: ");
cal1.sayHello();
cal1Method.invoke(cal1);
Annotation
Annotation이란?
- 자바에서 메타데이터를 제공하기 위한 문법 요소
- 일반적으로 런타임 시점에 동작합니다.
Annotation의 주요 기능
- 컴파일러에게 정보 제공 (ex. @Override로 컴파일러에게 해당 메소드가 재정의되었음을 알려줌)
- 런타임 시에 특정 행동을 유발(ex. @Autowired로 의존 관계를 주입할 수 있도록 함)
Reflection을 이용한 검증 어노테이션 구현 예제
@NotEmpty라는 어노테이션을 만들어서 필드 위에 붙이고 Reflection을 활용해서 그 어노테이션이 붙은 필드의 값이 비어있는지를 검사하는 메소드를 구현하여 유효값 검증 어노테이션을 만들 수 있습니다. (Bean Validation에 있는 @NotEmpty를 직접 구현해보기)
필드.isEmpty()로 일일이 검사해주는 것보다 어노테이션을 만들어서 검사하면 어노테이션 하나 붙이는 것만으로도 검사가 가능하기에 편리하고 코드의 중복을 줄일 수 있을 것입니다.
아래와 같이 @NotEmpty 어노테이션을 만들어줍니다.
// 특정 필드의 값이 비어있는지 아닌지 검사하는 어노테이션
// RetentionPolicy.RUNTIME: 런타임에 동작하도록 지정한 키워드
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD) // FIELD: 변수에 적용, METHOD: 메서드에 적용, TYPE: 클래스, 인터페이스 등에 적용
@interface NotEmpty {
String message() default "this field cannot be empty";
}
그리고 validate()라는 메소드를 만들어 어노테이션이 붙은 필드를 불러와서 null이 아닌지, empty가 아닌지 검사하는 로직을 작성해줍니다.
static void validate(Object object) throws IllegalAccessException {
Class<?> myClass = object.getClass();
Field[] fields = myClass.getDeclaredFields(); // Class 안에 모든 필드 불러오기
for(Field f : fields) {
if(f.isAnnotationPresent(NotEmpty.class)) { // isAnnotationPresent로 @NotEmpty가 붙어있는지 검사
f.setAccessible(true); // 해당 필드에 접근할 수 있도록 함
String value = (String)f.get(object);
if(value == null || value.isEmpty()) {
NotEmpty notEmpty = f.getAnnotation(NotEmpty.class); // 어노테이션을 가져오기
throw new IllegalArgumentException(notEmpty.message()); // @NotEmpty의 기본 메시지를 담아서 예외 발생
}
}
}
}
잘 실행되는지 확인하기 위해 아래와 같은 User 클래스를 만들고, 필드 위에 @NotEmpty를 붙여줍니다.
참고로 @NotEmpty(message = "name cannot be empty")
이렇게 기본 메시지를 변경할 수도 있습니다.
class User {
@NotEmpty(message = "name cannot be empty")
private String name;
@NotEmpty(message = "email cannot be empty")
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
}
메인메소드에서 아래처럼 name을 빈 스트링으로 하여 User 객체를 생성해주면
public static void main(String[] args) throws Exception {
String name = "dddd";
String email = "";
User user = new User(name, email);
validate(user);
}
예외가 잘 발생하는 것을 확인할 수 있습니다.
Reference
- beyond sw 7기 Java 수업 자료 및 수업 내용
- https://velog.io/@ych0716/reflection
'개념 공부 > Language' 카테고리의 다른 글
[Java] Collections (0) | 2021.05.23 |
---|---|
[C 개념] #1. 2학기 예습 (포인터, 다중 포인터) (0) | 2020.08.03 |