[트러블슈팅] Soft delete와 카카오 연결끊기(unlink) 400 오류
date
‣
slug
kakao-unlink-error-400
status
Published
tags
Troubleshooting
summary
Soft delete의 잘못된 구현으로 인해 발생한 카카오 unlink 오류에 대해 알아봅니다.
type
Post
문제상황
- 첫 번째 회원가입(카카오 연결), 회원탈퇴(연결끊기)까지는 정상작동한다.
- 두 번째 회원가입 후, 회원탈퇴 시 카카오 측에서 400 응답과 함께 다음과 같은 로그가 찍혔다.
400번 응답 : {“msg”:“NotRegisteredUserException”,“code”:-101}
- 처음에는 두 번째 회원 가입 시 문제가 생겨 카카오 연결이 되지 않은 상태에서 탈퇴 처리를 해서 발생하는 문제라고 생각했다. 또는, 회원가입 후 24시간 이내에 /users/me 같은 api를 호출하지 않아 자동 탈퇴처리가 되었다고 생각했다.(이 경우는 SDK를 사용했을 때는 발생하기 어려운 문제라 금방 제외되었다)
- 그러나 DB에 정상적인 social_id 가 저장되어 있었고, 구현 된 로직 상 social_id는 정상적으로 발급 된 토큰(검증된 토큰)에서만 가져오기 때문에, 이는 원인이라고 보기 어려웠다. 게다가 카카오 측에 문의한 결과, 다음과 같은 답변을 받았다.
인가코드 요청 후, 액세스 토큰만 발급 받으면 연결됩니다. 재가입 시도를 제한 하는 로직은 없습니다.
- 뭔가 이상함을 느껴, 애플 로그인으로도 2번의 회원가입과 탈퇴를 진행해보았고, 이내 원인을 찾을 수 있었다.
원인
결론은, Soft delete(를 적절히 구현하지 못한 나)의 문제였다.
나는 유저를 탈퇴처리하면서, 상태를 DELETED로 바꾸고 social_id 앞에 "delete." prefix를 붙이는 것으로 soft delete를 구현하였다. 그러나 이는 첫 번째 탈퇴에서만 정상동작하는 로직이었다.
Member Table
id (pk) | social_id (unique) | status |
1 | delete.abc123 | DELETED |
2 | abc123 | ACTIVE |
위의 멤버 테이블에서 1번 유저는 탈퇴 후 재가입을 진행하여 2번 유저가 된 상황이다.
이 상황에서 2번 유저가 재탈퇴시 "delete.social_id" 라는 중복된 social_id 값이 생기게 되고, 이는 unique 제약조건에 의해 DataIntegrityViolationException을 발생시키고 있었다.
그런데, 왜 나는 DB에서 발생하는 예외가 아닌 카카오에서 발생하는 400 응답을 마주하였을까?
그 이유는 아래와 같은 상황 때문이었다.
- 두 번째 탈퇴 요청을 받은 서버는 카카오 측에 연결끊기(unlink) 요청, 성공(200)
- social_id를 DB에 저장하는 과정에서 Duplicate 예외 발생, 서버는 400 응답을 반환하고 트랜잭션 롤백
- 그러나 카카오 측에서 이미 처리한 요청은 롤백되지 못함
- 그 이후 탈퇴 요청 시, 카카오 측에서는 400 응답을 보내고, 서버는 외부 API 호출 중 에러가 발생했다 판단하고 500 응답
이처럼 예외로 인해 내부 서비스 로직은 롤백되었지만 외부 호출은 롤백될 수 없기에 카카오에 중복된 unlink 가 요청되었고, 처음과 같은 로그를 마주한 것이다.
해결
원인을 장황하게 설명하였지만, 해결방법은 다소 간단하다.
기존의 "deleted."로만 구성되었던 prefix를 "delete.<date>T<time>." 형식으로 바꿔 해결하였다.
이 방법 외에도 unique 제약조건을 삭제하는 방법이나 복구 기능(DELETED 상태인 데이터를 찾아서 다시 ACTIVE 상태로 변경)을 구현하는 방법도 있으나, 아래와 같은 이유들로 prefix를 변경하기로 결정했다.
- Unique 제약조건을 지우면 db의 무결성을 해치고, social_id 만으로 단일 값을 조회하던 쿼리를 status도 함께 조회하도록 변경해야한다.
- 현 상황에서 복구 기능이 굳이 필요가 없으며, 지금 추가하지 않더라도 쉽게 추가가 가능하다.
- prefix를 수정하는 방법이 구현이 가장 간단하고, 삭제 이력을 남길 수도 있다.
찾다보니 나와 비슷한 고민을 하셨던 분이 계셔서 글을 첨부한다.
추가 개선 방안
여기서 나는 이 문제와는 별개로 두 가지 개선점을 찾을 수 있었다.
- 로그를 적절히 수집/조회 할 수 있는 도구가 필요하다.
나는 지금껏 직접 ec2 인스턴스에 접속해 docker logs 명령어를 활용하는 방법을 사용하고 있었다.
그러나 이 방법으로는 정상적인 요청과 비정상적인 요청이 마구잡이로 섞여 들어오는 상황에서, 원하는 로그만을 필터링해서 보기가 어려웠다.
결과적으로, 나는 원인을 찾기 위해 로그를 확인하였으나 DataIntegrityViolationException이 발생한 상황의 로그를 놓쳤고, 카카오의 400 응답만을 보고 다른 곳에서 원인을 찾고 있었다.
따라서 나는 Grafana Loki를 도입해 로그 수집 및 쿼리를 이용한 조회가 가능도록 하고, 예기치 못한 예외가 발생 시 웹훅으로 이를 알리는 아키텍쳐를 구축하고자 한다. 이는 추가적으로 다른 글에서 다루도록 하겠다.
- 외부 API 호출 시, 실패할 경우를 반드시 고려해야 한다.
다음으로는 카카오 응답은 성공하고 내부 로직에서 예외가 발생해 트랜잭션이 롤백되는 상황을 고려하지 못했다.
위와 같은 코드에서 카카오 API 호출은 성공하였으나, JPA 더티 체킹 후 flush 되는 과정에서 예외가 발생한다면 어떻게 될까?
카카오에서는 이미 연결이 끊어지고, 멤버 데이터는 롤백되는 상황이 발생한다.
따라서, 이러한 경우를 대비해 외부 API 호출을 최대한 마지막에 수행하거나, 더 나아가 로직을 따로 분리하여 수행할 필요가 있다.
여기서는 flush 과정에서 예외가 발생할 가능성이 높기 때문에, 카카오 API 호출을 트랜잭션 밖으로 빼내고, 트랜잭션이 정상종료된 뒤 API 호출이 이루어지도록 개선하였다.
마무리
간단한 문제였지만 많은 생각을 해볼 수 있었다. Soft delete의 적절한 구현 방법에서 조금 더 나아가, 외부 API 호출 시 고려사항이나 로그 수집의 필요성도 느낄 수 있었다. 같은 문제라도 어떻게 받아들이느냐가 참 중요한 것 같다.