[인프런워밍업스터디_BE_0기] 일곱 번째 과제!

 

문제 1

여섯번째 과제에서 만들었던 Fruit 기능들을 JPA를 이용하도록 변경해보세요!

 

오늘의 과제는 어제까지 선생님께 배운 다형성을 활용할 기회다! MemoryRepository를 사용하더라도, MySqlRepository를 사용하더라도 @Primary만 바꿔주면 되도록 변경해두었고, 주말에 미리 예습해두었던 JPA에서 사용할 수 있도록 Repository의 메서드도 맞춰둔 상황! JpaRepository만 만들고 바꿔주면 되겠지?!

 

[JPA를 적용한 Repository]

public interface FruitJpaRepository extends JpaRepository<Fruit, Long> {

    List<Fruit> findAllByName(String name);

    @Query("select new com.day4.fruitapp.dto.fruit.model.FruitStat(f.soldYn, sum(f.price)) from Fruit f where f.name = :name group by f.soldYn")
    List<FruitStat> getStats(@Param("name") String name);
}

하지만 역시 놓친 부분이 있었으니..

[기존 서비스 코드]

@Service
public class FruitServiceV1 implements FruitService{

    // 중략 ...

    @Override
    public void sellFruit(FruitSellReqeust reqeust) {
        Fruit fruit = fruitRepository.findById(reqeust.getId())
                .orElseThrow(() -> new FruitNotFoundException("과일 정보를 찾을 수 없습니다."));

        if (fruit.isSold()) {
            throw new FruitNotFoundException("이미 팔린 과일입니다.");
        }

        fruitRepository.sell(fruit.getId()); // getStats처럼 만들어야하나...?
    }

    // 후략 ...
}

팔린 과일에 대한 정보를 업데이트하는 부분인데.. FruitJpaRepository의 getStats 처럼 @Query 를 통해 구현할까도 싶었지만! sell이라는 팔렸다는 표현은 Fruit 도메인에 더 잘어울리는 것 같으니 다음과 같이 변경하자!

 

@Service
public class FruitServiceV2 implements FruitService {

    // 중략 ...

    @Override
    public void sellFruit(FruitSellReqeust reqeust) {
        Fruit fruit = fruitRepository.findById(reqeust.getId())
                .orElseThrow(() -> new FruitNotFoundException("과일 정보를 찾을 수 없습니다."));

        if (fruit.isSold()) {
            throw new FruitNotFoundException("이미 팔린 과일입니다.");
        }

        fruit.sell();
        fruitRepository.save(fruit);
    }

    // 후략 ...
}

각 api의 요청 결과도 그대로 잘 나온다!

 

문제 2

특정 과일을 기준으로 우리 가게를 거쳐갔던 과일 개수를 세고 싶다.

예를 들어

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

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

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

와 같은 세 데이터가 있을 때 사과를 기준으로 개수를 센다면 2를 반환한다.

 

API의 스펙은 다음과 같다.

1. HTTP method: GET

2. HTTP path: /api/v1/fruit/count

3. HTTP query: name=과일이름

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

5. HTTP 응답 Body

{

"count" : long

}

6. HTTP 응답 예시

{

"count" : 2

}

 

이번 요구사항은 간단하다. 바로 구현해보자!

[컨트롤러 클래스]

@RestController
public class FruitController {

    // 중략 ...

    @GetMapping("/api/v1/fruit/count")
    public FruitCountResponse getCount(@RequestParam String name) {
        return fruitService.getCount(name);
    }
}

 

[서비스 클래스]

@Service
public class FruitServiceImpl implements FruitService {

    // 중략 ...

    @Override
    public FruitCountResponse getCount(String name) {
        return new FruitCountResponse(fruitRepository.countByName(name));
    }
}

 

[리포지토리 클래스]

public interface FruitJpaRepository extends JpaRepository<Fruit, Long> {

    List<Fruit> findAllByName(String name);

    @Query("select new com.day4.fruitapp.dto.fruit.model.FruitStat(f.soldYn, sum(f.price)) from Fruit f where f.name = :name group by f.soldYn")
    List<FruitStat> getStats(@Param("name") String name);

    long countByName(String name);
}

DB에 없는 과일의 개수를 요청할 때 예외처리를 할 것인지 고민되었지만 거쳐갔던 과일의 개수를 반환하는 것이 요구사항이니 거쳐갔던 적이 없는 과일은 0을 반환하는 것이 맞는 것 같아 예외처리하지 않았다.

이제 API 요청 결과를 보자!
image

 

문제 3

아직 판매되지 않은 특정 금액 이상 혹은 특정 금액 이하의 과일 목록을 받고 싶다.

API 스펙은 다음과 같다.

1. HTTP method: GET

2. HTTP path: /api/v1/fruit/list

3. HTTP query: option=GTE또는option=LTE, price=기준 금액

- GTE : greater than equal의 의미

- LTE : less than equal의 의미

4. 예시 GET /api/v1/fruit/count?option=GTE&price=3000

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

5. HTTP 응답 Body

[{

"name" : String,

"price" : long,

"warehousingDate" : LocalDate

}, ...]

6. HTTP 응답 예시

[

{

"name" : "사과",

"price" : 4000,

"warehousingDate" : "2024-01-05"

},

{

"name" : "바나나",

"price" : 6000,

"warehousingDate : "2024-01-8"

}

]

 

이번 요구사항은 조금 복잡하지만 그래도 어렵진 않다! LTE, GTE라는 파라미터를 문자열로 받기 보다는 Enum으로 관리하면 좋을 것 같다.. 바로 구현해보자!

 

[요청 클래스와 Enum 클래스]

public class NotSoldFruitRequest {

    private PriceComparison option;
    private long price;

    public NotSoldFruitRequest(PriceComparison option, long price) {
        this.option = option;
        this.price = price;
    }

    public PriceComparison getOption() {
        return option;
    }

    public long getPrice() {
        return price;
    }
}

public enum PriceComparison {
    GTE,
    LTE
}

 

 

[컨트롤러 클래스]

@RestController
public class FruitController {

    // 중략 ...

    @GetMapping("/api/v1/fruit/list")
    public List<NotSoldFruitResoponse> getNotSoldFruits(NotSoldFruitRequest request) {
        return fruitService.getNotSoldFruits(request);
    }
}

 

[서비스 클래스]

@Service
public class FruitServiceImpl implements FruitService {

    // 중략 ...

    @Override
    public List<NotSoldFruitResoponse> getNotSoldFruits(NotSoldFruitRequest request) {
        List<Fruit> fruits = switch (request.getOption()) {
            case GTE -> fruitRepository.findAllBySoldYnAndPriceGreaterThanEqualOrderByName(request.getPrice());
            case LTE -> fruitRepository.findAllBySoldYnAndPriceLessThanEqualOrderByName(request.getPrice());
        };

        return fruits.stream().map(NotSoldFruitResoponse::new).collect(Collectors.toList());
    }
}

 

[리포지터리 클래스]

public interface FruitJpaRepository extends JpaRepository<Fruit, Long> {

    // 중략 ...

    List<Fruit> findAllBySoldYnAndPriceGreaterThanEqualOrderByName(String soldYn, long price);

    List<Fruit> findAllBySoldYnAndPriceLessThanEqualOrderByName(String soldYn, long price);
}

"N"을 직접 파라미터로 넘겨주는게 마음에 들지 않지만 시간이 부족하니 오늘은 여기까지..ㅠ

 

이제 요청해보자

image

image

댓글을 작성해보세요.

  • konakyeon3
    konakyeon3


    enum을 어떻게 적용하는지 궁금했었는데 좋은 답이 된 것 같습니다.