1. 개요
외부 시스템이 들어오면 성공의 기준부터 흔들린다.
루퍼스에서 PG 연동 과제를 진행하면서 가장 먼저 부딪힌 것도 그 문제였다. 요청을 보냈다는 사실과 상태가 실제로 확정되었다는 사실은 다를 수 있기 때문이다.
재밌게도 이 질문은, 실제로 이번 주 회사에서 회원 탈퇴 설계를 논의하면서 다시 돌아왔다. (아쉽게도 결론이 나진 않았다...)
도메인은 다르지만 질문은 같았다. 우리 시스템 안의 작업이 끝났다는 사실만으로 정말 성공이라고 말할 수 있는가.
▎(루퍼스 부트캠프 과제와 관련된 내용입니다, 읽지 않아도 무방)
pg-simulator 연동에서 이 질문은 구체적인 형태로 나타났다. requestPayment()가 정상 응답을 받았다고 해서 결제가 완료된 것이 아니다. pg-simulator가 콜백으로 최종 결과를 보내주기 전까지 결제 상태는 확정되지 않는다. 호출 성공과 상태 확정은 다른 시점에 일어난다.
회원 탈퇴도 마찬가지다.
우리 시스템에서 회원 정보를 지우는 것만으로 탈퇴가 끝난 것인지, 아니면 애플·카카오 같은 외부 서비스의 revoke 또는 unlink까지 끝나야 탈퇴가 완료된 것인지 먼저 정해야 한다.
중요한 것은 delete 쿼리 자체가 아니다.
내부 상태(내 서버)와 외부 상태(외부 서버)를 어디까지 함께 정리해야 성공으로 볼 것인지 정하는 일이다.
이 글에서는 회원 탈퇴를 단순 삭제가 아니라, 외부 시스템까지 포함된 상태 정리 흐름으로 보고, 그 안에서 가능한 설계 선택지와 상태 모델을 정리해보려 한다.
2. 회원 탈퇴는 왜 단순한 delete가 아닌가
회원 탈퇴를 단순한 delete로 보면 문제는 쉬워 보인다.
회원 데이터를 지우면 끝나는 일처럼 보이기 때문이다. 하지만 실제 서비스에서 탈퇴는 그렇게 끝나지 않는 경우가 많다.
탈퇴에는 내부 데이터 삭제만 있는 것이 아니다.
로그인 연동 해제, 외부 토큰 revoke, 제3자 서비스 unlink, 재가입 충돌 방지처럼 함께 정리해야 할 것들이 남는다. 이때부터 탈퇴는 단순 삭제가 아니라 여러 상태를 정리하는 흐름이 된다.
문제는 이 흐름이 한 번의 요청으로 끝나지 않을 수 있다는 점이다.
내부 데이터는 정리됐지만 외부 revoke가 실패할 수 있고, 외부 호출은 처리됐지만 우리 시스템은 아직 결과를 확정하지 못할 수도 있다.
이런 상황에서 탈퇴를 단순 delete로 취급하면 시스템은 실제 상태를 설명하지 못한다.
사용자에게는 탈퇴 완료를 보여줬지만 외부 연동은 남아 있을 수 있고, 반대로 외부 연동은 끊겼지만 내부 상태는 아직 완료로 바뀌지 않았을 수도 있다.
결국 회원 탈퇴에서 중요한 것은 데이터를 지웠는지가 아니다.
내부와 외부의 정리 작업을 어디까지 완료해야 탈퇴가 끝났다고 볼 것인지, 그 기준을 시스템 안에 어떤 상태로 남길 것인지가 더 중요하다
3. 내부 삭제와 외부 revoke 중 어디까지를 성공으로 볼 것인가
회원 탈퇴를 외부 시스템까지 포함된 흐름으로 본다면, 가장 먼저 정해야 하는 것은 구현 순서가 아니다.
내부 데이터 정리와 외부 revoke 중 어디까지를 탈퇴 성공으로 볼 것인지부터 정해야 한다.
가장 단순한 기준은 둘 다 성공해야만 탈퇴 성공으로 보는 방식이다.
우리 시스템 데이터도 정리되고 외부 서비스와의 연결도 끊겨야 탈퇴가 끝났다고 해석하는 것이다. 이 방식은 의미가 분명하다. 대신 외부 장애에 전체 탈퇴 흐름이 민감해진다.
반대로 내부 데이터 정리만 끝나면 우선 탈퇴 성공으로 보는 방식도 가능하다.
이 경우 외부 revoke 실패는 후속 작업으로 넘긴다. 장점은 외부 시스템 문제 때문에 탈퇴 자체가 계속 막히지 않는다는 점이다. 대신 일정 시간 동안 내부 상태와 외부 상태의 불일치를 허용해야 한다.
그래서 이 문제는 어느 방식이 더 깔끔한지의 문제가 아니다.
어떤 불일치를 허용할 것인지, 외부 장애를 어디까지 내부 기능의 실패로 받아들일 것인지 결정하는 문제에 가깝다.
예를 들어 이런 질문이 먼저 정리되어야 한다.
- 외부 revoke 실패 때문에 내부 탈퇴까지 막을 것인가
- 내부 탈퇴는 먼저 완료하고 외부 정리는 나중에 맞출 것인가
- 일정 시간 상태 불일치를 허용할 수 있는가
- 사용자에게는 어느 시점을 기준으로 완료를 보여줄 것인가
이 기준이 정해져야 예외도 해석할 수 있다.
외부 호출 실패를 곧바로 탈퇴 실패로 볼 수도 있고, 내부 탈퇴는 성공으로 두되 외부 정리만 재시도 대상으로 남길 수도 있다.
즉, 예외는 단순한 오류가 아니라 어떤 상태를 남기고 다음 처리를 어떻게 이어갈지 결정하게 만드는 신호에 가깝다.
▎(루퍼스 부트캠프 과제와 관련된 내용입니다, 읽지 않아도 무방)
pg-simulator 연동에서 이 선택은 타임아웃 상황에서 명확하게 나타났다. pg-simulator에 결제를 요청했는데 응답이 오지 않았을 때, 즉시 결제를 FAILED로 마킹할 것인지가 그 선택이었다. 타임아웃은 "요청이 실패했다"가 아니라 "응답을 받지 못했다"는 뜻이다. pg-simulator가 이미 결제를 처리했을 수 있기 때문에, 즉시 실패로 확정하지 않고 PENDING으로 유지했다. 지금의 불일치를 허용하되 나중에 맞추겠다는 결정이었다.
4. 가능한 설계 선택지 비교
내부 삭제와 외부 revoke를 어디까지 하나의 결과로 볼 것인지에 따라, 회원 탈퇴 흐름은 몇 가지 방식으로 나눌 수 있다. 중요한 것은 어떤 방식이 더 정답에 가깝냐가 아니라, 각 방식이 어떤 대가를 가지는지 아는 것이다.
1) 내부 삭제와 외부 revoke가 모두 성공해야 탈퇴 성공으로 보는 방식
가장 직관적이다.
내부와 외부가 모두 정리되어야 탈퇴가 완료되었다고 본다.
장점은 상태 의미가 분명하다는 점이다.
“탈퇴 완료”는 내부와 외부가 모두 정리된 상태라는 뜻이 된다.
단점은 외부 장애에 매우 민감하다는 점이다.
내부 처리가 끝났어도 외부 revoke가 실패하면 탈퇴 전체를 완료로 볼 수 없다. 결국 외부 상태가 내부 기능의 완료 여부를 좌우하게 된다.
2) 내부 삭제가 끝나면 우선 탈퇴 성공으로 보고, 외부 revoke는 후속 처리로 넘기는 방식
우리 시스템 기준의 완료를 먼저 인정하는 방식이다.
내부 데이터가 정리되면 사용자에게는 탈퇴 완료를 보여주고, 외부 revoke는 재시도나 후속 작업으로 넘긴다.
장점은 외부 시스템 문제 때문에 탈퇴가 계속 막히지 않는다는 점이다.
반면 단점은 내부와 외부 상태가 일정 시간 어긋날 수 있다는 점이다.
따라서 이 방식을 선택했다면, 외부 정리가 남아 있다는 사실을 시스템이 추적할 수 있어야 한다.
그렇지 않으면 부분 성공 상태가 장기적으로 방치될 수 있다.
▎(루퍼스 부트캠프 과제와 관련된 내용입니다, 읽지 않아도 무방)
pg-simulator 연동에서 이 방식을 선택했다. pg-simulator 호출이 타임아웃이나 서킷 브레이커로 막히더라도 결제를 즉시 FAILED로 확정하지 않고 PENDING으로 유지한다. 그리고 3분마다 실행되는 스케줄러가 PENDING 결제를 조회해 pg-simulator에 상태를 확인하고 동기화한다.
"지금의 불일치를 허용하되, 나중에 반드시 맞춘다"는 구조다. 이 방식에서 중요한 것은 후속 처리 장치가 반드시 함께 있어야 한다는 점이다. 그렇지 않으면 PENDING 상태는 영원히 방치된다.
3) 중간 상태를 두고 최종 완료를 나중에 확정하는 방식
성공과 실패를 바로 확정하지 않고 중간 상태를 두는 방식이다.
예를 들어 탈퇴 요청이 들어오면 바로 DELETED로 바꾸지 않고, DELETING 같은 상태로 먼저 전이한 뒤 내부 정리와 외부 revoke가 모두 확인되면 최종 완료로 바꾼다.
장점은 시스템이 현재 상황을 더 정직하게 드러낼 수 있다는 점이다.
아직 끝나지 않은 흐름을 억지로 성공이나 실패 중 하나로 밀어 넣지 않아도 된다.
대신 운영 복잡도는 올라간다.
상태 전이 규칙, 재시도 정책, 운영 기준까지 함께 설계해야 하기 때문이다.
결국 세 방식의 차이는 구현 난이도보다 무엇을 우선하느냐에 있다.
- 강한 정합성을 우선할 것인가
- 빠른 완료를 우선할 것인가
- 중간 상태를 드러내고, 나중에 최종 상태를 확정할 것인가
즉, 이 선택은 코드 스타일의 차이가 아니라 어떤 불일치를 허용하고 어떤 비용을 감당할 것인지 정하는 문제다.
5. 상태 모델과 재시도는 어떻게 볼 것인가
외부 시스템이 포함된 흐름에서는 성공과 실패를 한 번에 나누기 어렵다.
내부 처리와 외부 revoke가 서로 다른 시점에 끝날 수 있다면, 중간 과정을 상태로 드러내는 편이 더 자연스럽다.
예를 들어 회원 탈퇴를 아래처럼 나눌 수 있다.
- ACTIVE: 정상 상태
- DELETING: 탈퇴 요청은 들어왔지만, 내부 정리나 외부 revoke가 아직 끝나지 않은 상태
- DELETED: 내부와 외부 정리가 모두 끝난 상태
- DELETE_FAILED: 탈퇴 과정 중 일부가 실패해 추가 처리가 필요한 상태
▎(루퍼스 부트캠프 과제와 관련된 내용입니다, 읽지 않아도 무방)
pg-simulator 연동에서 Payment의 PENDING 상태가 DELETING과 같은 역할을 한다. "pg-simulator에 요청은 보냈지만 콜백이 오기 전까지 결과를 알 수 없다"는 사실을 상태로 드러내는 것이다. 콜백이 도착하면 SUCCESS 또는 FAILED로 전이하고, 콜백이 오지 않으면 스케줄러가 pg-simulator에 직접 조회해서 상태를 확정한다. 억지로 즉시 성공이나 실패로 밀어 넣지 않아도 되는 이유가 이 중간 상태 덕분이다.
이런 상태를 두는 이유는 단순하다.
시스템이 지금 어디까지 끝났고, 무엇이 아직 남았는지를 설명할 수 있어야 하기 때문이다.
외부 revoke에서 예외가 발생했다고 해도 모든 경우를 같은 실패로 보면 안 된다.
어떤 예외는 일시적이라 재시도할 가치가 있고, 어떤 예외는 이미 외부 상태가 반영되었을 가능성을 고려해야 하며, 어떤 예외는 요청 자체가 잘못되어 재시도해도 의미가 없다.
그래서 예외는 단순 오류가 아니라 다음 상태를 결정하게 만드는 신호로 다뤄야 한다.
예를 들어,
- 일시적인 외부 장애라면 DELETING 상태를 유지하고 재시도 대상으로 남길 수 있다
- 재시도해도 의미가 없는 요청 오류라면 DELETE_FAILED로 전이해 종료할 수 있다
- 외부에서는 이미 revoke가 끝난 것으로 판단할 수 있다면 멱등하게 성공 처리할 수도 있다
▎ (루퍼스 부트캠프 과제와 관련된 내용입니다, 읽지 않아도 무방)
pg-simulator 연동에서 예외를 이렇게 구분했다. ConnectException(연결 자체가 안 됨)과 HttpServerErrorException(PG 5xx)은 재시도 가능한 신호로 분류했고, SocketTimeoutException(타임아웃)은 재시도 불가로 구분했다. 타임아웃은 pg-simulator가 이미 처리했을 가능성이 있어 재시도하면 중복 결제가 발생할 수 있기 때문이다. 이를 위해 재시도 가능 여부를 담은 PgConnectionException이라는 커스텀 예외를 따로 만들었다. 예외를 잡는 것이 목적이 아니라, 이 예외가 "다음에 무엇을
해야 하는지"를 결정하는 신호가 되도록 설계한 것이다.
중요한 것은 상태 모델과 재시도 정책이 따로 떨어져 있지 않다는 점이다.
어떤 상태를 둘 것인지 정해야 어떤 예외를 재시도 대상으로 남길지 결정할 수 있고, 어떤 예외를 허용할 것인지 정해야 상태 전이도 설계할 수 있다.
또 중간 상태를 두었다면, 그 상태를 최종 상태로 넘기는 장치도 함께 있어야 한다.
DELETING 상태를 만들었다면, 재시도나 후속 확인을 통해 결국 최종 상태로 옮겨갈 수 있어야 한다. 중간 상태는 임시 보관이 아니라 관리 대상이어야 한다.
결국 상태 모델은 단순 분류가 아니다.
지금 어디까지 끝났는지, 무엇이 남았는지, 다음에 무엇을 해야 하는지를 시스템이 일관되게 설명하게 만드는 기준이다.
6. 마무리
회원 탈퇴는 겉으로 보면 단순한 delete처럼 보인다.
하지만 외부 시스템이 함께 얽히는 순간, 문제는 데이터 삭제 하나로 닫히지 않는다. 어디까지를 성공으로 볼 것인지 먼저 정해야 하고, 그 기준에 따라 상태 모델과 예외 해석, 재시도 방식도 함께 달라진다.
결국 회원 탈퇴는 delete 한 번의 문제가 아니다.
외부 시스템까지 포함된 흐름에서 성공의 경계를 설계하는 문제다.
이 문제는 회원 탈퇴에만 있는 것도 아니다.
외부 시스템이 포함되는 순간, 중요한 것은 호출 자체보다 성공의 기준과 상태를 어떻게 확정할 것인지다. 결국 설계해야 하는 것은 API 호출 한 번이 아니라, 불일치를 어떻게 다루고 최종 상태를 어떻게 정리할 것인지다.
끝!