어드민 쓰기 액션 전반에 감사 로그 확장
목차
어드민 영역 전반의 쓰기 작업에 감사 로그를 확장한 작업이다.
왜 지금이었나
감사 로그(audit log)는 보안 관점에서 "있으면 좋은 것"이 아니라 "없으면 나중에 반드시 후회하는 것"이다. 그런데 실제로 시스템을 운영하다 보면 초기엔 몇 가지 핵심 액션에만 붙이고, 나머지는 "나중에"로 미루게 된다. 이번 커밋은 그 "나중에"를 실행에 옮긴 케이스다.
트리거가 된 건 어드민 쪽에서 데이터가 변경됐는데 누가 언제 바꿨는지 추적이 안 됐던 상황이었다. 어드민 액션은 일반 사용자 액션보다 파급력이 훨씬 크다. 사용자 한 명의 상태를 바꾸는 건 그 사람에게만 영향이 가지만, 어드민이 통합(integration) 설정을 변경하거나 삭제하면 연결된 서비스 전체에 파문이 생긴다. 그 파문의 시작점을 기록하지 않는다는 건 사실 꽤 큰 맹점이었다.
변경된 파일들이 의미하는 것
src/app/admin/(protected)/layout.tsx
src/app/api/admin/insightflow/users/[id]/route.ts
src/app/api/admin/integrations/[id]/route.ts
src/app/api/admin/integrations/route.ts
src/components/layout/HeaderClient.tsx
src/components/layout/Sidebar.tsx
파일 목록을 보면 이번 작업의 범위가 꽤 넓다는 걸 바로 알 수 있다.
| 파일 | 역할 | 감사 로그 맥락 |
|---|---|---|
admin/(protected)/layout.tsx |
어드민 보호 라우트 공통 레이아웃 | 인증된 어드민 세션 컨텍스트 주입 지점 |
users/[id]/route.ts |
특정 사용자 읽기/쓰기 API | 사용자 정보 변경 액션 로깅 |
integrations/[id]/route.ts |
단건 통합 수정/삭제 | PATCH, DELETE 액션 로깅 |
integrations/route.ts |
통합 목록/생성 | POST 액션 로깅 |
HeaderClient.tsx / Sidebar.tsx |
어드민 UI 레이아웃 컴포넌트 | 현재 세션 정보 표시 or 컨텍스트 전달 관련 |
layout.tsx와 UI 컴포넌트(HeaderClient, Sidebar)까지 함께 변경된 게 눈에 띈다. 단순히 API 레이어에만 로그를 붙인 게 아니라, 어드민 레이아웃 수준에서 세션/사용자 컨텍스트를 더 명확하게 내려주는 작업도 병행한 것으로 보인다. 감사 로그에서 "누가"를 정확히 기록하려면 API route 핸들러에서 현재 어드민 세션 정보를 신뢰할 수 있게 읽을 수 있어야 한다. 그 기반이 layout 레벨에서 정리된 셈이다.
감사 로그 설계에서 놓치기 쉬운 것들
감사 로그를 "모든 쓰기 액션"으로 확장할 때, 단순히 console.log나 DB insert 한 줄 추가하는 게 아니라 아래 항목들을 같이 고민해야 한다.
- 무엇을 기록할 것인가: 액션 타입(
CREATE,UPDATE,DELETE), 대상 리소스 ID, 변경 전/후 값(diff), 요청자 ID, 타임스탬프, IP - 실패한 액션도 기록할 것인가: 성공한 것만 기록하면 보안 감사 목적을 절반밖에 못 채운다. 권한 없는 접근 시도도 남겨야 한다.
- 로그 저장 실패가 원래 작업을 막아야 하는가: 일반적으로 어드민 쓰기의 경우 로그 저장 실패가 작업 자체를 롤백시키도록 트랜잭션으로 묶는 게 더 안전하다. 단, 이 선택은 트레이드오프가 있다.
- 레이아웃/미들웨어 레벨 vs API 레벨: 공통 처리를 상위에 두면 누락 위험이 줄지만, 각 API별로 로그 메시지의 세부 맥락을 담기 어렵다.
// 감사 로그 공통 패턴 예시
async function withAuditLog<T>(
ctx: { adminId: string; action: string; resourceId: string },
fn: () => Promise<T>
): Promise<T> {
const result = await fn();
await db.auditLog.create({
data: {
adminId: ctx.adminId,
action: ctx.action,
resourceId: ctx.resourceId,
timestamp: new Date(),
},
});
return result;
}
이런 wrapper 패턴을 쓰면 각 route handler에서 감사 로그 로직이 반복되는 걸 막고, "쓰기 액션 = 감사 로그"라는 규칙을 코드 레벨에서 강제할 수 있다.
팀 관점 회고
이 작업을 진행하면서 팀원들과 나눈 이야기 중 하나가 "로그 커버리지를 어디까지 확장할 것인가"였다. 읽기(read) 액션까지 다 남기면 스토리지 비용과 노이즈가 커지고, 쓰기(write)만 남기면 민감한 데이터 조회 추적이 빠진다. 결론은 우선 모든 쓰기 액션을 완전히 커버하는 것을 1차 목표로 잡고, 민감 데이터 조회 로깅은 별도 정책으로 가져가는 방향이었다.
어드민 기능은 팀 내에서 보통 "내부용이니까 대충"이라는 인식이 생기기 쉬운데, 실제로 운영 이슈가 터지면 제일 먼저 어드민 로그를 파게 된다. 그때 로그가 없으면 추적 자체가 불가능해진다. 이번 확장이 그 공백을 메운 작업이었고, 나중에 반드시 "이때 해둬서 다행"이라고 할 시점이 올 거라 본다.
다음
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.