Framework/Orm&Mapper

[Transaction] Transaction에 대한 이해 (& 스프링의 @Transactionl)

simDev1234 2022. 10. 25. 15:19

|  개요

- 서비스에서 @Transactional 을 사용할 때와 사용하지 않을 때 수정과 삭제 시 차이를 보이는 걸 경험했다.

@Transactional update delete
사용 repository.save() 불필요 repository.delete() 시 삭제됨
미사용 repository.save() 필요 repository.delete() 시 삭제되지 않음

- 더불어 테스트에서 아래와 같이 @Transactional을 작성할 때, DB에 영구반영이 되지 않는 걸 확인했었다.

// 아래는 TEST 코드
@SpringBootTest
@Transactional
public class JpaMemoRepositoryTest{

    // 테스트 작성
    
}

- 왜 그럴까? 

(1) 수정 시 save하지 않아도 수정되는 이유

: @Transactional이 붙여진 메소드는 spring boot에서 트랜잭션으로 인식된다.  해당 메소드에서 DB의 row를 조회한 후, setter로 row에 변화를 준다고 하자. Hibernate는 persistent entities의 변동사항을 자동적으로 확인하고, 그에 따라 DB에 업데이트를 해준다. 

// default : dbms의 격리성 수준을 따르며, 부모와 별개로 실행되며, 읽기전용이 아님
@Transactional(isolation = Isolation.DEFAULT,
               progagation = Propagation.REQUIRED,
               readOnly = false){
    
}

(2) 삭제 시 @Transactional을 쓰지 않으면 삭제되지 않는 이유

: 이 부분은 정확하게는 모르겠지만, JPA의 PersistenceContext와 연관되어 있다고 한다. @Transactional을 명시하지 않을 경우, 해당 메소드는 트랜잭션으로 인식되지 않는다. 이 부분은 좀 더 알아보아야 할 것 같은데 save와 달리 delete는 삭제되지 않는다는 점만 일단 기록해두려 한다.

(3) 테스트에서 @Transactional을 쓸 경우 

: 스프링부트의 test로 지정된 패키지 내부의 클래스에 @Transactional을 쓰게 되면 모든 트랜잭션이 롤백되도록 되어 있다.

  따라서, 테스트에서 @Transactional을 쓸 경우 DB에 변동사항이 반영되지 않는다. 그럼에도 쓰는 이유는 서비스 로직을 파악하기 간편하기 때문. 검색해보면 일부 일부 사람들은 테스트에서는 해당 어노테이션을 쓰지 말 걸 권장하기도 하는데, 케바케인 것 같다.

(4) @Transactional와 연관해서 지연로딩과 즉시로딩에 대한 오류에 대해서도 알아볼 필요가 있다.

Lazy Loading 가능한 객체의 초기화를 지연시킨다. (참조하는 엔터티를 당장 쓰지 않을 경우)
Eager 객체의 초기화를 지금 당장 한다. (참조하는 엔터티를 지금 쓰는 경우)

-  A엔터티 내부에 @OneToMany 라는 Annotation으로 참조하는 B엔터티 데이터가 있다고 할 때, @OneToMany의 fetch type은 디폴트로 Lazy로 되어있다. 

- 이는 지연 로딩을 의미하는데, 지연 로딩의 경우, 참조된 B엔터티의 데이터들을 모두 읽지 않고 Proxy 객체(id값만 있음)를 통해 가져온다. 이 경우, 아래와 같이 코드를 작성하게 되면 오류를 발생시킨다. 

@RestController
class Controller{

    @GetMapping
    void findOneUserAndPetInfo(long userId){
    
    	User user = userService.findByUserId(userId);       
        List<Pet> pets = user.getPet(); // error!!
                
    }
    
}

- 이를 해결하는 방법은 서비스 메소드에 @Transactional을 붙이는 것이다. 그렇게 하면, Lazy Loading을 하더라도, 해당 메소드를 트랜잭션으로 인식해 필요한 데이터를 적시에 초기화한다. 

pf. https://zzang9ha.tistory.com/347

 

트랜잭션 개념

트랜잭션 : 데이터베이스의 상태를 변경시키기 위해 수행하는 작업단위

[출처] 코딩팩토리, https://wonit.tistory.com/462

- 트랜잭션은 DB의 상태를 변경시키는 것으로, CRUD(INSERT, SELECT, UPDATE, DELETE) 행위를 말한다.

(1) 트랜잭션의 연산 : Commit과 Rollback

- 트랜잭션은 순차적으로 Commit -저장-을 하며 실패해도 로그를 남겨, 앞의 트랜잭션이 다 끝나야 실제로 반영한다.

- 만약에, 트랜잭션이 비정상적으로 종료한다면 Rollback -철회-을 통해, 트랜잭션 전체 또는 부분적으로 결과를 취소한다.

(2) 트랜잭션의 상태

