반응형

들어가며

Transaction의 성질

  • 원자성(Atomicity)
    • 한 트랜잭션 내에서 실행한 작업들은 하나로 간주한다.
    • 모두 성공 또는 모두 실패해야 한다.
  • 일관성(Consistency)
    • 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다.
  • 격리성(Isolation)
    • 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않도록 격리해야한다.
  • 지속성(Durability)
    • 트랜잭션을 성공적으로 마치면 결과가 항상 저장되어야 한다.

Transactional 속성

  • isolation
    • 여러 트랜잭션들이 서로 어떻게 영향을 끼칠 것인지에 대한 설정
  • propagation
    • 이미 트랜잭션이 존재할 때 새롭게 발생하는 트랜잭션을 어떻게 처리할 것인지에 대한 설정
  • readOnly
  • rollbackFor

isolation

MySQL Transaction Isolation Level

MyBatis 활용하여 테스트시 유의점

  • 쿼리 속성으로 flushCache="true", useCache="false"를 해주어야함
  • 디폴트는 flushCache="false", useCache="true"
  • flushCache하는 시점은 commit(), rollback(), close(), SqlSession.clearCache()가 호출될 때임
  • 디폴트 설정일 경우 트랜잭션 내에서는 위 메소드가 호출되지 않아 flushCache 처리가 되지 않고, 한 번 조회한 쿼리 결과가 캐싱되어 재사용됨
  • 자동적으로 REPEATABLE_READ 효과가 발생하게 되므로 정확한 테스트가 불가능해짐

READ_UNCOMMITTED

  • User
    @AllArgsConstructor
    @NoArgsConstructor
    @Data
    public class User {
        private int id;
        private String name;
    }
    
  • UserMapper
    public interface UserMapper {
        List<User> selectUsers();
    }
    
  • UserMapper.xml
    <mapper namespace="com.example.springtransactional.mapper.UserMapper">
        <select id="selectUsers" resultType="com.example.springtransactional.model.User" flushCache="true" useCache="false">
            SELECT id, name FROM user
        </select>
    </mapper>
    
  • UserService
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
    
        @Transactional(isolation = Isolation.READ_UNCOMMITTED)
        public void test() {
            log.info("selectUsers-1 : {}", userMapper.selectUsers());
            // 대기
            log.info("selectUsers-2 : {}", userMapper.selectUsers());
        }
    }
    
    selectUsers-1 : [User(id=1, name=user1), User(id=2, name=user2), User(id=3, name=user3)]
    
  • session-2
    mysql> START TRANSACTION;
    mysql> UPDATE user SET name = 'updated' WHERE id = 1;
    mysql> INSERT INTO user (name) VALUES ('inserted');
    
  • UserService
    selectUsers-2 : [User(id=1, name=updated), User(id=2, name=user2), User(id=3, name=user3), User(id=4, name=inserted)]
    

READ_COMMITTED

  • 위 코드 그대로 활용
  • UserService
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
    
        @Transactional(isolation = Isolation.READ_COMMITTED)
        public void test() {
            log.info("selectUsers-1 : {}", userMapper.selectUsers());
            // 대기
            log.info("selectUsers-2 : {}", userMapper.selectUsers());
            // 대기
            log.info("selectUsers-3 : {}", userMapper.selectUsers());
        }
    }
    
    selectUsers-1 : [User(id=1, name=user1), User(id=2, name=user2), User(id=3, name=user3)]
    
  • session-2
    mysql> START TRANSACTION;
    mysql> UPDATE user SET name = 'updated' WHERE id = 1;
    mysql> INSERT INTO user (name) VALUES ('inserted');
    
  • UserService
    selectUsers-2 : [User(id=1, name=user1), User(id=2, name=user2), User(id=3, name=user3)]
    
  • session-2
    mysql> COMMIT;
    
  • UserService
    selectUsers-3 : [User(id=1, name=updated), User(id=2, name=user2), User(id=3, name=user3), User(id=5, name=inserted)]
    

