인프런 커뮤니티 질문&답변

kamser님의 프로필 이미지
kamser

작성한 질문수

실전! 스프링 데이터 JPA

낙관적 락에 대해 질문이 있습니다.

작성

·

232

0

Hibernate는 @Version을 사용하고,

Lock 옵션을 @Lock(LockModeType.OPTIMISTIC)을 사용할 경우에

NONE 모드와 다르게 엔티티를 수정하지 않고 단순히 조회만 해도 버전을 확인한다고 강사님 JPA 책에 작성되어있습니다.

 

실제 코드로 구현해보니 버전만 확인하는거 같더라구요

그 사이에 다른 트랜잭션이 해당 엔티티를 수정하여 버전이 변경되어도

ObjectOptimisticLockingFailureException 예외가 발생하지 않습니다.

 

간단하게 로직을 설명드리면

 

트랜잭션 A가 옵티미스틱 락 모드로 회원을 조회합니다. version = 0

트랜잭션 A를 5초 대기합니다.

트랜잭션 B가 회원을 수정하여 버전이 변경됩니다. version = 1

트랜잭션 B가 종료됩니다.

5초가 지나 트랜잭션 A가 종료됩니다.

트랜잭션 A가 종료될 때 옵티미스틱 락 모드라서 마지막에 버전을 확인합니다.

select version as version_ from member where id=?

그런데 트랜잭션 A가 종료될때에 회원 버전이 다르지만 예외가 발생하지 않습니다.

 

이러면 OPTIMISTIC의 용도가 트랜잭션을 커밋할 때 버전 정보를 조회해서 현재 엔티티의 버전과 같은지 검증한다. 만약 같지 않으면 예외가 발생한다고 작성되어있는데 예외가 발생하지 않는다면

 

강사님께서 설명해주신 조회한 엔티티는 트랜잭션이 끝날 때까지 다른 트랜잭션에 의해 변경되지 않아야한다. 조회 시점부터 트랜잭션이 끝날때까지 조회한 엔티티가 변경되지 않음을 보장한다.

이 말의 다른 의미가 어떤건지 궁금합니다 !

아니면 제가 테스트를 잘못하고 있는 걸까요..?

 

아래는 로직에 대한 간단한 코드입니다.

도메인

package org.example.stock_rt_1.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long id;

    private long personId;
    private int age;
    @Version
    private Long version;

    public Member(long personId, int age) {
        this.personId = personId;
        this.age = age;
    }

    public void addAge() {
        ++this.age;
    }
}

리포지토리

package org.example.stock_rt_1.repository;

import jakarta.persistence.LockModeType;
import org.example.stock_rt_1.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {

    @Lock(LockModeType.OPTIMISTIC)
    Optional<Member> findByPersonId(Long id);

}

서비스

package org.example.stock_rt_1.service;

import lombok.RequiredArgsConstructor;
import org.example.stock_rt_1.domain.Member;
import org.example.stock_rt_1.repository.MemberRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@RequiredArgsConstructor
public class MemberService {
    public final MemberRepository memberRepository;

    @Transactional
    public void addAge(long personId) {
        sleep(500); //findMember가 먼저 실행되야하기 때문에 넣었습니다.
        Member member = memberRepository.findByPersonId(personId).orElseThrow();
        member.addAge();
    }

    @Transactional
    public void findMember(long personId) {
        memberRepository.findByPersonId(personId);
        sleep(5000);
    }

    private void sleep(long mills) {
        try {
            Thread.sleep(mills);
        } catch (InterruptedException e) {
            System.out.println("e = " + e.getMessage());
        }
    }
}

테스트코드

package org.example.stock_rt_1.service;

import org.example.stock_rt_1.domain.Member;
import org.example.stock_rt_1.repository.MemberRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;


@SpringBootTest
class MemberServiceTest {

    @Autowired
    private MemberService memberService;
    @Autowired
    private MemberRepository memberRepository;
    
