진도표 4일차와 연결됩니다.
우리는 GET API와 POST API를 만드는 방법을 배웠습니다. 👍 추가적인 API들을 만들어 보며 API 개발에 익숙해져 봅시다!
💡 4일차 문제 1, 문제 2, 문제 3은 이어지는 문제입니다! 😊
우리는 작은 과일 가게를 운영하고 있습니다. 과일 가게에 입고된 "과일 정보"를 저장하는 API를 만들어 봅시다.
스펙은 다음과 같습니다.
HTTP method : POST
HTTP path: /api/v1/fruit
HTTP 요청 Body:
{
"name": String,
"warehousingDate": LocalDate,
"price": long
}
{
"name": "사과",
"warehousingDate": "2024-02-01",
"price": 5000
}
📌 한 걸음 더!
자바에서 정수를 다루는 가장 대표적인 두 가지 방법은int
와long
입니다.
이 두 가지 방법 중 위 API 에서long
을 사용한 이유는 무엇일까요?
Table
create table fruit
(
id bigint auto_increment,
name varchar(20),
price int,
stocked_date date,
sold_out tinyint, //tinyint => true, false로 사용
primary key (id)
);
Controller
@PostMapping("/api/v1/fruit")
public ResponseEntity<Void> saveFruitInfo (@RequestBody FruitInfoRequest fruitInfoRequest ){
apiService.saveFruitInfo(fruitInfoRequest);
return ResponseEntity.ok().build();
}
DTO(Request)
@Getter
public class FruitInfoRequest {
private String name;
private LocalDate warehousingDate;
private long price;
public FruitInfoEntity toEntity(){ //builder를 사용해 Entity로 변환
return FruitInfoEntity.builder()
.name(name)
.stocked_date(warehousingDate)
.price(price)
.build();
}
}
Entity
@Getter
@NoArgsConstructor
public class FruitInfoEntity {
private long id; //Id는 database에 생성시 auto_increment로 자동으로 증가
private String name;
private long price;
private LocalDate stocked_date;
private boolean sold_out;
@Builder
public FruitInfoEntity(String name, long price, LocalDate stocked_date) {
this.name = name;
this.price = price;
this.stocked_date = stocked_date;
this.sold_out = false; //여기서 sold_out=false(안팔림) 처리를 해줬습니다.
}
}
Service
public void saveFruitInfo(FruitInfoRequest request) {
fruitJdbcRepository.saveFruitInfo(request.toEntity()); //DTO를 Entity로 변환하고 Repository 전달
}
Repository
public void saveFruitInfo(FruitInfoEntity fruitInfoEntity){
String sql = "INSERT INTO inflearn.fruit (name, price, stocked_date, sold_out) VALUES (?, ?, ?, ?)";
jdbcTemplate.update(sql, fruitInfoEntity.getName(), fruitInfoEntity.getPrice(), fruitInfoEntity.getStocked_date(), fruitInfoEntity.isSold_out());
}
View
<h3>MyAPI 1</h3>
<div class="col-sm-8" style="background-color: #f5f4f1">
{<br>
<form id="frm">
<table class="table-borderless">
<tr>
<td> <span class="text-success">"name"</span>: </td>
<td> <input type="text" class="form-control" id="fruitName"></td>
</tr>
<tr>
<td> <span class="text-success">"warehousingDate"</span>: </td>
<td> <input type="date" class="form-control" id="warehousingDate"></td>
</tr>
<tr>
<td> <span class="text-success">"price"</span>: </td>
<td> <input type="number" class="form-control" id="fruitPrice"></td>
</tr>
</table>
</form>
}
<br><br>
<button class="btn btn-primary" th:onclick="saveFruitInfoBtn()">POST</button>
<br><br> //POST 버튼을 누르면 saveFruitInfoBtn() 함수를 사용하여 서버로 값을 보낼 예정입니다.
</div>
<br>
<div class="col-sm-8" style="background-color: #f5f4f1">
<span id="saveFruitInfo">
<h3 class="badge badge-light text-danger">실행 결과 : </h3><br>
</span>
</div>
</div>
API.js
function saveFruitInfoBtn() {
const fruitName = document.getElementById("fruitName").value;
const warehousingDate = document.getElementById("warehousingDate").value;
const fruitPrice = document.getElementById("fruitPrice").value;
//getElementById로 입력값을 받아옵니다.
if (!fruitName || !warehousingDate || !fruitPrice) { //셋중 하나라도 값이 없으면 true
alert("모든 값을 입력해주세요.")
} else {
fetch('api/v1/fruit', {
method: 'post',
headers: {"Content-Type": "application/json"},
body: JSON.stringify({"name": fruitName, "warehousingDate": warehousingDate, "price": fruitPrice}) //JSON화 하여 보냅니다.
})
.then(response=>{ // Controller로 ok(HttpStatus=200) 반환
if(!response.ok)
throw new Error('서버로부터 응답이 없습니다(saveFruit)')
//만약 ok(status=200)가 아니면 인텔리제이 콘솔창에서 이 오류창이 출력됩니다.
return response.status; //Body가 없이 status만 와서 .JSON()으로 반환하면 오류가 납니다.
})
.then(status => {
$("#saveFruitInfo").html(
`
<h3 class="badge badge-light text-danger">실행 결과 : </h3>
<span class="badge badge-ligth text-danger">${status}</span><br>
` //위에서 return한 status 그대로 view로 전달했습니다.
)
})
.catch(error => {
alert("입력값을 확인해주세요!")
})
}
}
결과(PostMan)
결과(Web)
📌 한 걸음 더! - 나의 생각
Java
에서int
는 32bit를 사용하고 long은64bit
를 사용한다.
개인마다unique-key
를 가져야하는@Id
나 , 수학적 연산에 사용되는 값의 경우
저장공간이 더 큰long
이int
에 비해 유리해서int
보다long
을 쓴다고 생각했다.
과일이 팔리게 되면, 우리 시스템에 팔린 과일 정보를 기록해야 합니다. 스펙은 다음과 같습니다.
put
/api/v1/fruit
{
"id": long
}
"id": 3
table
create table fruit
(
id bigint auto_increment,
name varchar(20),
price int,
stocked_date date,
sold_out tinyint, //tinyint => true, false로 사용
primary key (id)
);
Controller
@PutMapping("/api/v1/fruit")
public void soldFruitInfo(@RequestBody SoldFruitInfoRequest soldFruitInfoRequest){
apiService.soldFruitInfo(soldFruitInfoRequest);
}
📌 문득
ResponseEntity
로HttpStatus
를 지정하지 않아도 자동적으로 반환되는200 Ok
도
문제 1번 처럼 가져올 수 있는지가 궁금해져서ResponseEntity
를 사용하지 않았습니다.
DTO(Request)
@Getter
public class SoldFruitInfoRequest {
private long id;
}
Service
public void soldFruitInfo(SoldFruitInfoRequest request) {
fruitJdbcRepository.soldFruitInfo(request);
}
Repository
public void soldFruitInfo(SoldFruitInfoRequest soldFruitInfoRequest){
String idCheck = "SELECT sold_out FROM inflearn.fruit WHERE id=?";
List<Boolean> isNotExist = jdbcTemplate.query(idCheck, (rs, rowNum)->rs.getBoolean("sold_out"), soldFruitInfoRequest.getId());
if(isNotExist.isEmpty() || isNotExist.get(0)) throw new IllegalArgumentException("존재하지 않거나 이미 팔린 상품입니다.");
//없는 ID값 or 팔린 물건에 접근하려하면 IllegalArgumentException
String sql = "UPDATE inflearn.fruit SET sold_out=1 WHERE id=?";
jdbcTemplate.update(sql, soldFruitInfoRequest.getId());
}
📌 원래
Repository
는
1. 해당 ID가 존재하는지 확인하는 쿼리
2. 해당 ID가 팔렸는지 확인하는 쿼리
3. 해당 ID를 sold_out 처리하는 쿼리 형식 이였는데,
생각해보니boolean
을List
로 받으면.isEmpty()
도 사용할 수 있고,.get(0)
으로
팔렸는지 안팔렸는지 체크도 할 수 있지않나 싶어서 둘이 합쳤습니다.
View
<h3>MyAPI 2</h3>
<div class="col-sm-8" style="background-color: #f5f4f1"><br>
{<br>
<table class="table table-borderless">
<tr>
<td>
<span class="text-success">"id"</span>:
</td>
<td><input type="number" id="fruitId" class="form-control" style="width: 20%"></td>
</tr>
</table>
}<br><br>
<button class="btn btn-primary" th:onclick="soldFruitInfoBtn()">POST</button>
<br><br> //이번에도 동일하게 버튼을 누르면 함수가 실행됩니다.
</div>
<br>
<div class="col-sm-8" style="background-color: #f5f4f1">
<span id="soldFruitInfo">
<h3 class="badge badge-light text-danger">실행 결과 : </h3><br>
</span>
</div>
</div>
API.js
function soldFruitInfoBtn(){
const fruitId = document.getElementById("fruitId").value;
fetch("api/v1/fruit" , {
method: "put", // method = put
headers: {"Content-Type": "application/json"}, //헤더에 타입명시
body: JSON.stringify({"id": fruitId}) //{"id": fruitID} JSON형태
})
.then(response => {
if(!response.ok) throw new Error("서버로 부터 응답이 없습니다. (putError)")
return response.status; //이번에도 문제 1번과 동일하게 status만 반환합니다.
})
.then(status => {
$("#soldFruitInfo").html(
`
<h3 class="badge badge-light text-danger">실행 결과 : </h3>
<span class="badge badge-light text-danger">${status}</span>
` //여기에 출력해볼 예정
)
})
.catch(error=>{
alert("존재하지않거나 이미 팔린상품입니다.")
})
}
결과(Post Man)
결과(Web)
💡 따로
ResponseEntity
를 지정해주지 않아도, 기본적으로 반환되는 HTTP status도 받아올 수 있다는 것을
알게 되었습니다.
우리는 특정 과일을 기준으로 팔린 금액, 필리지 않은 금액을 조회하고 싶습니다.
예를 들어
1. (1,사과,3000원,판매 O)
2. (2,사과,4000원,판매 X)
3. (3,사과,3000원,판매 O)
와 같은 세 데이터가 있다면 우리의 API는 판매된 금액 : 6000원, 판매되지 않은 금액: 4000원 이라고 응답해야 합니다.
구체적인 스펙은 다음과 같습니다.
GET
/api/v1/fruit/stat
GET /api/v1/fruit/stat?name=사과
{
"salesAmount": long,
"notSalesAmount": long
}
{
"salesAmount": 6000,
"notSalesAmount": 4000
}
📌 한 걸음 더!
(문제 3번을 모두 푸셨다면) SQL의 sum, group by 키워드를 검색해 적용해보세요! 😊
table
create table fruit
(
id bigint auto_increment,
name varchar(20),
price int,
stocked_date date,
sold_out tinyint, //tinyint => true, false로 사용
primary key (id)
);
Controller
@GetMapping("/api/v1/fruit/stat")
public ResponseEntity<SalesAmountResponse> salesAmount(@RequestParam String name){
SalesAmountResponse salesAmountRequest = apiService.salesAmount(name);
return ResponseEntity.ok()
.body(salesAmountRequest);
}
📌 어차피 전달해야 할
인자(parameter)
가name
하나뿐이고, 추가적으로 처리할 로직도 없다고 생각해
RequestDTO
는 만들지 않았습니다.
Service
public SalesAmountResponse salesAmount(String name) {
return fruitJdbcRepository.salesAmount(name);
}
Repository
public SalesAmountResponse salesAmount(String name){
String salesAmountsql = "SELECT sum(price) FROM fruit WHERE sold_out = 1 AND name=? GROUP BY name";
String notSalesAmountsql = "SELECT sum(price) FROM fruit WHERE sold_out = 0 AND name=? GROUP BY name";
List<Long> salesAmount = jdbcTemplate.query(salesAmountsql, (rs, rowNum)-> rs.getLong("sum(price)"), name);
List<Long> notsalesAmount = jdbcTemplate.query(notSalesAmountsql, (rs, rowNum)-> rs.getLong("sum(price)"), name);
if(salesAmount.isEmpty() && notsalesAmount.isEmpty()) throw new IllegalArgumentException("존재하지 않는 과일입니다.");
// 팔린적도 없고, 현재 남아있는 물품도 없으면 존재하지 않는 과일이라 생각해 예외를 던져줬습니다.
long sales = salesAmount.isEmpty()?0:salesAmount.get(0);
long notsales = notsalesAmount.isEmpty()?0:notsalesAmount.get(0);
//삼항 연산자로 둘 중에 하나가 null일 경우, 그 값을 0으로 표시하도록 했습니다.
return new SalesAmountResponse(sales, notsales);
}
📌 이
Repository
도 원래 해당 이름이 존재하는지 확인하는 쿼리가 있었으나, 벨로그를 작성하던 도중
salesAmount
와notsalesAmount
둘 다 값이 없으면 존재하지 않는 과일 아닐까? 싶어서 급하게 바꿔봤습니다.
더 줄이고 싶은데 아직은 방법을 모르겠네요... 🥲
DTO(Response)
public record SalesAmountResponse(long salesAmount, long notSalesAmount) {
}
💡 인텔리한
intellij
가 추천해줘서record
를 사용해 봤습니다.
나중에 한번 record에 대해 따로 공부를 해야겠다고 느꼈습니다.
View
<h3>MyAPI 3</h3>
<div class="col-sm-8" style="background-color: #f5f4f1"><br>
<br>
<table>
<tr>
<td> <span class="text-success">"Name"</span>:</td>
<td> <input type="text" id="fruitSaleName"></td>
</tr>
</table>
<br><br>
<button class="btn btn-primary" th:onclick="salesAmountBtn()">GET</button>
<br><br> // 😉
</div>
<br>
<div class="col-sm-8" style="background-color: #f5f4f1">
<span id="salesAmountID">
<h3 class="badge badge-light text-danger">실행 결과 : </h3><br>
</span>
</div>
</div>
<div class="card-footer">Yeong_Huns</div>
API.js
function salesAmountBtn(){
const fruitSaleName = document.getElementById("fruitSaleName").value;
fetch(`/api/v1/fruit/stat?name=${fruitSaleName}` , {
method: 'get',
})
.then(response => {
if(!response.ok) throw new Error("서버로부터 응답이 없습니다.(3번오류)")
return response.json() //오늘 처음으로 response를 json화 했습니다.
})
.then(data=>{
const salesAmount = data.salesAmount;
const notSalesAmount = data.notSalesAmount; //반환된 json에서 Key:value 로 값을 가져옵니다.
console.log(salesAmount + " : " + notSalesAmount)
// AWS에서 Mysql 연동테스트 때문에 찍어둔건데 지우는걸 깜빡했습니다.
$("#salesAmountID").html(
`
<h3 class="badge badge-light text-danger">실행 결과 : </h3><br>
{<br>
<span class="text-success">"salesAmount"</span>: <span class="text-danger">${salesAmount},</span><br>
<span class="text-success">"notSalesAmount"</span>: <span class="text-danger">${notSalesAmount}</span><br>
}<br><br>
`
)
})
.catch(error=>{
alert("존재하는 과일이 아닙니다.")
})
}
결과(Post Man)
결과(Web)
후기
생각보다 오래걸렸습니다. 특히 AWS의 리눅스 환경에서 Build를 했을때
윈도우에선 멀쩡히 돌아가던게 설정해 둔 모든 오류를 뿜어대서 당황했습니다 ..
알고보니mysql
의Database
이름을 습관적으로 대문자로 적어서 생긴 문제였습니다.
윈도우에선 대소문자 구별을 하지 않았거든요 .. 🥲 (기본 설정)
반면 리눅스는 테이블 name 조차도 파일로 관리하기 때문에 대소문자를 구분하는 게 기본설정인 것이죠 😭
또 이렇게 리눅스에 대한 공부의 필요성을 느낀 하루 였습니다...
번외