코드네임 JY

[CRUD 연습] 엔티티티티 프레자일 본문

백엔드 공부

[CRUD 연습] 엔티티티티 프레자일

영재임재영 2023. 1. 10. 17:51

(절대로 공부하느라 정신 나간거 아님 주의)

(근데 진짜 포스팅 내용이랑 관련 있다... 아 정말이라고...)

🍚 @RequestBody vs @ResponseBody

API 통신을 구현하기 위해 컨트롤러에서는 두 가지 어노테이션을 사용할 수 있다.

@RequestBody : 클라이언트 → 서버 방향의 요청 / JSON 기반의 HTTP body를 자바 객체로 변환

✅ @ResponseBody : 서버 → 클라이언트 방향의 응답 / 자바 객체를 JSON 기반의 HTTP Body로 변환

 

🍜 JPA에서 Entity 사용할 때, 기본 생성자 반드시 써주기!

public class MusicSaveRequestDto {
    private String title;
    private String artist;
    private String album;
    private String lyrics;

    // Constructor
    public MusicSaveRequestDto(String title, String artist, String album, String lyrics) {
        this.title = title;
        this.artist = artist;
        this.album = album;
        this.lyrics = lyrics;
    }

    // Getter
    public String getTitle() {
        return title;
    }

    public String getArtist() {
        return artist;
    }

    public String getAlbum() {
        return album;
    }

    public String getLyrics() {
        return lyrics;
    }

    // Return to entity (repository.save()에 넘겨질 때는 Music 객체 타입으로 넘어가야)
    public Music toEntity() {
        return new Music(title, artist, album, lyrics);
    }
}

(참고로, Entity 자체를 모든 단계에서 사용하는 것은 좋지 않다! DTO를 따로 만들어서 구현하는 것이 좋다!)

위 DTO는 데이터를 POST 할 때 사용하는 MusicSaveRequestDto 객체 클래스이다.

 

테스트 코드에서  restTemplate.postForEntity(url, requestDto, Long.class)  코드를 통해 HTTP POST를 줄 수 있는데,

 

org.springframework.web.client.RestClientException:

Error while extracting response for type [class java.lang.Long] and content type [application/json];

nested exception is org.springframework.http.converter.HttpMessageNotReadableException:

JSON parse error: Cannot deserialize value of type java.lang.Long from Object value (token JsonToken.START_OBJECT); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException:

Cannot deserialize value of type java.lang.Long from Object value (token JsonToken.START_OBJECT)

at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 1]

 

음.. 문제가 하나가 아니라 여러가지가 생긴 것 같다.. 어떤 문제인지 하나하나 뜯어보자!

자세히 보니 반복되는 문장이 하나 있다. "Cannot deserialize~" 라는 문장이 보이는데, deserialize가 무엇이냐면!

 

Deserialize

✅ '역직렬화' (네트워크 통신으로 받은 데이터를 메모리에 쓸 수 있도록 변환)

✅ JSON → Object (객체 타입은 메모리에 저장 가능하잖아!)

✅ @RequestBody 역할

✅ Default Constructor를 사용해서 객체를 생성해야한다!

 

Serialize

✅ '직렬화' (네트워크 통신에 사용하기 위한 형식으로 변환)

✅ Object → JSON (JSON 타입으로 네트워크 통신할 수 있잖아!)

✅ @ResponseBody역할

✅ Getter를 사용해서 객채의 값을 가져올 수 있다!

 

그렇다면? 현재 Deserialize가 불가능하다는 뜻이고, JSON에서 객체 타입으로 변환할 때 문제가 생겼다는 뜻이라는 것..!

 

실제로 HTTP POST 통신을 할 때, 다음과 같은 과정으로 이루어진다.

1️⃣ POST 요청 (JSON 타입의 데이터)

2️⃣ JSON 타입의 데이터를 DTO 객체에 매핑(변환)

3️⃣ DTO 객체가 Controller → Service → Repository 순서로 넘어감

4️⃣ Hibernate(JPA 구현체)가 데이터베이스에 쿼리를 날려 데이터 저장

 

그 중에서, 2️⃣ 번 단계에 문제가 생긴 것이다! JSON 타입의 데이터를 객체에 매핑하지 못 했다는 것이다 ㅠㅠ

이 문제를 어떻게 해결할 수 있을까? 그렇다면 Java Reflection API 개념을 먼저 알아야 한다.

 

Java Reflection API

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

사실 무슨 말인지 잘 이해되지 않는다. 일단 다음 코드를 보자! (구글에 많이 나와있는 예제이다)

