인프런 워밍업 클럽0기 BE 2일차 - GET, POST API 만들어보기

덧셈, 뺄셈, 곱하기 계산기 만들기

method: GET

path: /api/v1/calc

queryParams: num1(int), num2(int)

 

Req 클래스

public class CalculatorReq {
    private int num1;
    private int num2;

    public CalculatorReq(int num1, int num2) {
        this.num1 = num1;
        this.num2 = num2;
    }

    public int getNum1() {
        return num1;
    }

    public int getNum2() {
        return num2;
    }
}

 

Res 클래스

public class CalculateResponse {
    private int add;
    private int minus;
    private int multiply;

    public CalculateResponse(int add, int minus, int multiply) {
        this.add = add;
        this.minus = minus;
        this.multiply = multiply;
    }

    public int getAdd() {
        return add;
    }

    public int getMultiply() {
        return multiply;
    }

    public int getMinus() {
        return minus;
    }
}

 

Controller 소스코드

    @GetMapping("/api/v1/calc")
    public CalculateResponse calculateTwoNumbers(CalculatorReq req) {
        int add = req.getNum1() + req.getNum2();
        int minus = req.getNum1() - req.getNum2();
        int multiply = req.getNum1() * req.getNum2();
        return new CalculateResponse(add, minus, multiply);
    }

dto 에서 각 값을 저장하게 만드는 것도 가능하겠지만, dto 에 로직이 들어가게 되어 유지보수성이 좋지 않을거라 판단하였습니다.

주요 로직일 경우 서비스 계층을 따로 두어 처리하는 것이 맞겠지만, 현재의 요구사항은 컨트롤러 상에서 처리하는 것도 복잡도를 증가시키지 않을거라 생각하였습니다.

 

실행결과
image

 

 

무슨요일인지 알려주는 API 만들기

 

Method: GET

Path: /api/v1/day-of-the-week

Param: Date(String)

 

Res 클래스

public class DayOfWeekResponse {
    String dayOfTheWeek;

    public DayOfWeekResponse(String dayOfTheWeek) {
        this.dayOfTheWeek = dayOfTheWeek;
    }

    public String getDayOfTheWeek() {
        return dayOfTheWeek;
    }
}

 

Controller

@GetMapping("/api/v1/day-of-the-week")
public DayOfWeekResponse dayOfTheWeek(@RequestParam String date) {
    LocalDate localDate = LocalDate.parse(date);
    DayOfWeek dayOfWeek = localDate.getDayOfWeek();
    String displayName = dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.ENGLISH).toUpperCase();

    return new DayOfWeekResponse(displayName);
}

 

이름의 형식을 바꿔주는 책임을 누가 가져야하는지 고민을 많이 했지만, 표현계층인 컨트롤러 계층에서 그 역할을 가져가는 것이 맞다 생각하여 위와 같이 작성하였습니다.

 

실행결과

image

예시의 날짜를 넣었을 때, SUN 이 출력되어 뭔가 잘못되었나 싶었지만, 달력을 열어보니 해당일은 일요일이 맞았습니다.

 

숫자의 합 반환하는 API 만들기

 

Method: POST

Path: /api/v1/sum-of-the-numbers

Param: numbers(array)

 

Req 클래스

package com.group.libraryapp.dto.calculator.request;

import java.util.ArrayList;
import java.util.List;

public class SumOfTheNumbersReq {
    private List<Integer> numbers = new ArrayList<>();

    public SumOfTheNumbersReq() {
    }

    public SumOfTheNumbersReq(List<Integer> numbers) {
        this.numbers = numbers;
    }

    public List<Integer> getNumbers() {
        return numbers;
    }
}

기본 생성자가 없을경우 에러가 발생한다. Jackson 에서 발생한 문제로

역직렬화 관련 에러 메시지가 발생하였다. 그런데..! 나머지 얘기는 고찰에서 이어서 하겠습니다.

 

Controller 소스코드

    @PostMapping("/api/v1/sum-of-the-numbers")
    public int sumOfTheNumbers(@RequestBody SumOfTheNumbersReq req) {

        return req.getNumbers().stream()
                .reduce(Integer::sum)
                .get();
    }

 

