Spring에서 @Transactional 사용시 주의점
Spring에서 @Transactional를 사용시 주의점이 있다. 바로 하위 메소드에 @Transactional를 거는 것이다.
Controller에서 Service를 호출해서 사용한다.
@PostMapping("/items")
public List<ItemDto> saveAll(@Valid @RequestBody List<ItemDto> itemDtos) {
return txTestService.saveAll(itemDtos);
}
보통 Service에서 바로 @Transactinal를 saveAll등 메소드에 걸지만, 하위메소드로 별도로 분리해서 사용할 경우가 있다. 처리할 로직이 많아서 분리할 경우인데 이때 최초 진입점(Controller 에서 바로 호출하는 메소드)에 @Transactional을 안 걸고 하위에 걸었다면 실제 @Transactional이 작동되지 않는다. 바로 아래의 경우이다.
public List<ItemDto> saveAll(List<ItemDto> itemDtos) {
return subSaveAll(itemDtos);
}
@Transactional
public List<ItemDto> subSaveAll(List<ItemDto> itemDtos) {
List<Item> items = itemDtos.stream().map(ItemDto::toEntity).collect(Collectors.toList());
items.forEach(item -> itemRepository.save(item));
return items.stream().map(ItemDto::of).collect(Collectors.toList());
최초 진입 메소드인 saveAll() -> subSaveAll() 를 호출하면서 트랜젠셕을 하위 메소드에 건 상태이다. 이럴 경우 트랜젝션이 적용되지 않는다.
그런데 만약 jpa에서 제공하는 saveAll 메소드를 사용하면 트랜젝션이 적용된다(심지어 하위메소드에 @Transactional를 붙이지 않음)
public List<ItemDto> subSaveAll2(List<ItemDto> itemDtos) {
List<Item> items = itemDtos.stream().map(ItemDto::toEntity).collect(Collectors.toList());
return itemRepository.saveAll(items)
.stream()
.map(ItemDto::of)
.collect(Collectors.toList());
}
saveAll 뿐 아니라, save, delete등 JPA에서 제공하는 메소드는 기본적으로 트랜젝션이 적용되어 있다. saveAll메소드는 엔티티 리스트를 일괄 처리하므로 begin trans -> save(1), save(2), save(3)… -> commit 등이 되는 것과 같다. 위에 subSaveAll 에서 안되는 이유는 각각 save가 begin trasn -> save(1) -> commit, begin trans -> save(2) -> commit 등으로 각각 별도의 트랜젠션이 되므로 전체 트랜젝션이 적용되지 않는 것이다.
트랜젠션은 엔티티 필드수정에서도 중요하다. 보통 jpa에서 엔티티의 필드를 수정함으로 업데이트등을 진행할 수 있다.
예를 들어 Item 엔티티에 아래와 같이 메소드를 통해 필드값을 직접 업데이트가 가능하다.
public void update(ItemDto itemDto) {
this.itemName = itemDto.getItemName();
this.itemType = itemDto.getItemType();
}
그런데 아래와 같이 하위메소드에 transactional을 걸어보자.
Controller
@PutMapping("/items/{id}")
public ItemDto update(@PathVariable Long id, @Valid @RequestBody ItemDto itemDto) {
itemDto.setItemId(id);
return txTestService.update(itemDto);
}
Service
public ItemDto update(ItemDto itemDto) {
return subUpdate(itemDto);
}
@Transactional
public ItemDto subUpdate(ItemDto itemDto) {
Item item = itemRepository
.findById(itemDto.getItemId())
.orElseThrow(() -> new IllegalArgumentException("유효하지 않은 itemID"));
item.update(itemDto);
return ItemDto.of(item);
}
Controller에서 update -> Service의 update > subUpdate로 처리해서 해당 id로 엔티티를 가져오고 업데이트를 진행해보면 실제 DB에서 update 가 전혀 진행되지 않는다. 바로 하위메소드에 Transactional을 걸었기 때문이다.
따라서 제대로 동작하기 위해서는 Controller에서 Service로 바로 진입하는 메소드에 @Transactional를 걸어야 된다.
@Transactional --> 반드시 진입메소드에 적용해야 한다.
public ItemDto update(ItemDto itemDto) {
return subUpdate(itemDto);
}
public ItemDto subUpdate(ItemDto itemDto) {
Item item = itemRepository
.findById(itemDto.getItemId())
.orElseThrow(() -> new IllegalArgumentException("유효하지 않은 itemID"));
item.update(itemDto);
return ItemDto.of(item);
}
한가지 더 테스트 해보자.
Item엔티티에는 deleteYn이라는 필드가 있다. 우리는 이 필드를 통해서 삭제 여부를 적용할 것이다. 즉 Controller에서 delete가 오면 이 필드가 업데이트 되는 것이다.
Controller
@DeleteMapping("/items")
public ItemDto delete(@RequestBody ItemDto itemDto) {
return txTestService.deleteEntity(itemDto);
}
@Transactional
public ItemDto deleteEntity(ItemDto itemDto) {
Item item = itemDto.toEntity();
item.delete();
return ItemDto.of(item);
}
ItemDto를 toEntity를 통해 Entity 타입으로 변환하고 거기에 item.delete() 메소드를 수행한다.
Item 엔티티
public void delete() {
this.deleteYn = "Y";
}
메소드에 @Transactional을 걸었으므로 업데이트 될 것이라 생각할 수 있지만 전혀 업데이트가 진행되지 않는다. 실제로 영속성 컨텍스트에 Item 엔티티가 없기 때문에 DB에 반영되지 않는다.
방법은 두 가지이다. itemRepository.save(item); 를 추가하는 방법과 jpa에 find메소드를 이용해서 Item을 불러와서 영속성 컨텍스트에 담는 것이다.
@Transactional
public ItemDto deleteEntity(ItemDto itemDto) {
Item item = itemDto.toEntity();
item.delete();
itemRepository.save(item)
return ItemDto.of(item);
}
@Transactional
public ItemDto deleteEntity(ItemDto itemDto) {
Item item = itemRepository.findById(itemDto.getItemId()).get();
item.delete();
return ItemDto.of(item);
}
이렇게 되면 잘 수행된다. Spring에서 @Transactional 처리 부분과 Jpa에서 entity 처리 부분은 컴파일 상 에러가 나지 않는 부분이므로 잘 이해하고 개발을 진행해야 한다.