Per ardua ad astra !
I'm On My Way
Per ardua ad astra !
전체 방문자
오늘
어제
  • 분류 전체보기 (126)
    • Algorithm (50)
      • 백준 (30)
      • SWEA (3)
      • JUNGOL (3)
      • Programmers (5)
      • LeetCode (2)
    • 안드로이드 개발 (6)
      • Java로 개발 (0)
      • Kotlin으로 개발 (3)
    • Spring (41)
      • Spring기본 (17)
      • JPA기본 (15)
      • JPA활용 SpringBoot 기본 (9)
      • API 개발 기본 (0)
    • 네트워크 (3)
    • 운영체제 (0)
    • Life (3)
      • 책 (0)
      • 자기계발 (1)
      • 일상 (2)
    • 노마드코더 (3)
      • python으로 웹 스크래퍼 만들기 (3)
    • 프로그래밍 언어 (17)
      • Java 기본 (2)
      • 코틀린 기본 (15)

블로그 메뉴

  • 홈
  • 방명록

인기 글

hELLO · Designed By 정상우.
Per ardua ad astra !

I'm On My Way

Spring/JPA활용 SpringBoot 기본

상품 도메인 개발, @Transactional에 대한 궁금증 해소

2021. 8. 20. 12:29

상품 도메인 개발

 

비즈니스 로직 추가

1. 재고 증가
2. 재고 감소

 


상품 엔티티 개발

package jpabook.jpashop.domain;

import jpabook.jpashop.exception.NotEnoughStockException;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

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

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

    private String name;

    private int price;

    private int stockQuantity;

    @OneToMany(mappedBy = "item")
    private List<ItemCategory> categories = new ArrayList<>();

    public void addStock(int quantity){
        stockQuantity += quantity;
    }

    public void removeStock(int quantity){
        if(stockQuantity - quantity < 0){
            throw new NotEnoughStockException("need more stock");
        }
        stockQuantity -= quantity;
    }

}


주의!!
 - 재고 감소시 재고가 0 미만이 되는 것에 주의하여 Exception을 만들도록 하자
 - 재고 증가와 감소 처럼 setter함수로 속성을 변경시켜주는 것은 좋지 않다.  
   => 변경할 일이 있으면 핵심 비즈니스 메소드를 만들어서 변경해야 한다. addSock과 removeStock함수를 만들었다.


상품 레포지터리 개발

package jpabook.jpashop.repository;

import jpabook.jpashop.domain.Item;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import java.util.List;

@Repository
@RequiredArgsConstructor
public class ItemRepository {
    private final EntityManager em;

    // 상품 등록
    public Long save(Item item) {
        if(item.getId() == null){
            em.persist(item);
        } else {
            em.merge(item);
        }
        return item.getId();
    }

    // 상품 목록 조회
    public List<Item> findAll() {
        String query = "select i from Item i";
        List<Item> resultList = em.createQuery(query, Item.class).getResultList();
        return resultList;
    }

    // 상품 하나 조회
    public Item findOne(Long id){
        Item findItem = em.find(Item.class, id);
        return findItem;
    }

}

 

주의 
 - item 등록시 id 값은 자동으로 할당 해주는데, 만약에 id 값이 있다면
   => em.merge를 사용하여 등록할 거임 (이미 있을 수도 있는 데이터에 덮어씌우는 느낌)
 - 그 밖의 특이점은 없다. @Transactional 어노테이션은 붙지 않는다. -> 어차피 Service에서 다루기 때문

 

 

상품 서비스 개발

package jpabook.jpashop.service;

import jpabook.jpashop.domain.Item;
import jpabook.jpashop.repository.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ItemService {

    private final ItemRepository itemRepository;

    // 상품 등록
    @Transactional
    public void saveItem(Item item) {
        itemRepository.save(item);
    }

    // 상품 조회
    public Item findItem(Long itemId) {
        return itemRepository.findOne(itemId);
    }

    // 상품 목록 조회
    public List<Item> findItems() {
        return itemRepository.findAll();
    }

    // 재고 증가
    public void addQuantity(Long id, int plusQuantity) {
        Item find = findItem(id);
        find.addStock(plusQuantity);
    }

    // 재고 감소
    public void minusQuantity(Long id, int minusQuantity) {
        Item find = findItem(id);
        find.removeStock(minusQuantity);
    }
}


 - 클래스 단위로 @Transactional(readOnly = true)를 달아주고 조회가 아닌 저장함수 saveItem에만
   별도의 @Transcational을 달아 주는 것 외엔 특이점이 없다.