실행결과

 

image

 

 

음... 근데 이상하다.

위에서 사용한 CaculatorReq 클래스를 보게 되면 역시 기본 생성자를 가지고 있지 않다.

(기본 생성자를 꼭 필요로 한다면 보통 리플렉션과 관련된 개념인데, 요청값에 대한 mapping 이 reflection 을 사용하지 않는다고 알고 있어서 의아했다.)

SumOfTheReq 클래스와 CaculatorReq 의 차이점을 생각해보기로 했다.

(손이 익숙하게 기본생성자를 만들거나, @NoArgs... 를 만들지 않았다 물론 lombok 이 없어서일수도 있지만)

자바에서의 타입은 기본형과 참조형으로 나뉜다.

  1. 기본형으로만 이루어지면 괜찮은걸까?

  2. Field의 갯수의 문제일까?

실험 1.

1개의 기본형만을 가지는 Dto

 

package com.group.libraryapp.dto.calculator.request;

public class Temp {
    private int number;

    public Temp(int number) {
        this.number = number;
    }

    public int getNumber() {
        return number;
    }
}

 

    @PostMapping("/api/v1/temp")
    public int temp(@RequestBody Temp temp) {
        return temp.getNumber();
    }

 

image

실험 2.

2개의 참조형을 갖는 Dto

package com.group.libraryapp.dto.calculator.request;

import java.util.ArrayList;
import java.util.List;

public class SumOfTheNumbersReq {
    private List<Integer> numbers = new ArrayList<>();
    private List<Integer> tests = new ArrayList<>();

//    public SumOfTheNumbersReq() {
//    }

    public SumOfTheNumbersReq(List<Integer> numbers, List<Integer>tests) {
        this.numbers = numbers;
        this.tests = tests;
    }

    public List<Integer> getNumbers() {
        return numbers;
    }

    public List<Integer> getTests() {
        return tests;
    }
}

 

    @PostMapping("/api/v1/sum-of-the-numbers")
    public int sumOfTheNumbers(@RequestBody SumOfTheNumbersReq req) {

        return req.getNumbers().stream()
                .reduce(Integer::sum)
                .get();
    }

 

image

음... Jackson의 문제가 맞았다는걸 알 수 있었다.

 

필드값이 하나인 상황에서 기본생성자가 없을 경우 Jackson의 에러

를 키워드로 검색해보니

https://github.com/FasterXML/jackson-databind/issues/3085

해당 글을 발견할 수 있었다 답변을 발췌하면

This is a well-known issue due to ambiguity of the 1-arg constructor case: single-argument could match either:

  1. Delegating case like "string-value" OR

  2. Properties-based case like {"name" : "value"}

and if user does not specify mode with

@JsonCreator(mode = JsonCreator.Mode.PROPERTIES) // or Mode.DELEGATING

Jackson will try to guess which one to use. In your case it likely guesses that DELEGATING mode is to be used and expects a String, not Object value.

라고 한다. 어떤 방식으로 역직렬화를 할지 몰라서 벌어지는 일이라고 하니 알아두면 좋을 것 같다.
@JsonCreator 로 해결 가능하다는 답변도 함께 제시주었다.(감사합니다)

@JsonCreator 를 통해 확인해보도록 하자

package com.group.libraryapp.dto.calculator.request;

import com.fasterxml.jackson.annotation.JsonCreator;

import java.util.ArrayList;
import java.util.List;

public class SumOfTheNumbersReq {
    private List<Integer> numbers = new ArrayList<>();

    @JsonCreator
    public SumOfTheNumbersReq(List<Integer> numbers) {
        this.numbers = numbers;
    }

    public List<Integer> getNumbers() {
        return numbers;
    }
    
}

 

image

잘 동작하는 모습을 볼 수 있었다.

 

개인적으로는 특정 클래스를 만들 때에 필드가 하나인 경우가 드물기 때문에 자주보기는 힘든 문제라고 생각하지만, 어떤 경우가 벌어질 지 모르니 알아두자!

댓글을 작성해보세요.