REPEATABLE_READ

  • 위 코드 그대로 활용
  • UserService
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
    
        // isolation 디폴트값은 Isolation.DEFAULT
        // MySQL의 기본 레벨은 REPEATABLE_READ, isolation 속성을 별도로 지정해주지 않으면 REPEATABLE_READ로 작동함
        @Transactional(isolation = Isolation.REPEATABLE_READ)
        public void test() {
            log.info("selectUsers-1 : {}", userMapper.selectUsers());
            // 대기
            log.info("selectUsers-2 : {}", userMapper.selectUsers());
            // 대기
            log.info("selectUsers-3 : {}", userMapper.selectUsers());
        }
    }
    
    selectUsers-1 : [User(id=1, name=user1), User(id=2, name=user2), User(id=3, name=user3)]
    
  • session-2
    mysql> START TRANSACTION;
    mysql> UPDATE user SET name = 'updated' WHERE id = 1;
    mysql> INSERT INTO user (name) VALUES ('inserted');
    
  • UserService
    selectUsers-2 : [User(id=1, name=user1), User(id=2, name=user2), User(id=3, name=user3)]
    
  • session-2
    mysql> COMMIT;
    
  • UserService
    selectUsers-3 : [User(id=1, name=user1), User(id=2, name=user2), User(id=3, name=user3)]
    

SERIALIZABLE

  • UserMapper
    public interface UserMapper {
        User selectUser(@Param("id") int id);
    
        void updateUser(User user);
    }
    
  • UserMapper.xml
    <mapper namespace="com.example.springtransactional.mapper.UserMapper">
        <select id="selectUser" resultType="com.example.springtransactional.model.User" flushCache="true" useCache="false">
            SELECT id, name FROM user WHERE id = #{id}
        </select>
    
        <update id="updateUser" parameterType="com.example.springtransactional.model.User">
            UPDATE user SET name = #{name} WHERE id = #{id}
        </update>
    </mapper>
    
  • UserService
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
    
        @Transactional(isolation = Isolation.SERIALIZABLE)
        public void test() {
            log.info("selectUser-1 : {}", userMapper.selectUser(1));
            userMapper.updateUser(new User(1, "user1-a"));
            // 대기
            log.info("selectUser-2 : {}", userMapper.selectUser(1));
        }
    }
    
    selectUser-1 : User(id=1, name=user1)
    
  • session-2
    mysql> START TRANSACTION;
    mysql> SELECT * FROM user WHERE id = 1;
    +----+-------+
    | id | name  |
    +----+-------+
    |  1 | user1 |
    +----+-------+
    
    mysql> UPDATE user SET name = 'user1-b' WHERE id = 1; // block됨, 다른 row를 수정할 경우에는 block되지 않음
    
  • UserService
    selectUser-2 : User(id=1, name=user1-a)
    
  • session-2
    mysql> COMMIT;
    mysql> SELECT * FROM user WHERE id = 1;
    +----+---------+
    | id | name    |
    +----+---------+
    |  1 | user1-b |
    +----+---------+
    

readOnly

설명

  • 트랜잭션을 읽기 전용으로 설정할 수 있다.
  • 특정 트랜잭션 작업 안에서 쓰기 작업이 일어나는 것을 의도적으로 방지하기 위해 사용할 수도 있다.
  • 읽기 전용 트랜잭션이 시작된 이후 INSERT, UPDATE, DELETE 같은 쓰기 작업이 진행되면 예외가 발생한다.
  • 기본 설정은 false

예시

@Transactional(readOnly = true)
public void outer() {
    testMapper.insert(); // 오류 발생
}

propagation

