문제
엑셀 파일로 장비 수천 행을 조직에 일괄 배정하는 기능:
- 사용자가 하는 일 — 시트에 장비번호와 조직명 기입 후 업로드
- 서버가 하는 일 — 조직명으로 조직을 찾아 매핑
- 문제 지점 — 조직명이 유일하지 않음
조직 트리에서 서로 다른 부모 아래 동명의 하위 조직 존재 가능. 사용자가 "1지점"으로 기입했으나 DB에는 1지점이 셋.
- 규모 — 수천 행짜리 업로드
- 모호한 행 — 몇 십 개 섞여 있음
나쁜 선택지 둘
흔히 하는 처리 둘 — 양쪽 다 나쁨:
전부 실패시키기
- 동작 — “조직명 ‘1지점’이 모호합니다”라며 업로드 거부
- 장점 — 안전
- 사용자 입장 — 3,000행을 올렸다가 40행 때문에 전부 거부
- 회피 방법 — 마땅찮음. 사용자가 조직 트리 구조를 알아야 하고, 조직 ID 직접 기입은 엑셀 업로드 취지에 위배
- 실무 결과 — 결국 이 기능을 안 쓰게 됨
조용히 하나 고르기
- 동작 — 후보 중 첫 번째나 ID가 작은 것 자동 선택
- 즉시 결과 — 업로드 성공, 화면에 초록불
- 실제 결과 — 40개 장비가 틀린 조직에 배정, 아무도 모름
-
파급 — 조직 배정은 권한 범위와 연결. 몇 달 뒤 “왜 이 장비가 여기 보이지”로 회귀
- 전자의 산물: 쓸 수 없는 기능
- 후자의 산물: 조용히 틀린 데이터
셋째 선택지: 되묻기
그래서 만든 것 — 파이프라인:
파싱 → 검증 → (모호하면 해소 모달) → 재검증 → 실행
각 단계의 역할:
- 파싱 — 엑셀을 읽어 행 DTO 목록으로 변환. 아직 DB 미조회
- 검증 — 각 행의 조직명을 DB와 대조. 아무것도 쓰지 않고 결과만 생성
-
해소 모달 — 모호한 조직명이 있으면 화면 표시.
findAllOrgCandidatesByNames로 해당 이름들의 후보를 한 번에 수집, 이름별로 사용자가 선택 - 재검증 — 사용자 선택을 받아 처음부터 다시 검증
- 실행 — 최종 확인 후 쓰기
후보 조회 시 상위 경로 동반 제공이 중요. 1지점 셋 중 고르라고만 하면 구분 불가.
사용자 선택의 전달 형태:
- 타입 —
Map<String, Long>형태의orgIdResolutions - 의미 — 조직명 → 사용자가 고른 조직 ID
- 질문 단위 — 행마다가 아니라 이름마다 한 번
- 예 — 3,000행에 모호한 이름 5종류면 질문 5번
// orgIdResolutions: 중복 조직명 → 사용자가 선택한 org_id. 비어 있으면 기존 동작과 동일.
주석의 마지막 문장이 설계 포인트:
-
orgIdResolutions가 비어 있을 때 — 기존 경로와 완전히 동일하게 동작 - 모호한 이름이 없는 업로드 — 모달을 아예 만나지 않고 코드 경로도 예전 그대로
- 구조 — 새 기능이 기존 흐름에 얹히는 게 아니라 옵션으로 부착
재검증을 왜 별도 단계로 두나
“이미 검증했는데 선택만 받으면 되지 않나”라고 생각하기 쉽다. 그렇지 않다.
이유 셋:
첫째, 신뢰 경계
-
orgIdResolutions의 출처 — 브라우저 - 후보 중 하나를 골랐다는 보장의 주체 — 클라이언트 코드. 즉 보장이 아님
- 서버가 할 일 — 그 org_id가 실제로 그 이름의 유효한 후보였는지 재확인
- 구현 — 해소값을 후보와 대조하는 코드 포함
둘째, 다른 검증이 되살아남
- 조직이 정해져야 판단 가능한 규칙 — 그 조직에 그 장비를 넣을 권한이 있는지, 이미 다른 조직에 배정돼 있지는 않은지
- 1차 검증 시점 — 조직 미정이라 이 규칙들 건너뜀
- 따라서 조직 확정 후 재실행 필요
셋째, 시간이 흐름
- 사용자가 모달에서 고민하는 동안 다른 관리자가 조직을 삭제했을 가능성
- 빈도 — 흔하지 않음
-
재검증이 있으면 공짜로 막힘
- 검증 로직을 재활용 가능하게 작성해두면 재검증은 동일 함수 재호출에 불과
- 비용: 거의 없음
검증 결과를 3단계로
결과가 OK/실패 두 값이면 표현력 부족. 세 단계로 분할:
| 상태 | 의미 | 처리 |
|---|---|---|
| OK | 문제 없음 | 그대로 실행 |
| WARNING | 실행되지만 사용자가 알아야 함 | 표시하고 진행 |
| SKIP | 이 행은 처리하지 않음 | 건너뛰고 나머지 진행 |
핵심은 SKIP이 전체 실패가 아니라는 것이다.
- 처리 불가 행 몇 개가 나머지 2,960행을 막지 않음
- 사용자 동선 — 결과 화면에서 어느 행이 왜 빠졌는지 확인 후 그것만 고쳐 재업로드
WARNING을 따로 둔 이유 — “되긴 되는데 의도한 게 맞나 싶은” 경우가 실제로 다수:
- OK로 뭉갤 때: 사용자가 인지 불가
- SKIP으로 뭉갤 때: 처리돼야 할 것이 미처리
매칭 키 폴백
관련해서 별도 커밋에는 매칭 키를 단계적으로 낮추는 인덱싱:
- 순서 — 3키 복합 → 2키 복합 → 단독
- 동작 — 가장 구체적인 조합으로 먼저 조회, 없으면 키를 하나 떼고 재조회, 그래도 없으면 단일 키
- 목적 — 사용자가 엑셀에 정보를 얼마나 채웠든 최선을 다해 매칭하되, 더 구체적인 정보가 있으면 그쪽 우선
이것도 같은 철학. 입력이 불완전하다고 바로 포기하지 않는다.
- 단, 포기하지 않는 것과 아무거나 고르는 것은 별개
- 폴백의 하한: 후보가 유일해지는 지점까지
- 그래도 여럿이면: 그 시점에 사용자에게 질의
남는 교훈
“전부 실패”와 “조용히 추측” 사이에 되묻기가 있다. 그리고 되묻기는 대체로 구현이 더 비싸다.
- 추가 비용: 상태 왕복 + 검증 2회 실행 + 모달 UI
- 양 극단을 고르게 되는 동기: 이 비용 회피
- 판단: 데이터 정합성이 걸린 기능에서는 낼 만한 비용
질문은 행 단위가 아니라 원인 단위로 묶어야 한다.
- 3,000행에 3,000번 질의 → 아무도 사용 안 함
- 모호한 이름 5개에 5번 질의 → 30초 소요
- 요구하는 정보는 동일, 사용자 경험은 완전히 상이
모호성 해소는 반드시 서버에서 다시 검증해야 한다.
- 선택지 제공의 의미: 클라이언트가 값을 생성해 전송 → 신뢰 불가
- 재검증 단계 누락 시: UX 개선 + 보안 악화