본문 바로가기

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

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

목차

     

    과제

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

    문제2. 과일 Entity Class를 이용해 특정 과일을 기준으로 지금까지 가게를 거쳐간 과일 개수를 세보세요.

    문제3. 아직 판매되지 않은 특정 금액 이상 혹은 특정 금액 이하의 과일 목록을 받아보세요.


    풀이.

    문제1

    더보기

    study_fruit 테이블에 매핑되도록 Fruit 객체를 생성해준다.

    package com.group.libraryapp.domain.study;
    
    import javax.persistence.*;
    import java.util.Date;
    
    @Entity
    @Table(name = "study_fruit")
    public class Fruit {
        @Id
        @GeneratedValue( strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(nullable = false, length = 20)
        private String name;
    
        @Column(name = "warehousingDate")
        private Date warehousingDate;
    
        private Long price;
    
        private boolean sold;
    
        public Fruit() {}
    
        public Fruit(String name, Date warehousingDate, Long price) {
            this.name = name;
            this.warehousingDate = warehousingDate;
            this.price = price;
        }
    
        public Long getId() {
            return id;
        }
    
        public String getName() {
            return name;
        }
    
        public Date getWarehousingDate() {
            return warehousingDate;
        }
    
        public Long getPrice() {
            return price;
        }
    
        public boolean isSold() {
            return sold;
        }
    
        public void updateSold(boolean isSold) {
            this.sold = isSold;
        }
    }

     

    API를 호출할 수 있도록 Controller, Service, Repository를 연결한다.

    package com.group.libraryapp.controller.study.controller;
    
    import com.group.libraryapp.controller.study.service.FruitService;
    import com.group.libraryapp.dto.study.d2.FruitSaveRequest;
    import com.group.libraryapp.dto.study.d2.FruitStatusResponse;
    import com.group.libraryapp.dto.study.d2.FruitSellRequest;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    
    @RestController
    public class FruitController {
    
        private final FruitService fruitService;
    
        public FruitController(FruitService fruitService) {
            this.fruitService = fruitService;
        }
    
        @PostMapping("/api/v1/fruit")
        public void saveFruit(@RequestBody FruitSaveRequest request) {
            fruitService.saveFruit(request);
        }
    
        @PutMapping("/api/v1/fruit")
        public void sellFruit(@RequestBody FruitSellRequest request) {
            fruitService.sellFruit(request);
        }
    
        @GetMapping("/api/v1/fruit/stat")
        public FruitStatusResponse searchFruitStat(@RequestParam String name) {
            return fruitService.searchFruitStat(name);
        }
    }

     

    package com.group.libraryapp.controller.study.service;
    
    import com.group.libraryapp.domain.study.Fruit;
    import com.group.libraryapp.domain.study.FruitRepository;
    import com.group.libraryapp.dto.study.d2.FruitSaveRequest;
    import com.group.libraryapp.dto.study.d2.FruitStatusResponse;
    import com.group.libraryapp.dto.study.d2.FruitSellRequest;
    import org.springframework.stereotype.Service;
    
    @Service
    public class FruitService {
    
        private final FruitRepository fruitRepository;
    
        public FruitService(FruitRepository fruitRepository) {
            this.fruitRepository = fruitRepository;
        }
    
        public void saveFruit(FruitSaveRequest request) {
            fruitRepository.save(new Fruit(request.getName(), request.getWarehousingDate(), request.getPrice()));
        }
    
        public void sellFruit(FruitSellRequest request) {
            Fruit fruit = fruitRepository.findById(request.getId()).orElseThrow(IllegalArgumentException::new);
            fruit.updateSold(true);
            fruitRepository.save(fruit);
        }
    
        public FruitStatusResponse searchFruitStat(String name) {
            Long count = countFruit(name);
    
            if (count<=0) throw new IllegalArgumentException();
    
            Long salesAmount = fruitRepository.sumPriceBySoldIsTrueAndName(name);
            Long notSalesAmount = fruitRepository.sumPriceBySoldIsFalseAndName(name);
    
            FruitStatusResponse fruitStatusResponse = new FruitStatusResponse();
            fruitStatusResponse.setSalesAmount(salesAmount);
            fruitStatusResponse.setNotSalesAmount(notSalesAmount);
    
            return fruitStatusResponse;
        }
    
        public Long countFruit(String name) {
            return fruitRepository.countByName(name);
        }
    }
    package com.group.libraryapp.domain.study;
    
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.data.jpa.repository.Query;
    
    public interface FruitRepository extends JpaRepository<Fruit, Long> {
    
        Long countByName(String name);
        
        @Query("SELECT SUM(price) FROM Fruit WHERE name = ?1 AND sold = true")
        Long sumPriceBySoldIsTrueAndName(String name);
        
        @Query("SELECT SUM(price) FROM Fruit WHERE name = ?1 AND sold = false")
        Long sumPriceBySoldIsFalseAndName(String name);
    }

    수정을 완료했으니 POSTMAN으로 실행을 해보겠다.

     

    save에서 에러가 발생했다.🥲

    sold필드가 있는데 Unknown column 'sold' in 'field list' 에러가 발생..

    데이터베이스에는 sold가 tinyInt인데 객체에는 boolean으로 되어있어서 그런가 싶어서 int로 바꿔보았으나 실패, 생성자에 sold가 안 들어가서 그런가 싶어서 sold를 넣어줘도 실패. 알고 보니 객체 클래스 이름을 'Fruit'로 해서 'fruit'테이블과 매핑되고 있었다..ㅎㅎ

    내가 매핑해야 될 테이블은 'study_fruit'이기 때문에 @Table(name = "study_fruit)를 추가해줬다.

     

    다시 실행하니 이번엔 Unknown column 'warehousing_date' in 'field list' 에러가 발생했다.

    데이터베이스의 필드명도 warehousingDate, 객체 필드명도 warehousingDate인데 왜 갑자기 warehousing_date에 매핑을 하지?

    warehousingDate필드에 @Column(name = "warehousingDate') 속성을 주고 실행 해 봐도 계속 warehousing_date로 매핑이 된다.

    찾아보니 Hibernate는 특정한 네이밍 전략을 따라서 엔티티 필드와 데이터베이스 컬럼을 매핑할 때 기본적으로 카멜 케이스를 스네이크 케이스로 변환해서 매핑한다고 한다.

    네이밍 전략을 바꾸는 방법은 application.yml파일에 설정하는 것이다.

    jpa:
      hibernate:
        naming:
            implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
            physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

    Hibername 네이밍룰을 지정해 줬다. ImplicitNamingStrategyJpaCompliantImpl은 보통 카멜 케이스를 스네티크 케이스로 변환하는데 사용자가 직접 이 전략을 설정하면 Hibernate가 카멜 케이스를 스네이크 케이스로 변환하지 않는다고 한다.

     

     

    searchFruitStat(특정 과일을 기준으로 팔린 금액, 팔리지 않은 금액을 조회) 쿼리를 작성할 때 기존에는 한 쿼리에서 그룹으로 나눈 뒤 그룹명을 지정해서 해당 그룹명과 필드명을 매핑해서 금액을 반환해 줬지만 이번에는 팔린 금액, 팔리지 않은 금액 쿼리를 따로 나눴다.

    그리고 sumPriceBySold는 네이밍 규칙과 매핑되지 않아서 실행 시 오류가 발생하길래 @Query를 사용해서 직접 쿼리를 작성했다.

    결과적으로는 성공!! 


    문제2

    가게를 거쳐갔던 과일의 개수는..?

    name을 조건으로 데이터의 개수를 반환하는 거니까 Spring Data JPA의 countByName을 사용하면 결과가 나올 것 같다. 코드를 만들어보자.

    어라? 근데 이미 데이터 유무를 판단하기 위해 Long CountByName(String name) 메서드를 이미 Repository에 만들어 뒀다.

    api가 해당 쿼리를 호출하도록 해보겠다. 순서대로 Controller, Service, Repository이다.

    @GetMapping("/api/v1/fruit/count")
    public ResponseEntity<Map<String,Integer>> countFruit(@RequestParam String name) {
        Long count = fruitService.countFruit(name);
    
        Map<String, Integer> response = new HashMap<>();
        response.put("count", count.intValue());
        return ResponseEntity.ok().body(response);
    }
    public Long countFruit(String name) {
        return fruitRepository.countByName(name);
    }
    Long countByName(String name);

    Http응답이 JSON형태이기 때문에 반환값 정수(Long)를 컨트롤러에서 직접 JSON 형식으로 변환해주었다.

     


    문제3

    판매되지 않은 특정 금액 이상 혹은 특정 금액 이하의 과일 목록을 받아오기.

    하나의 API에서 option값에 따라 이상, 이하가 결정되기 때문에 option값으로 분기 처리 후 쿼리를 호출했다.

    Fruit객체에서 name, price, warehousingDate 필드만 반환하고 싶기 때문에 interface를 만들어서 해당 interface를 반환해 줬다.

    package com.group.libraryapp.domain.study;
    
    import java.time.LocalDate;
    
    public interface FruitNotSalesMapping {
    
        String getName();
        Long getPrice();
        LocalDate getWarehousingDate();
    }
    @GetMapping("/api/v1/fruit/list")
    public List<FruitNotSalesMapping> notSalesList(@RequestParam String option, @RequestParam long price) {
        return fruitService.notSalesList(option, price);
    }
    public List<FruitNotSalesMapping> notSalesList(String option, long price) {
        if (option.equals("GTE")) {
            return fruitRepository.findFruitBySoldFalseAndPriceGreaterThanEqual(price);
        } else {
            return fruitRepository.findFruitBySoldFalseAndPriceLessThanEqual(price);
        }
    }
    List<FruitNotSalesMapping> findFruitBySoldFalseAndPriceGreaterThanEqual(Long price);
    List<FruitNotSalesMapping> findFruitBySoldFalseAndPriceLessThanEqual(Long price);

     

    성공했다!!


    JPA는 처음 사용해 봐서 과제하는데 너무 어려웠다..😢

     

    Spring Data JPA가 제공하는 쿼리 네이밍 룰을 어떻게 사용해야 되는지 몰라서 처음에는 마음대로 쿼리명을 적었더니 네이밍 규칙과 매핑되지 않아서 에러가 발생하거나 원하는 결과가 나오지 않았다.

    찾아보니 그럴 때는 @Query 어노테이션으로 직접 쿼리를 작성하면 해결된다고 나와 있었다.

    //완성되지 않은 코드
    @Query("SELECT name, price, warehousingDate FROM Fruit WHERE sold = false AND price >= ?1")
    List<FruitNotSalesMapping> findNotSalesFruitByPriceGreaterThanEqual(Long price);
    
    @Query("SELECT name, price, warehousingDate FROM Fruit WHERE sold = false AND price <= ?1")
    List<FruitNotSalesMapping> findNotSalesFruitByPriceLessThanEqual(Long price);

     

    @Query에 직접 쿼리를 작성해서 넘어가려고 했는데 뭔가 찜찜했다..

    직접 쿼리를 작성하지 않으려고 JPA를 쓰는 건데 @Query를 쓰면 직접 쿼리를 작성하는 거랑 다른게 뭐지???🧐

    이상하다.........  이게 아닌 거 같은데??

     

    생각해 보니 findFruit가 Fruit를 찾는 거고, 팔리지 않았다는 건 조건이니까 By 뒤에 붙어야 될 것 같았다.

    List<FruitNotSalesMapping> findFruitBySoldFalseAndPriceGreaterThanEqual(Long price);
    List<FruitNotSalesMapping> findFruitBySoldFalseAndPriceLessThanEqual(Long price);

    이렇게 하니까 @Query를 안 써도 원하는 결과값이 나왔다!!

     

    총합을 구하는 쿼리도 수정하고 싶은데 네이밍을 어떻게 줘야되는지 해결 방법을 찾지 못했다..

    @Query("SELECT SUM(price) FROM Fruit WHERE name = ?1 AND sold = true")
    Long sumPriceBySoldIsTrueAndName(String name);
    @Query("SELECT SUM(price) FROM Fruit WHERE name = ?1 AND sold = false")
    Long sumPriceBySoldIsFalseAndName(String name);

    오늘은 시간이 너무 오래 걸려서 과제 마감 시간을 지키지 못했다.

    마지막 과제였는데 너무 아쉽다... 😭