개발놀이터

@Transactional, 진짜 Checked Exception을 사용하면 안될까? 본문

Spring/Spring

@Transactional, 진짜 Checked Exception을 사용하면 안될까?

마늘냄새폴폴 2024. 7. 18. 21:27

이번 포스팅에서는 또 @Transactional 어노테이션에 대한 내용입니다. 제 블로그에 트랜잭션과 관련된 글이 일곱개나 되는데 이거까지 합치면 여덟개입니다... 

 

사골을 너무 우려서 뼈가 말랑말랑 해질거같은데 하지만 나름 중요한 내용인 것 같아서 또 우려보겠습니다. 

 

이번에 포스팅하게 된 이유는 트위터에서 어떤 개발자분이 @Transactional을 사용하려면 Checked Execption을 사용하지 말아야된다는 글이었습니다. 

 

그리고 공부하게 되었죠. 

 

Checked? UnChecked?

우선 본 내용으로 들어가기 전에 자바의 Exception의 종류에 대해서 짚고 넘어가도록 하겠습니다. 

 

기존 자바 언어 개발자들은 Checked Exception을 개발하면서 정말 잘 개발했다고 생각하게 됐다고합니다. 그렇게 처음 자바엔 Checked Exception밖에 없었다고 하죠. 

 

하지만 이후 문제점을 깨닫게 됩니다. 

 

처리할 수 없는 예외가 너무 많다.

예외를 모두 컴파일타임 때 체크해서 처리하도록 했더니 처리할 수 없는 예외가 등장하기 시작했습니다. 그리고 우리가 겪는 대부분의 예외는 처리가 불가능한 경우가 대부분입니다. 

 

예를 들면 데이터베이스 커넥션 예외라던가, 네트워크 연결 예외라던가, 파일 입출력 예외 등등이 그러하죠. 

 

이 예외가 터진다고 한들 자바입장에서 뭔가 하기가 힘들어졌습니다. 데이터베이스 커넥션 예외가 터졌다! 그럼... 뭐?

 

이 상황이 반복되는게 문제였습니다. 

 

처리할 수 없는 예외는 전부 던져버렸다. 

Checked Exception에서 처리할 수 없는 예외는 throws로 던져버려야합니다. 타고타고 들어간 로직에서 Checked Exception이 발생하면 위로위로위로 던져버리게 되었습니다. 

 

일단 보기에 굉장히 흉해졌습니다. 

public void businessLogic() throws 어쩌구예외, 저쩌구예외, 이런예외, 저런예외 {
	// logic
}

 

이짓이 너무나도 화난 개발자 한명이 다음과 같은 만행을 저지릅니다. 

public void businessLogic() throws Exception {
	// logic
}

 

이후 이 개발자가 개발한 로직에서 디버깅하기가 굉장히 힘들어졌습니다. 

 

그로인해 Exception이 OCP를 위배했다.

만약 JDBC를 이용한 상황에서 Checked Exception이 발생해서 이런 예외를 던졌다고 가정해봅시다. 가정입니다!

public void dbInit() throws JDBC예외 {
	
}

 

이 상황에서 JPA로 바꾼다면 어떻게 될까요? 예외를 전부 바꿔야합니다!

public void dbInit() throws JPA예외 {
	
}

 

이런 상황에서 기술적인 확장은 했지만 코드의 변경을 피할 수 없었습니다. 확장에 열려있고 변경에 닫혀있어야한다는 OCP에 위배됩니다. 

 

Unchecked Exception의 등장

이런 문제를 자바 언어 개발팀과 여러 팀에서 인지하고 Exception을 컴파일 타임에서 체크하지 않는 Unchecked Exception이 많이 개발되었습니다.

 

Unchecked Exception이 등장하면서 Exception을 어느 상황에서 사용해야하는지 기준이 명확해졌습니다. 

 

기본적으로 위에서의 이유 때문에 Unchecked Exception을 사용하면서 로그 시스템으로 장애 상황을 인지하는쪽으로 굳어지고 Checked Exception은 개발자간 협업할 때 이 예외를 반드시 처리하고 넘어가라는 명시적인 표현으로 사용하게 되었습니다. 

 

때문에 Checked Exception은 결제로직과 같은 예외상황을 반드시 처리해줘야 하는 상황에서 주로 사용하게 되었죠. 

 

 

