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

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

image

[1-1. 프로젝트 생성]

스프링 이니셜라이저에서 문제 풀이를 위한 스프링 프로젝트를 생성합니다.

 

  • Project: Gradle - Groovy

  • Language: Java

  • Spring Boot: 3.2.2

  • Project Metadata

    • Group: com.group

    • Artifact: fruit-app

    • Name: fruit-app

    • Description: practice

    • Package-name: com.group.fruit-app

  • Packaging: Jar

  • Java: 17

  • Dependencies: Spring Web / JDBC API / MySQL Driver

 

spring:
  datasource:
    url: "jdbc:mysql://localhost/playground"
    username: "root"
    password: "1234"
    driver-class-name: com.mysql.cj.jdbc.Driver

application.yml을 만들고 DB 연결 세팅을 해줍니다.

 

image일단 실행은 됩니다. 휴우

 

[1-2. 테이블 설계]

create table fruit
(
    id bigint auto_increment,
    name varchar(20),
    warehousing_date datetime,
    price int,
    is_sold boolean default 0,
    primary key (id)
); 

과일 테이블을 설계합니다. 기본키인 ID 컬럼은 auto_increment를 통해 데이터가 추가될 때마다 값이 자동으로 증가하도록 했습니다.

 

[1-3. 애플리케이션 개발]

public class FruitCreateRequest {
    private String name;
    private LocalDate warehousingDate;
    private Long price;

    public String getName() {
        return name;
    }

    public LocalDate getWarehousingDate() {
        return warehousingDate;
    }

    public Long getPrice() {
        return price;
    }
}

POST 요청 데이터를 담을 과일 생성 DTO를 제작합니다. price 필드의 타입이 long인 점이 눈에 띕니다. 이는 더 넓은 범위의 숫자를 담기 위함인데요. int 타입의 경우 대략 21억 (2,147,483,647)까지만 담을 수 있습니다. 과일값 가지고 너무 호들갑 떠는 게 아닌가 싶지만 요즘 같은 고물가 시대엔 혹시 모릅니다.

 

@RestController
public class FruitController {
    private final FruitService fruitService;

    public FruitController(FruitService fruitService) {
        this.fruitService = fruitService;
    }

    @PostMapping("/api/v1/fruit")
    public void saveFruit(@RequestBody FruitCreateRequest request) {
        fruitService.saveFruit(request);
    }
}
@Service
public class FruitService {
    private final FruitRepository fruitRepository;

    public FruitService(FruitRepository fruitRepository) {
        this.fruitRepository = fruitRepository;
    }

    public void saveFruit(FruitCreateRequest request) {
        fruitRepository.save(request.getName(), request.getWarehousingDate(), request.getPrice());
    }
}
public interface FruitRepository {

    void save(String name, LocalDate warehousingDate, Long price);
}
@Repository
public class FruitMySqlRepository implements FruitRepository {
    private final JdbcTemplate jdbcTemplate;

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

    @Override
    public void save(String name, LocalDate warehousingDate, Long price) {
        String sql = "insert into fruit (name, warehousing_date, price) values (?, ?, ?)";
        jdbcTemplate.update(sql, name, warehousingDate, price);
    }
}

이어서 컨트롤러, 서비스, 레포지토리를 만들어 줍니다. 레포지토리는 인터페이스와 구현 클래스로 나눠서 제작했습니다.

 

image애플리케이션을 실행하고 JSON 데이터와 함께 POST 요청을 보내봅시다 두근두근

 

image200 OK 응답을 받았고, DB에도 정상 등록되었습니다.

 


image

[2-1. 애플리케이션 개발]

public class FruitStockUpdateRequest {
    private Long id;

    public Long getId() {
        return id;
    }
}

PUT 요청 데이터를 담을 과일 재고 갱신 DTO를 생성합니다.

 

@RestController
public class FruitController {

    // 생략

    @PutMapping("/api/v1/fruit")
    public void updateFruitStock(@RequestBody FruitStockUpdateRequest request) {
        fruitService.updateFruitStock(request);
    }
}
@Service
public class FruitService {
    // 생략

    public void updateFruitStock(FruitStockUpdateRequest request) {
        if (fruitRepository.isFruitEmpty(request.getId())) {
            throw new IllegalStateException("등록되지 않은 과일입니다.");
        }

        fruitRepository.updateStock(request.getId());
    }
}
public interface FruitRepository {

    void save(String name, LocalDate warehousingDate, Long price);

    boolean isFruitEmpty(Long id);

    void updateStock(Long id);
}
@Repository
public class FruitMySqlRepository implements FruitRepository {
    // 생략

    @Override
    public boolean isFruitEmpty(Long id) {
        String sql = "select * from fruit where id = ?";
        return jdbcTemplate.query(sql, (rs, rowNum) -> 0, id).isEmpty();
    }

