본문 바로가기
activity/인프런 워밍업 클럽

[인프런 워밍업 클럽 0기] 일곱 번째 과제 - JPA 실습

by dani0312 2024. 2. 27.

 

강의

Spring Data JPA를 사용한 데이터베이스 조작

 

문자열 SQL의 한계점, 예를 들면 반복작업이 많다는 것 등의 SQL을 직접 작성할 시 문제점들에 대해 알아보았다.

그러며 보다 편리한 JPA가 등장하게 되었고 이와 관련된 영속성, Hibernate 등의 이론적인 내용에 대해 학습할 수 있었다.

기존에는 Jdbc를 이용하여 SQL문을 직접 작성하였지만, JPA를 이용하여 편리하게 쿼리가 자동으로 작성되도록 기존 코드를 리팩토링하였다. Spring Data JPA를 이용하여 데이터를 Create, Read, Update, Delete 하는 것을 실습하였다.


과제

◾문제1

◾풀이

🔻entity

  기존 Fruit클래스에서 @Entity , ID필드 + @Id,@GeneratedValue, 기본생성자를 추가하였다.

@Entity
public class Fruit {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private LocalDate warehousingDate;
    private long price;
    private boolean isSold;
    public void setIsSold(boolean isSold){
        this.isSold = isSold;
    }

    public Fruit(){}
    public Fruit(String name, LocalDate warehousingDate, long price) {
        this.name = name;
        this.warehousingDate = warehousingDate;
        this.price = price;
        this.isSold = false; //기본값이 자동으로 false 값을 갖도록 한다.
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public LocalDate getWarehousingDate() {
        return warehousingDate;
    }

    public long getPrice() {
        return price;
    }

    public boolean isSold() {
        return isSold;
    }

}

 

🔻controller

@RestController
@RequestMapping("/api/v3") //3번째(과제7) 컨트롤러이므로 v3로 변경
public class FruitControllerDay7 {
    private final FruitService_Day7 fruitServiceDay7;

    public FruitControllerDay7(FruitService_Day7 fruitServiceDay7) {
        this.fruitServiceDay7 = fruitServiceDay7;
    }

    // 과일 정보 저장 API
    @PostMapping("/fruit")
    public void saveFruit(@RequestBody FruitCreateRequest request){
        fruitServiceDay7.saveFruit(request.getName(),request.getWarehousingDate(),request.getPrice());
    }

    // 과일 정보 변경 API
    @PutMapping("/fruit")
    public void updateFruit(@RequestBody FruitUpdateRequest request) {
        fruitServiceDay7.updateFruit(request.getId());
    }

    // 팔린 금액과 팔리지 않은 금액 조회 API
    @GetMapping("/fruit/stat")
    public GetSalesAmountResponse getSalesAmount(@RequestParam String name){
        return fruitServiceDay7.getSalesAmount(name);
    }
}

 

🔻service

@Service
public class FruitService_Day7 {

    private final FruitRepository_Day7 fruitRepositoryDay7;

    public FruitService_Day7(FruitRepository_Day7 fruitRepositoryDay7) {
        this.fruitRepositoryDay7 = fruitRepositoryDay7;
    }
    public void saveFruit(String name, LocalDate warehousingDate,long price){
        fruitRepositoryDay7.save(new Fruit(name, warehousingDate, price));
    }
    public void updateFruit(Long id){
        Fruit fruit = fruitRepositoryDay7.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 과일의 ID를 찾을 수 없습니다."));
        fruit.setIsSold(true);
        fruitRepositoryDay7.save(fruit);
    }
    public GetSalesAmountResponse getSalesAmount(String name) {
        Long soldPrice = fruitRepositoryDay7.getTotalSoldPrice(name);
        Long notSoldPrice = fruitRepositoryDay7.getTotalNotSoldPrice(name);

        return new GetSalesAmountResponse(soldPrice, notSoldPrice);
    }

}

 

🔻Repository

public interface FruitRepository_Day7 extends JpaRepository<Fruit,Long> {
    @Query("SELECT SUM(f.price) FROM Fruit f WHERE f.name = :name AND f.isSold = true")
    Long getTotalSoldPrice(@Param("name") String name);

    @Query("SELECT SUM(f.price) FROM Fruit f WHERE f.name = :name AND f.isSold = false")
    Long getTotalNotSoldPrice(@Param("name") String name);
}

 

⚠️필드 에러 문제

`LocalDate warehousingDate` 이 필드 때문에 unknown_column 같은 에러가 계속 나왔다. 

해결과정은 따로 정리하였다. 

2024.02.28 - [웹 개발/error] - [error/jpa] SQL에러 Unknown Column in field list

 

[error/jpa] SQL에러 Unknown Column in field list

◾에러 파악하기 ✔️Background 기존에는 Jdbc를 이용하여 API개발을 진행하다가, Spring Data JPA를 사용하기 위한 코드로 리팩토링하였다. 리팩토링은 엔터티 한 개에 `@Entity`를 붙여 테이블과 매핑을

dani0312.tistory.com


◾문제2

◾풀이

🔻controller

@RestController
@RequestMapping("/api/v3") 
public class FruitControllerDay7 {
    
