@Getter
@NoArgsConstructor
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long productId;
private int cnt;
public void changeCnt(int cnt) {
this.cnt = cnt;
}
@Builder
private Product(int cnt) {
this.cnt = cnt;
}
}
public interface ProductRepository extends JpaRepository<Product,Long> {
}
@Slf4j
@RequiredArgsConstructor
@Service
public class ProductService {
private final ProductRepository productRepository;
@Transactional
public void changeProductCnt(Long productId, int cnt) {
String txName = TransactionSynchronizationManager.getCurrentTransactionName();
log.info("txName={}", txName);
Product product = productRepository.findById(productId).orElseThrow(() -> new RuntimeException("데이터 없음"));
product.changeCnt(cnt);
}
}
@Slf4j
@SpringBootTest
@Transactional
class ProductServiceTest {
@Autowired
private ProductRepository productRepository;
@Autowired
private ProductService productService;
@Autowired
private EntityManager em;
@Test
public void changeStatus() {
// 트랜잭션 이름 출력
String txName = TransactionSynchronizationManager.getCurrentTransactionName();
log.info("txName={}", txName);
// Product객체 생성
Product product = Product.builder()
.cnt(5)
.build();
// Product객체 저장
productRepository.save(product);
// Product객체의 상태 변경 메서드 실행
productService.changeProductCnt(product.getProductId(),100);
// dirtyChecking으로 변경된 내용을 반영하기 위해 flush 강제 실행
em.flush();
em.clear();
// 값을 변경한 객체 찾아오기
Product changedProduct = productRepository.findById(product.getProductId()).get();
// 값이 변경됐는지 검증
Assertions.assertThat(changedProduct.getCnt()).isEqualTo(100);
}
}
다시 한번 얘기하지만 위 코드는 아무런 문제 없이 잘 동작합니다. 이 상태에서 코드를 변경하면서 몇 가지 사항에 대해 얘기해보겠습니다.
여기부터는 검증되지 않은 저의 부정확한 '추측'을 다수 포함하고 있습니다. 이점 유의해서 읽어주시면 감사하겠습니다.
em.flush();
를 실행하지 않으면 어떻게 될까요? flush를 하지 않으면 DirtyChecking이 실행되지 않고, 그 상태로 clear만 진행했기 때문에 변경 사항이 DB에 반영되지 못했습니다. 결과적으로 가져온 product에서는 cnt가 100으로 변해있지 않아 검증에 실패하게 됩니다.@Slf4j
@RequiredArgsConstructor
@Service
public class ProductService {
private final ProductRepository productRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void changeProductCnt(Long productId, int cnt) {
String txName = TransactionSynchronizationManager.getCurrentTransactionName();
log.info("txName={}", txName);
Product product = productRepository.findById(productId).orElseThrow(() -> new RuntimeException("데이터 없음"));
product.changeCnt(cnt);
}
}
트랜잭션 전파(propagation)을 REQURIES_NEW로 설정함으로써 기존의 트랜잭션과 분리된 별도의 트랜잭션을 만들었습니다. 로그를 살펴보면 두 트랜잭션의 이름 또한 다르다는 걸 확인할 수 있습니다.
하지만 이 코드는 실패합니다. 단순히 flush를 실행하지 않았을 때는 검증에서 실패하지만, 이번에는 데이터가 존재하지 않는다는 에러가 발생합니다
저는 Entity를 찾지 못한 이유가 save를 진행하는 트랜잭션이 아직 커밋되지 않아서
라고 판단했습니다. 내부 트랜잭션인 changeProductCnt에서 findById를 하는 시점에 외부 트랜잭션인 changeStatus는 save는 실행했지만 아직까지 커밋되지 않은 상태
입니다. 대부분의 트랜잭션 격리수준 기본값은 READ_COMMITTED이기 때문에 아직 커밋되지 않은 데이터에 다른 트랜잭션이 접근하지 못해 발생한 상황이라 생각했습니다.
우선 격리수준을 READ_UNCOMMITTED로 낮추면 flush없이 update코드를 정상적으로 테스트할 수 있는지 확인하기로 했습니다.
@Slf4j
@SpringBootTest
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
class ProductServiceTest {
@Test
void changeStatus() {}
}
@Slf4j
@RequiredArgsConstructor
@Service
public class ProductService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void changeProductCnt() {}
}
하지만 여전히 Entity를 찾지 못합니다.
그런데 아래와 같이 코드를 설계하면 에러는 발생하지만 DirtyChecking에 의해 UPDATE쿼리 까지는 정상적으로 진행됩니다.
@Slf4j
@SpringBootTest
@Transactional
class ProductServiceTest {
@Test
void changeStatus() {}
}
@Slf4j
@RequiredArgsConstructor
@Service
public class ProductService {
@Transactional(propagation = Propagation.REQUIRES_NEW,
isolation = Isolation.READ_UNCOMMITTED)
public void changeProductCnt() {}
}
@Slf4j
@SpringBootTest
// @Transactional
class ProductServiceTest {
@Autowired
private ProductRepository productRepository;
@Autowired
private ProductService productService;
@Autowired
private EntityManager em;
@Test
public void changeStatus() {
String txName = TransactionSynchronizationManager.getCurrentTransactionName();
log.info("txName={}", txName);
Product product = Product.builder()
.cnt(5)
.build();
productRepository.save(product);
productService.changeProductCnt(product.getProductId(),100);
// em.flush();
// em.clear();
Product changedProduct = productRepository.findById(product.getProductId()).get();
Assertions.assertThat(changedProduct.getCnt()).isEqualTo(100);
}
}
해당 작업들을 롤백
하는 기능을 가지고 있습니다. @Transactional
과 em.flush();
, em.clear();
를 제거해도 테스트는 통과
합니다. 하지만 이 경우 테스트 코드의 결과가 롤백되지 않아 DB에 그대로 값이 남아있게 됩니다. (그래서 @Transactional을 사용하지 않는 경우 AfterEach시점에 deleteAllInBatch를 진행하는 게 일반적입니다)