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

한수현님의 프로필 이미지

작성한 질문수

재고시스템으로 알아보는 동시성이슈 해결방법

소스코드

중간테이블에 대한 낙관적 락 적용법

작성

·

147

·

수정됨

0

현재 Member 테이블과 Appointment 테이블이 존재하는데, N:N 관계이기 때문에 아래와 같이 AppointmentUser라는 중간 테이블이 존재합니다.

@Entity
@Table(name = "appointment_and_user")
public class AppointmentUser extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "apppointment_id", nullable = false)
    private Appointment appointment;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id", nullable = false)
    private Member member;

    @Enumerated(value = EnumType.STRING)
    @Column(name = "member_authority", nullable = false)
    private MemberAuthority memberAuthority;

    @Version
    private Long version;

하나의 AppointmentUser 테이블은 약속과 멤버의 id를 하나씩 가집니다.

 

여기서 레포지토리의 코드는 이러합니다.

public interface AppointmentUserRepository extends JpaRepository<AppointmentUser, Long> {

...

    @Lock(LockModeType.OPTIMISTIC)
    @Query("select au from AppointmentUser au where au.id = :id")
    AppointmentUser findByIdWithOptimisticLock(Long id);

findByIdWithOptimistic 메서드를 통하여 특정 유저-약속 테이블의 데이터를 통해 AppointmentUser 객체를 반환합니다.

 

서비스 계층의 코드는 아래와 같습니다.

@Transactional
public void updateAuthority(Long appointmentId, Long loginMemberId, Long targetMemberId) {
    Member loginMember = memberRepository.getById(loginMemberId);
    Member targetMember = memberRepository.getById(targetMemberId);
    Appointment appointment = appointmentRepository.getById(appointmentId);
    AppointmentUser loginAppointmentUser = appointmentUserRepository.getByMemberAndAppointment(loginMember, appointment);
    AppointmentUser targetAppointmentUser = appointmentUserRepository.getByMemberAndAppointment(targetMember, appointment);
    appointment.changeTitle("asd");

    validateIsAdminMember(loginAppointmentUser.getId());
    MemberAuthority targetAuthority = targetAppointmentUser.getMemberAuthority();

    targetAppointmentUser.updateAuthority(MemberAuthority.getAnotherAuthority(targetAuthority));
    appointmentRepository.save(appointment);
    appointmentUserRepository.save(targetAppointmentUser);
}

private void validateIsAdminMember(Long loginAppointmentUserId) {
    if (appointmentRepository.findByIdWithOptimisticLock(loginAppointmentUserId).getMemberAuthority() != MemberAuthority.ADMIN) {
        throw new NotAdminMemberException();
    }
}

 

<로직 설명>

  • 하나의 약속 내에 멤버 두명이 존재

  • 각 멤버들은 ADMIN or NORMAL 권한을 갖고 있음.

  • 두명 다 약속 내 에서 ADMIN 권한을 갖고 있다는 상황을 가정. (ADMIN은 AppointmentUser 엔티티의 MemberAuthority 라는 필드의 Enum 값입니다.)

  • 두명이 서로를 동시에 ADMIN에서 NORMAL로 권한을 박탈하는 경우, 하나의 약속 안에 ADMIN인 사람이 없어지는 예외적인 문제 상황이 발생함.

  • 그래서 @Version을 AppointmentUser 엔티티의 필드로 등록하여 해결하려고 했으나..

  • 사용자 A와 B가 있다고 할 때 service 코드 내의 validateIsAdminMember 메서드를 통해 상대방의 권한을 박탈하려는 유저(본인)가 ADMIN인지 검증하여 ADMIN이 맞다면 박탈하고, ADMIN이 아니라면, 예외를 던지게 끔 하는 로직에서,

  • 레포지토리 내의 findByIdWithOptimisticLock 메서드를 동시에 접근했을때 Version 필드를 통해 동시성 문제를 제어할 수 있다고 생각했으나..

  • validateIsAdminMember로 검증하는 A와 B의 AppointmentUser 엔티티는 서로 다른 객체(데이터)이기 때문에 서로 다른 테이블의 Version 값을 변경하기 때문에 동시성 보장이 안됨..

  • 그래서 Appointment에 Version필드를 넣어주려 했지만, Version값은 해당 테이블에 변화가 생겨야 변한다.

  • 하지만 로직상, AppointmentUser(중간테이블)에 변화가 생기는게 맞다...

위와 같은 중간 테이블 사용으로 인한 문제가 발생하였을 때 어떻게 강의자님이시라면 어떻게 해결하실지 궁금합니다!

 

답변 2

0

한수현님의 프로필 이미지
한수현
질문자

혹시 별도의 테이블로 Lock을 제어한다는게 어떠한 의미인지 여쭈어 봐도 될까요 ??..

최상용님의 프로필 이미지
최상용
지식공유자

AppointmentLock 과 같은 별도의 테이블을 생성한 이후 appointmentId 를 유니크로 키로 생성합니다.

그 후 역할을 변경하는 메소드 호출하기 전에 AppointmentLock 에 appointmentId 를 가진 데이터를 삽입하여 락을 획득한 후 역할을 모두 변경하였다면 appointmentId 가진 데이터를 삭제하여 락을 해제하는 방법입니다!

0

최상용님의 프로필 이미지
최상용
지식공유자

한수현님 안녕하세요.
단순히 해결만을 위한 것이라면 Version 필드를 Appointment 에 넣고 updatedAt 을 넣어서 변경을 발생시킬 수 있을 것 같습니다.

다만 올바른 해결방법으로는 안보이며 근본적으로 updateAuthority 메소드에 중복으로 접근해서는 안되보입니다.
이를위해 별도의 테이블로 Lock 을 제어할 수도 있으며, redis 를 사용하여 락을 제어하여 updateAuthority 메소드에 접근자체를 막을수도 있을것 같습니다.

정확히 어떤프로젝트이며 어떤 특성을 가진것인지 100% 이해하지 못한 상황에서 남기는 답변이므로 더 나은 방법이 존재할 수 있습니다.

감사합니다.