    @Test
    @DisplayName("회원 정보를 조회중 다른 트랜잭션에서 정보를 변경했다.")
    void addAge() throws InterruptedException {
        // given
        int personId = 5555;
        memberRepository.save(new Member(personId,15));
        
        // when
        Thread selectThread = new Thread(() -> 
                                 memberService.findMember(personId),"트랜잭션-A");
        selectThread.start();
        Thread updateThread = new Thread(() -> 
                                 memberService.addAge(personId),"트랜잭션-B");
        updateThread.start();

        selectThread.join();
        updateThread.join();
    }
}

로그

[트랜잭션-A] : select m1_0.id,m1_0.age,m1_0.person_id,m1_0.version from member m1_0 where m1_0.person_id=? Hibernate: select m1_0.id,m1_0.age,m1_0.person_id,m1_0.version from member m1_0 where m1_0.person_id=?
[트랜잭션-B] : select m1_0.id,m1_0.age,m1_0.person_id,m1_0.version from member m1_0 where m1_0.person_id=? Hibernate: select m1_0.id,m1_0.age,m1_0.person_id,m1_0.version from member m1_0 where m1_0.person_id=?
[트랜잭션-B] : update member set age=?,person_id=?,version=? where id=? and version=?
[트랜잭션-B] : select version as version_ from member where id=?
[트랜잭션-A] : select version as version_ from member where id=?

답변 2

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. kamser님

도움을 드리고 싶지만 질문 내용만으로는 답변을 드리기 어렵습니다.

실제 동작하는 전체 프로젝트를 압축해서 구글 드라이브로 공유해서 링크를 남겨주세요.

구글 드라이브 업로드 방법은 다음을 참고해주세요.

https://bit.ly/3fX6ygx

 

주의: 업로드시 링크에 있는 권한 문제 꼭 확인해주세요

 

추가로 다음 내용도 코멘트 부탁드립니다.

1. 문제 영역을 실행할 수 있는 방법

2. 문제가 어떻게 나타나는지에 대한 상세한 설명

링크: 공식 서포터즈

링크: 자주하는 질문

감사합니다.

0

안녕하세요, 인프런 AI 인턴입니다.

kamser님의 질문은 낙관적 락(LockModeType.OPTIMISTIC)을 사용할 때, 수정하지 않고 조회만 한 경우에도 트랜잭션이 같은 엔티티의 버전을 확인하고, 버전이 달라진 경우 ObjectOptimisticLockingFailureException 예외가 왜 발생하지 않는지에 관한 것입니다.

이와 유사한 상황과 질문에 대한 답변이 “낙관적 락 사용방법에 대한 질문” 포스트에서 다루어졌습니다. 해당 포스트에서는 낙관적 락의 사용 방법과 @Version 어노테이션을 사용할 때의 주의 사항들에 대해 논의되어 있습니다. 이 글에서는 기본적인 낙관적 락의 동작 원리와 설정 방법에 대한 설명이 제공되고 있는데, 사례와 직접적으로 일치하지는 않지만, 낙관적 락과 관련된 핵심 개념을 이해하는 데 도움이 될 수 있습니다.

질문하신 내용과 가장 밀접한 해답은, 낙관적 락이 적용된 상황에서 항목의 조회만 이루어지고, 실제로 수정(엔티티 변경)이 일어나지 않으면, JPA는 버전 갱신을 시도하지 않아 ObjectOptimisticLockingFailureException 예외가 발생하지 않는다는 점입니다. 예외는 엔티티에 실제 변경이 발생했을 때, 변경 사항을 커밋하기 전에 현재 버전과 데이터베이스의 버전을 비교하여 다를 경우에 발생합니다. 조회 시점부터 트랜잭션이 끝날 때까지의 변경 보장은, 엔티티에 대한 변경(수정, 삭제 등) 시도가 있을 때 해당됩니다.

@Version 어노테이션과 함께 @Lock(LockModeType.OPTIMISTIC)을 사용한 경우, 데이터가 변경되었을 때만 버전이 확인되고 충돌이 감지되어 예외가 발생하는 것이 기본 원칙입니다.

따라서, kamser님의 테스트 상황에서 버전이 변경된 후에도 예외가 발생하지 않는 것은, 예외 발생 조건(데이터의 실제 변경과 커밋 시도)이 충족되지 않았기 때문일 수 있습니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

kamser님의 프로필 이미지
kamser

작성한 질문수

질문하기