트랜잭션 기초읽기

특징

  • 독립성 : 하나의 트랜젝션은 다른 트랜젝션에 포함될수 없고 독립적이다.

  • 일관성 : 작업 처리의 결과가 항상 일관된다.

  • 원자성 : Database 에 모두반영 (Success) 혹은 모두 반영(Fail)되지 않음을 말한다.

  • 지속성 : 성공적으로 완료된 결과는 영구적용되어야 한다.

트랜젝션 commit & rollback

  • 성공상태이고 success로 반영된 상태

  • 프로세스가 성공아닌 실패로 끝나거나 비정상적으로 종료 되었을때 앞서 성공한 프레세들은 하나로 묶여서 수행전 상태로 되돌아가야 한다.

트랜젝션의 논리적 5가지 상태

  • Active : 현재 실행 중인 상태.

  • Failed : 트랜젝션이 실행중 오류로 인한 중단.

  • Aborted : 트랜잭션이 비정상 종료로 인하여 롤백 된 상태.

  • Partially Committed : 트랜젝션의 연산이 마지막까지 실행 되고 Commit 되기 직전상태.

  • Committed : 트랜젝션이 모두 성공적으로 진행되고 Commit 연산까지 실행된 상태.

Springboot 에서 트랜젝션?

일반적으로 spring-boot-starter-jdbc 와 spring-boot-starter-data-jpa 를 추가하면 springboot 가 알아서 DataSourceTransactionManager 와 JpaTransactionManager 을 주입하기 때문에 사용자는 별도로 @Transactional 어노테이션을 붙일 필요가 없다.

앞서 작성한 Spring Data JPA 다중 DataSource을 기준으로 테스트 코드를 작성해 본다.

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {

	@Autowired
	private UserRepository userRepository;

	@Test
	public void test() throws Exception {

		// 10 data 입력 
		userRepository.save(new User("AAA", 10));
		userRepository.save(new User("BBB", 20));
		userRepository.save(new User("CCC", 30));
		userRepository.save(new User("DDD", 40));
		userRepository.save(new User("EEE", 50));
		userRepository.save(new User("FFF", 60));
		userRepository.save(new User("GGG", 70));
		userRepository.save(new User("HHH", 80));
		userRepository.save(new User("III", 90));
		userRepository.save(new User("JJJ", 100));

		// validation 생략
	}

}

User 객체 @Max(50) 이므로 age가 50 보다 큰수입력시 Exception이 발생한다.

@Entity
@Data
@NoArgsConstructor
public class User {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @Max(50)
    private Integer age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

}
2022-12-26 11:55:29.581 ERROR 24424 --- [           main] o.h.i.ExceptionMapperStandardImpl        : HHH000346: Error during managed flush [Validation failed for classes [com.blake.demo.User] during persist time for groups [javax.validation.groups.Default, ] List of constraint violations:[ ConstraintViolationImpl{interpolatedMessage='최대수치50을 초과할수 없습니다.', propertyPath=age, rootBeanClass=class com.didispace.chapter310.User, messageTemplate='{javax.validation.constraints.Max.message}'} ]]

이미지에서 확인할수 있는것처럼 테스트진행중 예외가 발생하여 앞5개 데이터는 정상 입력되었지만 나머지 5개 데이터는 입력실패, 이때 모두 성공 or 실패로 처리하려면 트랜젝션기능을 이용할수 있다.

@Test
@Transactional
public void test() throws Exception {

    // test 내영은 생략

}

코드에서 처럼 @Transactional 를 붙여주면 끝이다.

public interface UserService {
    
    @Transactional
    User update(String name, String password);
    
}

트랜잭션 상세

앞서 작성 글중에서 Spring Data JPA 다중 DataSource 다중 datasource를 사용하는 환경일경우 트랜젝션 선언시 트랜잭션 매니저를 지정해 줘야 한다. 예를 들면

@Transactional(value="transactionManagerPrimary")

트랜젝션 매니저를 지정하는것 외에도 우리는 트랜젝션을 격리,전파 등 컨트롤할수 있다.

격리(Isolation)

개념 : 트랜잭션에서 일관성이 없는 데이터를 허용하도록 하는 수준을 말한다.

위치 : org.springframework.transaction.annotation.Isolation 에서 확인할수 있다.

public enum Isolation {
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);
}
  • DEFAULT : 기본 격리 수준(, DB의 Isolation Level을 따름)

  • READ_UNCOMMITTED : 커밋되지 않는(트랜잭션 처리중인) 데이터에 대한 읽기를 허용

  • READ_COMMITTED : 트랜잭션이 커밋 된 확정 데이터만 읽기 허용

  • REPEATABLE_READ : 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정하는 것을 막아준다. 선행 트랜잭션이 읽은 데이터는 트랜잭션이 종료될 때까지 후행 트랜잭션이 갱신/삭제를 보류한다.

  • SERIALIZABLE : 가장 강력한 격리수준이다. 순차적으로 진행해서 여러 트랜잭션이 동시에 같은 테이블 정보를 엑세스하지 못하게 한다.

지정방법

@Transactional(isolation = Isolation.DEFAULT)

트랜젝션의 전파(propagation)

개념 : propagation 속성을 이용해 호출한 쪽의 트랜잭션을 그대로 사용할 수도 있고, 새롭게 트랜잭션을 생성할 수도 있다. 트랜잭션의 경계를 정의하며 시작 방법을 결정하는 속성이다.

위치 : org.springframework.transaction.annotation.Propagation 에서 확인 할수 있다.

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);
}
  • REQUIRED : 부모 트랜잭션 내에서 실행하며 부모 트랜잭션이 없을 경우 새로운 트랜잭션을 생성한다.

  • SUPPORTS : 이미 진행중인 부모 트랜잭션이 있으면 참여하고, 없을 경우 Non-transaction으로 실행된다.

  • MANDATORY : 이미 진행중인 트랜잭션이 반드시 있어야만 실행되며, 혼자서는 독립적으로 트랜잭션을 진행하면 안 되는 경우에 사용한다.

  • REQUIRES_NEW : 부모 트랜잭션을 무시하고 무조건 새로운 트랜잭션이 생성, 이미 진행중인 트랜잭션이 있다면 잠깐 보류되고 해당 트랜잭션 경계가 종료 된 후 다시 시작된다.

  • NOT_SUPPORTED : Non-transaction으로 시작하며, 이미 진행중인 트랜잭션이 있으면 끝날 때까지 보류된 후 실행된다.

  • NEVER : Non-transaction으로 시작하며, 실행되며 부모 트랜잭션이 존재한다면 예외가 발생합니다.

  • NESTED : 이미 진행중인 부모 트랜잭션이 있을 경우 중첩 트랜잭션을 생성하여 실행되며, 생성된 중첩 트랜잭션은 부모 트랜잭션이 rollback되면 함께 되지만, 해당 트랜잭션안에서의 Commit/Rollback은 부모 트랜잭션에 영향을 주지 않는다.

propagation 설정을 아래와 같이 할수 있다.

@Transactional(propagation = Propagation.REQUIRED)

끝!

Last updated