4일차 PUT API, POST API 만들기 과제

nakyeonko3·2024년 2월 22일
0

❗목표
우리는 GET API와 POST API를 만드는 방법을 배웠습니다. 👍 추가적인 API 들을 만들어 보며 API 개발에 익숙해져 봅시다!
키워드 : PUT, DELETE, POST,GROUP BY

과제#4: PUT API, POST API 만들기 과제


문제 1


그리고 FruitController컨트롤러 클래스와 requset body에 데이터를 넣을 FruitCreateRequest DTO를 만들어서 해결했다.

풀이 과정

  1. fruit 테이블 생성 쿼리문 작성, application.yaml 수정
    먼저 sql 쿼리문을 작성해서 fruit 테이블을 먼저 만들어줬다.
    그리고 해당 fruit 테이블은 library 데이터베이스가 아닌 fruitshop이라는 데이터베이스 안에 저장하였다.
create database fruitShop;
use fruitShop;

create table fruit
(
    id              bigint auto_increment,
    name            varchar(25),
    warehousingDate date,
    price           bigint,
    isSold          boolean DEFAULT false,
    primary key (id)
);

application.yaml 파일의 datasource의 url 주소를 아래와 같이 fruitShop으로 변경 해줘야지 jdbc가 올바른 주소로 데이터 베이스와 연결을 할 수 있다.

  1. FruitController.java 컨트롤러 클래스와 FruitCreateRequest.java DTO 작성
@RestController
@RequestMapping("/api/v1/fruit")
public class FruitController {
    private final JdbcTemplate jdbcTemplate;

    public FruitController(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @PostMapping
    public void createFruit(@RequestBody FruitCreateRequest request) {
        String sql = "INSERT INTO fruit(name, warehousingDate, price) VALUES (?,?,?)";
        jdbcTemplate.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice());
    }
}

FruitCreateRequest를 작성할 때
생성자에 @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)LocalDate localDate를 사용해서 역직렬화 할 때 오류가 걸리지 않도록 했다. Working with Date Parameters in Spring | Baeldung

public class FruitCreateRequest {
    private final String name;
    private final LocalDate warehousingDate;
    private final long price;

    public FruitCreateRequest(String name, @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)LocalDate localDate, long price) {
        this.name = name;
        this.warehousingDate = localDate;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public LocalDate getWarehousingDate() {
        return warehousingDate;
    }

    public long getPrice() {
        return price;
    }
}

실행결과

전체 소스 코드는 소스 코드 링크에 첨부 하였다.

문제2


풀이과정

  1. controller 에 updateFruit 메서드 추가

그리고 해당 id가 fruit 테이블 안에 없을 때를 대비해서
해당 id가 검색하고 없다면 IllegalArgumentException 예외를 발생시키도록 코드를 작성했다.

    @PutMapping
    public void updateFruit(@RequestBody FruitUpdateRequest request) {
        System.out.println(request.getId());

       
        String sqlRead = "SELECT * FROM fruit WHERE id = ?";
        boolean isFruitNotExist = jdbcTemplate.query(sqlRead, (rs, rowNum) -> 0, request.getId())
                .isEmpty();
        if (isFruitNotExist) {
            throw new IllegalArgumentException();
        }

        String sqlUpdate = "UPDATE fruit SET isSold = True WHERE id = ?";
        jdbcTemplate.update(sqlUpdate, request.getId());
    }
  1. RequestBody 값을 담을 FruitUpdateRequest DTO 작성

생성자에 @JsonCreator 를 사용해서 해당 생성자를 이용해서 역직렬화가 일어나게 했다.

Spring에서는 @PutMapping나 @PostMapping에서는 매개변수로 받은 DTO를 이용해서 역직렬화를 한다면,

DTO에 Default constructor 생성자가 없으면 오류가 걸린다.

public class FruitUpdateRequest {

    final private Long id;


    public FruitUpdateRequest(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }
}
Cannot construct instance of `com.group.fruitshopapp.dto.FruitUpdateRequest` (although at least one Creator exists): cannot deserialize from Object value

