인프런 워밍업 클럽 백엔드 - 일곱 번째 과제

인프런 워밍업 클럽 백엔드 - 일곱 번째 과제

문제 1

과제 #6에서 만들었던 Fruit 기능들을 JPA를 이용하도록 변경해보세요.

먼저 domain 패키지를 만들어서 Fruit와 FruitRepository를 생성했습니다.

Fruit

@Entity
public class Fruit {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(length = 20)
    private String name;
    private LocalDate warehousingDate;
    private Long price;
    private boolean stat = false;

    protected Fruit() {
    }

    public Fruit(String name, LocalDate warehousingDate, Long price) {
        this.name = name;
        this.warehousingDate = warehousingDate;
        this.price = price;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public LocalDate getWarehousingDate() {
        return warehousingDate;
    }

    public Long getPrice() {
        return price;
    }

    public boolean isStat() {
        return stat;
    }

    public void updateFruitStat(boolean stat) {
        this.stat = stat;
    }
}

FruitRepository

public interface FruitRepository extends JpaRepository<Fruit, Long> {
    Optional<List<Fruit>> findAllByName(String name);
}

이름으로 검색할 수 있는 메소드, findByAllName을 생성해줍니다.

기존에 있던 서비스를 수정했습니다.

FruitServiceV2

@Service
public class FruitServiceV2 {

    private final FruitRepository fruitRepository;

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

    public void saveFruit(FruitRequest request) {
        fruitRepository.save(new Fruit(request.getName(), request.getWarehousingDate(), request.getPrice()));
    }

    public void soldFruit(FruitRequest request) {
        Fruit fruit = fruitRepository.findById(request.getId())
                .orElseThrow(IllegalArgumentException::new);
        fruit.updateFruitStat(true);
        fruitRepository.save(fruit);
    }

    public FruitSaleResponse readFruitSoldPrice(String name) {
        List<Fruit> fruits = fruitRepository.findAllByName(name)
                .orElseThrow(IllegalArgumentException::new);
        long salesAmount = 0;
        long notSalesAmount = 0;
        for (Fruit fruit : fruits) {
            if (fruit.isStat()) {
                salesAmount += fruit.getPrice();
            } else {
                notSalesAmount += fruit.getPrice();
            }
        }
        return new FruitSaleResponse(salesAmount, notSalesAmount);
    }
}

이렇게 작성하고 실행했을 때 문제가 발생합니다.

java.sql.SQLSyntaxErrorException: Unknown column 'warehousing_date' in 'field list'

이런 에러가 발생하는데 기본적으로 쿼리를 요청할 때 스네이크 케이스를 기준으로 요청하기 때문입니다. 데이터베이스에서 fruit 테이블을 생성할 때 컬럼명을 카멜케이스로 작성했기 때문에 에러가 발생합니다. 이 문제를 해결하기 위해서 application.yml에 네이밍 전략을 추가하도록 하겠습니다.

spring:
  jpa:
    hibernate:
      ddl-auto: none
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect

physical-strategyorg.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl로 설정하면 정상적으로 작동하게 됩니다.

 

문제 2

우리는 특정 과일을 기준으로 지금까지 우리 가게를 거쳐갔던 과일 개수를 세고 싶습니다.
<문제 1>에서 만들었던 과일 Entity Class를 이용해 기능을 만들어 보세요!

예를 들어

  1. (1, 사과, 3000원, 판매 O)

  2. (2, 바나나, 4000원, 판매 X)

  3. (3, 사과, 3000원, 판매 O)

와 같은 세 데이터가 있고, 사과를 기준으로 과일 개수를 센다면, 우리의 API는 2를 반환할 것입니다.

구체적인 스펙은 다음과 같습니다.

  • HTTP method : GET

  • HTTP path : /api/v1/fruit/count

  • HTTP query

    • name : 과일 이름

  • 예시 GET /api/v1/fruit/count?name=사과

