일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- SpringBootTest
- 키움
- testresttemplate
- 빈 스코프
- Java Reflection API
- 의존성 주입
- Not Acceptable
- jdbc template
- 스프링 IoC 컨테이너
- entity
- RunWith
- Controller
- 406 NOT_ACCEPTABLE
- 스프링 컨테이너
- 컴포넌트 스캔
- 좋은 객체지향 설계 원칙
- 랜덤 포트
- 통합 테스트
- JPA
- 스프링
- 405 METHOD_NOT_ALLOWED
- restTemplate
- 스프링 데이터 JPA
- 정적 컨텐츠
- ResponseEntity
- 기본 생성자
- jdbc
- 테스트 코드
- 가을야구
- 의존관계 자동 주입
- Today
- Total
코드네임 JY
[CRUD 연습] 엔티티티티 프레자일 본문
(절대로 공부하느라 정신 나간거 아님 주의)
(근데 진짜 포스팅 내용이랑 관련 있다... 아 정말이라고...)
🍚 @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를 사용하려면 반드시 기본 생성자를 넣어주어야하는 것을 꼭 기억하자!
아 ㅋㅋ 그럼 다들 엔티티 쓸 때 조심하자는 의미에서~~ (엔티티티티 프레자일 프레자일)
'백엔드 공부' 카테고리의 다른 글
[CRUD 연습] 406 Error in Spring (0) | 2023.01.12 |
---|---|
[CRUD 연습] RestTemplate으로 GET, POST (0) | 2023.01.11 |
[CRUD 연습] 컨트롤러 테스트 가이드 (0) | 2023.01.10 |
[CRUD 연습] Entity, Repository (0) | 2023.01.09 |
[CRUD 연습] 프로젝트 구상 (0) | 2023.01.08 |