
친구 기능을 네 조각으로 나눠 꼼꼼히 만들고, 각 조각마다 검증까지 마쳤습니다. 자동 테스트도 통과했고, 실제 브라우저 두 개로 대화 신청까지 돌려봤죠. 자신 있게 배포했습니다.
그리고 얼마 뒤, 버그가 터졌습니다.
이번 편은 그 버그를 추적한 이야기입니다. 결론부터 말하면, 범인은 친구 기능이 아니었습니다. 친구 기능은 그저, 예전부터 서버에 조용히 숨어 있던 버그를 처음으로 눈에 보이게 만들었을 뿐이었죠. 개인적으로 이번 시리즈에서 가장 인상 깊었던 디버깅입니다.
그 전에 — 배포하자마자 발견한 구조 문제 하나
진범 이야기 전에, 배포 직후 먼저 드러난 구조 문제를 짧게 짚고 갑니다.
원래 친구 목록을 별도 페이지(/friends)로 만들었습니다. 랭킹 페이지처럼 독립된 주소를 갖는 화면으로요. 그런데 여기에 함정이 있었습니다. PlayTalk은 페이지 주소가 바뀌면 화면이 통째로 새로 로드되고, 그때 소켓 연결도 끊겼다 다시 붙습니다.
문제는 대화 신청이 성사됐을 때였습니다. 친구 목록 페이지에서 대화 신청을 해서 매칭이 성사됐는데, 그 방으로 들어가려고 메인으로 이동하는 순간 소켓이 끊기면서 방이 파괴됐습니다. 어렵게 성사시킨 대화가 페이지 이동 한 번에 날아가는 거죠.
해결은 방향을 바꾸는 것이었습니다. 친구 목록을 별도 페이지가 아니라, 메인과 같은 화면 안에서 전환되는 내부 화면으로 바꿨습니다. 혼자 하기 화면처럼요. 이러면 주소가 안 바뀌니 소켓이 끊길 일이 없고, 대화가 성사되면 그 자리에서 바로 대화 화면으로 넘어갑니다.
돌아보면 이건 "친구 목록을 별도 페이지로 두자"는 초기 결정이 심어둔 지뢰였습니다. 그 결정을 할 때 "이 페이지에서 대화 신청을 하면 소켓이 끊긴다"는 걸 내다봤어야 했는데, 한참 뒤에야 연결됐죠. 다행히 되돌리는 작업이 오히려 코드를 더 단순하게 만들었습니다. 독립 페이지라 필요했던 소켓 재연결 처리가 통째로 사라졌거든요.
본론 — "한쪽만 계속 대화 중"
이제 진짜 버그입니다. 사용자에게서 온 리포트는 대략 이랬습니다.
친구랑 대화를 하고 끝냈는데, 친구 목록을 보니 **한쪽은 상대가 계속 '대화 중'**으로 뜨고, **다른 쪽은 '온라인'**으로 뜬다. '대화 중'으로 보이는 쪽에서 대화 신청을 누르면 "상대가 대화 중이라 신청할 수 없다"고 나온다.
분명 대화는 끝났는데, 서버는 한쪽을 아직 대화 중이라고 여기고 있었습니다. 게다가 이게 비대칭이었습니다. 한쪽만 그렇고 반대쪽은 멀쩡했죠.
결정적 단서
리포트에는 이상한 문장이 하나 더 붙어 있었습니다.
"'대화 중이라 신청 불가'가 뜰 때, 상대방이 브라우저를 새로고침하면 그 즉시 채팅이 시작된다."
이 한 문장이 사실상 사건을 풀었습니다. 새로고침은 소켓을 다시 연결하는 행위입니다. 그런데 새로고침하니 채팅이 시작된다? 이건 서버 쪽엔 방이 아직 살아있는데, 그 브라우저의 화면만 대화 상태가 아니었다는 뜻입니다. 즉 서버의 방 상태와 화면이 서로 어긋나 있었던 겁니다.
정리하면 가설은 이렇습니다. 대화가 끝났는데도 서버가 방 정보를 한쪽에서 정리하지 않고 남겨두고 있다. 그 남은 흔적 때문에 그 사람은 계속 '대화 중'으로 판정되는 것이다.
추적 — 대화를 끝내는 코드로
그럼 "대화를 끝낼 때 방을 정리하는 코드"를 봐야 했습니다. 서버에서 방을 떠나는 처리를 하는 함수를 열어보니, 문제가 바로 보였습니다.
누군가 방을 나가면, 이 함수는 나가는 사람의 방 정보만 지우고 있었습니다. 방 자체는 삭제하는데, 남는 상대방의 소켓에 붙어 있던 '나는 이 방에 있다'는 표시는 그대로 놔두고 있었던 겁니다.
그림을 그려보면 이렇습니다. A와 B가 대화 중이다가 A가 나갑니다. A는 자기 방 정보를 지우고 깔끔하게 떠납니다. 방도 삭제됩니다. 그런데 B의 소켓엔 "나는 방에 있음"이라는 낡은 표시가 그대로 남습니다. B가 "상대가 나갔다"는 알림을 받고 메인으로 돌아와도, 서버 입장에서 B는 여전히 방에 있는 사람인 거죠.
그리고 이번에 새로 만든 '상태 판정 함수'가 이 낡은 표시를 읽고 B를 '대화 중'으로 판정한 겁니다. A는 제대로 정리됐으니 '온라인', B는 흔적이 남았으니 '대화 중'. 리포트에서 본 비대칭이 정확히 설명됐습니다.
반전 — 이건 친구 기능 버그가 아니었다
여기서 중요한 사실을 깨달았습니다. 이 "한쪽만 방 정보가 안 지워지는" 문제는 친구 기능이 만든 게 아니었습니다. 방을 떠나는 이 함수는 원래부터 있던, 랜덤 매칭·게임 등 모든 대화 종료가 거쳐 가는 공통 코드였습니다. 그리고 이 함수는 처음부터 상대방 쪽 방 정보를 안 지우고 있었습니다.
그럼 왜 여태 몰랐을까요? 그 낡은 흔적을 읽는 코드가 지금까지 없었기 때문입니다. 예전엔 방 정보가 남아 있어도 딱히 그걸 보고 뭔가를 판정하는 곳이 없어서, 조용한 부작용에 그쳤습니다. 그런데 이번에 친구 기능의 '상태 판정 함수'가 그 흔적을 처음으로 읽기 시작하면서, 잠자던 버그가 겉으로 드러난 겁니다.
비유하자면, 친구 기능은 방에 조명을 켠 것뿐이었습니다. 먼지는 원래 거기 쌓여 있었고, 조명이 켜지자 비로소 보이게 된 거죠.
수정 — 근본을 고치다
수정 방향은 명확했습니다. 친구 기능 안에서 이 문제를 우회하는 게 아니라, 근본 원인인 방 정리 코드를 고치는 것이었죠. 방을 떠날 때 나가는 사람뿐 아니라 남는 상대방의 방 정보도 함께 지우도록 했습니다. 이러면 사용자가 어떤 경로로 나가든, 서버가 스스로 양쪽을 깔끔하게 정리합니다.
이 수정은 친구 기능만 고치는 게 아니라, 원래부터 있던 랜덤 매칭의 잠재적 문제까지 함께 없애는 것이었습니다. 다만 이 함수는 모든 대화 종료가 지나는 공통 경로라, 여기를 건드리면 친구 기능뿐 아니라 전체 매칭에 영향이 갑니다. 그래서 수정 후 "랜덤 매칭·게임 종료가 여전히 정상인가"를 특히 신경 써서 확인했습니다.
함께 딸려온 작은 버그도 하나 잡았습니다. 이미 친구인 사람과 다시 매칭됐을 때 '친구 추가' 버튼이 또 뜨는 문제였죠. 매칭되는 시점에 "이 상대가 이미 내 친구인지"를 화면이 몰라서 생긴 일이었습니다. 매칭 정보에 "이미 친구인지" 여부를 함께 실어 보내니 해결됐습니다.
정리하며 — 이번 일로 배운 것
이번 디버깅에서 얻은 교훈이 두 가지 있습니다.
첫째, 새 기능이 드러낸 버그가 반드시 새 기능의 버그는 아니다. 증상이 친구 기능에서 나타났다고 친구 기능만 파고들었다면, 진범을 못 찾고 엉뚱한 우회 코드만 늘렸을 겁니다. "이 증상의 진짜 원인이 어디인가"를 기능 경계 밖까지 넓혀 본 게 주효했습니다.
둘째, 사용자의 사소한 관찰이 결정적 단서가 된다. "새로고침하면 채팅이 시작된다"는 그 한 문장이 없었다면, 저는 상태 판정 로직만 계속 의심했을 겁니다. 그 문장 덕분에 "서버엔 방이 살아있다"는 방향으로 곧장 갈 수 있었죠. 좋은 버그 리포트는 정말 절반의 해결입니다.
친구 기능은 이렇게, 신원 설계부터 배포 후 버그 추적까지 긴 여정을 지나 자리를 잡았습니다. 세 편에 걸쳐 읽어주셔서 감사합니다.
마치며
이번 시리즈는 "재밌게 논 상대와 또 만나고 싶다"는 짧은 건의 하나에서 시작됐습니다. 그 요청을 따라가다 보니 신원이란 무엇인가, 실시간은 정말 필요한가, 버그의 진짜 주소는 어디인가까지 오게 됐네요. 작은 기능 하나에도 이렇게 많은 판단이 숨어 있다는 게, 개발의 재미인 것 같습니다.
👉 PlayTalk 에서 친구 기능을 직접 써보실 수 있습니다.