작성
·
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
0
한수현님 안녕하세요.
단순히 해결만을 위한 것이라면 Version 필드를 Appointment 에 넣고 updatedAt 을 넣어서 변경을 발생시킬 수 있을 것 같습니다.
다만 올바른 해결방법으로는 안보이며 근본적으로 updateAuthority 메소드에 중복으로 접근해서는 안되보입니다.
이를위해 별도의 테이블로 Lock 을 제어할 수도 있으며, redis 를 사용하여 락을 제어하여 updateAuthority 메소드에 접근자체를 막을수도 있을것 같습니다.
정확히 어떤프로젝트이며 어떤 특성을 가진것인지 100% 이해하지 못한 상황에서 남기는 답변이므로 더 나은 방법이 존재할 수 있습니다.
감사합니다.
AppointmentLock 과 같은 별도의 테이블을 생성한 이후 appointmentId 를 유니크로 키로 생성합니다.
그 후 역할을 변경하는 메소드 호출하기 전에 AppointmentLock 에 appointmentId 를 가진 데이터를 삽입하여 락을 획득한 후 역할을 모두 변경하였다면 appointmentId 가진 데이터를 삭제하여 락을 해제하는 방법입니다!