개발 slecs

파트너 포탈에 정산·계좌 셀프 조회 화면 추가

목차

단순 조회 하나도 운영팀을 거쳐야 하는 구조는 언젠가 반드시 손봐야 한다. 파트너가 본인 정산 계좌를 확인하고 싶을 때 슬랙이나 이메일로 요청하고, 운영팀이 DB를 열어 답장하는 흐름. 그 자체가 비상식적인 건 아닌데, 이런 루틴이 쌓이면 운영 인력이 실질적으로 소진됨. 요청 받고 확인하고 답하는 왕복 시간이 하루에 몇 건씩 붙으면 무시하기 어렵다.

파트너 입장에서도 불편한 구조다. 셀프서비스 채널이 없으면 정보를 확인하려다 포기하거나, 확인 없이 추측으로 처리하는 경우가 생긴다. 정산 계좌가 맞게 등록됐는지, 담당자 연락처가 최신인지를 스스로 확인할 수 없는 건 운영 쪽에도 리스크다. 잘못 등록된 계좌를 파트너가 모르고 있다가 정산일에 문제가 터지면 수습이 복잡해진다.

이번에 만든 화면은 계정 기본 정보, 정산 계좌, 담당자 연락처 세 덩어리를 읽기 전용 카드로 보여주는 구조다. 사이드바에 "내 정보" 진입점을 달고, 파트너 등급별 노출 분기까지 포함해서 컨트롤러, 쿼리, 뷰 세 계층 모두 변경이 들어갔다.

코드 작업 전에 정리해야 할 게 더 많았다. 수정 화면까지 같이 만들 건지, 어떤 항목을 보여줄 건지, 마스킹 범위와 위치는 어떻게 할 건지. 이 세 가지 합의가 끝난 다음에야 코드가 시작됐다. 수정은 이번 범위에서 뺐다. 정산 계좌 변경에는 추가 인증 단계가 필수라서, 같이 만들려 했으면 작업 범위가 세 배 이상 불어났을 거다.

마스킹을 뷰가 아닌 서비스 레이어에서 하는 이유

정산 계좌·사업자번호·담당자 연락처를 화면에 그대로 노출하는 건 꺼려졌다. 보안 이슈라기보다, 화면에 전체 값이 뜨면 캡처 한 장으로 정보가 유출되는 경로가 생긴다는 게 마음에 걸렸다. 파트너 포탈은 외부에 열린 시스템이고, 화면을 누가 볼지 모른다.

계좌번호:      1234-****-7890
사업자번호:    123-**-*****
담당자 연락처: 010-****-1234

마스킹 위치 후보는 세 군데였다: DB 쿼리, 서비스 레이어, 뷰.

DB에서 가공하는 건 가장 먼저 제외했다. 표시 로직이 쿼리에 박히면 동일 데이터를 다른 용도로 쓸 때 마스킹을 풀 방법이 없고, 단위 테스트도 어렵다. 뷰에서 마스킹하는 안은 구현이 빠르고 정책이 바뀌면 뷰만 고치면 된다는 장점이 있다. 그런데 그 방식은 컨트롤러나 뷰모델이 raw 값을 그대로 들고 있게 된다. 같은 파트너 객체를 다른 화면이나 API 응답에서 재사용할 때 마스킹이 누락된 채 나갈 수 있다. "뷰에서만 가린다"는 보장은 코드가 늘어날수록 깨지기 쉽다.

서비스 레이어에서 가공된 값을 DTO에 담아 내려보내고, 뷰는 받은 대로 출력만 하는 구조로 갔다. 마스킹 책임이 서비스 한 곳에만 있으면 나중에 정책이 바뀌어도 수정 지점이 하나다. 컨트롤러와 뷰는 raw 값을 볼 일이 없다. 같은 파트너 데이터를 다른 화면에서 재사용하더라도 서비스를 통하는 한 마스킹이 일관되게 적용된다.

계층 역할 변경 내용
사이드바 진입점 "내 정보" 메뉴, 파트너 등급별 노출 분기
컨트롤러 라우팅·인증 세션 식별자 검증, 불일치 시 403
서비스 데이터 가공 마스킹 처리 후 DTO 구성
쿼리 데이터 조회 파트너 + 정산 + 연락처 단일 조인
표시 읽기 전용 카드, 수정은 별도 화면으로 분리

