본문 바로가기
개념 공부/Language

[Java] Reflection을 활용해서 검증 어노테이션 구현하기

by clean01 2024. 6. 20.

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

'개념 공부 > Language' 카테고리의 다른 글

[Java] Collections  (0) 2021.05.23
[C 개념] #1. 2학기 예습 (포인터, 다중 포인터)  (0) 2020.08.03