이미지 재생성 전에 마켓으로 전송돼서 오류가 난다는 주장이 있었고 나는 그것을 코드로 반박하려고 조사를 시작했다.
Table of contents
Open Table of contents
단일 요청 흐름의 안전
이미지 업로드 부분을 보니 블로킹 방식의 HTTP 호출이었다. 이미지 서비스가 변환을 끝내고 응답할 때까지 기다리고 그 서비스도 동기로 답한다.
전송 로직은 그다음에 오므로 한 요청 안에서는 순서가 보장된다. 변환이 끝나야 다음 줄이 실행되니 재생성 전에 전송될 수 없다고 보고 타이밍 창이 없다고 결론을 냈다.
반문이 바꾼 조건
그 결론에 반문이 하나 왔다. 여러 사람이 같은 상품을 동시에 수정해서 보낼 때는 발생할 수 있지 않겠느냐는 것이었다.
내가 생각하지 못한 조건이었다. 두 사용자가 같은 상품을 동시에 편집하면 서로 다른 요청이므로 한쪽의 블로킹이 다른 쪽을 막지 않는다. 한쪽 변환이 다른 쪽 변환에 덮이거나 한쪽이 다른 쪽의 중간 상태를 보고 전송할 수 있다.
락 부재와 워커의 무확인
그러면 그것을 막는 장치가 있는지를 확인해야 했다. 상품과 전송 관련 서비스 전체에서 락 관련 호출을 검색하니 0건이었고 상품 단위 동시성 락이 하나도 없었다.
전송 워커도 함께 봤는데 결과물이 실제로 있는지 확인하지 않고 경로를 구성해서 보내고 있었다. 변환이 안 끝났으면 그 경로에 파일이 없고 마켓이 가져가려다 실패한다. 처음 결론과 정반대로 그 주장은 다중 요청 조건에서 맞을 수 있었다.
처음 결론을 그대로 전달했으면 버튼 잠금 요구가 불필요하다는 부정확한 답을 줬을 것이다. 그 주장을 한 쪽은 실제로 증상을 봤을 것이고 근거 없이 말한 것이 아니었다.
두 차원의 구분
이 건에서 얻은 것이 가장 큰 지점이 여기다. 코드 조사에서 단일 요청 흐름과 다중 요청 동시성을 별개 차원으로 반드시 구분해야 한다는 것이다.
단일 요청에서는 순서가 보장되는지를 보고 다중 요청에서는 같은 대상에 대한 동시 접근을 막는지를 본다. 앞의 것이 안전해도 뒤의 것은 별개이며 동기 호출은 그 요청 안의 순서만 보장할 뿐 다른 요청과의 관계는 아무것도 보장하지 않는다.
놓치기 쉬운 이유도 분명했다. 코드를 읽으면 한 흐름을 따라가게 되고 그 순서가 눈에 보이면 됐다고 느낀다. 다른 요청이 동시에 같은 흐름을 타는 것은 코드에 안 보이므로 상상해야 하는데 락을 검색하는 일이 그 상상을 대신해 준다.
반박하려는 조사의 편향
해결 방향으로는 둘을 적었다. 같은 상품에 대한 수정과 전송을 직렬화하는 락과 전송 직전에 결과물이 실제로 있는지 확인하는 게이트인데 뒤쪽이 가볍다. 락은 성능 부담과 데드락 위험이 따라온다.
이 조사가 반박에서 시작했다는 점도 기록해 둘 만하다. 반박하려고 조사하면 반박 근거만 찾게 되고 그것을 찾는 순간 멈춘다. 내가 처음 찾은 것이 동기 보장이었고 그것으로 반박에 충분했으므로 거기서 끝냈다. 반문이 없었으면 틀린 결론이 그대로 나갔을 것이고, 주장을 검증할 때는 그 주장이 맞는 조건도 같이 찾아야 한다.
정리
- 단일 요청의 순서 보장과 다중 요청의 동시성은 별개 차원이다
- 동기 호출은 그 요청 안의 순서만 보장한다
- 다중 접근을 막는지는 락 검색으로 확인한다
- 워커가 결과물 존재를 확인 안 하면 준비 안 된 것을 보낸다
- 반박하려고 조사하면 반박 근거만 찾는다
- 주장을 검증할 때 그 주장이 맞는 조건도 같이 찾는다
- 해결은 락과 전송 직전 게이트 둘이고 후자가 가볍다
- 증상을 봤다는 주장은 근거 없이 나온 것이 아니다