[인프런 워밍업 클럽] 과제7

Jiwon·2024년 5월 16일
0
post-thumbnail

📚 인프런 강의와 스터디를 바탕으로 한 과제를 풀이한 포스트입니다.

Spring Data JPA를 이용해 자동으로 쿼리 날림으로서 여러 기능 구현하기

<문제1> 기존의 Controller-Service-Repository가 JPA를 이용해 동작하도록 변경하기

<풀이>

  • 먼저 Fruit 테이블에 대응되는 Entity Class 만든다. 어떤 parameter도 받지 않는 기본 생성자를 protected로 작성했고, saveFruit을 위해 name, warehousing_date, price를 받는 생성자를 따로 만들었다. 판매 여부 purchase의 default 값은 false로 지정했다.
  • updateFruit 함수는 id를 받아 purchase 값을 true로 저장한다.

** fruit 테이블 명세

create table fruit ( 
	id bigint auto_increment,
	name varchar(20), 
    warehousing_date date,
	price bigint,
    purchase boolean default false,
    primary key (id)
); 

** @Entity 클래스 Fruit

@Entity
public class Fruit {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String name;
    @Column
    private LocalDate warehousing_date;
    @Column
    private long price;
    @Column
    private boolean purchase;

    public String getName() {
        return name;
    }

    public LocalDate getWarehousing_date() {
        return warehousing_date;
    }

    public long getPrice() {
        return price;
    }

    public boolean isPurchase() {
        return purchase;
    }

    protected Fruit() {
    }

    public Fruit(String name, LocalDate warehousing_date, long price) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException(String.format("잘못된 name(%s)이 들어왔습니다", name));
        }
        this.name = name;
        this.warehousing_date = warehousing_date;
        this.price = price;
        this.purchase = false;
    }

    public void updateFruit(long id) {
        this.purchase = true;
    }
}
  • 기존의 FruitServiceFruitServiceV1으로 바꾸고, JPA를 이용해 db를 조작하기 위해 새로 FruitServiceV2를 만들었다.

**FruitServiceV2

@Service
public class FruitServiceV2 {

    private final FruitRepository fruitRepository;

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

    // C
    public void saveFruit(FruitCreateRequest request) {
        Fruit fruit = fruitRepository.save(new Fruit(request.getName(),
                request.getWarehousingDate(), request.getPrice()));

    }

    // R
    public FruitResponse getFruit(String name) {
        ...
    }

    public FruitCount getFruitCount(String name) {
        ...
    }

    public List<FruitList> getFruitList(String option, long price) {
    	...
    }

    // U
    public void updateFruit(FruitUpdateRequest request) {

        Fruit fruit = fruitRepository.findById(request.getId())
                .orElseThrow(IllegalArgumentException::new);
        fruit.updateFruit(request.getId());
        fruitRepository.save(fruit);

    }
}
  • 이때 FruitRepository는 다음과 같이 JpaRepository를 상속받는다. 테이블의 매핑 객체인 Fruit과 Fruit 테이블의 id인 Long 타입을 각각 적어주었다.

** FruitRepository

public interface FruitRepository extends JpaRepository<Fruit, Long> {
    List<Fruit> findAllByName(String name);
    List<Fruit> findAllByPurchaseAndPriceGreaterThanEqual(boolean purchase, long price);
    List<Fruit> findAllByPurchaseAndPriceLessThanEqual(boolean purchase, long price);
    List<Fruit> findAllByNameAndPurchase(String name, boolean purchase);
}
  • 기존의 FruitControllerFruitServiceV2를 이용할 수 있도록 변경했다.

** FruitController

@RestController
public class FruitController {

    public FruitController(FruitServiceV2 fruitServiceV2) {
        this.fruitServiceV2 = fruitServiceV2;
    }
    
