본문 바로가기
기록, 회고/InFlearn Warming-up 0기 BE

[7일 차] - 과제 수행 : Spring Data JPA 사용하기

by TwoJun 2024. 2. 26.

인프런에서 주최하는 Warming-up 클럽 0기 백엔드 스터디에 참여하고 있다.

 

스터디에 참여하면서 배우게 된 내용을 전체적으로 정리하고 과제로 수행했던 내용들을 정리해 보고자 한다.

 

(1) 7일 차 : 2024-02-27(Tue)

 

(2) 과제 수행 : GitHub 코드 : https://github.com/twojun/InFlearn_WarmingUp_Club_BE_0

 

GitHub - twojun/InFlearn_WarmingUp_Club_BE_0: Inflearn Warming-up Club Back-end Study 0기 (Java, Spring)

Inflearn Warming-up Club Back-end Study 0기 (Java, Spring) - twojun/InFlearn_WarmingUp_Club_BE_0

github.com

 

 

 

 

 

1. 과제 수행 : 문제 1번

1-1. 이전 과제에서 만들어진 Fruit 기능들을 JPA를 사용할 수 있도록 변경

public interface FruitRepository extends JpaRepository<Fruit, Long> {

}

 

(1) FruitRepository를 별도로 생성하여 기본적인 CRUD는 해당 리포지토리를 의존하도록 설정한다.

 

 

 

 

 

2. 과제 수행 : 문제 2번

2-1. 문제 요구사항 : 특정 과일을 기준으로 가게를 거쳐간(판매된) 과일의 개수를 출력하는 API를 설계하자. 

(1) 예를 들어 위와 같은 판매 상황을 가지는 상황에서 사과를 기준으로 한다면 반환되는 수는 2다.

 

(2) HTTP Method : GET

(3) HTTP Path : /api/v2/fruit/count (본인은 /api/v2/fruit/count로 설정했다. 정상적인 결과만 반환된다면 상관없다.)

(4) HTTP Query parameter : name 

(5) Ex) /api/v1/fruit/count?name=사과

 

 

 

2-2. HTTP Response Body Example

 

응답 바디 예시

 

 

 

 

2-3. Fruit.java, FruitControllerV2.java, FruitServiceV2.java, FruitRepository.java

@Entity
@Getter
public class Fruit {

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

    private String name;
    private Long price;
    private LocalDate warehousingDate;
    private boolean salesStatus = false;

    public Fruit() {}

    // 과일 생성
    public Fruit(String name, Long price, LocalDate warehousingDate) {
        this.name = name;
        this.price = price;
        this.warehousingDate = warehousingDate;
    }

    // 판매 상태 변경
    public void setSalesStatus(boolean salesStatus) {
        this.salesStatus = salesStatus;
    }

}


@RestController
@RequiredArgsConstructor
public class FruitControllerV2 {

    private final FruitServiceV2 fruitServiceV2;

    // 과일 생성
    @PostMapping("/api/v2/fruit")
    public void createFruit(@RequestBody FruitCreateRequestDto request) {
        fruitServiceV2.createFruit(request);
    }

    // 과일 판매여부 수정
    @PostMapping("/api/v2/fruit/status/{id}")
    public void setSaleFruitCount(@PathVariable("id") long id, @RequestBody FruitSaleStateUpdateRequestDto request) {
        fruitServiceV2.setSaleFruitCount(id, request);
    }

    // 판매되었던 과일 개수 출력하기
    @GetMapping("/api/v2/fruit/count")
    public long getSaleFruitCount(@RequestParam String name) {
        return fruitServiceV2.getSaleFruitCount(name);
    }

}

- 엔티티 코드에서 보면 과일의 상태를 따로 업데이트해 주기 위한 도메인의 비즈니스 로직이 담긴 코드가 포함되어 있다는 있다 기본값은 미판매 상태 false이므로 이후 문제 요구사항을 구현하기 위해 판매 상태(true)로 바꾸기 위한 별도의 로직을 만든 것.

 

- 무분별한 @Setter 사용을 막기 위해 @Setter를 사용하지 않았다.

 

- 과일 상태 변경의 경우 테스트 케이스를 좀 더 편하게 설정하기 위해 별도의 편의 메서드를 만들어 둔 것이다.

(글을 작성하면서 생각해 보니.. 더 유의미한 이름으로 changeSalesStatus가 더 올바르지 않은 표현인가 싶다...)

 