- 트랜잭션은 실행중(Active)이거나

  - 커밋 일부 완료(Partially Commited), 커밋 모두 완료(Commited)

  - 실행중 오류 발생으로 실패(Failed), 비정상적 종료로 rollback 수행(Aborted)

- 하는 총 5가지의 상태를 가질 수 있다.

(3) 트랜잭션의 특징

- 트랜잭션에는 4가지의 특징이 존재하는데,

ACID 내용
Atomic (원자성) All or Nothing : 모든 작업이 실행되거나 or 모두 실행되지 않아야 한다.

rf. 결제서비스에 관한 트랜잭션 중 갑자기 오류발생으로 끊긴다면? 
    ㄴ 결제서비스에 관한 트랜잭션 전체는 실행되지 않게 된다. 
Consistency (일관성) 트랜잭션 작업 결과는 항상 일관적이어야 한다.

* 모든 트랜잭션이 종료된 후엔 모든 DB의 제약조건을 지키고 있는 상태여야 한다.

rf. 웹툰 결제를 할 때 최소 결제 단위는 100원이어야 한다면, 
    결제 서비스 트랜잭션이 실행될 때 내 계정엔 100원 이상이 남아있어야 한다.
Isolation (격리성/독립성) 트랜잭션은 다른 트랜잭션과 독립적이어야 한다.

rf. 현실적으로 트랜잭션의 격리성을 지키기가 어렵다 (성능과 안정성의 trade-off)
- 격리성의 단계 ) 
  READ_UNCOMMITED > READ_COMMITED > REPEATABLE_READ > SERIALIZABLE
- 대체로 현장에서는 REPEATABLE_READ 를 한다고 한다.
Durability (지속성) 트랜잭션이 완료되면 영구적으로 결과에 반영되어야 한다.

* 트랜잭션은 순차적으로 commit되며, commit이 실패해도 모든 로그를 남겨 db에 반영된다.

 

|  트랜잭션을 사용해보기

1. @EnableTransactionManagement 달기

- 일일히 @Transactional 메소드에 붙이지 않고, @SpringBootApplication이 있는 클래스에 붙인다.

@SpringBootApplication
@EnableTransactionManagement
class XXXX{

}

2. 서비스 클래스 또는 각각의 메소드에 @Transactional 달기

 

|  여러 트랜잭션이 경쟁하면 생길 수 있는 문제

1. Diry Read

: 어떤 트랜잭션이 Row를 수정하는 중에 다른 트랜잭션이 해당 Row를 읽는 경우

* 계좌 정보를 읽고 잔액을 수정하는 중에 만약 다른 트랜잭션이 진입한다면?

작업1   작업2  
게시글 1번 조회 A    
게시글 1번 수정중 B 게시글 1번 조회 A
게시글 1번 반영 B 게시글 1번 수정중 C
    게시글 1번 반영 C

2. Non-repeatable Read

: 시간 간격 두고 특정 Row를 조회한 결과가, 다른 트랜잭션의 개입으로 달라지는 것 * 일관성을 위배

작업1   작업2  
게시글 1번 조회 A    
    게시글 1번 조회 A
    게시글 1번 수정중 C
게시글 1번 조회 C 게시글 1번 반영 C

3. Phantom-read

: 시간 간격을 두고 특정 범위를 조회한 결과가, 다른 트랜잭션의 개입으로 달라지는 것 * 일관성을 위배

작업1   작업2  
게시글 1번~4번 조회 A,B,C,D    
    게시글 1번 조회 A
    게시글 1번 수정중 E
게시글 1번~4번 조회 E,B,C,D 게시글 1번 반영 E

 

|  스프링에서 트랜잭션 처리를 지원하는 방식

- 여러가지 방식이 있으나, 가장 자주 사용하는 방식은 @Transactional이다.

- 이를 선언적 트랜잭션 방식이라고도 부른다.

(1) 서비스 메소드에 @Transactional 을 넣어 해당 작업이 트랜잭션임을 명시

(2) 트랜잭션 기능이 적용된 프록시 객체 생성

(3) PlatformTransaction manager가 트랜잭션 전체가 성공했는지 확인하고 Commit 또는 Rollback

- 스프링 트랜잭션의 세부 설정들

* 모든 트랜잭션을 동기화처리(synchronized) 하게 되면 속도가 너무 느려진다.

* 적절한 설정을 통해 속도와 안전성 두 마리 토끼를 잡자

pf. 전파 수준이 아직 이해가 잘 가지 않는다.. 관련 자료를 차후에 더 보고 정리하자.

https://n1tjrgns.tistory.com/266

https://www.baeldung.com/spring-transactional-propagation-isolation

세부 설정 내용
Isolation(격리수준) 트랜잭션에서 일관성이 없는 데이터를 허용하는 수준

