상품 Repository, Service 개발

guswls·2023년 2월 6일

JPA 활용 1편

목록 보기
8/9
post-thumbnail

이번 포스팅은 김영한 이사님의 스프링 부트 JPA 활용 1편을 듣고 제작되었습니다.

회원에 이어서 이번엔 상품에 대한 RepositoryService를 개발해보고자 한다.

구현 기능은 다음과 같다.

  • 상품 등록
  • 상품 목록 조회
  • 상품 수정

1. 상품 도메인에 비지니스 로직 구현

Member와 달리 Item에는 엔티티 내부적으로 처리할 수 있는 비지니스 로직을 추가하고자 한다.

이번의 예를 들면 물품의 수량을 나타내는 stockQuantity의 경우 엔티티 내부에 존재하기 때문에 엔티티 내부에 메서드를 만들어서 나타내는 것이 객체지향적 관점에서 응집도가 높다고 한다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="dtype")
@Getter @Setter
public abstract class Item {

    @Id
    @GeneratedValue
    @Column(name = "item_id")
    private Long id;

    private String name;
    private int price;
    private int stockQuantity;

    @ManyToMany(mappedBy = "items")
    private List<Category> categories = new ArrayList<>();

    //==비지니스 로직==
    /**
     * stock 증가
     */
    public void addStock(int quantity){
        this.stockQuantity += quantity;
    }

    /**
     * stock 감소
     */
    public void removeStock(int quantity) throws NotEnoughStockException {
        int restStock = this.stockQuantity - quantity;
        if (restStock < 0){
            throw new NotEnoughStockException("need more stock");
        }

        this.stockQuantity = restStock;
    }
}
  • stockQuantity에 대한 비지니스 로직을 엔티티 내부에 구현했다. addStock은 재고의 증가, removeStock은 재고의 감소를 실행한다.
  • removeStock의 경우 재고가 0 이하면 안되기 때문에 0보다 작으면 예외를 발생하도록 하였다.
  • 예외는 NotEnoughStockException으로 따로 프로젝트에서 exception패키지를 만들어 구현할 예정이다.

    stockQuantity를 변경하려면??

    사실 예제에서는 편의성을 위해 Setter를 모두 활성화 해놓았지만 사실 객체지향적인 관점에서 봤을 때 stockQuantity의 변경을 외부에서 Setter를 통해 값을 변경하는 것은 좋은 방법은 아니다.
    객체 지향적인 관점에서 봤을 때 이런 경우는 엔티티 내부의 비지니스 로직을 호출함으로써 해결하는 것이 좋다.


2. NotEnoughStockException 구현

public class NotEnoughStockException extends RuntimeException {
    public NotEnoughStockException() {
        super();
    }

    public NotEnoughStockException(String message) {
        super(message);
    }

    public NotEnoughStockException(String message, Throwable cause) {
        super(message, cause);
    }

    public NotEnoughStockException(Throwable cause) {
        super(cause);
    }
}
  • 기본적으로 예외를 만들땐 필요한 메소드를 오버라이드 하는 것이 좋다.
  • 오버라이드할 메소드를 선택하기 위한 단축키는 crtl + o를 사용하자.

3. ItemRepository 구현

@Repository
@RequiredArgsConstructor
public class ItemRepository {

    private final EntityManager em;

    public void save(Item item){
        if(item.getId()==null){
            em.persist(item);
        }else{
            em.merge(item);
        }
    }

    public Item findOne(Long id){
        return em.find(Item.class, id);
    }

    public List<Item> findAll(){
        return em.createQuery("select i from Item i", Item.class)
                .getResultList();
    }
}
  • save는 데이터가 이미 존재할 때와 새로운 데이터를 저장하는 경우 두가지로 나누었다.
  • 만약 itemid가 존재하지 않는다면 새롭게 생성해야 하므로 perist를 하였다.
  • 만약 이미 데이터가 존재하는 경우 merge를 사용하였는데 이부분은 뒤에 추가 설명을 할 예정이다.
  • 그 외엔 member와 유사하다.

merge에 대한 설명은 뒤에서 다뤄주신다고 했으나 궁금해서 따로 찾아봤었다. 이 글을 참고하자.


4. itemService 구현

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
    private final ItemRepository itemRepository;

    public void saveItem(Item item){
        itemRepository.save(item);
    }

    public List<Item>findItems(){
        return itemRepository.findAll();
    }

    public Item findOne(Long itemId){
        return itemRepository.findOne(itemId);
    }
}
  • ItemServiceItemRepository에 단순히 위임만 하는 클래스로 구현되었다.
  • 이렇게 위임만 하는 경우 Contoller에서 Repository로 바로 접근하는 것에 대해서도 생각을 해볼 필요가 있다고 한다.

5. itemService 테스트 작성

@SpringBootTest
@Transactional
class ItemServiceTest {

    @Autowired ItemService itemService;
    @Autowired EntityManager em;

    @Test
    void 상품_저장() {
        // given
        Book book = new Book();
        book.setName("spring1");
        itemService.saveItem(book);
        
		//em.flush();
        //em.clear();

        // when`
        book.addStock(3);
        itemService.saveItem(book);

        em.flush();
        em.clear();

        // then
        assertThat(book).isEqualTo(book);
    }
}

테스트 케이스를 다음과 같이 짜보았다.

이 테스트 케이스에서 확인하고 싶은 것은 이미 데이터가 존재하는 경우 save를 하면 어떻게 되는지였다. 즉, merge가 어떤 동작으로 동작하는지이다.
쿼리는 UPDATE쿼리가 날아간 것을 확인할 수 있다. 만약 첫번째로 아이템을 저장한 후에 clearflush를 한 후에 테스트를 돌리면 SELECT후에 UPDATE가 나가는 것을 확인할 수있다. 이것이 바로 merge의 동작원리이다. 자세한 동작원리는 이 글을 참고하자.

총정리

이렇게 해서 Item에 대한 RepositoryService까지 구현을 모두 완료하였다. 대부분 Member에 대한 RepositoryService를 만들 때와 유사하였기 때문에 쉽게 넘어갔다.

이번에 핵심은 아무래도 엔티티 내부 필드에 대한 비지니스 로직을 바로 엔티티에 구현한 것이 될 것이다. 강의를 듣기 전에 생각을 해본다면 당연히 나중에 값을 변경할 소요가 발생할 때 Setter를 사용해서 값을 변경했을 것 같은데 그보다는 이렇게 내부 메서드로 만들어 사용하는 것이 구조적으로 훨씬 안정적인 것 같다.

또한 이번엔 내가 알고 싶은 것에 대해서 테스트 케이스를 직접 짜보기도 하였다. 물론 merge에 대해서 뒤에서 다룬다고 하였긴 했지만 저장과 업데이트를 한 메소드에서 다룬다는 것이 신기해서 궁금한 마음에 발생한 쿼리를 뜯어보고 구글링도 했던 것 같다.

profile
안녕하세요

0개의 댓글