(1) 우선 모든 기능이 포함된 Controller 코드이다.

 

(2) 과일 생성, 판매여부 수정의 경우 나머지 문제의 요구사항을 구현하기 위해 별도로 만든 메서드이다.

 

(3) 판매된 과일의 개수를 출력하는 메서드를 확인해 보자.

 

 

@RestController
@RequiredArgsConstructor
public class FruitControllerV2 {

    private final FruitServiceV2 fruitServiceV2;

    // 판매되었던 과일 개수 출력하기
    @GetMapping("/api/v2/fruit/count")
    public long getSaleFruitCount(@RequestParam String name) {
        return fruitServiceV2.getSaleFruitCount(name);
    }
}

(3) URL로 GET 요청을 보내면 쿼리 파라미터가 @RequestParam에 매핑되고 Service 계층의 메서드가 호출된다.

 

 

@Repository
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class FruitServiceV2 {

    private final FruitRepository fruitRepository;

    public long getSaleFruitCount(String name) {
        return fruitRepository.countByNameAndSalesStatusIsTrue(name);
    }
}

(4) 판매된 과일의 개수를 반환하는 서비스 로직은 간단하다. Repository 영역의 countByNameAndSalesStatusIsTrue(name)만 호출하면 관련된 쿼리가 실행되어 과일 개수를 long 형태로 반환한다. Repository 코드를 확인해 보자.

 

 

@Repository
public interface FruitRepository extends JpaRepository<Fruit, Long> {

    // select * from fruit where name = ? and salesStatus = true;
    long countByNameAndSalesStatusIsTrue(String name);
}

(5) 쿼리 메서드 기능으로 주석 처리된 쿼리가 실제로 수행된다.

 

 

 

2-4. 기능 테스트 

(1) 케이스 별도 생성

@Component
@RequiredArgsConstructor
public class InitDb {

    private final InitDbService initDbService;

    @PostConstruct
    public void init() {
        initDbService.initFruitDb();
    }

    @Component
    @Transactional
    @RequiredArgsConstructor
    static class InitDbService {

        private final EntityManager em;
        public void initFruitDb() {
            Fruit fruit1 = createFruit("사과", 4000L, LocalDate.of(2024, 02, 01));
            Fruit fruit2 = createFruit("바나나", 2000L, LocalDate.of(2024, 02, 01));
            Fruit fruit3 = createFruit("사과", 6500L, LocalDate.of(2024, 02, 01));
            Fruit fruit4 = createFruit("사과", 7000L, LocalDate.of(2024, 02, 01));
            Fruit fruit5 = createFruit("사과", 3000L, LocalDate.of(2024, 02, 01));
            Fruit fruit6 = createFruit("포도", 12000L, LocalDate.of(2024, 02, 01));
            Fruit fruit7 = createFruit("사과", 2500, LocalDate.of(2024, 02, 01));
            Fruit fruit8 = createFruit("사과", 5000L, LocalDate.of(2024, 02, 01));

            em.persist(fruit1);
            em.persist(fruit2);
            em.persist(fruit3);
            em.persist(fruit4);
            em.persist(fruit5);
            em.persist(fruit6);
            em.persist(fruit7);
            em.persist(fruit8);

        }

        private static Fruit createFruit(String name, long price, LocalDate warehousingDate) {
            return new Fruit(name, price, warehousingDate);
        }
    }
}

(2) 위와 같이 별도의 클래스를 하나 만들어서 서버 애플리케이션이 실행될 때마다 데이터가 생성되도록 하여 테스트 데이터를 좀 더 편하게 다룰 수 있도록 했다. 

 

 

Test data

(3) 데이터베이스 테이블에 들어간 데이터다. 위에서 언급되었던 과일 상태를 바꾸는 메서드를 별도로 호출해서 3, 4, 8번의 판매 상태를 True(판매됨)으로 설정했다.

 

 

정상 응답 확인

(4) 사과를 기준으로 3개가 판매되었으므로 판매 개수 3개가 정상적으로 반환된다.

 

 

 

 

 

3. 과제 수행 : 문제 3번

3-1. 문제 요구사항 : 아직 판매되지 않은 특정 금액 이상, 특정 금액 이하의 과일 리스트를 출력하는 API를 설계하자.

 

(1) HTTP Method : GET

(2) HTTP Path : /api/v1/fruit/list (본인은 /api/v2/fruit/list로 설정했다. 정상적인 결과만 반환된다면 상관없다.)

 

(3) HTTP Query parameter