REQUIRED

  • 부모 트랜잭션이 있으면 합류, 없으면 새로운 트랜잭션 생성
  • 디폴트 속성
  • InnerUserService
    @Slf4j
    @AllArgsConstructor
    @Service
    public class InnerUserService {
        private final UserMapper userMapper;
    
        @Transactional(propagation = Propagation.REQUIRED)
        public void addUser() {
            userMapper.insertUser(new User(0, "tom"));
    
            throw new IllegalStateException("for rollback");
        }
    }
    
  • UserService - 부모 트랜잭션이 있는 경우
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
        private final InnerUserService innerUserService;
    
        @Transactional
        public void addUser() {
            userMapper.insertUser(new User(0, "john"));
    
            innerUserService.addUser();
        }
    }
    
    mysql> select * from user;
    Empty set (0.00 sec)
    
  • UserService - 부모 트랜잭션이 없는 경우
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
        private final InnerUserService innerUserService;
    
        // @Transactional
        public void addUser() {
            userMapper.insertUser(new User(0, "john"));
    
            innerUserService.addUser();
        }
    }
    
    mysql> select * from user;
    +---------+------+
    | userSeq | name |
    +---------+------+
    |       1 | john |
    +---------+------+
    

SUPPORTS

  • 부모 트랜잭션이 있으면 합류, 없으면 트랜잭션 없이 진행
  • InnserUserService
    @Slf4j
    @AllArgsConstructor
    @Service
    public class InnerUserService {
        private final UserMapper userMapper;
    
        @Transactional(propagation = Propagation.SUPPORTS)
        public void addUser() {
            userMapper.insertUser(new User(0, "tom"));
    
            throw new IllegalStateException("for rollback");
        }
    }
    
  • UserService - 트랜잭션 있는 경우
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
        private final InnerUserService innerUserService;
    
        @Transactional
        public void addUser() {
            userMapper.insertUser(new User(0, "john"));
    
            innerUserService.addUser();
        }
    }
    
    mysql> select * from user;
    Empty set (0.00 sec)
    
  • UserService - 트랜잭션 없는 경우
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
        private final InnerUserService innerUserService;
    
        // @Transactional
        public void addUser() {
            userMapper.insertUser(new User(0, "john"));
    
            innerUserService.addUser();
        }
    }
    
    mysql> select * from user;
    +---------+------+
    | userSeq | name |
    +---------+------+
    |       1 | john |
    |       2 | tom  |
    +---------+------+
    

MANDATORY

  • 부모 트랜잭션이 있으면 합류, 없으면 예외 발생
  • 혼자서는 독립적으로 트랜잭션을 진행하면 안 되는 경우에 사용
  • InnserUserService
    @Slf4j
    @AllArgsConstructor
    @Service
    public class InnerUserService {
        private final UserMapper userMapper;
    
        @Transactional(propagation = Propagation.MANDATORY)
        public void addUser() {
            userMapper.insertUser(new User(0, "tom"));
    
            throw new IllegalStateException("for rollback");
        }
    }
    
  • UserService - 트랜잭션 있는 경우
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
        private final InnerUserService innerUserService;
    
        @Transactional
        public void addUser() {
            userMapper.insertUser(new User(0, "john"));
    
            innerUserService.addUser();
        }
    }
    
    mysql> select * from user;
    Empty set (0.00 sec)
    
  • UserService - 트랜잭션 없는 경우
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
        private final InnerUserService innerUserService;
    
        // @Transactional
        public void addUser() {
            userMapper.insertUser(new User(0, "john"));
    
            innerUserService.addUser();
        }
    }
    
    org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
    

NOT_SUPPORTED

  • 트랜잭션을 사용하지 않는 속성
  • 부모 트랜잭션이 있어도 참여하지 않는다.
  • InnserUserService
    @Slf4j
    @AllArgsConstructor
    @Service
    public class InnerUserService {
        private final UserMapper userMapper;
    
        @Transactional(propagation = Propagation.NOT_SUPPORTED)
        public void addUser() {
            userMapper.insertUser(new User(0, "tom"));
    
            throw new IllegalStateException("for rollback");
        }
    }
    
  • UserService
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
        private final InnerUserService innerUserService;
    
        @Transactional
        public void addUser() {
            userMapper.insertUser(new User(0, "john"));
    
            innerUserService.addUser();
        }
    }
    
    mysql> select * from user;
    +---------+------+
    | userSeq | name |
    +---------+------+
    |       2 | tom  |
    +---------+------+
    