    @Override
    public void updateStock(Long id) {
        String sql = "update fruit set is_sold = 1 where id = ?";
        jdbcTemplate.update(sql, id);
    }
}

컨트롤러, 서비스, 레포지토리에도 필요한 기능을 추가합니다. 문제에선 팔린 과일을 기록하라고 돼있습니다만, 저는 DB에 있는 과일 테이블의 IS_SOLD 컬럼값을 1로 바꾸는 식으로 진행했습니다. 이때 등록되지 않는 과일 정보를 수정하려는 경우 IllegalStateException을 일으키도록 했습니다.

 

imageimage현재 과일 테이블엔 ID 컬럼값이 1인 사과 데이터만 존재합니다. id를 5로 해서 PUT 요청을 보내니 예외가 정상적으로 발생했습니다.

 

imageimage이번엔 id를 1로 해서 PUT 요청을 보냈습니다. 200 OK 응답이 왔고, DB에도 잘 반영되었습니다.

 


image

[3-1. 테이블 세팅]

image우선 과일 테이블을 문제 조건과 동일하게 맞춰줍니다.

 

[3-2. 애플리케이션 개발]

public class FruitSalesRecordResponse {
    private Long salesAmount;
    private Long notSalesAmount;

    public Long getSalesAmount() {
        return salesAmount;
    }

    public void setSalesAmount(Long salesAmount) {
        this.salesAmount = salesAmount;
    }

    public Long getNotSalesAmount() {
        return notSalesAmount;
    }

    public void setNotSalesAmount(Long notSalesAmount) {
        this.notSalesAmount = notSalesAmount;
    }
}

 특정 과일의 판매된 총 금액과 판매되지 않은 총 금액을 담을 DTO입니다.

 

@RestController
public class FruitController {
    // 생략

    @GetMapping("/api/v1/fruit/stat")
    public FruitSalesRecordResponse getFruitSalesRecord(@RequestParam(value="name") String name) {
        return fruitService.getFruitSalesRecord(name);
    }
}

컨트롤러에 맵핑 메소드를 추가합니다. @RequestParam에 value 속성을 추가하지 않으면 예외가 발생하더군요. name이란 명칭 자체에 문제가 있지 않나 추측해봅니다.

 

@Service
public class FruitService {
    // 생략

    public void getFruitSalesRecord(String name) {
        if (fruitRepository.isFruitEmpty(name)) {
            throw new IllegalStateException("등록되지 않은 과일입니다.");
        }

        return fruitRepository.getSalesRecord(name);
    }
}
public interface FruitRepository {
    // 생략

    FruitSalesRecordResponse getSalesRecord(String name);

    boolean isFruitEmpty(String name);
}
 @Repository
public class FruitMySqlRepository implements FruitRepository {
    // 생략

    @Override
    public FruitSalesRecordResponse getSalesRecord(String name) {
        String sql = "select is_sold, sum(price) as total_price from fruit where name = ? group by is_sold";
        FruitSalesRecordResponse response = new FruitSalesRecordResponse();

        jdbcTemplate.query(sql, (rs, rowNum) -> {
            if (rs.getBoolean("is_sold")) {
                response.setSalesAmount(rs.getLong("total_price"));
            } else {
                response.setNotSalesAmount(rs.getLong("total_price"));
            }
            return 0;
        });

        return response;
    }

    @Override
    public boolean isFruitEmpty(String name) {
        String sql = "select * from fruit where name = ?";
        return jdbcTemplate.query(sql, (rs, rowNum) -> 0, name).isEmpty();
    }
}

오늘 과제의 하이라이트는 여기인 것 같습니다. 일단 2번 문제와 동일하게 등록되지 않는 과일에 대해선 예외를 일으키도록 했습니다.

 

image제가 작성한 SQL을 실행하면 ResultSet이 사진처럼 나오는데요. 이를 FruitSalesRecordResponse에 바로 담을 수가 없습니다. DB 컬럼과 엔티티의 필드가 일치하지 않기 때문입니다. 그래서 어떻게 쑤셔 넣을까 고민하다 JDBC 템플릿으로 맵핑하는 과정에서 IS_SOLD 컬럼값이 1이면 TOTAL_PRICE 컬럼값을 FruitSalesRecordResponse의 salesAmount 필드에 넣고, 그렇지 않으면 notSalesAmount 필드에 넣는 식으로 처리했습니다.

 

imageimage이제 GET 요청을 보내봅시다. 요청 파라미터로 "오렌지"를 전달하니 예외가 정상적으로 발생했습니다.

 

image이번엔 "사과"를 전달하니 정상적으로 응답되었습니다.

댓글을 작성해보세요.

채널톡 아이콘