    // ...

}
  • 기존의 기능인 save(과일 저장), get(특정 과일 기준으로 판매 금액, 판매되지 않은 금액 조회), update(판매 여부 반영)가 동작하는지 살펴보자. (save와 update는 기존 코드와 크게 달라지지 않아 생략)

** FruitServiceV2의 getFruit

public FruitResponse getFruit(String name) {
        long salesAmount = 0;
        long notSalesAmount = 0;

        List<Fruit> fruits = fruitRepository.findAllByName(name);
        if (fruits.isEmpty()) {
            throw new IllegalArgumentException();
        }

        for (Fruit fruit : fruits) {
            if (fruit.isPurchase()) {
                salesAmount += fruit.getPrice();
            }
            else {
                notSalesAmount += fruit.getPrice();
            }
        }

        return new FruitResponse(salesAmount, notSalesAmount);

    }
  • 입력받은 이름을 기준으로 모든 과일 데이터를 가져오고 Fruit 리스트에 담아준다.
  • 조회 결과가 존재하지 않는 경우 예외처리를 해준다. JPA에서 findAll()의 메소드는 List<> 형태로 return하기 때문에 return 값이 없다는 것을 확인하려면 isEmpty() 메소드를 활용해야 한다.
  • return 받은 여러 개의 fruit 데이터들에 대해 판매 여부를 확인해서 판매되었다면 그 과일 데이터의 가격을 salesAmount에 더하고, 판매되지 않았다면 notSalesAmount에 더한다.
  • salesAmount, notSalesAmount를 멤버 변수로 갖는 클래스 FruitResponse를 따로 만들어 생성자를 return하면 원하는 출력 결과를 얻을 수 있다.

** FruitResponse

public class FruitResponse {
    private long salesAmount;
    private long notSalesAmount;

    public FruitResponse(long salesAmount, long notSalesAmount) {
        this.salesAmount = salesAmount;
        this.notSalesAmount = notSalesAmount;
    }
    
    public FruitResponse(Fruit fruit) {
        this.salesAmount = salesAmount;
        this.notSalesAmount = notSalesAmount;

    }

    public long getSalesAmount() {
        return salesAmount;
    }

    public long getNotSalesAmount() {
        return notSalesAmount;
    }

}

<실행 결과>

<문제 2> 특정 과일을 기준으로 팔린 과일의 개수 세기

<풀이>

  • FruitControllergetFruitCount 함수
// hw6 #q2
    @GetMapping("/api/v1/fruit/count")
    public FruitCount getFruitCount(@RequestParam String name) {
        return fruitServiceV2.getFruitCount(name);
    }
  • FruitServiceV2getFruitCount 함수
public FruitCount getFruitCount(String name) {
        List<Fruit> fruits = fruitRepository.findAllByNameAndPurchase(name, true);
        if (fruits.isEmpty()) {
            throw new IllegalArgumentException();
        }

        long salesCount = 0;
        for (Fruit fruit : fruits) {
            salesCount += 1;
        }

        return new FruitCount(salesCount);
    }

과일 데이터에서 이름과 판매 여부가 true(판매되었음)을 조건으로 SQL 쿼리를 날린다. 조건에 해당하는 과일 데이터들을 for문으로 돌면서 해당하는 데이터들의 개수를 count한다. count를 멤버 변수로 갖는 클래스 FruitCount를 만들어 생성자를 return하면 원하는 출력 결과를 얻을 수 있다.

** FruitCount

public class FruitCount {

    private long count;

    public long getCount() {
        return count;
    }

    public FruitCount(long count) {
        this.count = count;
    }
}

<실행 결과>

<문제 3> 판매되지 않은 특정 금액 이상/이하의 과일 목록 조회하기

<풀이>

  • FruitControllergetFruitCount 함수
// hw6 #q3
    @GetMapping("/api/v1/fruit/list")
    public List<FruitList> getFruitList(@RequestParam String option, Integer price) {
        return fruitServiceV2.getFruitList(option, price);
    }
  • FruitServiceV2getFruitList 함수
