반응형
들어가며
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 |