- option : GTE 또는 LTE라는 문자열이 들어온다.

- 각각 Greater Than Equal, Less Than Equal이라는 의미를 가진다.

- price : 기준이 되는 금액이 입력된다.

 

(4) Ex) GET /api/v1/fruit/list?option=GET&price=3000 : 판매되지 않은 3,000원 이상의 과일 목록을 반환해야 한다.

 

 

 

 

3-2. FruitNoSalePriceAndCountResponseDto.java, FruitControllerV2.java, FruitServiceV2.java, FruitRepository.java

@Data
public class FruitNoSalePriceResponseDto {

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


    public FruitNoSalePriceResponseDto(Fruit fruit) {
        this.name = fruit.getName();
        this.price = fruit.getPrice();
        this.warehousingDate = fruit.getWarehousingDate();
        this.salesStatus = fruit.isSalesStatus();
    }
}

@RestController
@RequiredArgsConstructor
public class FruitControllerV2 {

    private final FruitServiceV2 fruitServiceV2;

    // 판매되지 않은 특정 금액, 이하 과일 목록들
    @GetMapping("/api/v2/fruit/list")
    public List<FruitNoSalePriceResponseDto> getNoSaleAmount(@RequestParam String option, @RequestParam long price) {
        return fruitServiceV2.getNoSaleAmount(option, price);
    }

    // 판매되지 않은 특정 금액, 이하 과일 목록들, 결과 반환 개수까지
    @GetMapping("/api/v2/fruit/list/result")
    public List<FruitNoSalePriceAndCountResponseDto> getNoSaleAmountAndResultCount(@RequestParam String option, @RequestParam long price) {
        return fruitServiceV2.getNoSaleAmountAndCount(option, price);
    }
}

 

(1) 현재 메서드가 2개로 분리되어 있다. 상단의 메서드는 판매되지 않은 과일 리스트만 반환하고 하단의 메서드는 리스트와 함께 리스트의 개수까지 반환하도록 설계했다.

 

(2) 리스트만 반환하면 되지만 리스트의 개수도 한 눈에 보여주고 싶었기 때문에 아래의 메서드를 기준으로 설명하겠다.

 

(3) 우선 FruitNoSalePriceAndCountResponseDto 객체가 조금 특이한 점을 갖는다. 해당 객체부터 확인해보자.

 

 

@Data
public class FruitNoSalePriceAndCountResponseDto {

    private Long resultSize;

    private String name;
    private LocalDate warehousingDate;
    private boolean salesStatus;

    public FruitNoSalePriceAndCountResponseDto(Long resultSize, Fruit fruit) {
        this.resultSize = resultSize;
        this.name = fruit.getName();
        this.warehousingDate = fruit.getWarehousingDate();
        this.salesStatus = fruit.isSalesStatus();
    }
}

 

(4) 리스트의 개수를 반환하기 위해 추가적인 resultSize를 선언했다.

 

(5) 현재 생성자로 과일 객체를 받아온다 이 객체는 추후 Repository에서 조회한 Fruit이며 Fruit을 대상으로 해당 DTO의 값을 모두 채우게 된다. 따라서 반환되는 JSON에는 각각의 Fruit 리스트에서 가져온 요소의 값이 저장될 것이다.

 

(6) 따라서 위의 컨트롤러의 메서드는 따라서 리스트를 반환하기 위한 쿼리를 호출하기 위해 서비스 계층의 메서드르 호출한다.

 

 

테스트 데이터

(7) 위와 같이 테스트 데이터가 존재하고 아직 판매되지 않은 12,000원 이하의 과일들의 리스트를 반환해 보자.

 

결과 1
결과 2

 

(8) 위와 같이 총 8개의 판매되지 않은 과일이면서 가격이 12,000원 이하인 과일이 모두 나오게 된다.

 

 

 

 

 

4. 개인 회고

(1) 스프링 데이터 JPA는 위에서 설명한 것처럼 스프링 프로젝트의 라이브러리 중 하나이다.

 

(2) 데이터베이스에 대한 접근 CRUD 등 다양한 기능을 높은 레벨로 추상화하여 개발자의 생산성을 높여준 도구이다.

 

(3) 이러한 라이브러리를 적재적소에 잘 활용하여 개발 효율을 높이는 것도 중요하지만 해당 라이브러리의 근간이 되는 JPA에 대한 이해가 우선 선행된 이후 사용하는 것이 적절해 보인다.

댓글