- DEFAULT : DATABASE의 기본 격리성 레벨 
- READ_UNCOMMITED : 트랜잭션 commit 전 다른 트랜잭션 조회 가능 (Dirty Read 발생)
- READ_COMMITED      : 트랜잭션 commit 후 다른 트랜잭션 조회 가능 (Dirty Read 방지)
- REPEATABLE_READ  : 트랜잭션이 조회중인 특정 Row에 대해 shared lock을 걸어, 다른 트랜잭션이 해당 Row을 조회하지 못하도록 하는 것 (Non-repeatable Read 방지)
- SERIALIZABLE            : 트랜잭션이 사용하는 모든 데이터에 대해 shared lock을 걸어, 다른 트랜잭션의 개입을 완전 봉쇄하는 것 (Phantom read 방지)

// 현장에선 REPEATABLE_READ을 주로 사용한다고 한다.

@Transactional(isolation = Isolation.REPEATABLE_READ)
Propagation(전파수준) 트랜잭션 동작 중 다른 트랜잭션을 호출하는 상황에서, 
트랜잭션을 시작하거나 기존 트랜잭션에 참여하는 방법에 대해 결정하는 속성값

* 트랜잭션 A 안에 트랜잭션 B를 호출하는 경우,
  트랜잭션 A는 부모 트랜잭션 , B는 자식 트랜잭션이 된다.

* 참여한다 = 부모 트랜잭션의 흐름 안에 포함되어 동작한다.


- REQUIRED             
  : 디폴트 속성. 
  : 실행 중인 트랜잭션(부모)이 있으면, 해당 트랜잭션 뒤에 붙는다.
  : 새로운 트랜잭션을 생성한다.
  : 예외가 발생하면 롤백되며 호출한 곳에도 롤백이 전파된다.
   
- SUPPORT
  : 실행 중인 트랜잭션(부모)이 있으면, 해당 트랜잭션을 계속 진행한다.
  : 실행 중인 트랜잭션(부모)이 없으면, 트랜잭션이 아닌 일반 로직으로써 동작한다.

- REQUIRES_NEW
  : 실행 중인 트랜잭션(부모)이 있으면, 해당 트랜잭션은 대기하고, 새 트랜잭션을 만든다.
  : 예외가 발생해도 호출한 곳에 롤백이 전파되지 않는다.

- NESTED
  : = 중첩 트랜잭션.
  : 실행 중인 트랜잭션(부모)이 있으면, save point를 지정하고 새로운 트랜잭션을 생성한다.
  : 만약 자식 트랜잭션에서 예외가 발생할 경우, save point까지 롤백된다.
  : 만약 실행 중인 트랜잭션(부모)이 없을 경우, REQUIRED와 동일하게 작동한다.

ex. 일기작성을 부모 트랜잭션으로, 로그 저장을 자식 트랜잭션으로
- 로그 작성을 실패해도 일기 작성은 롤백되지 않는다.
- 일기 작성이 실패하면 로그 작성도 롤백된다.

rf. 예제
https://oingdaddy.tistory.com/28
ReadOnly 트랜잭션을 읽기 전용 속성으로 지정

* 성능 최적화, 특정 트랜잭션 안에서 읽기 외 작업이 일어나는 것을 방지할 때 사용

@Transaction(readOnly=true)
트랜잭션 롤백 예외 예외 발생했을 때 트랜잭션 롤백시킬 경우를 설정

@Transaction(rollbackFor=Exception.class) : 예외 상황 발생 시 롤백된다
@Transaction(noRollBackFor=Exception.class) : 어떤 예외가 되더라도 커밋된다

* 디폴트 : RuntimeException, Error
timeout 일정 시간 내에 트랜잭션 끝내지 못하면 롤백 

@Transaction(timeout=10)

 

|  정리

- 트랜잭션의 개념

  (1) 트랜잭션은, DB의 상태를 변화시키는 하나의 작업 단위, 다시 말해 CRUD 행위를 말한다.

  (2) 트랜잭션은, 커밋과 롤백이라는 두 가지 연산을 통해,
       총 5가지의 상태 (실행중, 일부 커밋, 완전 커밋, 실패, 롤백) 를 가질 수 있다.

  (3) 트랜잭션의 특징으로는 ACID가 있는데,

      - 원자성은 All or Nothing을 말하며,
      - 일관성은 작업 처리 결과는 늘 일관적이어야 한다는 의미이며, 
      - 독립성은 한 트랜잭션이 실행되고 다른 트랜잭션이 수행되어야 한다는 의미이며,
      - 지속성은 한 번 변화를 시켰으면 그것이 영구적으로 반영되어야 한다는 걸 의미한다. 

 

[ 참고 및 출처 ]

- 부트캠프 수업 내용을 정리

- https://stackoverflow.com/questions/8190926/transactional-saves-without-calling-update-method

- https://kafcamus.tistory.com/31

- https://stackoverflow.com/questions/32269192/spring-no-entitymanager-with-actual-transaction-available-for-current-thread

- https://stackoverflow.com/questions/26601032/default-fetch-type-for-one-to-one-many-to-one-and-one-to-many-in-hibernate

- 기타 출처의 경우, 페이지 내 링크 참조