상품 비즈니스 설계

PPakSSam·2022년 2월 10일
0
post-thumbnail

상품 엔티티


@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DiscriminatorColumn(name = "dtype")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@Entity
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<>();

    // 비즈니스 메소드
    public void addStock(int quantity) {
        this.stockQuantity += quantity;
    }

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

DDD (도메인 주도 개발)

Item에서 stockQuantity를 줄이는 비즈니스 로직에 대해서 생각해보자.
많은 개발자들은 이 비즈니스 로직을 서비스 레이어에서 다음과 같이 구현할 것이다.

public void removeStock(long itemId, int stock) {
    Item item = itemRepository.findOne(itemId);
    
    int restStock = item.getStockQuantity() - stock;
    if(restStock < 0) {
        throw new NotEnoughStockException("need more Stock");
    }
    
    item.setStockQuantity(restStock);
}

그런데 DDD는 이 비즈니스 로직을 도메인으로 가지고 온다.
그 결과가 바로 Item 엔티티의 removeStock 메소드이다.
그러면 서비스 레이어 코드가 다음과 같이 변한다.

public void removeStock(long itemId, int stock) {
    Item item = itemRepository.findOne(itemId);
 	item.removeStock(stock);   
}

상품 기능 테스트


테스트 코드를 보기 전에 뼈대가 되는 코드를 먼저

public class ItemRepository {
    private final EntityManager em;

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

        System.out.println("영속화 여부: " + em.contains(item));
    }
}    

상품 레포지토리이며 상품을 save하는 기능이다

public class ItemService {

    private final ItemRepository itemRepository;

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

상품 서비스이며 @Transactional이 있다는 것을 인식하길 바란다.

@SpringBootTest
public class ItemServiceTest {
    @Autowired
    ItemService itemService;

    @Autowired
    EntityManager em;

    @AfterEach
    void afterEach() {
        itemService.deleteAll();
    }
    
    private Movie 영화_등록(String name, int price, int stockQuantity, 
    				      String director, String actor) {
        Movie movie = new Movie(name, price, stockQuantity, director, actor);

        // 트랜잭션 시작
        itemService.saveItem(movie); // ItemService 내에서 영속화되어 movie는 Id를 할당받음
        // 트랜잭션 끝 -> 영속성 컨텍스트 종료

        return movie;
    }
}
  • 테스트코드에는 @Transactional을 붙이지 않았다.

  • 영화_등록 메소드가 실행할 때는 아직 트랜잭션이 시작되지 않았으며 ItemService의 saveItem이 실행될 때 비로소 트랜잭션이 시작된다. (saveItem 메소드에는 @Transactional이 붙어있으므로)

  • 마찬가지로 saveItem 메소드가 끝나면 트랜잭션도 종료된다.

  • saveItem 메소드를 실행하면 em.persist가 실행되고 따라서 영속화가 되므로 movie 객체는 Id를 할당받는다.

  • 따라서 반환된 movie는 Id를 가지고 있는 상태이다.

@Test
void save() {
    // when
    Movie movie = 영화_등록("영화1", 10000, 10, "감독1", "배우1");

    // then
    assertThat(movie.getId()).isNotNull();
}

위의 코드는 영화_등록 메소드를 제대로 이해했다면 알 수 있는 내용이므로 패스 ~

@Test
void findOneTest() {
    // given
    Movie movie = 영화_등록("영화1", 10000, 10, "감독1", "배우1");

    // when
    // 영속성 컨텍스트가 이미 종료되었으므로 find할 때 movie와는 다른 Movie 객체를 생성
    Item findMovie = itemService.findOne(movie.getId());

    // then
    assertThat(findMovie.getId()).isEqualTo(movie.getId());
    assertThat(findMovie).isNotEqualTo(movie);
}
  • findOneTest에는 @Transactional이 없으므로 findOneTest 실행시에는 트랜잭션이 시작되지 않는다.

  • 영화_등록 메소드에서 트랜잭션이 시작되고 종료된다.

  • itemService.findOne이 실행되는 시점에서는 트랜잭션이 이미 종료되었고 따라서 영속성컨텍스트가 없다.

  • 영속성 컨텍스트가 존재하지 않으므로 findMovie 객체와 movie 객체는 서로 다르다.

@Test
@Transactional
void findOneWithTransaction() {
    // given
    Movie movie = 영화_등록("영화1", 10000, 10, "감독1", "배우1");

    // when
    // @Transactional이 붙어있으므로 트랜잭션 유지 -> 영속성 컨텍스트 유지됨
    // 영속성 컨텍스트가 유지되므로 findOne을 하면 영속성컨텍스트에 있는 movie 객체를 가져옴
    Item findMovie = itemService.findOne(movie.getId());

    // then
    assertThat(findMovie.getId()).isEqualTo(movie.getId());
    assertThat(findMovie).isEqualTo(movie);
}
  • findOneWithTransaction에는 @Transactional이 있으므로 findOneWithTransaction 실행시에는 트랜잭션이 시작된다.

  • 영화_등록 메소드가 종료된 후에도 트랜잭션은 유지가 되므로 영속성 컨텍스트도 여전히 존재한다.

  • itemService.findOne이 실행되는 시점에 영속성 컨텍스트가 존재하므로 영속성 컨텍스트에 있는 movie 객체를 가져온다.

  • 따라서 findMovie와 movie 객체는 같다.

@Test
@Transactional
void findOneWhenEntityMangerClear() {
    // given
    Movie movie = 영화_등록("영화1", 10000, 10, "감독1", "배우1");
    em.flush();
    em.clear();

    // when
    // em.clear()로 영속성컨텍스트의 모든 정보가 사라짐
    // 따라서 findOne 객체는 영속화된 movie 객체와는 다른 객체 생성
    Item findMovie = itemService.findOne(movie.getId());

    // then
    assertThat(findMovie.getId()).isEqualTo(movie.getId());
    assertThat(findMovie).isNotEqualTo(movie);
}
  • 이번에는 트랜잭션이 존재하나 em.clear를 한 상태이다.
    -> 영속성 컨텍스트에 저장된 엔티티가 하나도 없게 된다.

  • itemService.findOne이 실행되는 시점에 영속성 컨텍스트에 저장된 엔티티가 하나도 없으므로 movie 객체를 가져오지 못한다.

  • 따라서 itemService.findOne이 실행되면 새로운 객체가 생성된다.

  • findMovie 객체와 movie 객체는 다르다.

profile
성장에 대한 경험을 공유하고픈 자발적 경험주의자

0개의 댓글