    ...
    // DAY7 특정 과일 모든 기록 개수
    @GetMapping("/fruit/count")
    public GetFruitCountResponse getFruitCount(@RequestParam String name) {
        return fruitServiceDay7.getFruitCount(name);
    }
}

 

🔻service

@Service
public class FruitService_Day7 {
	...
        public GetFruitCountResponse getFruitCount(String name) {
        Long fruitCount = fruitRepositoryDay7.countByName(name);
        return new GetFruitCountResponse(fruitCount);
    }

}

 

🔻Repository

public interface FruitRepository_Day7 extends JpaRepository<Fruit,Long> {
	...
    Long countByName(String name);
}

 

Postman으로 사과라는 값을 쿼리로 전송하였을 시 3이라는 응답이 잘 전송된다. 실제 데이터베이스에 사과는 3개이므로 값이 잘 계산된다는 것을 확인할 수 있다.

 

 

⚠️406 에러

응답 Dto에 Getter를 빼먹어서 생긴 에러이다. dto에 왜 getter가 필요할지에 대해 에러 해결과 함께 정리하였다.

2024.02.29 - [웹 개발/spring] - [spring] dto클래스에 Getter가 필요한 이유

 

[spring] dto클래스에 Getter가 필요한 이유

📌DTO 클래스에 Getter가 필요한 이유 ◾DTO란 DTO(Data Transfer Object)란 데이터를 이동(Transfer)시키기 위한 객체이다. Client가 Controller에 요청을 보낼 때 RequestDto의 형식으로 데이터를 객체로 받고, contro

dani0312.tistory.com


◾문제3

◾풀이

문제에서 option의 값은 LTE 또는 GTE만 받는 것이므로 enum을 활용했다. 이런 경우 enum을 활용하면 코드도 깔끔해지고 유지보수도 쉬워진다고 한다.

 

🔻eunm

public enum ComparisonOption {
    LTE, // Less Than Or Equal
    GTE // Greater Than or Equal
}

 

🔻dto

public class GetFruitCountResponse {
    private Long count;
    public GetFruitCountResponse(Long count) {
        this.count = count;
    }

    public Long getCount() {
        return count;
    }
}
public class GetFruitsByAmountRangeRequest {
    private ComparisonOption option;
    private Long price;

    public GetFruitsByAmountRangeRequest(ComparisonOption option, Long price) {
        this.option = option;
        this.price = price;
    }

    public ComparisonOption getOption() {
        return option;
    }

    public Long getPrice() {
        return price;
    }
}

 

🔻controller

@RestController
@RequestMapping("/api/v3") 
public class FruitControllerDay7 {
    
    ...
    // 특정 금액 이상 또는 이하의 과일 목록 조회
    @GetMapping("/fruit/list")
    public List<GetFruitResponse> getFruitsByAmountRange(GetFruitsByAmountRangeRequest request){
        return fruitServiceDay7.getFruitsByAmountRange(request.getOption(), request.getPrice());
    }
}

 

🔻service

@Service
public class FruitService_Day7 {
	...
    public List<GetFruitResponse> getFruitsByAmountRange(ComparisonOption option, Long price){
        List<Fruit> fruits;
        if (option == ComparisonOption.LTE)
            fruits = fruitRepositoryDay7.findByPriceLessThanEqual(price);
        else
            fruits = fruitRepositoryDay7.findByPriceGreaterThanEqual(price);
        
        List<GetFruitResponse> response = new ArrayList<>();
        for (Fruit fruit : fruits) {
            response.add(new GetFruitResponse(fruit.getName(),fruit.getPrice(),fruit.getWarehousingDate()));
        }
        return response;
    }
}

 

🔻repository

public interface FruitRepository_Day7 extends JpaRepository<Fruit,Long> {
	...
    List<Fruit> findByPriceGreaterThanEqual(Long price);
    List<Fruit> findByPriceLessThanEqual(Long price);
}

 

 

🔨 리팩토링

초기에는 option의 값이 LTE또는 GET가 아닌 값을 쿼리 값에 넣었을 때 (예를 들면 `?option=abc`) 에러 처리를 해주어야 한다고 생각하여 아래와 같이 else문을 따로 구분을 해주었다. 그러나 생각해보니 enum값으로 받도록 해주었기 때문에 아래 2가지 케이스 모두 확인한 결과 Bad Request에러가 발생한다.

 

➡️ 즉 굳이 아래처럼 else문으로 따로 처리해주지 않아도 Enum으로 값을 받도록 하면 시스템에서 자체적으로 값 체크를 해서 에러를 반환해준다!

 //before
	if (option == ComparisonOption.LTE) { 
            fruits = fruitRepositoryDay7.findByPriceLessThanEqual(price);
        } else if (option == ComparisonOption.GTE) {
            fruits = fruitRepositoryDay7.findByPriceGreaterThanEqual(price);
        } else {
            throw new IllegalArgumentException();
        }
//after
	if (option == ComparisonOption.LTE) { 
            fruits = fruitRepositoryDay7.findByPriceLessThanEqual(price);
        } else {
            fruits = fruitRepositoryDay7.findByPriceGreaterThanEqual(price);
        }

 

POSTMAN에서 option=GET, price=6000으로 API를 요청하였고, 결과를 정상적으로 응답받았다.

 

우선 다하긴 했는데 service부분을 stream이나 뭔가를 이용하면 더 코드를 간결히 리팩토링 할 수 있을 것 같다. 오늘은 dto에 getter가 필요한 이유 등 이것저것 파고들었더니 시간이 조금 지연이 되어서 마무리하였다. 조만간 시간이 될 때 다시 리팩토링하러 와야겠다.

 

전날 컨디션이 별로 안 좋은채로 서울로 졸업식을 다녀왔더니 아직 회복이 안되어서 그 날 과제 마감기한을 지키지 못하였다. 안타깝다ㅠ 

 

 

잘못된 내용이나 더 좋은 의견이 있다면 댓글로 알려주시면 감사하겠습니다❤️

좋은 하루 되세요😊


Reference

자바와 스프링 부트로 생애 최초 서버 만들기 [서버 개발 올인원 패키지] / 최태현 / 인프런 강의


/* 내가 추가한 코드 */ /* 내가 추가한 코드 끝끝 */