얼쑤 앱은 악보 화면을 떠날 때 그동안 보인 시간과 연습으로 인정한 시간을 기록합니다. 네트워크가 끊겨도 기록을 남길 수 있도록 앱은 닫힌 구간을 기기의 대기열에 넣었다가 다시 보냅니다. 저는 팀과 이 전송 규칙을 정리하고 서버의 저장 API를 구현했습니다.
이 API에서 상태 코드는 앱의 다음 동작을 정합니다. 5xx나 연결 오류가 오면 구간을 보관하고 다음 기회에 보냅니다. 400이나 404가 오면 해당 구간은 대기열에서 빼고 오류를 기록합니다. 401에는 토큰을 갱신하는 경로가 따로 있습니다. 리뷰에서 같은 요청을 다시 보내도 해결될 이유가 없는 두 가지 경우가 500으로 분류되는 것을 확인했습니다.
회원 행 누락은 401
서버는 서명과 만료 조건을 통과한 토큰에서 회원 ID를 읽습니다. 이 검증만으로는 그 ID의 회원 행이 DB에 있는지 알 수 없습니다. 행이 없는 ID로 체류 기록을 저장하면 MySQL이 외래 키 위반으로 INSERT를 거절했고, 이 예외가 HTTP 500으로 전달됐습니다.
저는 저장소에서 MySQL의 참조 행 누락 오류인 1452만 따로 처리했습니다. 같은 요청이 먼저 저장되어 발생한 중복 키는 원래 재전송 경로를 유지하고, 다른 무결성 오류는 그대로 전달했습니다. 이 요청의 체류 기록이 참조하는 행은 곡과 회원입니다. 외래 키 위반이 나면 곡의 존재를 다시 확인해서, 곡이 사라졌으면 404를 보내고 곡이 있으면 회원이 없는 경우로 보고 401을 보냅니다. 저장 전에 곡을 조회했어도 저장 사이에 삭제될 수 있어 실패한 INSERT를 기준으로 이 경로를 처리했습니다.
수정 뒤에는 실제 HTTP와 임시 MySQL로 확인했습니다. 곡은 존재하게 두고 회원 행이 없는 ID로 서명한 쿠키를 보냈더니 401과 UNAUTHENTICATED가 돌아왔고, 검사 대상 회원들의 체류 기록은 0행이었습니다. 외래 키 변환을 제거하자 같은 통합 테스트가 다시 실패했습니다.
DB 저장 범위를 벗어나면 400
형식 검사에서는 ISO 8601 문자열을 읽어 같은 순간의 UTC 시각으로 바꿉니다. 컨트롤러 검사에 2026-10-28T14:00:00+09:00을 보내자 저장소 대역을 거친 성공 응답에는 2026-10-28T05:00:00Z가 들어 있었습니다. 같은 검사에서 숫자로 보낸 시각과 오프셋 없는 날짜·시간 문자열은 400으로 거절됐습니다. 형식 검사를 통과한 시각에는 저장 범위가 맞는지도 별도로 확인해야 했습니다.
ISO 8601 시각을 파싱할 수 있는 범위는 MySQL의 DATETIME에 넣을 수 있는 범위보다 넓습니다. 파싱에 성공한 시각을 그대로 저장하면 DB에서 거절될 수 있고, 아주 먼 시각은 그보다 앞선 구간 길이 계산에서도 문제를 일으킬 수 있습니다. 그래서 시작과 종료 시각을 구간 길이 계산이나 저장 전에 검사하도록 했습니다. 저장 범위를 벗어나면 API가 400과 TIME_OUT_OF_RANGE를 반환합니다.
도메인 테스트는 1000년 직전과 10000년 시작 시각을 각각 거절했습니다. 저장 가능한 마지막 시각인 9999년 12월 31일의 마지막 마이크로초에 길이가 0인 구간은 이 검사를 통과했습니다. 범위의 양쪽에서 같은 규칙이 적용되는지 확인하는 테스트였습니다.
이 결과는 서버가 저장할 수 없는 시각과 토큰에 대응하는 회원이 없는 경우를 각각 구분해 응답한다는 것을 보여줍니다. 앱이 400에서 해당 기록을 빼고 401에서 인증 갱신을 시도하는 것은 팀이 정한 전송 규칙입니다. 대기열에 남는 기록의 수나 운영에서 반복 전송된 횟수는 이 서버 검증에서 재지 않았습니다.

댓글
댓글과 반응은 GitHub 계정으로 로그인해 남길 수 있어요.