본문으로 바로가기

Java Reflection API와 JPA에 대하여

JPA를 공부하다보면 'JPA는 Entity로 사용할 객체에 반드시 기본 생성자가 있어야 합니다.' 라는 내용을 접한 적이 있을 것이다. 최근 공부하면서 '그냥 그런가 보다' 하고 넘어갔던 내용들을 하나씩 이유를 살펴보면서 기억하려고 한다.

먼저 이 개념을 이해하기 위해서는 Java Reflection의 개념을 이해해야 한다. Java Reflection 은 자바 입문서에서 잘 다루지 않는 내용이기 때문에 나같은 초보 개발자 에겐 다소 생소한 개념이었다.

여러 블로그들을 돌아다니면서 Java Reflection 이 무엇인지 개념부터 알아보았다.

Java Reflection 이란?

구체적인 클래스 타입을 알지 못해도, 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API

자바에서 제공하는 리플렉션(Reflection)은 C, C++과 같은 언어를 비롯한 다른 언어에서는 볼 수 없는 기능이다. 이미 로딩이 완료된 클래스에서 또 다른 클래스를 동적으로 로딩(Dynamic Loading)하여 생성자(Constructor), 멤버 필드(Member Variables) 그리고 멤버 메서드(Member Method) 등을 사용할 수 있도록 한다.

그러니까, 컴파일 시간(Compile Time)이 아니라 실행 시간(Run Time)에 동적으로 특정 클래스의 정보를 객체화를 통해 분석 및 추출해낼 수 있는 프로그래밍 기법이라고 표현할 수 있다.

 

여기서 구체적인 클래스 타입을 알지 못해도 가 무슨 말인지 이해할 수 없었다. '자신이 작성한 코드인데 어떻게 클래스 타입을 모를수가 있을까 ?'라고 생각했기 때문이다.

아래 간단한 예제를 보고 위 내용을 이해해보자.

public class Music {
    private String singer;
    private String title;

    public Music(String singer, String title) {
        this.singer = singer;
        this.title = title;
    }
    public Music(){

    }

    public String getTitle() {
        return title;
    }

    public String getSinger() {
        return singer;
    }
}

가수 정보와 곡 정보를 담은 간단한 Music 클래스가 있다.

 

그리고 위와 같이 객체를 생성할 수 있다.

(이 부분이 이해가 되지 않으면 자바의 다형성 에 대해서 조금 더 공부하면 될 것 같다.)

 

우리가 이렇게 music이라는 객체를 생성했지만 Music클래스에서 제공하는 getTitle()getSinger() 메소드를 사용할 수 없다.

자바는 정적 언어로 컴파일 시점에 타입을 결정한다. 따라서 컴파일을 진행할때 music이라는 객체의 타입을 Object로 결정한 것이다.

 

컴파일 시점에 타입을 결정하는 정적 언어 : Java, C, C++ 등등

런타임 시점에 타입을 결정하는 동적 언어 : JavaScript, Ruby 등등

컴파일 타임 : 작성한 코드를 기계어로 변환하여 실행가능한 프로그램으로 변환시키는 과정을 컴파일이라 하며, 이 과정동안 일어나는 시간을 컴파일타임 이라고 한다.

런타임 : 컴파일 과정을 마친 프로그램은 사용자에 의해 실행되어 지며, 이러한 응용프로그램이 동작되는 때를 런타임(Runtime) 이라고 부른다.

즉 music이라는 객체는 자신이 Object타입이라는 사실만 알 뿐, Music 클래스에 대해서는 전혀 알지 못한다.

이제 위에서 설명한 구체적인 클래스 타입을 알지 못해도 의 의미를 어느정도 파악할 수 있게 되었다.

이제 Java Reflection API를 이용해서 구체적인 클래스 타입을 알지 못하지만 그 클래스의 메소드에 접근을 해보자.

  1. 먼저 .class를 사용하여 해당 클래스를 바로 참조하는 방법이다.
 public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Object music = new Music("IU", "YOU AND ME");

        Class clazz = Music.class;
        Method getTitle = clazz.getMethod("getTitle");
        String title = (String)getTitle.invoke(music,null);
        System.out.println(title);
    }
}
출력결과 : YOU AND ME
  1. .forName() 메소드를 이용하여 패키지명을 통해 클래스를 로드하는 방법이다.
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException {
        Object music = new Music("IU", "YOU AND ME");

        Class clazz2 = Class.forName("자바리플랙션.Music");
        Method getSinger = clazz2.getMethod("getSinger");
        String singer = (String)getSinger.invoke(music,null);
        System.out.println(singer);
    }

}
출력결과 : IU

이 외에도 Java Reflection API는 여러가지 기능을 제공한다.

getDeclaredConstructor() : 인자가 없는 생성자를 가져온다.

getDeclaredConstructor(Param) : Param에 인자(타입) 을 넣으면 그 타입과 일치하는 생성자를 가져온다

Class clazz = Class.forName("자바리플랙션.Music");
Constructor constructor =  clazz.getDeclaredConstructor(String.class,String.class);
System.out.println(constructor);
출력결과 : public 자바리플랙션.Music(java.lang.String,java.lang.String)

이 위와 같은 일을 가능하게 하는 이유는 Java Reflection 이 자바 코드가 컴파일 시점에 static 영역에 저장되는 정보들을 활용하기 때문이다. 따라서 클래스의 이름만 알고있다면 언제든지 static 영역을 참고해서 그 정보들을 사용할 수 있게 되는 것이다.


다행스럽게도

이렇게 복잡한 Java Reflection을 우리가 코딩을 하면서 직접 사용하는 일은 극히 드물다. 우리가 작성한 코드의 구체적인 클래스를 모를 일이 거의 없기 때문이다.

대부분 프레임워크나 라이브러리 등 사용하는 사람이 어떤 클래스를 만들지 모르는 경우 리플렉션을 통해 동적으로 이 문제를 해결하기 위해 많이 사용된다.

또한, Java Reflection은 몇가지 큰 단점들을 가지고 있다.

  • 이미 인스턴스를 만들었음에도 불구하고 굳이 필드와 리플렉션을 이용해서 접근하거나 사용할 경우 불필요한 메모리 낭비를 야기한다.
  • 컴파일 타임에 확인되지 않고 런타임 시에만 발생하는 문제들이 생길수 있다.
  • 접근 지시자를 무시할 수 있다.

이러한 단점들 때문에 리플렉션의 사용 최대한 피할 수 있으면 피하는 것이 좋다.


결론

이제 맨 위에서 언급한 JPA는 Entity로 사용할 객체에 반드시 기본 생성자가 있어야 합니다. 의 이유를 정리할 차례다.

java Reflection가져올 수 없는 정보 중 하나가 바로 생성자의 인자 정보들 이다. 따라서 기본 생성자 없이 파라미터가 있는 생성자만 존재한다면 java Reflection이 객체를 생성할 수 없게 되는 것이다.

추가로 김영한님의 인프런 강의 : 자바 ORM 표준 JPA 프로그래밍 햇병아리 님의 질문글 중, 좋은 내용이 있어서 소개하려고 한다.

@Getter @Setter
@Entity
public class Member {

    @Id 
    private Long id;
    private String name;

    public Member(Long id, String name){
        this.id = id;
        this.name = name;
    }
}

이렇게 기본 생성자가 없어도 코드가 동작하는 경우가 있는데, 아래 김영한 님의 답변을 참고하면 좋을 것 같다.

 

Reference