테스트 파일

package jpabook.jpashop.service;

import jpabook.jpashop.domain.item.Book;
import jpabook.jpashop.repository.ItemRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
class ItemServiceTest {

    @Autowired
    ItemRepository itemRepository;
    @Autowired
    ItemService itemService;

    @Test
    @Rollback(value = false)
    public void 아이템_조회() throws Exception {
        // given
        Book book = new Book();
        book.setName("꽃을 보든 너를 본다");
        book.setPrice(10000);

        // when
        itemService.saveItem(book);

        // then
        assertEquals(book, itemRepository.findOne(book.getId()));
    }

}

 


Transactional에 대한 궁금증 해소

  1. 보통 트랜잭션이 필요하면 Service 계층을 만든다. 그냥 단순 조회, 단순 저장만 하는경우 같이 트랜잭션이 필요 없는 경우에는 Transantion없이 처리하거나 아니면 Repository 계층에 트랜잭션 거는 밥법을 사용한다.  참고로 Controller에서는 트랜잭션을 사용하지 않는다.

  2. Repository에서 @Transactional을 안붙이는 이유는 Service에서 @Transactional을 달아주고 Service에서 메소드를 호출하거나 하여 레포지토리까지 트랜색션이 적용 되기 때문이다.

  3. 그럼 Service에서 @Transactional 어노테이션이 있는데, Test파일에서도 @Transactional 어노테이션이 있으니까  Service에서 @Transactional을 생략해도 되는거 아닌가 ??
       => Service에서 @Transactional을 생략해도 테스트는 정상 작동한다. 하지만 테스트말고 모든 실행환경에서 @Transactional이 붙어 있으라는 보장이 없고 Service에도 달아주는 것이 안전하다.

  4. 그럼 만약 Service에 @Transactional을 붙여주고, Test에서 @Transactional을 붙이지 않는다면 ??
     => 테스트 실패가 된다. 객체가 같지 않음.
      이유: "JPA와 스프링을 함께 사용하면 영속성 컨텍스트가 DB 트랜잭션 마다 각각 따로 만들어집니다.
       지금 save한 book을 저장하는 영속성 컨텍스트와 itemService에서 조회하는 영속성 컨텍스트가 서로 다릅니다.
    이렇게 영속성 컨텍스트가 다르면 서로 다른 객체를 생성하게 됩니다. 참고로 스프링은 이미 시작된 트랜잭션이 있으면 처음 트랜잭션을 기본적으로 이어받습니다. " by 김영한 선생님  
     
  5. 그럼 굳이 왜 Repository가 아닌 Service에 @Transactional을 달아줘야 할까
     => "비즈니스 로직은 보통 여러 리포지토리를 호출하는데요. 만약에 해당 비즈니스 로직에 문제가 발생했을 경우에는 해당 비즈니스 로직과 관련된 부분을 모두 롤백해야 합니다. 그래서 일반적으로 비즈니스 로직의 시작점인 서비스에 트랜잭션을 사용합니다." by 김영한 선생님  

 

 

 

이 게시물은 '실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발' 강의를 수강하고 정리한 내용임을 밝힙니다.

출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-%ED%99%9C%EC%9A%A9-1/

 

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의

실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., 본

www.inflearn.com

 

 

    'Spring/JPA활용 SpringBoot 기본' 카테고리의 다른 글
    • MVC 계층 개발 1. 홈화면, 회원등록 및 목록 조회 (Spring-boot-starter-validation 사용) (thymeleaf 배우기) (BindingResult, @Valid)
    • 주문 도메인 개발
    • 회원 도메인 개발(레포지터리, 서비스, 테스트) && 의존주입 생략 과정, JUnit5를 이용한 테스트, 내부 DB로 전환
    • 도메인 분석 설계 (JPA를 활용해서 엔티티 클래스 개발)
    Per ardua ad astra !
    Per ardua ad astra !
    개발자 지망생이며 열심히 공부하고 기억하기 위한 블로그입니다.

    티스토리툴바