0. 문제 상황
팀바팀에서는 회원을 나타내는 'Member' 엔티티와 RefreshToken을 나타내는 'Token' 엔티티가 존재했습니다.
엔티티 설계를 진행할 때, Member와 Token 엔티티는 1:1 관계로 설계를 진행했습니다.
Member가 생성될 때 Token이 생성되고, Member가 삭제될 때 Token도 삭제되므로 생명주기도 비슷해서
직접 참조를 하는 것으로 설계를 진행했습니다.
이때, Member라는 엔티티에서 Token을 직접 참조하면 편하긴 하겠지만,
닉네임, 이메일 등 회원 정보가 담겨있는 Member 테이블에 인증 정보인 Token 정보가 담긴다는 것이 조금 이상했습니다.
이후에 Token만 만료시킬 상황에도 Member에서 참조를 하게 된다면 token_id를 null로 처리해야 했습니다.
그래서 Member 엔티티에 Token에 대한 참조를 하기보다는 Token에서 어떤 Member의 토큰인지 참조하는 것이 좋다고 판단했습니다.
또한 Member에서 Token을 조회해야 하는 일이 별로 없었기 때문에 단방향으로 설정했습니다.
따라서, Member-Token 엔티티 연관관계 설정을 다음과 같이 진행했습니다.
Member 엔티티
public class Member extends BaseEntity {
// 토큰 정보 X
...
}
Token 엔티티
public class Token extends BaseEntity {
...
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;
...
}
이렇게 Token에서 Member를 참조하는 단방향 설계로 엔티티를 구현했습니다.
이렇게 엔티티를 구현한 후에, 회원 탈퇴를 구현할 때 문제가 발생했습니다.
우선 회원 탈퇴의 서비스 로직은 다음과 같았습니다.
MemberService 회원 탈퇴 로직
public void leaveMember(final MemberEmailDto memberEmailDto) {
final Member member = memberRepository.findByEmail(new Email(memberEmailDto.email()))
.orElseThrow(() -> new MemberException.MemberNotFoundException(memberEmailDto.email()));
memberRepository.delete(member);
}
회원 탈퇴 로직은 간단했습니다.
MemberRepository에서 member를 delete하는 로직만 수행하여 구현을 끝냈습니다.
이때, 문제가 발생했던 것은 이전 연관관계 설정으로 인해서 Token 테이블에 Member의 FK가 존재했기 때문에
FK가 설정된 Token 테이블의 레코드를 먼저 delete 하지 않고 Member를 지우게 되면
데이터 정합성을 지키기 위해 DB 단에서 에러가 발생하는 문제가 생겼습니다.
1. 적용하지 않은 해결 방법 2가지
해당 문제에 대한 해결 방법으로는 3가지 정도가 존재했습니다.
결론적으로는 스프링 이벤트를 적용해서 해결했지만,
다른 방법들에는 무엇이 있고, 왜 적용하지 않았는지 기록하려고 합니다!
1-1. cascade delete 설정
처음에는 cascade delete로 하면 하나의 엔티티를 삭제할 때 연관된 엔티티도 삭제되므로 해결할 수 있지 않을까? 하고 생각했습니다.
하지만 요구사항과 전이 방향이 반대가 되는 것을 깨닫게 되었습니다.
현재 요구 사항은 Member가 제거될 때 Token이 삭제되는 것입니다.
하지만 기존 설계대로라면 FK가 Token에 있기 때문에 FK에 cascade delete를 설정한다면,
전이 방향이 반대로 Token이 삭제될 때 Member도 삭제되는 방향으로 진행됩니다.
물론 회원 탈퇴 로직에서 Token만을 삭제하면 구현이 되긴 하지만
Member 탈퇴인데 지우는 엔티티 주체가 Token이라는 문제가 있었고,
RefreshToken을 관리하는 Token의 시나리오 중에 비정상적인 토큰 요청이 들어오면 해당 토큰을 만료시키는 시나리오도 존재헀습니다.
이때 Token을 만료하기 위해 Token을 삭제할 때 해당 멤버도 삭제되면 안되기 때문에 cascade를 걸 수 없었습니다.
1-2. 회원 탈퇴 서비스 로직에서 Token 먼저 삭제
앞서 살펴본 회원 탈퇴 서비스 로직에서 문제가 발생했던 것은 Member를 delete 하기 전에
Member의 ID를 FK로 가진 Token을 delete 하지 않았기 때문에 발생했습니다.
따라서, 다음과 같이 서비스 로직을 수정하면 해결할 수 있었습니다.
public class MemberService {
private final MemberRepository memberRepository;
private final TokenRepository tokenRepository;
...
public void leaveMember(final MemberEmailDto memberEmailDto) {
final Member member = memberRepository.findByEmail(new Email(memberEmailDto.email()))
.orElseThrow(() -> new MemberException.MemberNotFoundException(memberEmailDto.email()));
tokenRepository.deleteByMember(member);
memberRepository.delete(member);
}
}
이런 식으로 토큰을 먼저 delete 한 후에 member를 delete 하는 식으로 구현할 수 있었습니다.
해당 방법의 문제점은 다음과 같았습니다.
- Member <-> Token 패키지 간의 순환참조 발생
- Token 엔티티 -> Member 참조
- MemberService -> TokenRepository 참조
2. 스프링 이벤트 적용
위의 2가지 방법 중에서 1번 방법은 문제 상황이 분명하기 때문에 2번 방법의 문제점인 의존성을 해결하는 방식으로 접근했습니다.
따라서, Member와 Token의 의존성을 해결하기 위해 스프링 이벤트를 도입해서 해당 문제를 해결하게 되었습니다!
스프링 이벤트에 대해서는 다른 포스팅에서 한번 다뤘습니다!
따라서 개념적인 부분이나 고려할 요소들에 대한 지식들은 해당 포스팅을 보고 이해해주시면 감사하겠습니다!
https://ksh-coding.tistory.com/111
결론적으로 스프링 이벤트를 적용한 코드는 다음과 같습니다.
MemberService
@Transactional
public void leaveMember(final MemberEmailDto memberEmailDto) {
final Member member = memberRepository.findByEmail(new Email(memberEmailDto.email()))
.orElseThrow(() -> new MemberException.MemberNotFoundException(memberEmailDto.email()));
publisher.publishEvent(new MemberLeaveEvent(member.getId()));
memberRepository.delete(member);
log.info("사용자 회원 탈퇴 - 회원 이메일 : {}", member.getEmail().getValue());
}
- MemberService에서 Token 삭제를 위해 TokenRepository를 참조해서 삭제하는 것이 아닌, Member 삭제 이벤트만 발행
- Member 삭제 이벤트를 발행한 후 Member 삭제 로직 수행
TokenEventListener
@Slf4j
@Component
@RequiredArgsConstructor
public class TokenEventListener {
private final TokenRepository tokenRepository;
@EventListener
public void deleteToken(final MemberLeaveEvent memberLeaveEvent) {
Long memberId = memberLeaveEvent.memberId();
tokenRepository.deleteByMemberId(memberId);
log.info("토큰 삭제 By 사용자 회원 탈퇴 이벤트 Listen - 회원 ID : {}", memberId);
}
}
Member 삭제 이벤트를 Listen하는 Listener를 Token 패키지에 생성해서 Event를 Listen했습니다.
- Member 삭제 이벤트가 발행되면 해당 이벤트를 Listen하여 deleteToken 수행
이렇게 스프링 이벤트를 적용하면서 Member <-> Token 패키지의 순환참조가 발생하지 않고
Token -> Member로의 한 방향 의존만 존재하도록 의존성을 분리하게 되면서
FK 설정으로 인한 회원 탈퇴 로직의 에러도 해결하게 되었습니다!
'우아한테크코스 5기 팀바팀 Project > Trouble Shooting' 카테고리의 다른 글
구글 로그인 Trouble Shooting (feat. Base64 / Base64Url) (1) | 2023.08.20 |
---|---|
[Spring] LocalDateTime 원하는 Format으로 바인딩하기 (feat.@DateTimeFormat, @JsonFormat) (0) | 2023.07.15 |