들어가며
이전에 작성한 글(https://kongdevlog.tistory.com/16)에서 유니크 키 제약 조건에 대한 이해가 부족하여 오버 엔지니어링(+ 잘못된 작업)을 하게되었습니다.
해당 과정에 대한 원인을 분석하고 개념을 정리하기 위해 글을 작성합니다.
이전 구현
@Transactional
public MemberLoginServiceDto login(String phoneNumber) {
Member member = createOrFindMemberByPhoneNumber(phoneNumber);
if (member.isBanned()) {
throw new BannedMemberException();
}
...
return new MemberLoginServiceDto(accessToken, refreshToken, member.isProfileSettingNeeded());
}
private Member createOrFindMemberByPhoneNumber(String phoneNumber) {
return memberCommandRepository.findByPhoneNumber(phoneNumber).orElseGet(() -> create(phoneNumber));
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
protected Member create(String phoneNumber) {
try {
return memberCommandRepository.save(Member.fromPhoneNumber(phoneNumber));
} catch (DataIntegrityViolationException e) {
throw new MemberLoginConflictException(phoneNumber);
}
}
상황 : Member Entity의 phoneNumber 컬럼에 유니크 제약 조건이 걸려있으며, id의 GenerationType 은 Identity 입니다.
해당 코드를 구현하고, 글을 작성하였던 이유는 동시성 문제로 인해 멤버가 두 개가 생성이 되는 상황이 발생할 수 있었기 때문입니다. 또한, Login Transaction 이 완료되기 전에 create Transaction 이 완료가 되어야 토큰 발행전에 예외를 잡을 수 있을 것이라고 생각했기 때문입니다.
잘못한 점.
첫번째로 잘못된 점은 내부 메서드(this.method)에는 AOP가 적용이 되지 않는다. 입니다. 즉, 독립적인 트랜잭션을 생성하기 위해 붙인 어노테이션인 @Transactional(propagation = Propagation.REQUIRES_NEW)은 사실상 login 메서드 아래에서 실행되는 메서드이기 때문에 트랜잭션을 위한 프록시 객체를 거치지 못합니다. 즉, 어노테이션을 붙여도 의도대로 동작하지 않을 것입니다.
그런데 왜, 해당 코드로 테스트를 했는데 잘 진행되었던 것일까? 이유는 바로 id의 GenerationType 은 Identity 와 유니크 제약 조건 때문이었습니다.
로그인 메서드가 완료가 되는 시점에 트랜잭션이 커밋이 될 것이라는 착각을 하고 있었던 것입니다. 왜냐하면, id 값을 받아오기 위해선 member를 save 하는 시점에 쿼리를 전송하기 때문입니다.
즉, 제가 두번째로 잘못한 점은 JPA Save 로 인한 유니크 제약 조건에 의한 인덱스 잠금을 고려하지 못하였던 것입니다. 만약 010-1234-5678 로 로그인 메서드를 실행하는 두 개의 트랜잭션 A,B가 있다고 가정하겠습니다.
Transaction A : findByPhoneNumber 실행 ---------------------------------- create 010-12345678 로 인한 유니크 인덱스 잠금.
Transaction B : ----------------------- findByPhoneNumber 실행 ------------------------------- 대기 ---------------
이후 Transaction A의 login 메서드가 끝나는 시점 (커밋되는 시점)에 Transaction B가 실행되게 됩니다. 따라서 이미 Transaction B가 create 함수를 호출하는 시점에는 Transaction A의 Member가 존재하기 때문에 예외를 반환하게 됩니다!
결론
JPA의 id GeneartionType 이 Identity 이므로 save 시점에 insert 쿼리가 전송되며, 유니크 제약 조건으로 인해 인덱스 잠금이 발생한다.
따라서 create 메서드를 따로 독립적인 트랜잭션으로 관리할 필요가 없었다...!
'Back-End > Server' 카테고리의 다른 글
| [팀 프로젝트] Enum 활용하기. (0) | 2025.04.22 |
|---|---|
| [JPA] 영속성 컨텍스트 (0) | 2025.04.11 |
| [팀 프로젝트] queryDsl 도입과 테스트 코드 (0) | 2025.01.21 |
| [Kong's Blog] 프로젝트 회고와 리팩토링 (4) - 스케쥴링 적용 (0) | 2025.01.17 |
| [Kong's Blog] 프로젝트 회고와 리팩토링 (3) - 배치 사용해보기 (0) | 2025.01.15 |
댓글