public class Car {
    private final String name;
    private int position;

    public Car(String name, int position) {
        this.name = name;
        this.position = position;
    }

    public void move() {
        this.position++;
    }

    public int getPosition() {
        return position;
    }
}
public static void main(String[] args) {
    Object obj = new Car("Tesla", 0);
    obj.move();  // 컴파일 에러 발생
}

자바에서 Object 클래스는 모든 클래스의 상위 클래스기 때문에, 다형성을 활용하면 위와 같이 객체를 선언할 수 있다.

하지만 해당 객체의 인스턴스를 사용해서 move 메서드를 실행하면, 컴파일 에러가 발생한다.

 

자바는 컴파일 시점에 타입을 결정하는 정적(Static) 언어인데, 컴파일 시점에서 obj는 현재 자신의 타입이 'Object' 라는 것만 안다.

여기서 바로 위 정의에 대한 힌트가 하나 나온다. '구체적인 클래스 타입을 알지 못해도' 부분이 바로 이런 것을 의미한다.

public static void main(String[] args) {
    Object obj = new Car("Tesla", 0);
    Class carClass = Car.class;
    Method move = carClass.getMethod("move");
    move.invoke(obj, null);
}

따라서 여기서 Reflection API를 사용하면 위와 같이 코드를 작성할 수 있다.

어쨌든 이런 방식으로 클래스의 이름만으로 구체적인 클래스에 대한 정보를 가져올 수 있게 해준다!

 

 

다시 본론으로 돌아와서, 위에서 발생한 컴파일 에러는 JSON에서 객체 타입으로 변환할 때 문제가 생겼다는 뜻이다.

JSON 데이터는 아무것도 건드린게 없는데? 맞다. 그렇다면 문제는 '객체' 에게 있다.

 

'JSON 타입의 데이터를 DTO 객체에 매핑(변환)'하는 과정을 조금 더 구체적으로 설명해보자면,

① JSON 데이터를, ② DTO 객체를 생성하여, ③ 필드 값에 맞게 데이터를 매핑시켜주어야 하는 상황이다.

 

JPA의 구현체인 Hibernate는 동적으로 객체를 생성할 때 아까 말했던 Reflection API를 사용한다. (이번 플젝에서 JPA 사용하니까)

하지만 Hibernate가 Reflection API를 통해 동적으로 객체를 생성할 때, 생성자의 인자 정보는 읽어오지 못 한다.

public class MusicSaveRequestDto {
    private String title;
    private String artist;
    private String album;
    private String lyrics;

    // Constructor <- 이 부분을 읽어오지 못 함
    public MusicSaveRequestDto(String title, String artist, String album, String lyrics) {
        this.title = title;
        this.artist = artist;
        this.album = album;
        this.lyrics = lyrics;
    }

	...
}

즉, 위 코드에서 만든 생성자는 Hibernate가 읽어오지 못 한다는 것이다. 그래서 객체 자체가 현재 생성되지 않은 상태이다. 

그럼 일단 객체를 생성하는 것이 중요한데? 그래서 바로 기본 생성자(Default Constructor)를 사용하는 것이다.

 

일단 기본 생성자라도 있으면 Hibernate는 객체를 생성할 수 있게 된다!

그리고 객체가 생성되기만 하면, 필드 값들은 Reflection API 기능을 통해서 가져올 수 있는 것이니까!

(클래스의 이름만으로 클래스에 대한 구체적인 정보 가져올 수 있게 해주니까)

 

정리하면! 다음과 같이 정리할 수 있겠다.

컴파일 에러 : Cannot deserialize value of type java.lang.Long from Object value

 상태 : 객체 생성 자체가 되어있지 않은 상태, 따라서 JSON에서 객체 타입으로 변환하지 못함(=Cannot deserialize)

 원인 : JPA의 구현체인 Hibernate가 동적으로 객체를 생성할 때 생성자의 인자 정보를 읽어올 수 없기 때문

 해결 : 기본 생성자를 만들어서 객체를 생성하게 해주면 된다!

 

이전 포스팅에서 구체적인 원인은 모르고 해결 방법만 알았는데, 실제로 이런 문제였다니..!

JPA에서 Entity를 사용하려면 반드시 기본 생성자를 넣어주어야하는 것을 꼭 기억하자!

 

아 ㅋㅋ 그럼 다들 엔티티 쓸 때 조심하자는 의미에서~~ (엔티티티티 프레자일 프레자일)

Comments