[DB/SPRING] @Transactional
Transaction?
- 데이터베이스의 상태를 변화시키기 위해 수행하는 작업의 단위
- 작업의 단위는 사용자가 어떤 행위를 했을 때 수행되는 명령문들을 합친 것이라고 생각하면 된다.
EX) 저장버튼을 누름 -> DB)Insert 후 목록 최신화를 위해 Select 동시 수행(작업단위 = 트랜잭션) -> 최신화된 목록 확인
트랜잭션(Transaction)의 특징
- 원자성(Atomicity) : 트랜잭션이 데이터베이스에 모두 반영되거나, 전혀 반영되지 않아야 한다.
- 일관성(Consistency) : 트랜잭션의 작업 처리 결과가 항상 일관성이 있어야 한다.
- 독립성(Isolation): 어떤 하나의 트랜잭션이라도, 다른 트랜잭션의 연산에 끼어들 수 없다.
- 지속성(Durability): 트랜잭션이 성공적으로 완료되었을 경우, 결과는 영구적으로 반영되어야 한다.
트랜잭션의 COMMIT / ROLLBACK
# COMMIT
- 변경된 데이터를 테이블에 영구적으로 반영함
# ROLLBACK
- 변경되었으나 아직 COMMIT되지 않은 모든 내용들을 취소하고 데이터베이스를 이전 상태로 되돌림
@Transactional
1. Spring은 트랜잭션을 시작하고 메서드가 정상적으로 종료되면 트랜잭션을 commit한다.
2. 만약 예외가 발생하면 트랜잭션을 Rollback하게 된다.
위 두 문장은 @Transactional 어노테이션을 이해하고 사용하기 위한 핵심이자 중요한 내용이다.
동작방식을 정확하게 이해하고 적용해보도록 하자.
JDBC에서의 트랜잭션
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class TransactionExample {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
try {
// JDBC 드라이버 로드
Class.forName("com.mysql.jdbc.Driver");
// 데이터베이스 연결
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/yourdatabase", "username", "password");
// 자동 커밋 비활성화
conn.setAutoCommit(false);
// SQL 실행을 위한 Statement 객체 생성
stmt = conn.createStatement();
// 첫 번째 SQL 쿼리 실행
String sql1 = "INSERT INTO your_table(column1, column2) VALUES('value1', 'value2')";
stmt.executeUpdate(sql1);
// 두 번째 SQL 쿼리 실행
String sql2 = "UPDATE your_table SET column1 = 'new_value' WHERE condition = true";
stmt.executeUpdate(sql2);
// 트랜잭션 커밋
conn.commit();
System.out.println("트랜잭션 커밋 완료");
} catch (SQLException se) {
// 롤백 수행 및 오류 처리
try {
if (conn != null)
conn.rollback();
} catch (SQLException re) {
re.printStackTrace();
}
se.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 리소스 해제
try {
if (stmt != null)
stmt.close();
} catch (SQLException se2) {
}
try {
if (conn != null)
conn.close();
} catch (SQLException se) {
se.printStackTrace();
}
}
}
}
-> try-catch문으로 exception이 발생했을 경우 rollback을 할 수 있도록 코드를 죄다 작성했었다.
Spring Annotation (@Transactional)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void createUser(User user) {
userRepository.save(user);
}
@Transactional(rollbackFor = Exception.class)
public void updateUser(User user) {
userRepository.save(user);
// 특정 조건에 따라 예외를 발생시킴
if (user.getAge() < 0) {
throw new IllegalArgumentException("나이는 음수일 수 없습니다.");
}
}
}
-> 이렇게 @Transactional을 Service단에 붙이면 유지보수가 쉬운 장점이 있다.
-> Exception 발생 시 작업 내용을 rollback 할 수 있도록 설정했다.
@Transactional의 속성
각 속성은 트랜잭션의 동작을 세부적으로 제어하는 데 사용된다.
1. Propagation (전파수준) : 트랜잭션 전파 속성을 지정하며, 여러 트랜잭션 경계 사이에서 메서드 호출이 어떻게 동작해야 하는지를 결정한다. ex) REQUIRED, REQUIRES_NEW, NESTED
import org.springframework.transaction.annotation.Transactional;
@Transactional(propagation = Propagation.REQUIRED)
public class ServiceA {
@Autowired
private ServiceB serviceB;
public void methodA() {
// 이 메서드의 트랜잭션은 REQUIRED로 설정됨
serviceB.methodB();
}
}
public class ServiceB {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// 이 메서드의 트랜잭션은 REQUIRES_NEW로 설정됨
}
}
REQUIRED (기본 값)
- 항상 트랜잭션이 실행된다.
- 기존 부모 트랜잭션이 있으면 해당 트랜잭션을 사용하고, 없으면 새로운 트랜잭션을 생성하여 사용한다.
REQUIRES_NEW
- 항상 새로운 트랜잭션에서 실행된다.
- 기존 트랜잭션이 있으면 일시중단시킨다.
SUPPORTS
- 기존 부모 트랜잭션이 있으면 해당 트랜잭션을 사용하여 실행하고, 없으면 트랜잭션을 사용하지 않는다.
NESTED
- 항상 트랜잭션이 실행된다.
- 기존 트랜잭션이 있으면, 해당 트랜잭션에 Save point를 만들고 기존 트랜잭션 내에 중첩 트랜잭션을 만든다.
- 오류 발생 시 Save point로 rollback한다.
- 하위 트랜잭션은 상위 트랜잭션의 영향을 받지만, 반대로 상위 트랜잭션은 하위 트랜잭션에 영향을 덜 받게 된다.
2. Isolation (격리수준) : 트랜잭션 격리 수준을 지정하며, 여러 트랜잭션이 동시에 실행될 때 데이터의 일관성과 격리 수준을 조절한다.
=> 기본 설정은 DEFAULT
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Isolation;
@Transactional(isolation = Isolation.READ_COMMITTED)
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUserById(Long userId) {
// 이 메서드가 실행되는 동안 다른 트랜잭션에서 변경한 데이터를 읽을 수 있음
return userRepository.findById(userId).orElse(null);
}
}
level 0 : READ_UNCOMMITTED
- 트랜잭션 처리 중 또는 아직 커밋되지 않은 데이터를 다른 트랜잭션이 읽는 것을 허용한다.
- Dirty Read가 발생한다.
Dirty Read를 허용하게 되면, 데이터가 나타났다 사라졌다하는 현상이 생길 수 있다.
정합성에 문제가 많기 때문에 거의 사용하지 않고 권장하지도 않는다.
level 1 : READ_COMMITTED
- 트랜잭션 처리 중 다른 트랜잭션에서 접근할 수 없도록 막는다.
- 트랜잭션 종료 전까지는 다른 트랜잭션에서 해당 데이터에 접근이 불가하다.
- Dirty Read를 방지할 수 있다.
level 2 : REPEATABLE_READ
- 트랜잭션 처리 중 SELECT 문에서 사용하는 모든 데이터에 shared lock이 걸린다.
- Non-Repeatable Read를 방지한다.
(하나의 트랜잭션 내에서 같은 데이터의 값이 달라짐, 같은 쿼리문 서로 다른 결과)
- Phantom Read는 발생할 수 있다.
(하나의 트랜잭션 내에서 없던 데이터가 생김, 다른 트랜잭션에서 수행한 변경작업 레코드가 보였다 안보였다 하는 현상)
level 3 : SERIALIZABLE
- 가장 엄격한 격리 수준을 적용하였다.
- 완벽한 일관성에 Phantom Read 방지가 가능하다.
- 모든 격리 수준 관련 문제 해결이 가능하지만, 성능 최악이어서 거의 사용하지 않는다.
3. readOnly : 읽기 전용 트랜잭션 여부를 지정한다. 읽기 작업만 수행하고 변경 작업을 수행하지 않는 경우 true로 설정하여 성능을 향상시킬 수 있다.
import org.springframework.transaction.annotation.Transactional;
@Transactional(readOnly = true)
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUserById(Long userId) {
// 변경 작업을 수행하지 않는 읽기 전용 트랜잭션
return userRepository.findById(userId).orElse(null);
}
}
4. timeout : 트랜잭션 타임아웃을 설정하며, 트랜잭션이 지정된 시간 내에 완료되지 않으면 롤백된다.
import org.springframework.transaction.annotation.Transactional;
@Transactional(timeout = 60) // 60초 동안 트랜잭션 완료되지 않으면 롤백
public class UserService {
@Autowired
private UserRepository userRepository;
public void createUser(User user) {
userRepository.save(user);
}
}
5. rollbackFor : 롤백할 예외 타입을 지정하며, 지정된 예외가 발생하면 트랜잭션이 롤백된다.
noRollbackFor : 롤백하지 않을 예외 타입을 지정하며, 지정된 예외가 발생해도 트랜잭션이 롤백되지 않는다.
import org.springframework.transaction.annotation.Transactional;
@Transactional(rollbackFor = IllegalArgumentException.class, noRollbackFor = NullPointerException.class)
public class UserService {
@Autowired
private UserRepository userRepository;
public void updateUser(User user) {
if (user.getAge() < 0) {
throw new IllegalArgumentException("나이는 음수일 수 없습니다."); // 롤백됨
}
if (user.getName() == null) {
throw new NullPointerException("이름이 null입니다."); // 롤백되지 않음
}
userRepository.save(user);
}
}
- 비정상적 종료로 인한 rollback이 발생할 경우에는 트랜잭션의 일부 작업만 데이터베이스에 반영되는 것을 방지해 데이터 일관성을 유지할 수 있다.
- transaction을 사용하려는 메서드는 반드시 public으로 선언되어야 한다.
-> 프록시 객체로 외부에서 접근 가능한 인터페이스를 제공해야하기 때문이다.
@Transactional(rollbackFor = Exception.class)
- transaction은 unckecked exception일 경우에만 Rollback이 되고, checked exception은 @Trasactional을 붙인다고 해서 자동 Rollback이 되지 않는다. 따라서 어노테이션 안에 rollbackFor = Exception.class를 붙여줘야 checked exception에 대한 Rollback처리가 가능함을 기억해야한다.