본문 바로가기

프로젝트/여행지 오픈 API

[ElasticSearch] spring data elasticsearch Null 출력 이슈(Getter, Reflection)

목차

    이슈 : Getter와 리플렉션, 그리고 프록시(JPA와의 차이)

     

    개수는 맞는 것 같은데.. 값이 나오지 않는다.

     

    @Getter 추가의 경우, 제대로 값이 반환된다.

     


    Documnet 객체에 Getter를 붙이지 않을 경우 값이 반환되지 않는다.
    @Getter는 Lombok 라이브러리의 어노테이션이며, 이를 클래스 레벨에 붙이면 해당 클래스의 모든 필드에 대한 getter 메소드를 자동으로 생성해준다. 만약 @Getter 어노테이션이 없다면, 필드에 접근하는 getter 메소드가 생성되지 않아서 외부에서 해당 필드의 값을 읽을 수 없다..!

     

    리플렉션을 사용할 수 없기 때문이다.

     

    Spring Data Elasticsearch는 리플렉션을 사용하여 도메인 객체의 필드 값을 읽어내는데, 이 과정에서 getter 메소드를 통해 필드의 값을 가져온다. @Getter가 없으면 해당 필드에 대한 getter 메소드가 없으므로, Spring Data Elasticsearch는 필드 값을 가져올 수 없고, 결과적으로 null 값이나 기본값을 반환하게 된다.

    즉, @Getter 없이는 Elasticsearch에서 데이터를 검색한 후, 검색 결과를 해당 Entity의 필드에 바인딩할 때 필드의 값을 가져올 방법이 없으므로, null이나 기본값으로 처리되는 것이다. 따라서 @Getter 어노테이션을 붙여서 해당 필드에 대한 getter 메소드를 제공해야 한다.

     

    리플렉션이란?

    리플렉션(Reflection)은 자바에서 클래스 또는 객체의 정보를 런타임에 조사하고 수정할 수 있는 기능이다. 즉, 프로그램이 실행 중일 때, 해당 프로그램이 자신이 가진 구조(클래스, 필드, 메소드 등)에 대한 정보를 알아내거나 변경할 수 있는 것을 말한다.

    이를 사용하면 다음과 같은 일들을 할 수 있다:

    • 런타임에 클래스의 메소드, 필드, 어노테이션 등에 접근하여 정보를 얻어낼 수 있다.
    • 런타임에 객체의 필드에 값을 할당하거나, 메소드를 호출할 수 있다.
    • 새로운 객체를 생성하거나, 배열을 조작하는 등의 작업을 할 수 있다.

    예를 들어, Spring Data Elasticsearch는 리플렉션을 사용하여 개발자가 정의한 엔티티 클래스의 메타데이터(필드 정보, 메소드 정보 등)를 읽어서 Elasticsearch로부터 받은 데이터를 해당 클래스의 인스턴스에 매핑한다.

    @Getter가 붙어 있지 않으면, 리플렉션을 통해 필드의 값을 읽을 수 있는 public getter 메소드가 없기 때문에, 데이터를 객체의 필드에 할당할 수 없다. 그 결과 객체의 필드 값들이 모두 null이 되거나 초기값을 가지게 되는 것이다.

    간단히 말해, 리플렉션은 클래스의 구조를 런타임에 파악하고 이용할 수 있게 해주며, @Getter는 리플렉션을 사용하는 프레임워크가 필드 값을 읽을 수 있도록 getter 메소드를 자동으로 제공하는 역할을 한다.

     

     

    간단한 리플렉션 예시

    //간단한 클래스 객체를 하나 생성해보자.
    @Getter @Setter
    public class Person {
        private String name;
        private int age;
    
        public Person() {
        }
    
    }

     

    리플렉션을 사용해서 클래스 정보를 얻고, 객체를 조작할 수 있다.

     

    public class ReflectionExample {
        public static void main(String[] args) {
            try {
                // Person 클래스의 Class 객체를 얻음
                Class<?> cls = Class.forName("Person");
    
                // Person 클래스의 인스턴스 생성
                Object person = cls.newInstance();
    
                // 모든 필드 가져오기
                Field[] fields = cls.getDeclaredFields();
                for (Field field : fields) {
                    System.out.println("Field name: " + field.getName());
                    System.out.println("Field type: " + field.getType().getName());
                }
    
                // private 필드에 접근하기 위해 접근성 변경
                Field nameField = cls.getDeclaredField("name");
                nameField.setAccessible(true);
    
                // 'name' 필드에 값을 할당
                nameField.set(person, "John Doe");
    
                // 'name' 필드의 값을 가져오기
                String nameValue = (String) nameField.get(person);
                System.out.println("Name: " + nameValue);
    
                // 모든 메소드 가져오기
                Method[] methods = cls.getDeclaredMethods();
                for (Method method : methods) {
                    System.out.println("Method name: " + method.getName());
                }
    
                // 'setName' 메소드를 찾아서 호출
                Method setNameMethod = cls.getMethod("setName", String.class);
                setNameMethod.invoke(person, "Jane Doe");
    
                // 'getName' 메소드를 찾아서 호출
                Method getNameMethod = cls.getMethod("getName");
                String name = (String) getNameMethod.invoke(person);
                System.out.println("Updated Name: " + name);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

     

     

    이 코드는 Person 클래스의 인스턴스를 생성하고, 해당 인스턴스의 필드에 접근하여 값을 변경한 다음, 메소드를 호출하여 그 값을 다시 가져오는 과정을 리플렉션으로 수행한다.

     

    - cls.getDeclaredFields()를 통해 클래스의 모든 필드를 조회할 수 있다.
    - field.setAccessible(true)를 통해 접근 제어자가 private인 필드에도 접근할 수 있도록 한다.
    - field.set()field.get()을 통해 필드의 값을 설정하고 읽을 수 있다.
    - cls.getDeclaredMethods()로 클래스의 모든 메소드를 조회할 수 있다.
    - method.invoke()를 통해 메소드를 실행할 수 있다.

     

    리플렉션을 사용하면 이런 식으로 클래스의 내부 정보에 접근하고 조작할 수 있다. 그러나 실제 프로덕션 코드에서는 리플렉션의 사용을 최소화하는 것이 좋다. 리플렉션은 타입 안정성을 해칠 수 있고, 성능에도 영향을 줄 수 있으며, 내부 구조에 대한 과도한 접근은 보안 문제를 일으킬 수 있기 때문이다.

     

    그 외에 자세하고 쉬운 개념 정리는 다음 포스팅을 참조할 것.

     

    https://jeongkyun-it.tistory.com/225

     

    [Java] 리플렉션 (Reflection)이란 무엇일까? (개념/ 예시)

    서론 이번 포스팅에서 다룰 내용은 '리플렉션'이다. 최근 "리플렉션이 무엇인가요?" 라는 질문을 받았는데, 제대로 된 답변을 못한 것 같다. C# 개발을 할 때 분명 사용은 해보았지만 개념적으로

    jeongkyun-it.tistory.com

     

     

    JPA는 Getter 안 붙여도 되던데 ElasticSearch는 왜?

     

    가장 큰 의문이었다. 검색 결과 똑같은 Spring Data 기반이지만, 내부 동작 방식이 조금 달랐다.


    Spring Data JPA와 Spring Data Elasticsearch는 내부적으로 객체를 다루는 방식에 있어서 다른 접근 방식을 취한다.

    Spring Data JPA의 경우, JPA 구현체(예를 들어, Hibernate)는 자바의 프록시 메커니즘을 사용하여 엔티티 클래스의 프록시를 생성한다. 프록시를 통해서, getter와 setter 없이도 필드에 직접 접근할 수 있는 바이트코드 조작이 가능하다. 또한, JPA는 @Access 어노테이션을 통해 필드 레벨 혹은 프로퍼티 레벨 접근을 명시할 수 있는 옵션을 제공하여, 필드 접근 방식을 지정할 수 있게 해준다. 따라서, 엔티티 필드에 직접 접근하거나 필요에 따라 getter/setter를 통해 접근하는 것이 가능하다.

     

    Spring Data JPA의 엔티티 프록시는 실제 엔티티 클래스의 인스턴스 대신에 사용되는 객체로, Hibernate 같은 JPA 구현체가 데이터베이스에서 데이터를 실제로 로딩하기 전까지 실제 엔티티 대신에 사용될 수 있다. 이 프록시는 실제 엔티티 클래스를 상속받아 만들어지고, 엔티티의 메소드 호출 시 필요에 따라 데이터 로딩을 지연시키는 등의 추가 작업을 수행할 수 있다.

     

    반면에, Spring Data Elasticsearch는 Elasticsearch 서버와 통신하기 위해 JSON 형태로 데이터를 직렬화하고 역직렬화해야 한다. 이 과정에서 Spring Data Elasticsearch는 Java의 리플렉션 API를 사용하는데, 특히 메소드 리플렉션을 사용하여 getter 메소드를 찾고 호출한다. 객체의 필드에 저장된 데이터를 JSON으로 변환하기 위해서는 해당 필드의 getter 메소드가 필요하다. 이는 내부적으로 데이터를 읽고 쓰는 방식이 엔티티의 필드 직접 접근이 아니라, getter/setter 메소드를 통한 접근을 기본으로 하기 때문이다.

     

    따라서, Spring Data Elasticsearch를 사용할 때 엔티티 클래스에 @Getter를 붙여주지 않으면, Elasticsearch가 엔티티의 데이터를 JSON으로 변환할 때 필요한 메소드를 찾을 수 없게 되어 데이터를 올바르게 처리하지 못하고 결과적으로 null이나 빈 값을 반환하게 되는 것이다.