  • HTTP 응답 Body

{
    "count": long
}
  • HTTP 응답 Body 예시

{
    "count": 2
}

 

저는 판매된 특정 과일만 count 했습니다.
먼저 컨트롤러에 추가해줍니다.

Controller

@GetMapping("/api/v1/fruit/count")
public FruitSoldCountResponse countSoldFruit(String name) {
    return fruitService.countSoldFruit(name);
}

 

Repository

Integer countByNameAndStat(String name, boolean stat);

리포지토리에 이름과 판매 상태로 조회하는 메소드를 작성합니다. SELECT * FROM fruit WHERE name = ? AND stat = ?

 

Service

public FruitSoldCountResponse countSoldFruit(String name) {
    return new FruitSoldCountResponse(fruitRepository.countByNameAndStat(name, true));
}

판매된 과일만 조회하기 때문에 stat을 받는 부분에 true를 넣었습니다. 그리고 Body 형태로 받기 위해 Response를 하나 생성해주었습니다.

 

Response

public class FruitSoldCountResponse {

    private Integer count;

    public FruitSoldCountResponse(Integer count) {
        this.count = count;
    }

    public Integer getCount() {
        return count;
    }
}

예시대로 실행했을 때,
image결과가 잘 나오는 것을 볼 수 있습니다.

 

문제 3

우리는 아직 판매되지 않은 특정 금액 이상 혹은 특정 금액 이하의 과일 목록을 받아보고 싶습니다.
구체적인 스펙은 다음과 같습니다.

  • HTTP method : GET

  • HTTP path : /api/v1/fruit/list

  • HTTP query

    • option : "GTE" 혹은 "LTE"라는 문자열이 들어온다.

      • GTE : greater than equal의 의미

      • LTE : less than equal의 의미

    • price : 기준이 되는 금액이 들어온다.

  • 예시 1 - GET /api/v1/fruit/list?option=GTE&price=3000

    • 판매되지 않은 3000원 이상의 과일 목록을 반환해야 한다.

  • 예시 2 - GET /api/v1/fruit/list?option=LTE&price=5000

    • 판매되지 않은 5000원 이하의 과일 목록을 반환해야 한다.

  • HTTP 응답 Body

[{
    "name": String,
    "price": long,
    "warehousingDate": LocalDate,
}, ...]
  • HTTP 응답 Body 예시

[
    {
        "name": "사과",
        "price": 4000,
        "warehousingDate": "2024-01-05",
    },
    {
        "name": "바나나",
        "price": 6000,
        "warehousingDate": "2024-01-08",
    }
]

먼저 컨트롤러에 추가해줍니다.
Controller

@GetMapping("/api/v1/fruit/list")
public List<FruitJpaResponse> getFruitsByPriceRange(@RequestParam String option, @RequestParam Long price) {
    return fruitService.getFruitsByPriceRange(option, price);
}

 

Repository

Optional<List<FruitJpaResponse>> findAllByPriceGreaterThanEqual(Long price);
Optional<List<FruitJpaResponse>> findAllByPriceLessThanEqual(Long price);

"GTE" 문자열을 받으면 실행 findAllByPriceGreaterThanEqual = SELECT * FROM fruit WHERE price >= ?
"LTE" 문자열을 받으면 실행 findAllByPriceLessThanEqual = SELECT * FROM fruit WHERE price <= ?

 

Service

public List<FruitJpaResponse> getFruitsByPriceRange(String option, Long price) {
    if ("GTE".equals(option)) {
        return fruitRepository.findAllByPriceGreaterThanEqual(price).orElseThrow(IllegalArgumentException::new);
    } else if ("LTE".equals(option)) {
        return fruitRepository.findAllByPriceLessThanEqual(price).orElseThrow(IllegalArgumentException::new);
    } else {
        throw new IllegalArgumentException();
    }
}

option으로 받는 문자열을 비교해서 각 문자열에 맞는 쿼리 실행할 수 있도록 if문을 사용했습니다. "GTE"나 "LTE"가 아닌 다른 문자열이 오면 예외 처리를 할 수 있도록 했습니다. 응답 예시처럼 결과가 나올 수 있도록 Response 생성했습니다.

 

Response

public class FruitJpaResponse {

    private String name;
    private Long price;
    private LocalDate warehousingDate;

    public String getName() {
        return name;
    }

    public Long getPrice() {
        return price;
    }

    public LocalDate getWarehousingDate() {
        return warehousingDate;
    }

    public FruitJpaResponse(String name, Long price, LocalDate warehousingDate) {
        this.name = name;
        this.price = price;
        this.warehousingDate = warehousingDate;
    }
}

 

결과

image

댓글을 작성해보세요.