본문 바로가기
학습

트랜잭션이란?

by 공덕뉸나 2023. 7. 3.

실무를 하다보니 꼭 알아야하지만 기억이 안나는 개념들이 몇몇 있었다. 그 중 하나가 트랜잭션에 관한 내용이었다.

그래서 Spring에서 사용되는 트랜잭션에 대한 개념을 공부하여 정리해 보았다.

 

트랜잭션이란?

- 모든 작업들이 성공적으로 완료되어야 작업 묶음의 결과를 적용하고, 어떤 작업에서 오류가 발생했을 때는 이전에 있던 모든 작업들이 성공적이었더라도 없었던 일처럼 완전히 되돌리는 것

- 데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위

 

데이터베이스를 다룰 때 트랜잭션을 적용하면 데이터 추가, 갱신, 삭제 등으로 이루어진 작업을 처리하던 중 오류가 발생했을 때 모든 작업들을 원상태로 되돌릴 수 있다.

즉, 모든 작업들이 성공해야만 최종적으로 데이터베이스에 반영되는 것이다.

 

Spring에서 @Transactional을 이용하여 트랜잭션 처리를 하는 방법

DB와 관련된, 트랜잭션이 필요한 서비스 클래스 혹은 메서드에 @Transactional 어노테이션을 달아주면 된다.

이는 해당 범위 내 메서드가 트랜잭션이 되도록 보장해주며 선언적 트랜잭션이라고도 한다.

(직접 객체를 만들 필요 없이 선언만으로도 관리를 용이하게 해주기 때문)

 

클래스, 메서드에 모두 @Transactional 어노테이션을 붙이면 메서드 레벨의 @Transactional 선언이 우선 적용된다.

일련의 작업들을 묶어서 하나의 단위로 처리하고 싶다면 @Transactional을 활용하자.

 

@Transactional로 생성된 프록시 객체는 @Transactional이 적용된 메소드가 호출될 경우 PlatformTransactionManager를 사용하여 트랜잭션을 시작하고, 정상 여부에 따라 Commit 혹은 Rollback 동작을 수행한다.

 

이때 프록시 패턴이란 디자인 패턴 중 하나로, 어떤 코드를 감싸면서 추가적인 연산을 수행하도록 강제하는 방법이다.

트랜잭션의 경우, 트랜잭션의 시작과 연산 종료시의 커밋 과정이 필요하므로 프록시를 생성하여 해당 메서드의 앞뒤에 트랜잭션의 시작과 끝을 추가하는 것이다.

 

단, private/protected 메서드는 @Transactional을 무시한다.

 

JPA에서 @Transactional이 있는 경우 왜 자동으로 업데이트가 될까?

JPA에는 따로 update에 관한 쿼리문이 존재하지 않는다.

변경 감지나 병합을 통해서 실행하게 된다.

 

변경감지 - dirty checking

변경 감지는 영속 컨텍스트의 개념에 대해 알아야한다.

 

영속성 컨텍스트란 JPA에 존재하는 엔티티 매니저를 통해 쿼리문을 날리면 자동으로 해당 엔티티는 영속성 컨텍스트에 들어가서 트랜잭션이 끝나는 시점까지 따로 관리하게 된다.

변경감지(더티 체킹)는 영속 컨텍스트에 들어가 있는 영속 엔티티에 한해서 update 쿼리를 날려준다.

findById로 찾은 엔티티는 영속성 컨텍스트다. (id를 기반으로 실제 DB에 있는 영속성 상태 엔티티를 찾아온 것)

이미 영속상태이기 때문에 save 호출도 필요가 없다.

 

코드로 예를 들어보자.

public User getOne(String userId) {
    return userRepo.findById(userId)
        .orElseThrow(() -> new EntityNotFoundException("not exists " + userId));
}
public void updateUser(String userId, UpdateUserDto updateUserDto) {
    User user = this.getOne(userId); 
    user.updateUser(updateUserDto);
}

여기에서 User는 영속성 엔티티다.

public void updateUser(UpdateUserDto request) {
    this.userName = request.getUserName();
    this.address = request.getAddress();
    this.email = request.getEmail();
    this.phoneNumber = request.getPhoneNumber();
}

User는 영속성 엔티티로 관리되며 @Transactional로 인해 로직이 끝날 때 JPA에서 트랜잭션 COMMIT 시점에 변경감지한 후 flush하고, 변경된 값을 update 시켜주기 때문에 따로 save 메소드를 사용할 필요 없다.

 

동작과정을 살펴보자.

1. 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회한다.

2. 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 교체한다. (병합)

3. 트랜잭션 커밋 시점에 DB에 update 쿼리가 실행된다.

 

병합은 모든 필드를 교체하므로 값이 없으면 NULL로 업데이트 할 위험이 있다.

변경 감지 기능을 사용하면 원하는 것만 선택해서 변경할 수 있다.

따라서 엔티티를 변경할 때는 병합은 자제하고 변경 감지를 이용하는게 좋다.

 

@Transactional 없이 JPA 사용 (CRUD)

기본적으로 save와 delete에 대해서는 @Transactional이 사용되고 있다.

select문인 find에 대해서는 @Transactional(ReadOnly = true) 옵션이 사용되고 있다.

 

즉, JPA를 사용하면서 @Transactional을 따로 설정하지 않는다면 select 메서드에 대해서는 @Transactional(ReadOnly = true)가 적용되고 나머지 save, update, delete에 대해서는 @Transactional이 자동으로 사용되어진다. (ReadOnly = false)

 

CRUD 관련 예시를 한 번 살펴보자.

 

위 코드는 저장 메소드(save)이다.

저장 메소드에 대해서는 Transactional 옵션이 추가로 선언되었다.

 

위 코드는 조회 (find)이다.

find, getById, exists 모두 트랜잭션 옵션에 대한 추가 선언이 없다.

위 코드는 삭제(delete)이다.

저장 메소드(save)와 마찬가지로 트랜잭션 옵션을 선언하고 있다.

 

따라서 조회를 제외한 저장, 삭제 메서드는 @Transactional을 별도로 선언하지 않아도 자동으로 사용되어지는 것이다.

 

코드를 짜면서 실수로 트랜잭션 어노테이션을 사용하지 않고 왜 코드가 제대로 동작하지 않는지 헤맸던 경험이 있다.

너무나 당연한 이유였는데 막상 겪으니 트랜잭션이 생각나지 않았다.. 

JPA에서 트랜잭션이 어떻게 동작되는지 다시 한 번 학습하고 나니 이제 까먹지 않을 것 같다.

나처럼 코드 다 짜놓고 어노테이션 놓치는 사람들이 오래 헤매지 않고 도움이 되길 바라며 글 마친다! 망망