개발 slecs

WHERE 절 누락으로 전체 포인트 잔고가 덮어써진 사고와 수정

목차

사고 발견 경위

어드민에서 특정 파트너 한 명의 포인트 잔고만 보정해주려고 값 입력하고 저장 눌렀는데, 잠시 뒤 운영팀에서 "다른 파트너들 잔고도 다 바뀐 것 같다"는 제보가 옴. 로그 까보니 진짜로 전체 행이 같은 값으로 덮어써짐. 식은땀 났음.

운영DB 백업이 직전 시점에 떠있어서 다행히 즉시 롤백 가능했는데, 만약 야간이었으면 정산 한 번 갈릴 뻔했음.

원인

문제의 SQL 매퍼를 열어보니 이런 모양이었음.

<update id="updateBalance">
  UPDATE point_balance
  SET balance = #{balance},
      updated_at = NOW()
  <where>
    <if test="userId != null">
      user_id = #{userId}
    </if>
  </where>
</update>
  • 호출 쪽에서 DTO 만들 때 userId 필드를 채우긴 했는데, 어드민 화면 폼에서 hidden 으로 넘긴 값이 빈 문자열로 들어옴
  • null 체크만 하고 빈 문자열 검사 안 해서 <where> 블록이 통째로 비활성
  • 결과적으로 WHERE 절 없는 UPDATE 가 실행되어 모든 행이 갱신됨

<if test="userId != null and userId != ''"> 한 줄 빠진 게 화근. ORM 단에서 막히겠지 하고 안일했음.

수정 내용

세 가지 레이어로 막음.

레이어 조치
SQL 매퍼 빈 문자열 검사 추가, 식별자 누락 시 WHERE 1=0 으로 강제 무행
서비스 식별자 검증 후 통과 못하면 예외 던지고 메서드 진입 자체를 막음
가드 단일 행 업데이트 메서드는 LIMIT 1 명시, 전체 갱신은 별도 메서드로 분리

특히 마지막이 핵심이었음. "단일 갱신용 SQL"과 "벌크 갱신용 SQL"을 같은 매퍼 ID로 쓰던 게 진짜 원인이었고, 이걸 분리하니까 의도 자체가 명확해짐.

배운 점

  • <where> + <if> 조합은 편하지만 조건 다 떨어지면 WHERE 가 사라진다는 걸 잊지 말 것
  • "단일 행 업데이트"는 SQL 레벨에서 LIMIT 1 또는 PK 필수화로 못박아야 안전함
  • DTO 검증을 서비스 진입 직후에 강제하는 게 매퍼 if 체크보다 훨씬 명시적임
  • 운영DB 백업 주기를 더 짧게 가져가자는 얘기를 팀에 꺼냄. 사고는 언제든 또 남

회고

매퍼 한 줄 빠뜨린 건데 영향 반경은 전체 잔고였음. SQL 은 "절차적으로 안전하게 짠다"는 감각이 부족했다는 걸 인정함. 앞으로 단일/벌크 SQL 은 무조건 분리하고, 매퍼 리뷰 시 "WHERE 절이 동적으로 사라질 수 있는가"를 체크리스트에 박아둘 예정.

끝.

댓글 0

첫 댓글 달아줘.