티스토리 뷰

 

 

이전 프로젝트는 현재 담당하고 있는 것 보다 규모가 훨씬 커서 테이블의 종류가 많았다.

 

그래서 다양한 테이블에 insert하는 매서드의 경우 중간에 오류가 발생하는 경우를 대비해 별도의 Exception을 정의하고 

 

데이터 무결성을 위해서 rollback 처리를 반드시 해줬어야 했다.

 

그런데 현재 프로젝트로 넘어와서 코드를 확인해보니,

 

@Transational 어노테이션만 붙이고, rollback은 따로 처리하지 않고 있었다.

 

지금 생각해보면 이유라기보다는 개발자의 미숙에 가까운게, 해당 프로젝트는 단 하나의 CustomException이 없었다.

 

Checked Exception이 없기 때문에 굳이 rollbackfor를 정의할 필요가 없다는 생각이었을까?

 

정말 안전할지 알아보자.

 

Transcational을 이해하기 위해서는 Exception의 종류가 중요하다.

 

간단히 짚고가자.

 

CheckedException과 UnCheckedException

 

CheckedException은 사용자가 의도적으로 발생시킨 익셉션이다.

 

컴파일 시점에 확인할 수 있으며, Exception을 상속받아 만든 익셉션들이 여기에 포함된다.

 

try-catch 블록으로 감싸야 하며, IOException, SQLException 등이 여기에 포함된다.

 

UnCheckedException은 의도치 않게 발생한 익셉션으로,

 

RuntimeException을 상속받는 익셉션들이 여기에 포함된다. 런타임 때 발생하며, 주로 버그 때문에 발생한다.

 

예를 들어, 널 포인터 참조, 잘못된 타입 캐스팅 등으로  NullPointerException, IndexOutOfBoundsException가 Unchecked Exception에 해당된다.

롤백 여부에 대해서 익셉션이 중요하다고 언급한 이유는,

 

@Transactional이 없어도, UnChekcedException은 스프링 단에서 롤백을 시켜준다.

 

그런데 JPA를 쓸 때 조금 문제가 있다.

 

UnChekcedException인 RuntimeException을 throw한 아래 코드는 rollback이 될까?

fun insertTest() {
	var a = 1
	helloRepository.save(1, a++)
	if(a == 2) {
		throw RuntimeException()
	}
}

 

정답은 롤백이 되지 않는다.

 

RuntimeException을 강제 유발해서 문제가 생길거라 생각하지만 아니다.

 

함정은 save 매서드에 있다. JPA 라이브러리에 구현된 save 매서드를 뜯어보면 아래와 같다.

@Transactional
public <S extends T> S save(S entity) {
    Assert.notNull(entity, "Entity must not be null");
    if (this.entityInformation.isNew(entity)) {
        this.em.persist(entity);
        return entity;
    } else {
        return this.em.merge(entity);
    }
}

띠용 save 매서드 안에 @Transactional이 있다.

 

이것 때문에 아래와 같이 전체 매서드에 @Transactional을 묶어줘야 한다.

@Transactional
fun insertTest() {
	var a = 1
	helloRepository.save(1, a++)
	if(a == 2) {
		throw RuntimeException()
	}
}

 

클래스(컨트롤러, 서비스) 단위로 붙여줘도 롤백은 정상 동작한다.

@Service
@Transactional
class InsertService

 

CheckedException을 강제로 throw할 때는 save가 정상 수행된다.

@Transactional
fun insertTest() {
	var a = 1
	helloRepository.save(1, a++)
	if(a == 2) {
		throw Exception()
	}
}

CheckedException을 롤백 시키고 싶으면, rollbackFor 옵션을 사용하면된다.

@Transactional(rollbackFor = [Exception::class])
fun insertTest() {
	var a = 1
	helloRepository.save(1, a++)
	if(a == 2) {
		throw Exception()
	}
}

Exception만 주의하면, 사용하기 간단하지 않나? 란 생각이 들 수 있는데, 추가적인 사용 주의 점이 있다. 

 

@Transactional 사용 시 주의점

1. 적용되는 순서가 있다.

클래스 매서드 - 클래스 - 인터페이스 매서드(JPA 매서드들) - 인터페이스 순서로 적용된다.

 

2. public 매서드에만 적용되고, private, protected 매서드에서의 @Transactional은 스프링에 의해 무시된다.

 

3. 중첩된 Transactional을 쓸 때 조심해야 한다.

자칫 잘못 사용하면 교착상태에 빠질 수 있다.

 

4. javax,jakarta Transcational과 Spring에서 제공하는 Transactional은 다르다.

지원하는 기능과 세부 옵션이 다르니 주의해야한다.

 

몇 가지 더 있는데, 기본적이거나 당장 떠오르는게 없다.

 

결론은 잘 알고 써라다.

 

그리고 JPA 인터페이스 매서드를 단일로 사용할 때는 굳이 사용할 필요 없다고 한다.

마치며

처음에 @Transcational을 매서드에 붙여도 동작하지 않았었다. 

 

그런데 어느순간부터 생각처럼 동작하기 시작했는데, 이유를 아직 찾지 못했다. 어딘가에 옵션이 있는건가?

(정확히는 javax/jakarta 의 Trascational로 한번 바꾼 후, spring에서 제공하는 Transactional로 바꾸니 생각처럼 동작함)

 

라이브러리를 스위칭하면서 옵션이 재설정되면서 정상동작하게 됐다는 말도 있는데 정확히 모르겠다.

 

그래도 애매하게 알고있던 Transcational에 대해 자세히 알아본 것 같다.

 

 

- 참고 사이트

https://medium.com/javarevisited/checked-and-unchecked-exceptions-in-java-19166e68b66f

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함