NEVER

  • 트랜잭션을 사용하지 않는 속성
  • 부모 트랜잭션이 존재하면 예외 발생
  • InnserUserService
    @Slf4j
    @AllArgsConstructor
    @Service
    public class InnerUserService {
        private final UserMapper userMapper;
    
        @Transactional(propagation = Propagation.NEVER)
        public void addUser() {
            userMapper.insertUser(new User(0, "tom"));
    
            throw new IllegalStateException("for rollback");
        }
    }
    
  • UserService
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
        private final InnerUserService innerUserService;
    
        @Transactional
        public void addUser() {
            userMapper.insertUser(new User(0, "john"));
    
            innerUserService.addUser();
        }
    }
    
    org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'
    
    mysql> select * from user;
    Empty set (0.00 sec)
    

REQUIRES_NEW

  • 부모 트랜잭션을 무시하고 무조건 새로운 트랜잭션 생성
  • 부모 트랜잭션과 자식 트랜잭션간 서로 영향을 끼치지 않음
  • 자식 트랜잭션에서 롤백된 경우
    @Slf4j
    @AllArgsConstructor
    @Service
    public class InnerUserService {
        private final UserMapper userMapper;
    
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void addUser() {
            userMapper.insertUser(new User(0, "tom"));
    
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }
    
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
        private final InnerUserService innerUserService;
    
        @Transactional
        public void addUser() {
            userMapper.insertUser(new User(0, "john"));
    
            innerUserService.addUser();
        }
    }
    
    mysql> select * from user;
    +---------+------+
    | userSeq | name |
    +---------+------+
    |       1 | john |
    +---------+------+
    
  • 부모 트랜잭션에서 롤백된 경우
    @Slf4j
    @AllArgsConstructor
    @Service
    public class InnerUserService {
        private final UserMapper userMapper;
    
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void addUser() {
            userMapper.insertUser(new User(0, "tom"));
        }
    }
    
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
        private final InnerUserService innerUserService;
    
        @Transactional
        public void addUser() {
            userMapper.insertUser(new User(0, "john"));
    
            innerUserService.addUser();
    
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }
    
    mysql> select * from user;
    +---------+------+
    | userSeq | name |
    +---------+------+
    |       2 | tom  |
    +---------+------+
    

NESTED

  • 중첩 트랜잭션은 트랜잭션 안에 다시 트랜잭션을 만드는 것
  • 새 트랜잭션을 만든다는 것은 REQUIRES_NEW와 같음
  • 부모 트랜잭션은 자식 트랜잭션에 영향을 받지 않고, 자식 트랜잭션은 부모 트랜잭션에 영향을 받음
  • 자식 트랜잭션에서 롤백된 경우
    @Slf4j
    @AllArgsConstructor
    @Service
    public class InnerUserService {
        private final UserMapper userMapper;
    
        @Transactional(propagation = Propagation.NESTED)
        public void addUser() {
            userMapper.insertUser(new User(0, "tom"));
    
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }
    
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
        private final InnerUserService innerUserService;
    
        @Transactional
        public void addUser() {
            userMapper.insertUser(new User(0, "john"));
    
            innerUserService.addUser();
        }
    }
    
    mysql> select * from user;
    +---------+------+
    | userSeq | name |
    +---------+------+
    |       1 | john |
    +---------+------+
    
  • 부모 트랜잭션에서 롤백된 경우
    @Slf4j
    @AllArgsConstructor
    @Service
    public class InnerUserService {
        private final UserMapper userMapper;
    
        @Transactional(propagation = Propagation.NESTED)
        public void addUser() {
            userMapper.insertUser(new User(0, "tom"));
        }
    }
    
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
        private final InnerUserService innerUserService;
    
        @Transactional
        public void addUser() {
            userMapper.insertUser(new User(0, "john"));
    
            innerUserService.addUser();
    
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }
    
    mysql> select * from user;
    Empty set (0.00 sec)
    

