개발 slecs

파트너 포탈 거래 내역 조회 시 권한 불일치로 0건 반환되던 버그 수정

목차

파트너 포탈에서 거래 내역 조회가 0건으로 돌아온다는 제보가 들어왔다. 처음엔 데이터 문제인 줄 알고 DB부터 확인했는데, 데이터는 있었다. 로그를 까보니 요청 파라미터에 targetSysId=GLOBAL이 박혀 있었다. 글로벌 권한 운영자가 쓰던 키를 파트너 계정이 그대로 물고 들어오니 결과 집합이 0건이 되는 구조였다.

사실 이런 버그가 까다로운 건, 에러가 나지 않아서다. 200 OK에 빈 배열 하나 오면 모니터링 입장에서는 "정상 응답"이다. 경보도 안 울린다. 데이터가 원래 없는 건지, 권한 때문에 걸러진 건지 구분되지 않은 채 사일런트하게 흐른다. 담당자가 "이상한데?" 하고 직접 제보해줘서 잡을 수 있었던 거지, 아니었으면 한참 모를 뻔했다.

왜 이런 구조가 생겼나

멀티테넌트 시스템에서 GLOBAL 같은 sentinel 값은 꽤 자연스럽게 생긴다. "전체 시스템 대상으로 조회한다"는 의미를 하나의 키로 표현하고 싶을 때 enum에 GLOBAL을 박아두면 편하다. 운영자 입장에서는 맞는 설계다. 운영자는 실제로 전 시스템을 볼 수 있으니까.

문제는 이 sentinel이 호출자 컨텍스트에 따라 의미가 달라진다는 점을 처음부터 설계에 명시하지 않은 것. 운영자에게 GLOBAL은 횡단 조회 권한을 포함한 값이지만, 파트너에게 GLOBAL은 권한 밖 요청이다. 같은 파라미터 값이라도 누가 보냈느냐에 따라 해석이 달라져야 하는데, 권한 분기 로직이 그걸 처리하지 않았다.

targetSysId=GLOBAL
 ├─ 운영자 컨텍스트 → 전체 시스템 조회 (정상)
 └─ 파트너 컨텍스트 → 권한 밖 → 0건 (버그)

파라미터가 어떻게 파트너 요청에 박히느냐도 따져봐야 했다. 가능성이 여러 개다.

  • 즐겨찾기로 저장해둔 운영자 URL을 파트너가 공유받아 그대로 사용
  • 운영자가 링크를 직접 파트너에게 전달
  • 직전 세션 캐시가 파라미터를 물고 이월

파트너가 의도적으로 GLOBAL을 찍은 게 아니라, 자기도 모르게 들고 들어온 케이스가 대부분이었다. 그러니 "파트너한테 올바른 파라미터 쓰라고 안내"로는 해결이 안 된다.

어디서 차단할 것인가

처음엔 화면단에서 파트너 세션이면 targetSysId=GLOBAL을 아예 못 보내게 막는 방법을 생각했다. 구현 자체는 간단하다. 근데 URL 직접 입력이나 즐겨찾기, 외부 링크를 커버하지 못한다. 프런트를 우회하는 경로가 너무 많다.

위치 장점 단점
프런트 차단 구현 간단 URL 직타·즐겨찾기 우회됨
서버 폴백 모든 진입 경로 커버 분기 늘어남

결론은 서버에서 받아치는 것. 프런트는 보조다.

수정 방향은 조회 진입 지점에서 세션 주체를 먼저 판별하고, 파트너이면 GLOBAL을 본인 소속 sysId로 치환하는 폴백을 넣는 것으로 잡았다.

  • 세션 주체 타입 확인 (운영자/파트너)
  • 파트너이고 targetSysId == GLOBAL이면 본인 sysId로 교체
  • 운영자·관리자는 기존 동작 그대로 유지
  • 치환 발생 시 감사 로그 한 줄 기록

코드로 표현하면 대략 이런 모양이다.

// 조회 진입 지점 - targetSysId 정규화
String resolvedSysId = targetSysId;

if (SessionContext.isPartner() && "GLOBAL".equals(targetSysId)) {
    resolvedSysId = SessionContext.getPartnerSysId();
    auditLog.write(
        "[SysId 폴백] partner=%s 요청=%s → 치환=%s",
        SessionContext.getPartnerId(), targetSysId, resolvedSysId
    );
}
// 이후 resolvedSysId 로만 조회 진행

감사 로그가 단순해 보여도 중요하다. 치환이 조용히 일어나면 나중에 "이 파트너는 왜 전체 데이터를 못 봤지?" 같은 질문이 생겼을 때 추적이 안 된다. 로그를 집계하면 이 폴백이 얼마나 자주 발생하는지도 파악할 수 있는데, 수치가 높으면 프런트 쪽 파라미터 오염도를 간접적으로 보여준다.

회고 - sentinel 값은 단순 enum이 아니다

이번에 다시 체감한 게 두 가지다.

첫째, sentinel 값을 단순 enum으로 보면 이런 폴백이 누락되기 쉽다. GLOBAL처럼 "전체"를 의미하는 키가 시스템에 존재한다면, 그 키의 해석이 호출자 권한 레벨과 무관한지 먼저 확인해야 한다. 무관하다면 그냥 써도 된다. 그런데 권한 레벨에 따라 의미가 달라지는 sentinel이라면, 그 해석을 담당하는 레이어가 반드시 서버 쪽에 있어야 한다. 클라이언트 방어는 보조 수단이지 주 방어선이 될 수 없다.

둘째, "비었음"과 "권한 때문에 비었음"은 다른 상태인데, 둘을 같은 응답으로 묶으면 모니터링 사각지대가 생긴다. 실제 데이터가 없는 것과 권한 미스매치로 아무것도 보지 못하는 것은 사용자 경험과 디버깅 양쪽에서 완전히 다른 상황이다. 이번처럼 제보가 없었다면 지표로는 전혀 감지가 안 된다. 별도 응답 필드나 메트릭 레이블이라도 달아야 한다.

다음 스텝은 잔액, 정산, 원장 등 다른 조회 화면에 동일 패턴이 있는지 점검하는 것. 같은 구조가 또 나오면 sentinel 폴백 로직을 공통 가드로 추출해서 한 군데서 관리하는 게 맞다. 지금은 거래 내역 조회 하나에 넣었지만, 여러 군데 흩어지면 나중에 같은 버그가 다른 화면에서 또 나온다. 권한 미스매치 0건 케이스를 별도 메트릭으로 분리하는 것도 같이 묶을 예정이다.

댓글 0

첫 댓글 달아줘.