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