@Transactional과 롤백 전략

여기까지 내용은 인프런의 영한님 강의를 들으셨다면 들어보신 내용일겁니다. 그래서 오늘의 본 내용인 @Transactional과 롤백 전략입니다. 

 

스프링에서 만약 @Transactional을 사용한 로직이라면 트랜잭션을 생성해 트랜잭션의 원자성을 보장받을 수 있게 됩니다. 트랜잭션이 실패하면 모든 상황을 롤백해야하는데 스프링에서 그 전략을 Exception 상황을 기반하여 처리합니다. 

 

Exception이 발생하면 롤백을 하는것이죠. 

 

@Transactional은 Exception 중 Unchecked Exception이 발생할 때만 롤백을 진행하게 됩니다. 이에 대한 이유는 자세하게 알 수 없었지만 스프링 공식문서에서 그 이유를 대강 짐작할 수 있었습니다. 

 

 

"스프링 컨테이너는 애플리케이션 예외 상황(Checked Exception)이 아닌 시스템 예외상황(Unchecked Exception)에 트랜잭션을 자동적으로 롤백하도록 설계되었다" 라는 이야기입니다. 

 

그리고 첨언으로 "Checked Exception에서 롤백을 하려면 커스텀하게 설정해야한다" 라는 내용이 나와있습니다. 

@Transactional(rollbackFor = MyCheckedException.class)
public void logic() {
	
}

 

위와같은 방식으로 커스텀하게 처리해줄 수 있습니다. 

 

하지만 여기서 Checked Exception을 try/catch에서 잡아버리면 롤백을 할 수 없습니다. 이때 궁금증이 떠올랐습니다. Checked Exception의 존재의의는 예외 상황을 반드시 처리해야한다고 명시적으로 알리는 용도라는 것을 생각하고 다시 보면 Checked Exception과 try/catch는 뗄래야 뗄 수 없는 상황이라고 생각했습니다. 

 

때문에 Checked Exception과 try/catch문을 둘 다 사용하고 싶은 경우를 알아보니 다음과 같은 방법이 있었습니다. 

 

예외를 던질까 말까~ 던질까 말까~

@Transactional(rollbackFor = MyCheckedException.class)
public void logic() {
	try {
    	// logic
        doSomething();
    }
    catch (MyCheckedException e) {
    	throw e;
    }
}

 

이렇게 예외를 try 문에서 던지고 catch에서 잡고 다시 예외를 던지는겁니다. 

 

이렇게 하면 @Transactional의 rollbackFor에 잡혀서 롤백을 하게됩니다. 

 

트랜잭션 인터셉터 사용하기

또한, 신기한 방법이 있었는데 아래와 같은 방식으로 예외를 처리할 수 있습니다. 

@Transactional
public void myMethod() {
    try {
        // logic
        doSomthing();
    } catch (MyCheckedException e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

 

이렇게 TransactionAspectSupport라는 트랜잭션 인터셉터를 이용해서 예외가 발생하면 롤백을 바로 진행시키는겁니다. 

 

 

마치며

흔히 스프링 개발자들 사이에서 "@Transactional에 Checked Exception를 사용하는 것이 안티패턴이다!" 라는 것으로 갑론을박을 하게 되는데 저는 개인적으로 그렇게 안티패턴이라고 정의하면서까지 기피해야할 것은 아니라고 생각합니다. 

 

실전에서 @Transactional을 사용하게 되는 경우는 비즈니스 로직에 트랜잭션을 태워서 보내는 용도로 사용하는데 그 와중에서 Checked Exception을 사용해야 하는 경우도 있을거라고 생각합니다. 

 

물론, "이럴거면 그냥 UnChecked Exception 사용하는거랑 뭐가 다르냐" 라고 반박하실 수도 있겠습니다. 하지만 Checked Exception은 언급했다시피 개발자들이 협업할 때 "이 예외는 반드시 처리해라!" 라고 명시적으로 알려주기 위한 용도로 바뀌었기 때문에 항상 100퍼센트 옳은 것은 없다 라는 말이 떠오르게 되더군요. 

 

이렇게 @Transactional과 Checked Exception에 대한 포스팅을 마치도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!