rollbackFor

설명

  • 선언적 트랜잭션(@Transactional)에서는 UncheckedException이 발생하면 롤백한다.
  • 반면에 예외가 발생하지 않거나 CheckedException이 발생하면 커밋한다.
  • CheckedException을 커밋 대상으로 삼은 이유는 CheckedException이 예외적인 상황에서 사용되기보다는 리턴 값을 대신해서 비즈니스적인 의미를 담은 결과를 돌려주는 용도로 많이 사용되기 때문이다.
  • CheckedException, UnCheckedException 상관 없이 롤백이 될 수 있도록 설정할 수 있도록 해주는 속성이 rollbackFor

예시

@Transactional
public void test() {
    testMapper.insert();

    throw new RuntimeException("unchecked exception"); // 롤백O
}
@Transactional(rollbackFor = {Exception.class})
public void test() {
    testMapper.insert();

    throw new RuntimeException("unchecked exception"); // 롤백O
}
@Transactional
public void test() throws Exception {
    testMapper.insert();

    throw new Exception("checked exception"); // 롤백X
}
@Transactional(rollbackFor = {Exception.class})
public void test() throws Exception {
    testMapper.insert();

    throw new Exception("checked exception"); // 롤백O
}

기타

Rollback 처리되지 않는 케이스 및 해결

  • try문에서 DB 쓰기작업 처리 이후 오류 발생시 catch문에서 별도로 상위로 오류를 throw해주지 않으면 롤백 처리되지 않음
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
    
        @Transactional
        public void test() {
            try {
                userMapper.insertUser(new User(0, "john"));
    
                throw new IllegalStateException();
            } catch (Exception e) {
                log.error("ERROR", e);
            }
        }
    }
    
  • 선언적 Transactional인 경우 TransactionAspectSupport를 활용하여 롤백 처리를 해주어야함
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
    
        @Transactional
        public void test() {
            try {
                userMapper.insertUser(new User(0, "john"));
    
                throw new IllegalStateException();
            } catch (Exception e) {
                log.error("ERROR", e);
    
                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            }
        }
    }
    
  • 선언적 Transactional이 아닌 경우 TransactionTemplate을 활용하여 롤백 처리를 해주어야함
    @Slf4j
    @AllArgsConstructor
    @Service
    public class UserService {
        private final UserMapper userMapper;
        private final TransactionTemplate transactionTemplate;
    
        public String test() {
            return transactionTemplate.execute(status -> {
                try {
                    userMapper.insertUser(new User(0, "john"));
    
                    if (true) {
                        throw new IllegalStateException();
                    }
    
                    return "Hello World";
                } catch (Exception e) {
                    log.error("ERROR", e);
    
                    status.setRollbackOnly();
    
                    return null;
                }
            });
        }
    }
    

multi-thread로 처리시 주의사항

  • Transactional은 단일 스레드에서 동작하므로 아래처럼 multi-thread로 처리시 원하는대로 롤백되지 않는다.
@Slf4j
@RequiredArgsConstructor
@Service
public class UserService {
    private final UserMapper userMapper;

    @Transactional
    public void addUsers() {
        IntStream.range(0, 100).parallel().forEach(value -> {
            userMapper.insertUser(new User(value, "john-" + value));

            if (value > 20) {
                throw new IllegalStateException("for rollback");
            }
        });
    }
}

참고

반응형

'Development > Spring' 카테고리의 다른 글

[Spring] Dependency Injection  (0) 2020.12.27
[Spring] Distributed Lock (with MySQL, Redis)  (4) 2020.12.27
[Spring] AOP  (0) 2020.12.27
[Spring] Cookie & Session  (0) 2020.12.27
[Spring] Cache (with Redis)  (0) 2020.12.27

+ Recent posts