❗목표
우리는 GET API와 POST API를 만드는 방법을 배웠습니다. 👍 추가적인 API 들을 만들어 보며 API 개발에 익숙해져 봅시다!
키워드 :PUT
,DELETE
,POST
,GROUP BY
프로젝트 폴더 구조
그리고 FruitController
컨트롤러 클래스와 requset body에 데이터를 넣을 FruitCreateRequest
DTO를 만들어서 해결했다.
fruit
테이블 생성 쿼리문 작성, application.yaml
수정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
가 올바른 주소로 데이터 베이스와 연결을 할 수 있다.
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;
}
}
전체 소스 코드는 소스 코드 링크에 첨부 하였다.
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());
}
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로 바뀌었다.
jdbc.template.query
, group by
, map
키워드에 대해 이해하고 있다면 쉽게 풀 수 있는 문제였다.
gpt를 사용하면 1분 안에 풀 수 있지만,
gpt처럼 위의 키워드를 알지 못하는 나는 2시간 이상의 시간이 소요되었다.
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
에 저장한다.
isSold
는 true
또는 false
다.
실제로 실행 해보면 resultmap 안에 아래처럼 이런식으로 저장이 된다.
true
는 팔린 과일
의 합계, false
에는 팔리지 않은 과일
의 합계가 들어가 있는 상태이다.
{
true : 6000,
false : 4000
}
이제 resultmap
값을 FruitGetStatResponse
라는 DTO에 전달한다.
return new FruitGetStatResponse(resultmap.get(true), resultmap.get(false));
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()는 해당 list가 비어 있다면 false를 리턴한다.
boolean isFruitNotExist = jdbcTemplate.query(sql,(rs, rowNum) -> 0,request.getId()).isEmpty();
원인은 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
속성을 다른 테이블로 분리시켜야 될까? 정규화에 대해 더 공부해보자
스프링 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을 한 메서드 들어가는 매개변수 dto에는 기본생성자(default constructor)가 아닌 전체 필드 초기화 생성자 하나만 만들어야함.
리퀘스트 데이터를 객체로 역직렬화 과정에서 생성자가 1개면 그 생성자를 호출, 2개면(기본생성자가 있으면) 그 기본생성자를 호출 한다.
: @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)