[인프런 워밍업 클럽 0기] BE 4일차 과제

김영훈·2024년 2월 22일
0
post-thumbnail

진도표 4일차와 연결됩니다.

우리는 GET API와 POST API를 만드는 방법을 배웠습니다. 👍 추가적인 API들을 만들어 보며 API 개발에 익숙해져 봅시다!

💡 4일차 문제 1, 문제 2, 문제 3은 이어지는 문제입니다! 😊

문제 1.

우리는 작은 과일 가게를 운영하고 있습니다. 과일 가게에 입고된 "과일 정보"를 저장하는 API를 만들어 봅시다.
스펙은 다음과 같습니다.

  • HTTP method : POST

  • HTTP path: /api/v1/fruit

  • HTTP 요청 Body:

{
 	"name": String, 
  	"warehousingDate": LocalDate,
  	"price": long
}
  • HTTP 요청 Body 예시
{
 	"name": "사과", 
  	"warehousingDate": "2024-02-01",
  	"price": 5000
}
  • 응답 : 성공시 200

    📌 한 걸음 더!
    자바에서 정수를 다루는 가장 대표적인 두 가지 방법은 intlong입니다.
    이 두 가지 방법 중 위 API 에서 long을 사용한 이유는 무엇일까요?


구현 1.

  • 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>&nbsp;&nbsp;<span class="text-success">"name"</span>:&nbsp;&nbsp;</td>
		<td>&nbsp;&nbsp;<input type="text" class="form-control" id="fruitName"></td>
	</tr>
	<tr>
		<td>&nbsp;&nbsp;<span class="text-success">"warehousingDate"</span>:&nbsp;&nbsp;</td>
		<td>&nbsp;<input type="date" class="form-control" id="warehousingDate"></td>
	</tr>
	<tr>
		<td>&nbsp;&nbsp;<span class="text-success">"price"</span>:&nbsp;&nbsp;</td>
		<td>&nbsp;<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나 , 수학적 연산에 사용되는 값의 경우
저장공간이 더 큰 longint에 비해 유리해서 int 보다 long을 쓴다고 생각했다.



문제 2.

과일이 팔리게 되면, 우리 시스템에 팔린 과일 정보를 기록해야 합니다. 스펙은 다음과 같습니다.

  • HTTP method: put
  • HTTP path: /api/v1/fruit
  • HTTP 요청 Body
{
	"id": long
}
  • HTTP 요청 Body 예시

	"id": 3
    
  • 응답 : 성공시 200

구현 2.

  • 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);
    }

📌 문득 ResponseEntityHttpStatus 를 지정하지 않아도 자동적으로 반환되는 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 처리하는 쿼리 형식 이였는데,

생각해보니 booleanList로 받으면 .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>
                &nbsp;&nbsp;<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도 받아올 수 있다는 것을
알게 되었습니다.


문제 3.

우리는 특정 과일을 기준으로 팔린 금액, 필리지 않은 금액을 조회하고 싶습니다.
예를 들어
1. (1,사과,3000원,판매 O)
2. (2,사과,4000원,판매 X)
3. (3,사과,3000원,판매 O)
와 같은 세 데이터가 있다면 우리의 API는 판매된 금액 : 6000원, 판매되지 않은 금액: 4000원 이라고 응답해야 합니다.
구체적인 스펙은 다음과 같습니다.

  • HTTP method : GET
  • HTTP path : /api/v1/fruit/stat
  • HTTP query
    • name : 과일 이름
  • 예시 GET /api/v1/fruit/stat?name=사과
  • HTTP 응답 Body
{
	"salesAmount": long,
    "notSalesAmount": long
}
  • HTTP 응답 Body 예시\
{
	"salesAmount": 6000,
    "notSalesAmount": 4000
}

📌 한 걸음 더!
(문제 3번을 모두 푸셨다면) SQL의 sum, group by 키워드를 검색해 적용해보세요! 😊


구현 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

@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도 원래 해당 이름이 존재하는지 확인하는 쿼리가 있었으나, 벨로그를 작성하던 도중
salesAmountnotsalesAmount 둘 다 값이 없으면 존재하지 않는 과일 아닐까? 싶어서 급하게 바꿔봤습니다.
더 줄이고 싶은데 아직은 방법을 모르겠네요... 🥲

  • 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>&nbsp;&nbsp;<span class="text-success">"Name"</span>:</td>
            <td>&nbsp;<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>
                &nbsp;&nbsp;<span class="text-success">"salesAmount"</span>: <span class="text-danger">${salesAmount},</span><br>
                &nbsp;&nbsp;<span class="text-success">"notSalesAmount"</span>: <span class="text-danger">${notSalesAmount}</span><br>
                }<br><br>
                `
            )
        })
        .catch(error=>{
            alert("존재하는 과일이 아닙니다.")
        })
}
  • 결과(Post Man)

  • 결과(Web)


  • 후기

    생각보다 오래걸렸습니다. 특히 AWS의 리눅스 환경에서 Build를 했을때
    윈도우에선 멀쩡히 돌아가던게 설정해 둔 모든 오류를 뿜어대서 당황했습니다 ..
    알고보니 mysqlDatabase 이름을 습관적으로 대문자로 적어서 생긴 문제였습니다.
    윈도우에선 대소문자 구별을 하지 않았거든요 .. 🥲 (기본 설정)
    반면 리눅스는 테이블 name 조차도 파일로 관리하기 때문에 대소문자를 구분하는 게 기본설정인 것이죠 😭
    또 이렇게 리눅스에 대한 공부의 필요성을 느낀 하루 였습니다...

  • 번외

    삽질의 흔적들... 😭

0개의 댓글