| 개요
- 서비스에서 @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
- 기타 출처의 경우, 페이지 내 링크 참조
'Framework > Orm&Mapper' 카테고리의 다른 글
[JPA] Lazy Loading 개념 (0) | 2022.11.11 |
---|---|
[Entity] 중복데이터 저장 방지 (Unique Key, Index) (0) | 2022.11.11 |
[JPA] 인텔리J에서 JPA Entity 기반 ERD 그리기 (0) | 2022.10.19 |
[JPA] repository.save()의 반환값이 안 들어올 때 (0) | 2022.10.19 |
[JPA] JPA 전반에 대해 간략히 이해하기 (0) | 2022.10.06 |