@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;
}
}
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 객체는 다르다.