강의 출처
진도표 2일차와 연결됩니다
우리는 GET API와 POST API를 만드는 방법을 배웠습니다. 👍 추가적인 API 들을 만들어 보며 API 개발에 익숙해져 봅시다!
문제 1
API 명세 작성
- HTTP Method > GET
- HTTP Path > /api/v1/calc
- 쿼리 파라미터 > int num1, int num2
- 반환 타입 > JSON
코드
- dto - CalculatorRequest
public class CalculatorRequest {
private int num1;
private int num2;
public CalculatorRequest(int num1, int num2) {
this.num1 = num1;
this.num2 = num2;
}
public int getNum1() {
return num1;
}
public int getNum2() {
return num2;
}
}
- dto - CalculatorResponse
public class CalculatorResponse {
private int add;
private int minus;
private int multiply;
public CalculatorResponse(CalculatorRequest num) {
this.add = num.getNum1() + num.getNum2();
this.minus = num.getNum1() - num.getNum2();
this.multiply = num.getNum1() * num.getNum2();
}
public int getAdd() {
return add;
}
public int getMinus() {
return minus;
}
public int getMultiply() {
return multiply;
}
}
- controller - ApiController
@RestController
@RequestMapping("/api/v1")
public class ApiController {
@GetMapping("/calc")
public CalculatorResponse calculator(CalculatorRequest request) {
return new CalculatorResponse(request);
}
}
포스트맨 테스트
문제 2
API 명세 작성
- HTTP Method > GET
- HTTP Path > /api/v1/day-of-the-week
- 쿼리 파라미터 > date=yyyy-dd-mm
- 반환 결과 > JSON "dayOfTheWeek" : "요일"
코드
- dto - DayOfTheWeekRequest
public class DayOfTheWeekRequest {
private LocalDate date;
public DayOfTheWeekRequest(LocalDate date) {
this.date = date;
}
public LocalDate getDate() {
return date;
}
}
- dto - DayOfTheWeekResponse
public class DayOfTheWeekResponse {
private String dayOfTheWeek;
public DayOfTheWeekResponse(String dayOfTheWeek) {
this.dayOfTheWeek = dayOfTheWeek;
}
public String getDayOfTheWeek() {
return dayOfTheWeek;
}
}
- controller - ApiController
@GetMapping("/day-of-the-week")
public DayOfTheWeekResponse getDayOfTheWeek(DayOfTheWeekRequest request) {
LocalDate date = request.getDate();
DayOfWeek dayOfWeek = date.getDayOfWeek();
return new DayOfTheWeekResponse(dayOfWeek.toString());
}
포스트맨 테스트
코드 변경
문제 예제 처럼 요일을 3글자 영문으로 받기 위해 코드를 조금 손 봤다.
- dto - DayOfTheWeekResponse
생성자 매개 변수를 String으로 받았었는데 그냥 DayOfWeek 타입으로 받고
생성자 안에서 getDisplayName()과 toUpperCase()를 사용하여 요일이 3글자 영문으로 필드에 저장되게 했다.
참고로 DayOfWeek.getDisplayName(TextStyle.SHORT, Locale.US)는 Sun으로 값을 반환하기 때문에
toUpperCase() 메서드로 모든 알파벳을 대문자로 변경해줬다.
public class DayOfTheWeekResponse {
private String dayOfTheWeek;
public DayOfTheWeekResponse(DayOfWeek dayOfTheWeek) {
this.dayOfTheWeek = dayOfTheWeek.getDisplayName(TextStyle.SHORT, Locale.US).toUpperCase();
}
public String getDayOfTheWeek() {
return dayOfTheWeek;
}
}
- controller - ApiController
@GetMapping("/day-of-the-week")
public DayOfTheWeekResponse getDayOfTheWeek(DayOfTheWeekRequest request) {
LocalDate date = request.getDate();
DayOfWeek dayOfWeek = date.getDayOfWeek();
return new DayOfTheWeekResponse(dayOfWeek);
}
포스트맨 테스트
문제 3
API 명세 작성
- HTTP Method > GET
- HTTP Path > /api/v1/sum
- HTTP Body > JSON 숫자 배열
- 반환 결과 > 숫자 - 숫자들의 총 합
코드
- dto - SumRequest
public class SumRequest {
private List<Integer> numbers;
public SumRequest(List<Integer> numbers){
this.numbers = numbers;
}
public List<Integer> getNumbers() {
return numbers;
}
}
- controller - ApiController
@PostMapping("/sum")
public int sum(@RequestBody SumRequest request) {
List<Integer> numbers = request.getNumbers();
int sum = 0;
for(int i = 0; i < numbers.size(); i++) {
sum += numbers.get(i);
}
return sum;
}
포스트맨 테스트
에러 발생
500... 에러가 발생했다. 서버 콘솔창 에러는 아래와 같다.
Cannot construct instance of `~.request.SumRequest` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 2, column: 5]
에러 내용에 'default constructor' 단어가 있길래 SumRequest에 기본 생성자를 추가해줬더니 오류가 사라졌다.
public class SumRequest {
private List<Integer> numbers;
public SumRequest() {} // 추가
public SumRequest(List<Integer> numbers){
this.numbers = numbers;
}
public List<Integer> getNumbers() {
return numbers;
}
}
코드 변경 후 포스트맨 테스트를 다시 해보니 정상적으로 결과가 반환되었다.
여태 강의 들으면서 기본 생성자를 만든 적이 없었는데 갑자기 기본 생성자가 없다고 에러를 발생시키다니요..!
도대체 왜 갑자기 난 걸까 하다가 기존에 강의를 들으면서 작성했던 코드와 현재 코드를 비교했다.
그나마 제일 유사한 코드인 UserController를 먼저 확인했다. (@RequestBody를 사용해서)
@PostMapping("/user")
public void saveUser(@RequestBody UserCreateRequest request) {
users.add(new User(request.getName(), request.getAge()));
}
HTTP Method도 POST로 동일, @RequestBody 어노테이션도 동일하게 사용했는데 왜 저 코드에선 에러가 안났을까?
정답은 UserCreateRequest 클래스에 존재한다.
public class UserCreateRequest {
private String name;
private Integer age;
public String getName() {
return name;
}
public Integer getAge() {
return age;
}
}
바로 아무런 생성자도 선언되어 있지 않아서 기본 생성자가 생략되어 있기 때문에 발생하지 않은 것으로 보인다..!
그래서 테스트 겸 UserCreateRequest에 생성자를 하나 추가해서 포스트맨 테스트를 돌려보았다.
public class UserCreateRequest {
private String name;
private Integer age;
public UserCreateRequest(String name, Integer age) { // 추가
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public Integer getAge() {
return age;
}
}
근데 놀랍게도 아무런 에러가 발생하지 않았다..?!
그럼 매개변수가 있는 생성자가 있을 때 기본 생성자가 없는 경우에 나는 에러가 아니라는 결론이 도달했다.
이것 저것 구글링 해본 결과 @RequestBody를 사용할 경우 기본 생성자가 필요하고, @Setter가 필요 없다는 글을 발견한다.
초보인 나는 나와의 케이스가 다른 거 같아 디버깅 위치 참고만 했다. 그런데 POST의 /user와 POST의 /sum의 차이를 찾긴 쉽지 않았다.
그나마 찾은 차이는 AbstractMessageConverterMethodArgumentResolver.class의 아래 코드이다.
readWithMessageConverters()의 아래 코드에서 /user는 해당 코드를 지나가면 body에 정상적인 값을 가져오고, /sum의 경우엔 exception이 터져버린다.
/sum의 경우엔 ObjectReader.java의 _bindAndClose() 메서드에서 result 값을 정상적으로 반환하지 못한다. 즉, ObjectMapper에서 역직렬화를 제대로 못해주는 것 같았다.
몇 시간 동안 계속 디버그를 돌리고돌리고돌리고돌리고돌리고돌리고돌리고돌리고돌리고돌리고 하다가 답이 안 나올 것 같아 그래도 구글링하면서 찾아 본 단어들을 조합해서 다시 구글링을 도전했다.
직렬화와 역직렬화
- Java Object → JSON 파싱 : 직렬화(Serialize)
- JSON → Java Object : 역직렬화(Deserialize)
- @RequestBody가 역직렬화를 자동으로 처리해준다.
HttpMessageConverter
- 파라미터에 @RequestBody가 있으면 HttpMessageConverter를 통해 HTTP body를 매핑해줌
- HttpMessageConverter는 Http request message를 역직렬화 해줌
- Object 멤버 변수가 오직 하나일 경우 @JsonCreator로 Object를 자동으로 설정해줘서 Jackson이 제 역할을 하지 못함
- 나는 이 경우 때문에 발생한 오류였다...! 그래서 기본 생성자를 추가하라는 오류 메세지가 뜬 것!!
생성자를 인수를 하나 더 추가해보자
- dto - SumRequest
public class SumRequest {
private List<Integer> numbers;
private String test;
// public SumRequest(List<Integer> numbers) {
// this.numbers = numbers;
// }
public SumRequest(List<Integer> numbers, String test) {
this.numbers = numbers;
this.test = test;
}
public List<Integer> getNumbers() {
return numbers;
}
public String getTest() {
return test;
}
}
포스트맨 테스트
와 진짜 열받게 잘 된다! 필드를 하나 더 추가해주니 놀랍게도 아주 잘 동작된다.
해결 방법 (@JsonCreator)
@JsonCreator를 생성자 메서드 위에 붙여주면 아주 잘 동작된다.
public class SumRequest {
private List<Integer> numbers;
@JsonCreator
public SumRequest(List<Integer> numbers) {
this.numbers = numbers;
}
public List<Integer> getNumbers() {
return numbers;
}
}
어정쩡한 결론
- POST 메서드 /user의 경우엔 기본 생성자가 없지만 인자를 받는 생성자(인자 2개 이상)이 있어 에러가 안났던 것임
- POST 메서드 /sum의 경우엔 기본 생성자가 없고 인자를 받는 생성자가 1개의 인자를 받는다면 에러가 발생하므로 @JsonCreator 어노테이션 추가!
다음엔 디버그를 좀 더 진득하니 해봐야겠다..! 그래도 에러 원인은 혼자 못 발견하겠지만...! 시간 때문에 너무 대충한 느낌이라 좀 아쉽다.