ㅅㅇ

[Spring boot] @Transactional 어노테이션 본문

SW_STUDY/SpringBoot

[Spring boot] @Transactional 어노테이션

SO__OS 2023. 3. 8. 20:06

1. Transactional 이란 ?

데이터베이스 트랜잭션은 데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위. 데이터에 대한 하나의 논리적 실행 단계라 할 수 있다.

 

여기서 단위라는 말을 사용했는데, 쉽게 말하면 더 이상 쪼개질 수 없는 최소의 연산

트랜잭션의 목적은 트랜잭션을 조작하는 기능은 사용자가 데이터 베이스 완전성을 유지하는데 확신을 심어주게 하는 것이다.

 

어떤 연산에 트랜잭션이 보장된다면, DB에서 의도치 않은 값이 저장되거나 조회되는 것을 막을 수 있다.

2. @transactional

@transactional은 클래스나 메서드에 붙여줄 경우, 해당 범위 내 메서드가 트랜잭션이 되도록 보장해준다.

직접 객체를 만들 필요 없이 선언만으로도 관리를 용이하게 해주기 때문에 선언적 트랜잭션이라고 한다.

 

SpringBoot 의 경우 선언적 트랜잭션에 필요한 여러 설정이 이미 되어있는 탓에 더 쉽게 사용할 수 있다.

 

3. SpringBoot 에서의 선언적 트랜잭션

  • 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다.
    서비스 클래스에서 @transactional 을 사용할 경우, 해당 코드 내의 메서드를 호출할 때 영속성 컨텍스트가 생기게 된다.
  • 영속성 컨텍스트는 트랜잭션 AOP가 트랜잭션을 시작할 때 생겨나고, 메서드가 종료되어 트랜잭션 AOP가 트랜잭션을 커밋할 경우 영속성 컨텍스트가 flush되면서 해당 내용이 반영되기에 이후 영속성 컨텍스트 역시 종료가 되게 된다. 이러한 방식으로 영속성 컨텍스트를 관리해 주기 때문에, @transactional을 쓸 경우 트랜잭션의 원칙을 정확히 지킬 수 있다.

 4. 예제

@Service
public class AccountService {

	@Autowired
	private AccountRepository accountRepository;

@Transactional
	public AccountDTO login(LoginDTO login) throws Exception {

		AccountEntity account = accountRepository.findByAccountEmail(login.getAccountEmail())
				.orElseThrow(() -> new ServerErrorRequestException("아이디를 잘 못 입력하셨습니다. 입력하신 내용을 다시 확인해주세요."));

		boolean matches = passwordEncoder.matches(login.getAccountPassword(), account.getAccountPassword());

		if (!matches) {

			throw new ServerErrorRequestException("비밀번호를 잘못 입력했습니다. 입력하신 내용을 다시 확인해주세요.");
		}
		return AccountDTO.toDTO(account);

	}
}

 

  • @transactional 메소드 를 사용하게 되면, login() 메서드가 실행될 경우 해당 메서드는
  • 연산이 고립되어, 다른 연산과의 혼선으로 인해 잘못된 값을 가져오는 경우가 방지된다.
  • 연산의 원자성이 보장되어, 연산이 도중에 실패할 경우 변경사항이 Commit되지 않는다.

=> 위 속성이 보장되기 때문에, 해당 메소드를 실행하는 동안 오류가 발생해도 rollback 해서 DB 에 해당 오류로 인한 결과가 반영되지 않도록 할 수 있다.

 

5. 유의점

  • 같은 트랜잭션 내에서 여러 EntityManager를 쓰더라도, 이는 같은 영속성 컨텍스트를 사용한다.
  • 같은 EntityManager 를 쓰더라도, 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다.
  • 트랜잭션은 보통 서비스 계층에서 시작하므로, 서비스 계층이 끝나는 시점에 트랜잭션이 종료되면서 영속성 컨텍스트도 함께 종료된다.
  • 따라서, 조회한 entity가 Service/Repository 계층에서는 영속성 컨텍스트에서 관리되며 영속 상태를 유지하지만, 프리젠테이션 계층 controller나 view 에서는 준영속 상태가 된다!!

=> 준영속 상태는 엔티티가 영속성 컨텍스트에서 분리된 것을 뜻하는데, 이 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다. 그리고 이때의 문제는 바로 지연 로딩(Lazy Loading)이다.

 

@Entity
public class Post {
	@Id @GeneratedValue
    private int postId;
    
    @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩 전략
    private Account accountId;
}
  • 이 엔티티가 Service 에서 조회된다고 해도 서비스의 해당 find 메소드가 종료된다면 영속성 컨텍스트가 닫히고 반환된 entity 는 준영속 상태가 된 것이다.

lazy loading 전략을 사용했으므로 비어있는 프록시 객체로 존재했는데, 해당 객체에서 실제로 값을 뽑아 쓰려고 하니 예외가 발생한다. 만약 준영속이 아니었다면 영속성 컨텍스트를 통해 Account 를 조회하고 값을 반환했을 것이다.

 

=> 이러한 준영속 상태의 lazy loading 이슈를 해결할 방법에는 여러 방법이 있으나, 가장 간단하게 서비스와 컨트롤러 간에 DTO 를 사용하면 된다. Entity 는 오로지 서비스, 레퍼지토리 에서만 쓰고 그 외엔 DTO 로 해결하면 되는 것.