강의
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
자바와 스프링 부트로 생애 최초 서버 만들기 [서버 개발 올인원 패키지] / 최태현 / 인프런 강의
'activity > 인프런 워밍업 클럽' 카테고리의 다른 글
[인프런 워밍업클럽 0기 BE] 세 번째 발자국 (3주차 회고) (0) | 2024.03.10 |
---|---|
[인프런 워밍업클럽 0기 BE] 첫 번째 발자국 (1주차 회고) (0) | 2024.03.03 |
[인프런 워밍업 클럽 0기] 여섯 번째 과제 - API 역할 분리(Controller, Service, Repository) (0) | 2024.02.25 |
[인프런 워밍업 클럽 0기] 다섯 번째 과제 - Clean Code(클린코드) (0) | 2024.02.23 |
[인프런 워밍업 클럽 0기] 네 번째 과제 - API 개발하기 (0) | 2024.02.22 |