본문 바로가기

Education/인프런 워밍업 클럽(BE 0기)

인프런 워밍업 클럽 - BE 0기, 과제 #4

목차

     

    과제

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

    • HTTP method: POST
    • HTTP path: /api/v1/fruit
    • HTTP 요청 Body
    {
        "name": String,
        "warehousingDate": LocalDate,
        "price": long
    }

    문제2. 과일이 팔리게 되면, 우리 시스템에 팔린 과일 정보를 기록해야 합니다.

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

     문제3. 우리는 특정 과일을 기준으로 팔린 금액, 팔리지 않은 금액을 조회하고 싶습니다.

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

     


    풀이

    우선 1번 문제인 과일 정보를 저장하기 위해 과일 DTO 클래스를 생성하고, 과일 테이블을 만들자!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package com.group.libraryapp.dto.study;
     
    import java.util.Date;
     
    public class FruitRequest {
        
        private String name;
        private Date sarehousingDate;
        private long price;
     
        public String getName() {
            return name;
        }
     
        public Date getSarehousingDate() {
            return sarehousingDate;
        }
     
        public long getPrice() {
            return price;
        }
    }
    cs

     

    create table study_fruit( name varchar(20), warehousingDate DATE, price bigint);

     

    과일 정보를 저장하는 API를 만들었다.

    @PostMapping("/api/v1/fruit")
    public void saveFruit(@RequestBody FruitRequest request) {
        String sql = "INSERT INTO study_fruit (name, warehousingDate, price) VALUES (?, ?, ?)";
        jdbcTemplate.update(sql, request.getName(), request.getSarehousingDate(), request.getPrice());
    }

    API를 생성하고, 포스트맨으로 돌린 뒤 table을 확인해보니 "warehousingDate" 데이터가 들어가지 않았다.

     

    에러는 출력되지 않았는데, 왜 들어가지 않은걸까?

    확인을 위해서 디버깅을 해보았다.

    앗...😅 오타가 있었다.. FruitRequest 객체의 warehousingDate의 철자를 고친 뒤 다시 run!

    데이터가 잘 저장되었다!


    다음엔 2번 문제인 팔린 과일 정보를 기록해 보도록 하겠다.

    문제를 보니 팔린 과일을 구분하기 위해 id값을 가지고 있어야 되는 것 같다.

    table 정보를 수정하자... id를 자동으로 값이 증가하게 만들고, primary key로 지정했다.

    alter table study_fruit
     add column id bigint auto_increment,
     add primary key (id);

    id값이 잘 저장되어 있는 것을 확인할 수 있다.

     

    판매여부를 알려면 판매여부 컬럼도 필요할텐데..? 다시 테이블을 수정하자.😅

    alter table study_fruit
     add column sold TINYINT(1);

    판매여부 컬럼명을 sold로 줘서 판매(1), 미판매(0)로 값을 저장하도록 하겠다.

     

    이제 정말로 팔린 과일 정보를 기록하는 API를 작성해 보겠다.

    HTTP 요청 Body에 id가 넘어온다. id가 long 타입이기 때문에 DTO클래스를 만들어서 객체로 가져와야 된다.

    package com.group.libraryapp.dto.study;
    
    public class FruitSellRequest {
        
        private long id;
    
        public long getId() {
            return id;
        }
    }

     

    @PutMapping("/api/v1/fruit")
    public void sellFruit(@RequestBody FruitSellRequest request) {
        String sql = "UPDATE study_fruit SET sold = 1 WHERE id = ?";
        jdbcTemplate.update(sql, request.getId());
    }

     

    판매 api를 실행 후 id 1번인 사과의 sold컬럼 값이 1로 바뀐것을 확인할 수 있다.

     

    하지만! 여기서 마무리하면 안 되고 강의에서 배웠던 것을 떠올려서 잘못된 id값이 들어올 수 없도록 처리해 줘야 된다.

    @PutMapping("/api/v1/fruit")
    public void sellFruit(@RequestBody FruitSellRequest request) {
        String readSql = "SELECT * FROM study_fruit WHERE id = ?";
        boolean isIdNotExist = jdbcTemplate.query(readSql, (rs, rowNum) -> 0, request.getId()).isEmpty();
    
        if (isIdNotExist) {
            throw new IllegalArgumentException();
        }
        String sql = "UPDATE study_fruit SET sold = 1 WHERE id = ?";
        jdbcTemplate.update(sql, request.getId());
    }

    이렇게 해서 과일을 판매하기 전에 해당 과일의 존재 여부를 확인하고, 존재하면 판매되는 것으로 수정을 완료했다.


    마지막으로 3번 문제인 특정 과일을 기준으로 팔린 금액, 팔리지 않은 금액을 조회하도록 하겠다.

    응답에 필요한 DTO클래스를 만들어 주고, API를 작성했다.

    package com.group.libraryapp.dto.study;
    
    public class FruitSearchResponse {
    
        private long salesAmount;
        private long notSalesAmount;
    
        public FruitSearchResponse() {
        }
    
        public FruitSearchResponse(long salesAmount, long notSalesAmount) {
            this.salesAmount = salesAmount;
            this.notSalesAmount = notSalesAmount;
        }
    
        public void setSalesAmount(long salesAmount) {
            this.salesAmount = salesAmount;
        }
    
        public void setNotSalesAmount(long notSalesAmount) {
            this.notSalesAmount = notSalesAmount;
        }
    
        public long getSalesAmount() {
            return salesAmount;
        }
    
        public long getNotSalesAmount() {
            return notSalesAmount;
        }
    }

     

    쿼리를 작성하다가 난관에 부딪혔다..

    select sum(price)
    from study_fruit
    where name = "사과"
    group by sold;

    GROUP BY를 통해 판매 여부에 따라 그룹으로 묶었는데, 출력된 값이 판매된 그룹의 값인지, 판매되지 않은 그룹의 값인지 어떻게 알지?

    찾아보니 sold 컬럼의 값을 기반으로 별칭을 지정하여 각 그룹을 식별할 수 있다고 한다.

    SELECT SUM(price) AS total_price,
           CASE WHEN sold = 1 THEN 'salesAmount' ELSE 'notSalesAmount' END AS sold_group
    FROM study_fruit
    WHERE name = "사과"
    GROUP BY sold;

     

    데이터를 잘 받아왔으니, 이제 응답 객체에 잘 담아주기만 하면 된다!

     

    @GetMapping("/api/v1/fruit/stat")
    public List<FruitSearchResponse> searchFruitStat(@RequestParam String name) {
        String readSql = "SELECT COUNT(*) FROM study_fruit WHERE name = ?";
        int count = jdbcTemplate.queryForObject(readSql, Integer.class, name);
    
        if (count <= 0) {
            throw new IllegalArgumentException();
        }
    
        String sql = "SELECT SUM(price) AS total_price, " +
                     "CASE WHEN sold = 1 THEN 'salesAmount' ELSE 'notSalesAmount' END AS sold_group " +
                     "FROM study_fruit WHERE name = ? GROUP BY sold";
    
        return jdbcTemplate.query(sql, (rs, rowNum) -> {
            FruitSearchResponse fruitSearchResponse = new FruitSearchResponse();
    
            String sold_group = rs.getString("sold_group");
            if("salesAmount".equals(sold_group)) {
                fruitSearchResponse.setSalesAmount(rs.getInt("total_price"));
            } else if ("notSalesAmount".equals(sold_group)) {
                fruitSearchResponse.setNotSalesAmount(rs.getInt("total_price"));
            }
            return fruitSearchResponse;
        }, name);
    }

     

    이상하다...?🥸 왜...객체가 2개가 나오지? 반환 타입을 List로 해야 된다고 에러가 발생할 때 부터 뭔가 이상했는데..

     

     

    그룹 별로 필드에 값을 넣어주는 게 정말 어려웠다.. response 객체를 2개 생성해 줘서 결과가 List형식으로 나왔다.

    찾아보니 람다식 내에서는 각 행에 대해 객체를 생성하고 설정한다고 한다. 람다식이 한 번 호출되고 결과 집합의 첫 번째 행만 처리되는 식.. 그 이후의 행이 처리되지 않으니 객체가 추가된 것이라고 함.

    rs.next()를 사용해서 다음 행이 있는지 확인하면서 모든 행을 처리한 후 객체를 return하게 해야된다.

    어렵다.. 다음에 또 찾아볼 것 같다.🥹🥹

    @GetMapping("/api/v1/fruit/stat")
    public FruitSearchResponse searchFruitStat(@RequestParam String name) {
        String readSql = "SELECT COUNT(*) FROM study_fruit WHERE name = ?";
        int count = jdbcTemplate.queryForObject(readSql, Integer.class, name);
    
        if (count <= 0) {
            throw new IllegalArgumentException();
        }
    
        String sql = "SELECT SUM(price) AS total_price, " +
                     "CASE WHEN sold = 1 THEN 'salesAmount' ELSE 'notSalesAmount' END AS sold_group " +
                     "FROM study_fruit WHERE name = ? GROUP BY sold";
    
        return jdbcTemplate.query(sql, (rs) -> {
            FruitSearchResponse fruitSearchResponse = new FruitSearchResponse();
    
            while (rs.next()) {
                String sold_group = rs.getString("sold_group");
                int total_price = rs.getInt("total_price");
    
                if("salesAmount".equals(sold_group)) {
                    fruitSearchResponse.setSalesAmount(total_price);
                } else if ("notSalesAmount".equals(sold_group)) {
                    fruitSearchResponse.setNotSalesAmount(total_price);
                }
            }
    
            return fruitSearchResponse;
        }, name);
    }

     

    성공!!!

     

     

     

    오늘 과제도 끝!!!🙌