문제
기존 권한 모델:
-
user_level정수 하나 - 숫자가 클수록 많은 권한
한계:
- “조회는 되는데 삭제는 안 되고, 다른 화면에서는 승인 권한이 있다” 같은 요구 → 정수 하나로 표현 불가
- 새 권한이 생길 때마다 숫자 구간 재해석 필요
-
그 해석이 코드 여기저기에 분산
-
결정: 권한 코드 기반 모델로 전면 교체
-
PERM_<리소스>_<행위>형태의 권한 코드 정의 - 역할에 코드 매핑
- 사용자에게 역할 부여
캐싱은 쉬웠다
- 새 모델의
hasPermission(code)— 사용자의 권한 코드 집합에 해당 코드가 있는지 조회 - 호출 지점이 매우 많음
- 컨트롤러의
@PreAuthorize - 인터셉터의 페이지 접근 체크
- JSP의 버튼 노출 여부
- 컨트롤러의
- 요청 하나에 수십 번 호출 → 매번 DB 조회 불가
저장소 선택:
- 권한은 요청 중에 불변, 사용자별 고정 → 세션이 자연스러운 저장소
- 로그인 시 권한 코드 전량을
HttpSession에Set<String>으로 적재 - 이후 조회는 세션에서 수행
// AuthorizationService: hasPermission / getCurrentUserPermissions 세션 캐시 적용
- 여기까지 소요: 30분
- 진짜 난제는 그 다음
어려운 건 무효화 지점을 빠짐없이 찾는 것이었다
캐시 도입 즉시 발생하는 질문 — 권한이 바뀌면 그 세션의 캐시는 어떻게 되나?
- 관리자가 권한 회수 → 사용자 세션에 옛 권한 집합 잔존 → 회수 미반영
-
화면상으로는 회수 표시, 실제로는 계속 사용 가능
- 권한 시스템에서의 성격: 단순 버그가 아니라 보안 결함
무효화가 필요한 지점:
| 사건 | 영향 |
|---|---|
| 권한 부여 | 해당 사용자 캐시 무효화 |
| 권한 회수 | 해당 사용자 캐시 무효화 |
| 역할 변경 | 해당 사용자 캐시 무효화 |
| 역할에 매핑된 권한 코드 변경 | 그 역할을 가진 모든 사용자 |
| 권한 만료 | 만료된 사용자 전부, 그것도 시간이 지나면 자동으로 |
- 앞의 셋 — 명시적 호출로 처리. 권한 부여·회수 서비스에 캐시 무효화 메서드 호출 추가
- 네 번째 — 영향받는 사용자를 조회해야 판별 가능
- 다섯 번째 — 아무도 아무것도 하지 않았는데 권한이 바뀌는 경우
만료 처리:
-
PermissionExpiryScheduler가 매일 새벽 2시에 만료된 권한 비활성화 - 새벽 2시 선택 이유 — 사용자가 적어 그 시점에 세션이 남아 있을 가능성이 낮음
-
한계 — 만료 시각과 실제 비활성화 사이에 최대 하루의 창
- 정확한 해법은 아님
- 수용 근거: 권한 만료가 분 단위 정확도를 요구하는 요건이 아니라는 판단
캐시를 넣는 순간 “권한이 바뀌는 모든 경로”를 완전히 열거해야 하는 숙제가 생긴다. 그리고 그 목록이 완전한지는 증명하기 어렵다. 이게 캐싱의 실제 비용이다 — 코드 몇 줄이 아니라, 시스템 전체에서 특정 상태를 바꾸는 지점을 전부 안다는 주장.
접근 제어를 두 층으로 나눴다
| 층 | 담당 | 방식 |
|---|---|---|
| 페이지 접근 | MenuAccessInterceptor |
URL을 MENU_* 코드로 매핑, 메뉴 권한 없으면 진입 차단 |
| API 접근 | 컨트롤러 @PreAuthorize
|
고객 관리·이미지·이상 감지·장애 분석·알림 등 전면 적용 |
둘 다 필요한 이유:
- 인터셉터만 있으면 → 화면을 안 거치고 API를 직접 호출하는 경로가 열림
-
@PreAuthorize만 있으면 → 권한 없는 사용자가 빈 화면을 보게 됨. 나쁜 UX - 역할 분담: 화면 진입은 인터셉터, 실제 데이터는 애노테이션
스트랭글러 패턴: 4일간의 공존
- 전제: 새 시스템 구축이 곧 옛 시스템 소멸은 아님
2026-03-30 feat: 권한 관리 시스템 전면 개편 (Phase 0~3 + 마이그레이션 3건)
...4일간 신구 시스템 공존...
2026-04-03 refactor: 권한 기반 API 보호 및 레거시 PermissionService 제거
3월 30일 커밋 — 새 시스템 수립:
- 마이그레이션 1: 버튼 권한 코드 44개와 역할별 기본 매핑
- 마이그레이션 2: UI 힌트 컬럼(설명·아이콘·영향 영역)
-
마이그레이션 3: 카테고리 값 정리
- 이 시점에 옛
user_level기반PermissionService는 그대로 생존 — 삭제하지 않음 - 이유: 새 시스템이 실제 화면들에서 제대로 도는지 확인하는 동안 옛 경로가 안전망
4일 뒤 4월 3일 커밋이 마감:
- 레거시
PermissionService·PermissionDao·매퍼 XML·관련 DTO 전량 삭제 - 삭제 규모 — 매퍼 XML 196줄, 서비스 121줄, 합계 453줄
- 같은 커밋에 미보호 엔드포인트
@PreAuthorize마감 - 권한 부여·회수 이력 조회 API 추가
이 4일이 이 작업에서 제일 중요한 부분이다.
- 신구를 한 커밋에 교체했을 경우: 문제 발생 시 되돌릴 지점 부재
- 공존 기간 확보 시: 새 시스템 오류 발생해도 옛 코드가 저장소에 잔존 → 롤백은 커밋 1개
동시에 이 기간을 길게 끌면 안 된다.
- 두 권한 시스템 공존 중에는 “어느 쪽이 진짜인가”가 모호
- 그 모호함 자체가 보안 위험
- 4일 = 검증에 충분하고 잊어버리기에는 짧은 길이
남는 교훈
캐싱의 난이도는 캐시를 채우는 쪽이 아니라 비우는 쪽에 있다. 세션에 Set<String>을 넣는 건 몇 줄이다. 그 집합이 언제 틀려지는지를 전부 아는 것이 실제 작업이다.
권한 캐시는 특히 틀린 방향이 정해져 있다.
- 캐시가 오래돼서 권한이 실제보다 적게 보임 → 사용자 불편
-
많게 보임 → 보안 사고
- 따라서 회수 계열 경로의 무효화가 부여 계열보다 훨씬 중요
레거시 제거 커밋을 별도로 남기면 작업이 끝났다는 표시가 된다. 453줄 삭제 커밋이 없었다면 옛 PermissionService는 “혹시 쓰는 데가 있을까 봐” 몇 달을 더 살아남았을 것이다. 교체 작업의 완료 조건은 새 코드가 도는 게 아니라 옛 코드가 없어지는 것이다.