4개 도메인 병렬 분석 결과 + 통합 요약 + 시나리오 중심 정리.
총 25개 버그 (CRITICAL 3 / HIGH 11 / MEDIUM 7 / LOW 4) 식별.
실제 수정은 별도 커밋 (commit 68c1cb5b).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.1 KiB
부서관리 SQL/매퍼 버그 헌팅 (2026-05-08)
대상: backend-spring/src/main/resources/mapper/department.xml
❌ 1. updateDepartment WHERE 절에 DELETED_AT IS NULL 누락 — HIGH
department.xml:160-179
<update id="updateDepartment" parameterType="map">
UPDATE DEPT_INFO SET ... WHERE DEPT_CODE = #{dept_code}
</update>
소프트 삭제된 부서도 update 가 통과해버림. 컨트롤러는 getDepartmentIncludingDeleted 로 검증한 뒤 호출하므로 deleted 부서에 대해서도 흐름이 진행됨. 그 후 service 의 재조회 (selectDepartmentByCode — DELETED_AT IS NULL) 가 null 반환 → controller 가 404 응답. DB 는 이미 update 되었는데 사용자는 404 받음 → silent corruption.
→ WHERE DEPT_CODE = #{dept_code} AND DELETED_AT IS NULL 추가 필수. update 0 row 면 service 가 null 반환하여 404 도 일관됨.
⚠️ 2. selectDepartments — 글로벌 부서 모든 회사에 노출 — MEDIUM
department.xml:29 (D.COMPANY_CODE = #{company_code} OR D.COMPANY_CODE = '*')
글로벌 (*) 부서가 모든 회사 트리에 자식으로 섞여 표시됨. PARENT_DEPT_CODE 일치까지 고려하면 어느 부모 아래로 들어갈지 비결정적. 의도된 사양인지 확인 필요. INVYONE 은 DB-per-tenant 인데 글로벌 부서가 동일 DB 에 들어가는 경우 — 멀티 테넌시 모델과 충돌.
✅ 3. selectChildDeptCount — 회사 격리 — OK (DB-per-tenant)
department.xml:182-189 — WHERE PARENT_DEPT_CODE = #{dept_code} 만 있어 회사 필터 없음. 단 INVYONE 은 DB-per-tenant 라 같은 DB 안에 다른 회사 부서가 거의 없음 (글로벌 * 제외). 실제 위험은 낮음.
✅ 4. selectNextDeptNumber 회사 필터 없음 — OK (DB-per-tenant)
department.xml:108-112 — 전역 시퀀스. DB-per-tenant 라 회사별 충돌은 발생 안 함. 단 글로벌 (*) 부서까지 같은 시퀀스 공유는 의도된 듯.
⚠️ 5. #{date}::date cast — MEDIUM
department.xml:150-151, 173-174 — null 또는 valid date 형식이면 OK. 잘못된 형식 ("2026/05/08", "abc") 이면 SQLException → 500. service 가 nullIfBlank 만 처리하지 형식 검증 안 함. controller 도 안 함. 사용자가 비정상 날짜 입력 시 5xx 누출.
✅ 6. selectDuplicateDeptName 정규화 — OK
department.xml:91-97 — TRIM(LOWER(...)) 양쪽 적용. 케이스/공백 정규화 OK. 단 update 흐름에서 호출 안 됨 (backend 리포트 #3 참조).
⚠️ 7. selectDeptMembers INNER JOIN — LOW/MEDIUM
department.xml:213-230 — JOIN USER_INFO U ON UD.USER_ID = U.USER_ID (INNER). 사용자가 USER_INFO 에서 삭제되었지만 USER_DEPT row 가 남으면 orphan 멤버 안 보임. member_count (selectDepartments) 는 USER_DEPT 만 카운트하므로 카운트와 실제 표시되는 멤버 수가 불일치 가능.
❌ 8. searchUsers ILIKE 와일드카드 미이스케이프 — MEDIUM
department.xml:233-248, service 의 params.put("search", "%" + search + "%"). 사용자 입력의 %/_ 가 와일드카드로 처리됨. _ 단독 검색 시 모든 사용자 매칭. SQL injection 은 parameterized 라 안전하지만 enumeration 가능. ESCAPE 절 또는 사전 escaping 권장.
⚠️ 9. deleteUserDeptByDeptCode dead query — LOW
department.xml:192-195 — 주석에 "Slice 2.1 이후 사용 안 함" 명시. 안 쓰는 쿼리 잔존. 정리 권장.
✅ 10. selectCompanyName LIMIT 1 — OK (PK)
department.xml:100-105 — LIMIT 1 만 있고 ORDER BY 없음. company_code 가 PK 면 단일 행만 매칭되므로 OK. PK 가 아니면 비결정적.
⚠️ 11. insertDeptMember UNIQUE 제약 의존 — MEDIUM
department.xml:283-286 — INSERT 만. service 가 selectExistingMember 로 사전 체크하지만 race 시 두 트랜잭션이 동시 통과 → 두 INSERT. USER_DEPT 에 (USER_ID, DEPT_CODE) UNIQUE 제약이 있어야 race 방어. mapper 외부 스키마 확인 필요 — 없으면 중복 row 가능.
⚠️ 12. selectFirstUserDept 비결정적 — LOW
department.xml:266-272 — ORDER BY CREATED_DATE ASC LIMIT 1. 동시 INSERT 시 동일 NOW() 다중 row 가능 → 비결정적 첫 row. primary 자동 승격 시 어떤 부서가 선택될지 예측 불가. 보조 정렬 키 (예: DEPT_CODE) 추가 권장.
⚠️ 13. selectDepartments member_count 와 includeDeleted — LOW
department.xml:7-39 — LEFT JOIN USER_DEPT UD. USER_DEPT 는 soft-delete 컬럼 없음. 부서가 deleted 여도 USER_DEPT row 는 보존되므로 (멤버 보존 정책) member_count 는 그대로. 휴지통 표시 시 의미 있는 카운트인지 UI 정책 확인.
⚠️ 14. GROUP BY 에 PARENT_DEPT_CODE 포함되었으나 필요한가 — LOW
department.xml:33-37 — GROUP BY 에 모든 SELECT 컬럼 나열 (PostgreSQL 요구). MEMBER_COUNT 만 집계라 OK. 단 컬럼 추가 시 GROUP BY 도 같이 늘려야 하는 운영 부담. 향후 컬럼 추가 시 누락 위험.
⚠️ 15. dept_name UNIQUE 제약 없음 — HIGH (스키마 종속)
mapper 만으로는 확정 불가하지만, selectDuplicateDeptName 를 SQL 레벨로 보강하는 UNIQUE 제약 (예: (COMPANY_CODE, LOWER(TRIM(DEPT_NAME))) WHERE DELETED_AT IS NULL) 이 없으면 race 시 중복 부서명 공존. backend 리포트 추가 발견 A 와 동일 결함 — DB 스키마 차원의 방어 필요.
우선순위
| 등급 | 항목 |
|---|---|
| HIGH | #1 update 의 DELETED_AT 누락 (silent corruption) / #15 dept_name UNIQUE 부재 |
| MEDIUM | #2 글로벌 부서 회사 트리 혼입 / #5 date cast 5xx / #8 LIKE 와일드카드 / #11 USER_DEPT UNIQUE |
| LOW | #7 INNER JOIN orphan / #9 dead query / #12 first-dept 비결정성 / #13-14 |
| OK | #3 #4 #6 #10 |
즉시 수정 권장
updateDepartmentWHERE 절에AND DELETED_AT IS NULL추가- USER_DEPT (USER_ID, DEPT_CODE) UNIQUE 제약 확인 / 추가
- DEPT_INFO (COMPANY_CODE, dept_name) partial UNIQUE 인덱스 (DELETED_AT IS NULL) 추가
- searchUsers 입력값에 LIKE escaping 적용
- service 또는 controller 에서 date 형식 검증 → 400 반환