본문 바로가기
별밤 일지/개발

[Java] 리플렉션, ObjectMapper

by 별밤 에디터 2024. 3. 12.

 

우리가 SpringBoot 에서 rest api 를 개발할 때 흔히 쓰는 @RequestBody 가 정확히 어떻게 동작하는지 생각해본 적이 있는가?

 

    /**
     * 앱 관측적합도 상세 페이지를 위한 실시간 날씨 정보 제공
     */
    @PostMapping("/observationalFit/weatherPage")
    public Mono<WeatherInfo> getWeatherInfo(@RequestBody AreaTimeDTO areaTime) {
        return observationalFitService.getWeatherInfo(areaTime);
    }

 

위 코드는 별 헤는 밤 어플의 관측적합도 상세 페이지에서 실시간 날씨 정보를 조회하는 api 이다. 요청 body 로 AreaTimeDTO 객체를 받고 있는데, 외부에서 요청이 들어올 때는 해당 객체 타입으로 들어오는 것이 아닌 json 형태로 요청이 들어오는데 어떻게 AreaTimeDTO 타입으로 받을 수 있는 것일까.

위 내용이 헷갈린다면 우리가 api 테스트를 할 때 사용하는 postman 에서 요청 body 를 json 형태로 보낸다는 것을 떠올리면 좀 더 이해하기 쉬울 것이다.

결론부터 이야기하자면 SpringBoot 에서 제공하는 ObjectMapper 가 그 역할을 해주고, ObjectMapper 는 자바의 리플렉션 기능을 활용하여 동작한다.

 

리플렉션이란

리플렉션은 힙 영역에 로드된 Class 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성할 수 있도록 지원하고, 해당 인스턴스의 필드와 메소드를 접근 제어자와 상관 없이 사용할 수 있도록 지원하는 API이다.

힙 영역에 로드된 클래스 타입의 객체를 가져오는 데에는 3가지 방법이 있다.

  1. 클래스.class 로 가져오기
  2. 인스턴스.getClass() 로 가져오기
  3. Class.forName("클래스명") 으로 가져오기

아래와 같은 Member 클래스를 정의했다고 하자.

 

public class Member {

    private String name;

    protected int age;

    public String hobby;
}

public Member() {} // 생성자 a

public Member(String name) { // 생성자 b
    this.name = name;
}

private Member(String name, int age) { // 생성자 c
    this.name = name;
		this.age = age;
}

public void hello(){
		System.out.println("hell0");
}

public void myName(String name){
		System.out.println("myName is " + name);
}

 

아래와 같이 3가지 방법으로 클래스의 인스턴스를 가져올 수 있다.

 

// 1.
Class<Member> memberClass1 = Member.class;

// 2. 
Member member = new Member("별린이", 25, "밤 하늘 여행");
Class<? extends Member> memberClass2 = member.getClass();

// 3.
Class<?> memberClass3 = Class.forName("com.server.tourApiProject.Member");

 

위와 같은 코드는 개발할 때 사용한 적이 별로 없을 것이다. (일단 필자는 그러하다)

그럼 리플렉션은 언제 활용할까? 바로 동적으로 클래스를 사용해야할 때 이다. 즉, 런타임 시점에 클래스가 필요할 때 사용하게 되는데

  • IntelliJ의 자동완성 기능
  • 스프링 어노테이션

가 그 예시이다.

리플렉션을 사용해서 가져올 수 있는 정보는 다음과 같다.

  • Class
  • Constructor
  • Method
  • Field

 

Class 를 찾는 방법은 위에서 설명했고, 나머지를 찾는 방법은 다음과 같다.

  • Constructor
    1. 인자가 없는 생성자 가져오기: getDeclaredConstructor()
    2. 인자가 있는 생성자 가져오기: getDeclaredConstructor(Param)
    3. 모든 생성자 가져오기: getDeclaredConstructors()
    4. public 생성자만 가져오기: getConstructors()
Class<?> memberClass = Class.forName("com.server.tourApiProject.Member");

// 1. 생성자 a 가져와짐
Constructor constructor1 = memberClass.getDeclaredConstructor();

// 2. 생성자 b 가져와짐
Constructor constructor2 = memberClass.getDeclaredConstructor(String.class);

// 3. 생성자 a, b, c 가져와짐
Constructor constructor3 = memberClass.getDeclaredConstructors();

// 4. 생성자 a, b 가져와짐
Constructor constructor4 = memberClass.getConstructors();

 

  • Method
    getDeclaredMethod() 메서드에 인자로 가져오고 싶은 메서드 파라미터 정보를 넣으면 된다.
Class<?> memberClass = Class.forName("com.server.tourApiProject.Member");

// 1. hello() 가져오기
Method method1 = memberClass.getDeclaredMethod("hello", null);

// 2. myName() 가져오기
Method method2 = memberClass.getDeclaredMethod("myName", String.class);

// 3. 모든 메서드 가져오기
Method methods[] = memberClass.getDeclaredMethods();

// 4. public 메서드와 상속받은 메서드 가져오기
Method methods[] = memberClass.getMethods();

 

  • Field
    위와 비슷한 맥락으로 getDeclaredField() 를 사용하면 된다
Class<?> memberClass = Class.forName("com.server.tourApiProject.Member");

// 1. name 가져오기
Field field1 = memberClass.getDeclaredField("name");

// 2. 모든 필드 가져오기 (단, 상속받은 객체는 가져오지 않음)
Field[] field = memberClass.getDeclaredFields();

// 2. public 필드와 상속받은 필드 가져오기
Field[] field = memberClass.getFields();

 

위와 같은 리플렉션을 활용해 ObjectMapper 는 객체로부터 Json 형태의 문자열을 만들어내고, 역으로 Json 형태의 문자열로 객체를 만들 수 있다.

이 개념을 직렬화(Serialize), 역직렬화(Deserialize) 라고 한다.

 

ObjectMapper 는 직렬화 과정에서 wrtieValueAsString() 이라는 메서드를 사용하는데, 이때 기본 설정으로 public 필드 또는 public 형태의 getter 만 접근이 가능하므로, @Getter 를 반드시 만들어주는 것이 좋다.

 

평소에 직렬화, 역직렬화에 대해 간단한 개념만 알고 있었지 정확한 동작 방식은 모르고 있었는데 이번 기회에 어떻게 동작하는지 알게되었다. 앞으로 기본적으로 사용하는 기능들에 대해 어떻게 동작하는지 알면 오류 발생 시 원인을 분석하는 데에 도움이 될 것 같다.