메뉴 닫기

자바의 @Transactional 어노테이션

어저께 본 면접에서 "자바의 @Transactional 어노테이션의 옵션이 무엇무엇 있느냐?" 라는 질문을 들었는데 제대로 대답하지 못했다. 전에 프로젝트 할 때는 기껏해야 readOnly 정도만 사용해 봤었으니까. 그래서 아쉬움을 잊기 전에 글로 정리하려고 한다.

먼저, 트랜잭션이란 뭘까?

트랜잭션의 정의는 데이터베이스의 상태를 변경하는 작업 또는 한 번에 (batch) 수행되어야 하는 연산들을 의미한다.

이를 위해서 자바의 @Transactional 어노테이션은 begin, commit 작업을 자동으로 수행해주고, 예외 발생 시 자동으로 롤백 (원자성) 해 주도록 되어 있다.

기본적으로 트랜잭션은 다음의 4가지 성질을 만족하여야 한다고 볼 수 있다.

  • 원자성: 한 트랜잭션 내에서 실행한 작업들은 하나의 단위로 처리하여야 한다. 즉 같은 트랜잭션 내의 모든 작업들은 트랜잭션의 실행이 종료되었을 때 전부 실행되거나 실행되지 않은 상태여야 한다.
  • 일관성: 트랜잭션은 무결성, 일관성 있는 데이터베이스 상태를 유지하여야 한다.
  • 격리성: 동시에 실행되는 여러 트랜잭션들이 서로에게 영향을 끼치지 않도록 격리된 상태에서 작동해야 한다.
  • 영속성: 트랜잭션을 성공적으로 마치면 그 결과는 (다른 트랜잭션으로 변경하지 않으면) 영구히 저장되어야 한다.

@Transaction의 옵션들

isolation

격리 레벨을 제어한다.

  • DEFAULT : 기본값. 데이터베이스 설정에 따라 아래 넷 중 하나를 선택한다.
  • READ_UNCOMMITED : 아직 commit되지 않은 데이터에 대한 다른 트랜잭션의 읽기를 허용
    그러나 이렇게 하면 Dirty Read 문제가 발생한다. 트랜잭션 A가 데이터를 수정하는 동안 트랜잭션 B가 이를 아무 제한 없이 자유롭게 읽을 수 있는 것이기 때문에 만약에 값을 읽어온 이후 트랜잭션 A가 오류로 롤백되어 버린다면 트랜잭션 B가 읽어온 데이터는 유효하지 않게 된다.
  • READ_COMMITED : commit된 데이터에 대해서만 다른 트랜잭션의 읽기 허용. Dirty Read를 방지할 수 있다.
    그러나 Non-repetabie read 문제로부터는 자유롭지 못한데, 이는 트랜잭션 A가 데이터를 조회하는 도중에 트랜잭션 B가 내용을 변경한다면 트랜잭션 A가 다시 조회했을 때 바뀐 데이터를 읽어오게 되는 문제이다.
  • REPEATABLE_READ : 선행 트랜잭션이 읽기 중인 데이터에 대해서 다른 트랜잭션은 수정이 불가능한 읽기 전용 상태가 된다. 이에 따라 읽기 중인 데이터를 다른 트랜잭션이 변경할 수 없어 한 트랜잭션 내에서 같은 데이터를 여러 번 쿼리하더라도 항상 같은 결과를 받을 수 있다.
    마지막으로, Phantom read 문제가 발목을 잡는다. 예를 들어서 회원 목록을 불러오는 트랜잭션 A가 있고 회원 가입을 처리하는 트랜잭션 B가 있다고 하자. 트랜잭션 A가 반복적으로 회원 명수를 조회해야 할 때, 도중에 새로운 회원이 가입한다면 읽어오는 값이 변하고 말 것이다. 이것까지 방지하려면 더 높은 격리 레벨을 사용해야 한다.
  • SERIALIZABLE : 가장 높은 격리 정책으로 한 트랜잭션이 완료될 때까지 해당 트랜잭션이 읽어오는 모든 데이터에 Shared lock을 걸어 다른 트랜잭션이 데이터를 수정할 수 없게 된다. 성능 저하의 우려가 있으므로 주의해서 사용해야 한다.

propagation

‘전파 속성’ 이라고도 한다. 간단히 말하면 새로운 트랜잭션을 시작하거나, 기존 트랜잭션에 작업을 추가하는 방법을 결정짓는 속성값이라고 볼 수 있다.

  • REQUIRED : 이미 진행중인 트랜잭션이 있다면 거기에 ‘참여’ 하고, 아니라면 새로운 트랜잭션을 만들어 시작한다. 기본값은 바로 이 설정이다.
  • REQUIRES_NEW : 항상 새로운 트랜잭션을 만든다. 이미 진행중인 트랜잭션이 있으면 새로 만들어진 트랜잭션을 우선 처리하고, 그 다음에 마저 처리하도록 한다.
  • SUPPORT : 이미 진행중인 트랜잭션이 있으면 해당 트랜잭션에서 작업을 실행하고, 없으면 트랜잭션 없이 실행한다.
  • NOT_SUPPORT : 이미 진행중인 트랜잭션이 있다면 실행을 보류한다. 트랜잭션의 실행이 끝나거나 처음부터 실행 중인 트랜잭션이 없다면 바로 트랜잭션 없이 작업을 실행한다.
  • NEVER : 아무런 트랜잭션도 진행중이지 않을 때에만 작업을 수행하고 실행 중인 트랜잭션이 하나라도 있다면 Exception을 발생시킨다.
  • NESTED : 이미 진행중인 트랜잭션이 있으면 중첩 트랜잭션으로 실행하며, 존재하지 않으면 새로운 트랜잭션을 만들어 시작한다.

noRollBackFor / rollbackFor

트랜잭션 실행 중 특정 Exception이 발생시 롤백 처리하지 않도록 하거나, 무조건 롤백하도록 예외를 걸어 주는 옵션이다.

참고로 자바의 트랜잭션은 기본적으로는 Unchecked Exception 만을 롤백하고 있다.

@Transactional(noRollbackFor=NullPointerException.class)
public List findMembersByRoomNumber(int roomNumber) {

}

timeout

처리 시간이 일정 시간 이상 소요되면 실행을 중단하고 롤백한다. 기본값은 -1로 시간제한을 두지 않는다는 뜻이다.

readOnly

매우 간단하다. true면 SELECT 외의 INSERT, UPDATE, DELETE를 사용할 수 없고 사용하면 예외를 발생시킨다. false가 기본값이며, 당연히 읽기쓰기에 제한이 없게 된다.

(이래서 엄마가 한 번에 한 가지씩만 하랬어…)

이렇게 정리하다 보니 예전에 수행했던 프로젝트에 땜빵해놨던 트랜잭션 충돌 문제도 제대로 해결할 수 있을 것 같다. 꼭 단위 테스트 개별로 돌릴 때는 안 터지는데 전체 한번에 돌리면 터져서 @Transactional 어케 쓰는지도 모르고 클래스마다 떡칠해서 땜빵해둔 게 있었는데…

Posted in Java, Spring Boot, 개발

댓글 남기기