"심화학습"이 되시겠습니다. 주요 내용은...
- @Transactional 이 어떻게 동작하는지
(이전 강의 정리할 때 대충 봐서 이미 알죠) - 잘못된 @Transactional 의 사용: private 메소드, 같은 클래스의 메소드
- @Transactional 의 2가지 속성 학습: isolation, propagation
이 되시겠습니다.
@Transactional 의 동작 방식: AOP 사용
이전 강의를 정리하면서 봤던 그림(강의에는 없었고 공식 문서였음)에 해당하는 내용을 소스를 까보면서 살펴봅니다.
출처야 당연히 공식문서구요.
TransactionAspectSupport 라는 클래스를 찾아갑니다. 호출시에 디버그 브레이크를 잡아보면 확인할 수 있어요. 콜스택에 있습니다. invokeWithTransaction 메소드를 찾아가면... 찾아갈 것도 없죠. 콜 스택에 있죠? 이 함수가 좀 긴데요.
중간 지점에 PlatformTransactionManager가 나옵니다. 강의에서는 스리슬쩍 넘어갔는데 강의 제목에 해당하는 클래스가 되시겠습니다.
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
소스를 대충 읽어보면 메소드의 데코레이션... 아니 어노테이션에 트랜젝션이 있는지, 그거하고 연관되어 있는 트랜젝션이 있는지 확인한 후에 트랜젝션이 있다면 트랜젝션을 만들어서 호출하는 것 같습니다. AOP 의 advice 종류 중 around advice 이므로 내부에서 직접 대상 함수를 호출해주고 있네요.
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
}
이 밑의 catch 부분을 보면 트랜젝션을 언제 어떻게 롤백하는지를 확인할 수 있습니다.
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
같은 파일의
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
생략
if (txInfo.transactionAttribute != null
&& txInfo.transactionAttribute.rollbackOn(ex)) { // <<<<<<< rollbackOn
생략
생략
}
DefaultTransactionAttribute 의 rollbackOn 은 이전 강의의 내용처럼 unchecked exception 을 대상으로 합니다 (소스 생략). 그 말은 rollbackOn 을 집어넣으면 우리가 원하는 클래스에서 롤백이 된다는 말이겠죠? Transactional.rollbackFor 의 주석문서에는 친절하게도 이런 점이 자세히 쓰여있습니다.
@Transactional(rollbackFor = Exception.class)
public void putBookAndAuthor() throws Exception {
생략
적당히 DB 조작을 하세요
throw new Exception("asdf");
}
위에걸 호출하는 테스트를 만들어서 디버그를 잡아보세요
생략
public @interface Transactional {
/**
* 생략
* <p>By default, 생략 See
* {@link 패키지생략.DefaultTransactionAttribute#rollbackOn(Throwable)}
* 생략
*/
Class<? extends Throwable>[] rollbackFor() default {};
생략
}
코드를 파보는 게 책보다 훨씬 빠르고 정확하다는 잔소리 아닌 잔소리를 듣고 다음으로.
@Transactional 이 적용되지 않는 잘못된 사용 2
: private method 에서 사용하기, 같은 클래스에서 사용하기
아래 두 가지 상황에서 @Transactional 어노테이션이 적용되지 않습니다. 콜 스택도 좀 다르게 나옵니다.
- private 메소드에 @Transactional 이 붙어있는 경우
- 같은 클래스의 다른 메서드에 @Transactional이 붙어있는 경우
(A 클래스의 fun a() 가 @Transactional fun b() 를 호출)
이건 스프링 컨테이너의 동작방식과 관련이 있다, "Bean 진입 시 어노테이션을 읽기 때문" 이라고 강의에서 설명해주셨는데요.
저번 강의내용을 정리하면서 검색할 때 봤던 글이 있거든요? 근데 그 때는 이해를 못 했었는데 지금은 이해가 가는 글이 있습니다.
요는, 스프링이 메소드를 콜할 때에는 AOP를 활용해서 우리의 객체를 감싸는 Proxy 객체를 만든 뒤 Proxy에다가 호출을 한다는 거죠. 그렇기에 프록시 객체를 만드는 시점에서만 메소드에 어노테이션이 있는지 보는거구요.
(제가 이해한 내용이 틀린 설명일수도 있음, 어디까지나 대충 그런 방식으로 동작할거다는 예상임)
private에 대해서는 프록시를 생성하지 않을테니까 당연할 테구요...
한 번 콜 스택을 볼까요.
내부 메서드를 호출하는 케이스
확연히 차이가 있죠. 내부 메소드를 호출할 때에는 그냥 AOP 없이 직접 호출되는 점을 확인하실 수 있습니다.
private 메서드는 외부에서 호출할 수 없다는 점 때문에 마찬가지로 같은 클래스를 거쳐야 합니다. 확인할 필요도 없을 것 같네요. 리플렉션으로 호출하는 사례는 AOP랑 프록시가 동작할지부터가 궁금하긴 한데... 정말 필요할 때 보면 되겠죠.
@Transactional 의 속성 2가지: isolation, propagation
이전에 @Transactional 이 두 가지가 있다고 확인하고 넘어갔었는데, 이번 프로퍼티는 Spring Data JPA 쪽에만 있는 내용입니다.
- propagation: Spring Data JPA에는 필드가 있고, JPA에는 없습니다
(value가 대신하며, transaction type이 propagation 이라고 하셨는데 지금은 무슨 말인지 머르겠다능 :mollu_kejang: ) - isolation: 역시 Spring Data JPA 에만 있습니다. 이번 강의와 다음 강의... 는 안 봤지만 이거에 대해서 자세히 살펴봅니다.
DB에서 트랜젝션의 ACID 중 I인 isolation 속성은 성능과의 적절한 밸런스를 맞추기 위해 조절이 가능합니다. 총 4가지가 있고 분명 학교에서도 배웠던 거 같은데 까먹었다능
강의 화면에 문서가 나와있길래 일시정지하고 읽었다능... 그리고 그거 표로 정리할 수 있다능...
격리 명칭 (Isolation.격리_명칭) |
격리 수준 | Dirty Read | Non-repeatable Read | Phantom Read |
READ_UNCOMMITED | Lv 0 (최하) | ❗ | ❗ | ❗ |
READ_COMMITED | Lv 1 | 🛡️ | ❗ | ❗ |
REPEATABLE_READ | Lv 2 | 🛡️ | 🛡️ | ❗ |
SERIALIZABLE | Lv 3 (최상) | 🛡️ | 🛡️ | 🛡️ |
이번 강의에서는 Dirty Read 가 발생하는 READ_UNCOMMITED 만 살펴봤습니다. 원래대로라면 다음 강의도 봤어야 했겠지만 ㅎㅎ...
위키백과가 더 자세합니다. 스프링 코드의 주석부분에도 문서화되어있지만요.
https://en.wikipedia.org/wiki/Isolation_(database_systems)
- 더티 리드:
- 다른 트랜젝션이 커밋 전에 바꾼 필드를 읽을 수 있습니다.
- 다른 트랜젝션이 롤백되면, 내 트랜젝션은 롤백 이전의 값을 여전히 가지고 있게 됩니다.
- 재현 불가능한 읽기:
- 다른 트랜젝션이 이미 커밋한 바꾼 필드를 읽을 수 있습니다.
- 다른 트랜젝션이 커밋하기 전과 후에 내 트랜젝션이 읽은 값이 달라지게 됩니다.
즉, 읽었을 때 똑같은 값이 나오지 않을 수 있습니다.
- 팬텀 리드:
- 다른 트랜젝션이 커밋한 내용이 내 커밋의 where 조건에 추가로 해당되면 그것도 조회될 수 있습니다.
- 처음 where 로 select 했을때와, 다른 트랜젝션이 커밋한 후의 where 로 select 했을때의 데이터 개수가 다를 수 있습니다.
더티 리드 (READ_UNCOMMITED)
격리 명칭 (Isolation.격리_명칭) |
격리 수준 | Dirty Read | Non-repeatable Read | Phantom Read |
READ_UNCOMMITED | Lv 0 (최하) | ❗ | ❗ | ❗ |
다음과 같이 두고 첫 번째 격리단계에서 발생할 수 있는 문제를 살펴봅니다. 기본값은 DB에 따라 다르다네요.
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
확인을 용이하게 하기 위해 나머지 한 쪽의 트랜젝션은 SQL로 직접 실행합니다.
start transaction;
update book set category = 'none' where true; # IDE에서 해서 그런가 아님 PGSQL이라 그런가 where 필요
commit;
더티 리드를 확인하려는 것이므로 테스트에는 read를 두 번 둡니다.
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void get(Long id) {
System.out.println(">>> " + bookRepo.findById(id));
System.out.println(">>> " + bookRepo.findAll());
System.out.println(">>> " + bookRepo.findById(id));
System.out.println(">>> " + bookRepo.findAll());
}
이제 브레이크 포인트를 잡고 적당히 확인할 수 있을 것 같은 순서대로 실행하면 category 부분이 none으로 바뀐 걸 테스트의 두 번째 부분에서 읽는 걸 확인하실 수 있습니다.
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC
#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online
아니 오늘은 별 내용 없는데 정리가 오래걸렸어 7시 반부터 시작했는데 지금 열시야... 이론이 많아서 그런가
여담인데, 이 글의 스크린샷에는 강의를 캡처한 게 하나도 없습니다. 직접 해봤다고 진짜... 강사님 IDE에 한글패치 안까셨다고... (지적당하지도 않았는데 억울해하는 건 뭐야 ㅋㅋㅋㅋ)
'FastCampus - 한번에 끝내는 Java|Spring 웹 개발 > 04 JPA' 카테고리의 다른 글
JPA Ch 7 트랜젝션 매니저의 propagation - 패스트캠퍼스 챌린지 35일차 (0) | 2022.02.27 |
---|---|
JPA Ch 7 영속성 - 트랜젝션 매니저 (5) - 패스트캠퍼스 챌린지 34일차 (0) | 2022.02.26 |
JPA Ch 7 영속성 - 트랜젝션 - 패스트캠퍼스 챌린지 32일차 (0) | 2022.02.24 |
JPA Ch 7 영속성 - 생명주기 - 패스트캠퍼스 챌린지 31일차 (0) | 2022.02.23 |
JPA Ch 7 영속성 - 엔티티 캐시 (1차 캐시) - 패스트캠퍼스 챌린지 30일차 (0) | 2022.02.22 |