public List<FruitList> getFruitList(String option, long price) {

        List<FruitList> fruitLists = new ArrayList<>();
        if (option.equals("GTE")) {
            List<Fruit> fruits = fruitRepository.findAllByPurchaseAndPriceGreaterThanEqual(false, price);
            for (Fruit fruit: fruits) {
                fruitLists.add(new FruitList(fruit));
            }
            return fruitLists;

        }
        else if (option.equals("LTE")){
            List<Fruit> fruits = fruitRepository.findAllByPurchaseAndPriceLessThanEqual(false, price);
            for (Fruit fruit: fruits) {
                fruitLists.add(new FruitList(fruit));
            }

            return fruitLists;
        }
        else { throw new IllegalArgumentException(); }

    }
  • option 문자열을 확인해서 findAllByPurchaseAndPriceGreaterThanEqual 혹은 findAllByPurchaseAndPriceLessThanEqual를 통해 쿼리를 날린다. 이때 판매되지 않은 과일 목록을 select해야 하므로 parameter로 boolean purchase = false를 넣어줬다.
  • 문제에 제시된 출력 결과에서는 name, price, warehousingDate만 제시되어 있었기에 단순히 Fruit을 return하는 것으로는 같은 출력 결과를 낼 수 없었다. purchase 필드까지 함께 출력되었기 때문이다.
  • 즉, 특정 필드를 제거한 결과만 return해야 하는데, 특정 조건으로 필터링을 하는 메소드는 있었지만 특정 필드만 제거하는 메소드는 찾을 수가 없어서 새로운 클래스 FruitList를 만들었다.
  • FruitList 클래스는 name, price, warehousingDate만을 멤버 변수로 갖기 때문에, 이를 리턴하면 원하는 필드 값만 출력할 수 있다.
  • 조회된 과일 데이터들을 한 개씩 살펴보며 클래스 FruitList를 생성자를 통해 만들고 리스트 배열에 add(추가)한다. 최종적으로는 FruitList 타입의 리스트를 리턴한다.

** FruitList

public class FruitList {

    private String name;
    private long price;
    private LocalDate warehousingDate;

    public FruitList(Fruit fruit) {
        this.name = fruit.getName();
        this.price = fruit.getPrice();
        this.warehousingDate = fruit.getWarehousing_date();

    }

    public FruitList(String name, long price, LocalDate warehousingDate) {
        this.name = name;
        this.price = price;
        this.warehousingDate = warehousingDate;
    }

    public String getName() {
        return name;
    }

    public long getPrice() {
        return price;
    }

    public LocalDate getWarehousingDate() {
        return warehousingDate;
    }
}

<실행 결과>

<정리하며>

  • 확실히 문자열 SQL을 직접 사용하는 것보다 훨씬 편리하다. 특히 Select문은 조건에 따라 길이가 길어지기도 해서 코드 작성할 때 불편했는데, 간결하게 작성할 수 있다는 점이 좋았다. 지금은 기본으로 제공되는 메소드만 이용했는데, 직접 쿼리를 작성할 수도 있다고 해서 다음에 쓸 일이 생기면 그렇게도 이용해보고 싶다.
  • 자바 객체의 필터링 때문에 시간이 많이 걸렸는데, 해결이 돼서 다행이다. 자바는 쓰면 쓸수록 객체를 얼마나 잘 다룰 수 있는지가 코드의 간결성을 결정하는지 더 잘 느껴진다.
  • Fruit API로 이렇게 오래 과제를 할 줄 알았으면 처음부터 패키지와 클래스 분리에 신경썼을텐데 처음에 패키지 구분 없이 원래 하던 패키지에 새로 클래스 만들었던 게 오늘 과제가 되어서야 후회됐다. 소규모의 프로젝트여도 패키지 구분, 클래스 구분은 필수라는 점을 다시 한번 유념하자.

[Reference]

profile
écoute les murmures du cœur

0개의 댓글

관련 채용 정보