트랜젝션(Transaction)

goose_bumps·2024년 7월 18일

1. 트랜잭션이란?

트랜잭션이란?
여러 개의 SQL 쿼리를 날리는 메서드가 있다고 가정해보자. 이때 한 개의 쿼리만 안될 때 어디서 문제가 발생했는지 찾아야 한다.

public void pointUser(User user){
	userInfo.findAll();
    userInfo.save(user);
    userInfo.update(user);
    }

예를 들어, 위처럼 user 객체를 통해 사용자의 점수를 책정하는 함수가 있다고 가정해보자. 이 때 트랜잭션을 적용하면 하나라도 안된다면 pointUser라는 함수 전체가 실행이 안된다.
즉, 서로 하나로 묶여있어 쪼갤 수 없는 것이다. 이것을 쪼갤 수 없는 업무의 단위, 트랜잭션이라고 한다.

CLI에서 트랜잭션의 명령어를 직접 입력하여 사용할 수 있다.

  • start transaction : 트랜잭션 시작
  • commit : 정상종료
  • rollback : 실패처리

트랜잭션을 시작한 후 정상종료 하지 않으면 다른 곳에서 접속된 CLI에서 데이터를 조회할 수 없다. 다시 말해, 정상종료 시키면 데이터에 접근이 가능하다.

반대로, rollback(실패처리)된 경우에도 데이터를 조회할 수 없다는 것이다.

2개의 CLI를 사용하여 확인해보자.

좌측은 트랜잭션을 시작하여 새로운 데이터를 저장하였고 우측은 데이터를 조회한 CLI다.
아직 트랜잭션을 정상종료 하지 않아 데이터 조회가 불가능하다.

이제 commit(정상종료)시켜주면 데이터 조회가 가능하다.

반대로 rollback으로 종료 시키면 데이터 저장이 되지 않아 데이터 조회도 불가능하다.

Spring에서 트랜잭션은 이러한 원리로 작동한다고 생각하면 된다.

SQL 쿼리가 하나라도 실패한다면 rollback이 되어 데이터 저장 자체가 되지 않는다.
모든 쿼리가 성공해야만 commit이 되어 정상적으로 저장이 되고 조회가 가능한 것이다.

2. SpringBoot에서 적용

SpringBoot에 트랜잭션을 적용하려면 @Transactional을 추가해주면 된다.
이 때 org.springframework.transaction.annotation을 선택해주어야 한다.(javax.transaction도 있는데 전혀 다른 거다)

userServiceV2 모든 함수에 트랜잭션을 적용해주었다.
이제 해당 함수가 시작할 때 트랜잭션이 적용되는 것이다. 여기서 readOnly 옵션을 사용할 수 있는 에너테이션이 있는데, 함수에서 select 쿼리만 날리는 경우 사용이 가능하며 readOnly=true로 설정해준다.

트랜잭션을 userServiceV2에 적용한 이유는 서비스 계층에서 주로 여러 쿼리를 다루기 때문에 트랜잭션을 관리할 필요가 있기 때문이다.

@Transactional의 유용성을 예시를 통해 설명하겠다.
사용자 데이터를 저장하는 saveUser 메서드를 예로 들겠다.

public void saveUser(UserCreateRequest request){
        userRepository.save(new User(request.getName(),request.getAge()));
        throw new IllegalArgumentException();
    }

saveUser 메서드 마지막 줄에 예외를 던지는 코드를 추가하였다. 트랜잭션이 적용되지 않는다면 서버 실행 중 예외가 발생하여 오류가 생기지만 사용자 데이터는 저장되는 기괴한 현상이 발생한다.
즉, 첫 번째 줄은 실행되고 두 번째 줄로 인해 종료된 것이다.

하지만, 트랜잭션이 적용된다면 rollback이 발생하여 데이터 저장이 되지 않는다. 서버가 예외 발생으로 비정상적으로 종료되면 데이터도 저장되지 않는다는 것이다.

이처럼 트랜잭션은 서비스 계층에서 없어서는 안되지만 한 가지 한계점이 있다.
바로 Checked Exception의 경우 rollback이 되지 않는다는 것이다.

3. 영속성 컨텍스트

영속성 컨텍스트는 테이블과 매핑된 Entity 객체를 저장/관리하는 역할을 한다.
(User테이블과 매핑된 User 객체라 생각하면 된다)

Spring에서 트랜잭션을 사용하면 영속성 컨텍스트가 생겨나 트랜잭션 종료 시 같이 종료된다.

영속성 컨텍스트에는 4가지 중요한 기능이 있다.

  • 변경 감지 : 영속성 컨텍스트에 불려진 Entity는 명시적으로 save해주지 않아도 변동이 감지되면 자동으로 저장한다
@Transactional
public void updateUser(UserUpdateRequest request){
    User user = userRepository.findById(request.getId()).orElseThrow(IllegalArgumentException::new);
    user.updateName(request.getName());
    //userRepository.save(user);
}

updateUser 함수에서 save 쿼리를 없애도 트랜잭션을 사용하면 영속성 컨텍스트로 인해 user에 새로운 정보가 업데이트 되면 이를 감지하여 자동으로 저장된다는 것이다.

  • 쓰기 지연 : DB에 insert,delete,update SQL을 바로 날리는 것이 아닌, 트랜잭션이 commit 될 때 까지 모았다가 정상종료 시 한꺼번에 날리는 것

왜 SQL을 굳이 한꺼번에 날릴까?
Spring이 DB에 SQL을 날릴 때 1회 통신을 한다.
만약, 함수에 100개의 SQL 쿼리가 있으면 100회의 통신을 해야하기 때문에 성능이 효율적이지 못할 것이다.
이것을 영속성 컨텍스트가 한 번에 모아서 1회 통신으로 전체 SQL쿼리를 날리면 성능면에서 훨씬 효율적이게 된다.

  • 1차 캐싱 : 쓰기 지연과 비슷한 기능으로 ID를 기준으로 Entity를 기억하는 것

쉽게 말하면 한 번 조회했던 데이터가 있을 경우 ID를 기억했다가 동일한 데이터를 조회하는 쿼리를 날릴 경우 잠시 저장했던(캐싱하고 있던) 데이터를 사용하는 것이다.
이렇게 캐싱한 데이터를 재사용하면 통신 회수를 줄일 수 있다!

0개의 댓글