권한 분기와 쿼리 별칭 정리

조회 화면이라 권한 처리가 단순할 거라 생각했는데 착각이었다. 파트너 계층 구조가 있어서 "상위 파트너가 하위 파트너 정보를 볼 수 있냐"는 질문이 바로 나왔다.

요구사항에서 명시적으로 분리했다. "내 정보"는 본인 파트너만 본인 데이터를 본다. 상위 계층이어도 하위 파트너 정보는 열람 불가. 상위에서 하위를 보는 기능은 별도 "파트너 관리" 메뉴 담당이고, "내 정보"와 교차하지 않는다는 원칙을 처음부터 깔았다. 이 원칙이 있어서 컨트롤러 처리가 단순하게 정리됐다.

  • 세션의 파트너 식별자와 요청 식별자가 다르면 403
  • 같으면 조회, 예외 없음

케이스를 늘리지 않았기 때문에 나중에 헷갈릴 여지가 없다. 권한 범위를 늦게 합의했으면 쿼리를 먼저 짜놓고 나중에 뜯어고치는 상황이 됐을 거다. "이 화면이 보는 주체는 누구인가"를 기획 단계에서 확정해두는 게 얼마나 중요한지 다시 확인했다.

쿼리는 단일 조인으로 세 덩어리를 한 번에 끌어왔다. 파트너 조회, 정산 조회, 연락처 조회를 따로 날리면 왕복이 세 번이고, 읽기 전용 화면에서 그럴 이유가 없었다. 그런데 정산 정보가 별도 테이블 구조라 조인 깊이가 예상보다 커졌고, 각 테이블에서 비슷한 이름의 컬럼이 겹치면서 별칭 충돌이 생겼다. 규칙 없이 별칭을 붙이다 보면 쿼리가 길어질수록 어느 값이 어느 테이블에서 온 건지 추적하기 어려워진다.

테이블마다 별칭 prefix를 고정하는 방식으로 정리했다.

-- p_ : 파트너  /  b_ : 정산  /  c_ : 연락처
SELECT
  p.id        AS p_id,
  p.name      AS p_name,
  b.account   AS b_account,
  b.bank_code AS b_bank_code,
  c.name      AS c_name,
  c.phone     AS c_phone
FROM partners p
LEFT JOIN billing_accounts b ON b.partner_id = p.id
LEFT JOIN contacts c         ON c.partner_id = p.id
WHERE p.id = :partnerId

prefix 규칙 하나만 있어도 DTO 매핑이 읽기 편해지고, 컬럼을 추가할 때 어느 테이블 소속인지 혼동이 없다. 별거 아닌 것 같아도 조인이 깊어질수록 효과가 크다.

회고

이번 작업에서 시간이 많이 간 건 코드보다 정책 합의였다. 권한 범위, 마스킹 위치, 수정 화면 분리 여부. 세 가지 결정이 끝나고 나서야 코드 작업이 수월해졌다. 기술 결정보다 정책 결정이 오래 걸리는 건 이런 작업마다 반복되는 패턴인데, 일찍 논의를 열어야 코드 시작이 늦지 않는다. 사이드바에 메뉴 하나 다는 작업이 단순해 보여도, 뒤에 붙는 권한 분기와 데이터 가공 정책이 코드보다 더 오래 걸린다는 걸 기획 단계에서 미리 보여주는 것도 팀장 역할이다.

읽기 전용으로 먼저 낸 건 잘한 선택이었음. 수정까지 한 번에 가려 했으면 정산 계좌 변경 인증, 감사 로그, 유효성 검증이 한꺼번에 딸려와서 일정이 터졌을 거다. "지금 필요한 것만"이라는 기준이 범위를 지켰다.

진작 만들었어야 한다는 생각도 든다. 단순 조회 문의 하나는 작아도 누적되면 의미 있는 시간이다. 셀프서비스 채널은 운영팀 부하를 줄이면서 파트너 경험도 같이 개선하는 작업이라, 우선순위를 더 일찍 올렸으면 좋았을 것 같다.

다음 단계는 수정 화면 분리, 정산 계좌 변경 시 추가 인증, 변경 이력 감사 로그. 사이드바 권한 매트릭스 문서화도 이번에 같이 해두지 않으면 다음 메뉴 붙일 때 같은 논의가 처음부터 반복될 게 뻔하다.

댓글 0

첫 댓글 달아줘.