인프런 워밍업 클럽 백엔드 - 일곱 번째 과제
문제 1
과제 #6에서 만들었던 Fruit 기능들을 JPA를 이용하도록 변경해보세요.
먼저 domain 패키지를 만들어서 Fruit와 FruitRepository를 생성했습니다.
Fruit
@Entity
public class Fruit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 20)
private String name;
private LocalDate warehousingDate;
private Long price;
private boolean stat = false;
protected Fruit() {
}
public Fruit(String name, LocalDate warehousingDate, Long price) {
this.name = name;
this.warehousingDate = warehousingDate;
this.price = price;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public LocalDate getWarehousingDate() {
return warehousingDate;
}
public Long getPrice() {
return price;
}
public boolean isStat() {
return stat;
}
public void updateFruitStat(boolean stat) {
this.stat = stat;
}
}
FruitRepository
public interface FruitRepository extends JpaRepository<Fruit, Long> {
Optional<List<Fruit>> findAllByName(String name);
}
이름으로 검색할 수 있는 메소드, findByAllName
을 생성해줍니다.
기존에 있던 서비스를 수정했습니다.
FruitServiceV2
@Service
public class FruitServiceV2 {
private final FruitRepository fruitRepository;
public FruitServiceV2(FruitRepository fruitRepository) {
this.fruitRepository = fruitRepository;
}
public void saveFruit(FruitRequest request) {
fruitRepository.save(new Fruit(request.getName(), request.getWarehousingDate(), request.getPrice()));
}
public void soldFruit(FruitRequest request) {
Fruit fruit = fruitRepository.findById(request.getId())
.orElseThrow(IllegalArgumentException::new);
fruit.updateFruitStat(true);
fruitRepository.save(fruit);
}
public FruitSaleResponse readFruitSoldPrice(String name) {
List<Fruit> fruits = fruitRepository.findAllByName(name)
.orElseThrow(IllegalArgumentException::new);
long salesAmount = 0;
long notSalesAmount = 0;
for (Fruit fruit : fruits) {
if (fruit.isStat()) {
salesAmount += fruit.getPrice();
} else {
notSalesAmount += fruit.getPrice();
}
}
return new FruitSaleResponse(salesAmount, notSalesAmount);
}
}
이렇게 작성하고 실행했을 때 문제가 발생합니다.
java.sql.SQLSyntaxErrorException: Unknown column 'warehousing_date' in 'field list'
이런 에러가 발생하는데 기본적으로 쿼리를 요청할 때 스네이크 케이스를 기준으로 요청하기 때문입니다. 데이터베이스에서 fruit 테이블을 생성할 때 컬럼명을 카멜케이스로 작성했기 때문에 에러가 발생합니다. 이 문제를 해결하기 위해서 application.yml
에 네이밍 전략을 추가하도록 하겠습니다.
spring:
jpa:
hibernate:
ddl-auto: none
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
properties:
hibernate:
show_sql: true
format_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
physical-strategy
를 org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
로 설정하면 정상적으로 작동하게 됩니다.
문제 2
우리는 특정 과일을 기준으로 지금까지 우리 가게를 거쳐갔던 과일 개수를 세고 싶습니다.
<문제 1>에서 만들었던 과일 Entity Class를 이용해 기능을 만들어 보세요!
예를 들어
(1, 사과, 3000원, 판매 O)
(2, 바나나, 4000원, 판매 X)
(3, 사과, 3000원, 판매 O)
와 같은 세 데이터가 있고, 사과를 기준으로 과일 개수를 센다면, 우리의 API는 2를 반환할 것입니다.
구체적인 스펙은 다음과 같습니다.
HTTP method :
GET
HTTP path :
/api/v1/fruit/count
HTTP query
name : 과일 이름
예시
GET /api/v1/fruit/count?name=사과
HTTP 응답 Body
{
"count": long
}
HTTP 응답 Body 예시
{
"count": 2
}
저는 판매된 특정 과일만 count 했습니다.
먼저 컨트롤러에 추가해줍니다.
Controller
@GetMapping("/api/v1/fruit/count")
public FruitSoldCountResponse countSoldFruit(String name) {
return fruitService.countSoldFruit(name);
}
Repository
Integer countByNameAndStat(String name, boolean stat);
리포지토리에 이름과 판매 상태로 조회하는 메소드를 작성합니다. SELECT * FROM fruit WHERE name = ? AND stat = ?
Service
public FruitSoldCountResponse countSoldFruit(String name) {
return new FruitSoldCountResponse(fruitRepository.countByNameAndStat(name, true));
}
판매된 과일만 조회하기 때문에 stat을 받는 부분에 true를 넣었습니다. 그리고 Body 형태로 받기 위해 Response를 하나 생성해주었습니다.
Response
public class FruitSoldCountResponse {
private Integer count;
public FruitSoldCountResponse(Integer count) {
this.count = count;
}
public Integer getCount() {
return count;
}
}
예시대로 실행했을 때,
결과가 잘 나오는 것을 볼 수 있습니다.
문제 3
우리는 아직 판매되지 않은 특정 금액 이상 혹은 특정 금액 이하의 과일 목록을 받아보고 싶습니다.
구체적인 스펙은 다음과 같습니다.
HTTP method :
GET
HTTP path :
/api/v1/fruit/list
HTTP query
option : "GTE" 혹은 "LTE"라는 문자열이 들어온다.
GTE : greater than equal의 의미
LTE : less than equal의 의미
price : 기준이 되는 금액이 들어온다.
예시 1 -
GET /api/v1/fruit/list?option=GTE&price=3000
판매되지 않은 3000원 이상의 과일 목록을 반환해야 한다.
예시 2 -
GET /api/v1/fruit/list?option=LTE&price=5000
판매되지 않은 5000원 이하의 과일 목록을 반환해야 한다.
HTTP 응답 Body
[{
"name": String,
"price": long,
"warehousingDate": LocalDate,
}, ...]
HTTP 응답 Body 예시
[
{
"name": "사과",
"price": 4000,
"warehousingDate": "2024-01-05",
},
{
"name": "바나나",
"price": 6000,
"warehousingDate": "2024-01-08",
}
]
먼저 컨트롤러에 추가해줍니다.
Controller
@GetMapping("/api/v1/fruit/list")
public List<FruitJpaResponse> getFruitsByPriceRange(@RequestParam String option, @RequestParam Long price) {
return fruitService.getFruitsByPriceRange(option, price);
}
Repository
Optional<List<FruitJpaResponse>> findAllByPriceGreaterThanEqual(Long price);
Optional<List<FruitJpaResponse>> findAllByPriceLessThanEqual(Long price);
"GTE" 문자열을 받으면 실행 findAllByPriceGreaterThanEqual
= SELECT * FROM fruit WHERE price >= ?
"LTE" 문자열을 받으면 실행 findAllByPriceLessThanEqual
= SELECT * FROM fruit WHERE price <= ?
Service
public List<FruitJpaResponse> getFruitsByPriceRange(String option, Long price) {
if ("GTE".equals(option)) {
return fruitRepository.findAllByPriceGreaterThanEqual(price).orElseThrow(IllegalArgumentException::new);
} else if ("LTE".equals(option)) {
return fruitRepository.findAllByPriceLessThanEqual(price).orElseThrow(IllegalArgumentException::new);
} else {
throw new IllegalArgumentException();
}
}
option으로 받는 문자열을 비교해서 각 문자열에 맞는 쿼리 실행할 수 있도록 if문을 사용했습니다. "GTE"나 "LTE"가 아닌 다른 문자열이 오면 예외 처리를 할 수 있도록 했습니다. 응답 예시처럼 결과가 나올 수 있도록 Response 생성했습니다.
Response
public class FruitJpaResponse {
private String name;
private Long price;
private LocalDate warehousingDate;
public String getName() {
return name;
}
public Long getPrice() {
return price;
}
public LocalDate getWarehousingDate() {
return warehousingDate;
}
public FruitJpaResponse(String name, Long price, LocalDate warehousingDate) {
this.name = name;
this.price = price;
this.warehousingDate = warehousingDate;
}
}
결과
댓글을 작성해보세요.