에러 메시지 번역 : com.group.fruitshopapp.dto.FruitUpdateRequest`의 인스턴스를 구성할 수 없습니다(생성자가 하나 이상 존재하지만): 객체 값에서 역직렬화할 수 없습니다.

이 문제는 간단하게 DTO 에 기본생성자를 만들어주거나

public class FruitUpdateRequest {

    private Long id;

    public FruitUpdateRequest(){

    }
    public FruitUpdateRequest(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }
}

아니면 DTO의 생성자에 @JsonCreator를 생성자에 붙여주면 해당 생성자를 이용해서 역직렬화가 일어나서 오류가 걸리지 않는다.

public class FruitUpdateRequest {

    final private Long id;

    @JsonCreator
    public FruitUpdateRequest(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }
}

실행결과

isSold가 true로 바뀌었다.

문제3


풀이과정

jdbc.template.query, group by, map 키워드에 대해 이해하고 있다면 쉽게 풀 수 있는 문제였다.

gpt를 사용하면 1분 안에 풀 수 있지만,
gpt처럼 위의 키워드를 알지 못하는 나는 2시간 이상의 시간이 소요되었다.

  1. 컨트롤러에 getStatOfFruit 메서드 추가
    @GetMapping("/stat")
    public FruitGetStatResponse getStatOfFruit(@RequestParam String name) {
        String sql = "SELECT isSold, SUM(price) as SUM from fruit WHERE name = ? GROUP BY isSold";

        Map<Boolean, Long> resultmap = new HashMap<>();
        jdbcTemplate.query(sql, (rs, rowNum) -> {
            boolean isSold = rs.getBoolean("isSold");
            long price = rs.getLong("SUM");
            resultmap.put(isSold, price);
            return null;
        }, name);
        return new FruitGetStatResponse(resultmap.get(true), resultmap.get(false));
    }

jdbcTemplate.query 코드 부분이 상당히 복잡한데 설명하자면 다음과 같다.
sql 를 통해 쿼리결과물 여러 줄 코드에 해당 람다식을 각각 실행한다.

 boolean isSold = rs.getBoolean("isSold");
long price = rs.getLong("SUM");
resultmap.put(isSold, price);

price 값과 isSold 값을 추출해서 resultmap에 저장한다.
isSoldtrue또는 false다.

실제로 실행 해보면 resultmap 안에 아래처럼 이런식으로 저장이 된다.
true팔린 과일의 합계, false에는 팔리지 않은 과일의 합계가 들어가 있는 상태이다.

{
	true : 6000,
    false : 4000
}

이제 resultmap 값을 FruitGetStatResponse라는 DTO에 전달한다.

return new  FruitGetStatResponse(resultmap.get(true), resultmap.get(false));
  1. FruitGetStatResponse DTO 생성
public class FruitGetStatResponse {
    private final Long salesAmount;
    private final Long notSalseAmount;

    public FruitGetStatResponse(Long salesAmount, Long notSalseAmount) {
        this.salesAmount = salesAmount;
        this.notSalseAmount = notSalseAmount;
    }

    public Long getNotSalseAmount() {
        return notSalseAmount;
    }

    public Long getSalesAmount() {
        return salesAmount;
    }
}

실행결과

각종 오류와 배운점들


isEmpty() 메서드

isEmpty()는 해당 list가 비어 있다면 false를 리턴한다.

boolean isFruitNotExist = jdbcTemplate.query(sql,(rs, rowNum) -> 0,request.getId()).isEmpty();

에러 'library.fruit' doesn't exist

원인은 fruit테이블의 위치가 library가 아닌 fruitShop라는 데이터베이스에 위치해 있어서 이런 에러가 발생했다.

java.sql.SQLSyntaxErrorException: Table 'library.fruit' doesn't exist
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:120) ~[mysql-connector-j-8.0.31.jar:8.0.31]
create database fruitShop;
use fruitShop;

예외처리

의도된 실행 흐름 제어를 할 때 쓰인다.
데이터 누락이나 유효성 검증을 할 때 예외 처리가 유용하게 쓰인다.
모든 것을 예외 처리 할 수는 없으므로 클라이언트에서 제어 가능하고 오류로 부터 복구 가능한 것들만 예외처리를 진행한다.

Unchecked Exceptions — The Controversy (The Java™ Tutorials > Essential Java Classes > Exceptions)

같은 이름 클래스 오류

같은 이름을 가지는 클래스가 있어서 실행이 안되는 오류가 있었다.
중복되는 클래스 파일을 제거해서 해결함

'userController' for bean class [com.group.libraryapp.controller.libraryapp.controller.UserController] conflicts with existing, non-compatible bean definition of same name and class 

테이블을 언제 분리 해야될까?

fruit테이블의 isSold 속성을 다른 테이블로 분리시켜야 될까? 정규화에 대해 더 공부해보자

@GetMapping LocalData 로 직접 받기

스프링 3.X에서는
http request의 날짜 데이터 '2022-11-20'같은 String 데이터를 바로 LocalDate타입으로 변환할 수 있다.

스프링 2.7에서는 String 타입 데이터를 LocalDate 변환하는 과정에서 에러가 걸린다.
Working with Date Parameters in Spring | Baeldung

직접 converter를 등록하여 데이터를 원하는 형태로 컨버팅해서 받는 방법도 존재한다.
Spring Type Conversion :: Spring Framework

    @GetMapping("/day-of-the-week")
    public CalculatorDayOfWeek getDayofweek(@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
        return new CalculatorDayOfWeek(date);
    }

@GetMapping을 한 메서드

: @GetMapping을 한 메서드 들어가는 매개변수 dto에는 기본생성자(default constructor)가 아닌 전체 필드 초기화 생성자 하나만 만들어야함.

리퀘스트 데이터를 객체로 역직렬화 과정에서 생성자가 1개면 그 생성자를 호출, 2개면(기본생성자가 있으면) 그 기본생성자를 호출 한다.

@PostPapping는 왜 기본 생성자가 있어야 오류가 안 뜰까?

: @ResponeBody 에 붙어 있는 dto는 기본생성자가 있어야됨. 객체로 역직렬화과정에서 무조건 기본생성자가 필요함. 없으면 오류 가 뜬다.
아니면 아래와 같이 DTO의 생성자를 @JsonCreator 어노테이션을 넣어주면된다.

public class FruitUpdateRequest {

    final private Long id;

    @JsonCreator
    public FruitUpdateRequest(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }
}

참고


자바-스프링부트-서버개발-올인원-인프런

How to Throw Exceptions (The Java™ Tutorials > Essential Java Classes > Exceptions)

profile
웹개발자를 지망하고 있는 대학생, 진순파

0개의 댓글