Programming/SPRING

[DB/SPRING] @Transactional

너굴위 2024. 4. 28. 22:12
728x90
반응형

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처리가 가능함을 기억해야한다.
728x90
반응형