본문으로 바로가기

감사로그 — logAdminAction 패턴

관리자 기능을 갖춘 백엔드에서 "누가 · 언제 · 무엇을 · 왜" 의 네 축을 기록하는 감사로그 (audit log) 는 단순한 관행 이상입니다. 개인정보보호법 · GDPR 같은 규정 준수, 사고 조사, 권한 오남용 방지의 실질 수단.

6회 조회약 3분 읽기
X에 공유 새 창에서 열림
목차

관리자 기능을 갖춘 백엔드에서 "누가 · 언제 · 무엇을 · 왜" 의 네 축을 기록하는 감사로그 (audit log) 는 단순한 관행 이상입니다. 개인정보보호법 · GDPR 같은 규정 준수, 사고 조사, 권한 오남용 방지의 실질 수단.

1. 감사로그 가 답하는 네 질문

컬럼 예 의미
누가 user_id · user_email · ip_address 행위 주체. 내부 사용자 + (있다면) 외부 IP
언제 created_at TIMESTAMPTZ UTC 기준 1초 이상 해상도
무엇을 action · resource · resource_id DELETE + catalog.user + 12345 같은 평면 설명
details JSONB.reason 자유 문자열. 파괴적 · PII 관련은 30~100자 최소 강제

표준은 없으나 위 네 축을 모두 가지면 1 차 회귀 · 분쟁 대응은 가능.

2. 최소 스키마 (PostgreSQL)

CREATE TABLE IF NOT EXISTS audit_logs (
  id          BIGSERIAL PRIMARY KEY,
  user_id     UUID,                                    -- NULL 허용: 시스템·cron
  user_email  TEXT,
  action      VARCHAR(40) NOT NULL,
  resource    VARCHAR(80) NOT NULL,                    -- 'catalog.user' 같은 dot-namespaced
  resource_id TEXT,
  details     JSONB NOT NULL DEFAULT '{}'::jsonb,
  ip_address  INET,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_audit_logs_resource_created
  ON audit_logs (resource, created_at DESC);

CREATE INDEX IF NOT EXISTS idx_audit_logs_user_created
  ON audit_logs (user_id, created_at DESC);

resource 에 도메인 접두사 (catalog.user · content.post · profile.announcement) 를 두면 ILIKE 'catalog.%' 로 필터한 도메인별 감사 뷰가 편리해집니다.

3. 헬퍼 함수 — 위험도별 실패 정책

모든 작업에 같은 정책을 쓰지 않습니다. 조회·되돌릴 수 있는 일반 변경은 비동기 감사를 사용해 지연을 격리할 수 있습니다. 삭제·권한 변경·초기화는 실행 전 intent를 동기 저장하고, 저장 실패 시 본 작업을 시작하지 않습니다.

// audit.ts
export function logAdminAction(input: {
  action: string;
  resource: string;
  resourceId?: string;
  details?: Record<string, unknown>;
  request?: Request;
}): void {
  const run = async () => {
    const session = await resolveSession(input.request);       // 쿠키에서 user_id·email
    const ip = await resolveClientIp(input.request);
    await pool.query(
      `INSERT INTO audit_logs
         (user_id, user_email, action, resource, resource_id, details, ip_address)
       VALUES ($1,$2,$3,$4,$5,$6,$7)`,
      [session.userId, session.email, input.action, input.resource,
       input.resourceId ?? null, input.details ?? {}, ip]
    );
  };
  // intentional: 예외는 logger.error, 메인 요청은 계속
  run().catch((e) => logger.error('audit_log_failed', e));
}

request 미전달 시 Next.js App Router 의 cookies() fallback 으로 user_id 을 채우면 Server Action 에서도 NOT NULL (또는 nullable) 제약을 만족할 수 있습니다.

const audited = await logAdminActionDurable({
  action: 'DB_INIT_INTENT', resource: 'database', details: { mode }, request,
});
if (!audited) return { status: 503, mutationApplied: false };

결과 감사가 실패한 시점에는 작업이 이미 반영됐을 수 있습니다. 이 경우 일반 500이나 성공을 반환하지 말고 mutationApplied: true, auditFailed: true, 재실행 금지를 함께 알려 운영자가 중복 부작용을 피하게 합니다.

4. reason 을 강제하는 이유

개인정보 삭제 · 권한 변경 · 포인트 수동 가감 같이 사후 검증이 중요한 작업은 사유 필드를 30~100자 최소 강제합니다. 검증은 요청을 거절하는 쪽 (API 레이어) 에서, 저장은 details.reason JSONB 키.

if (destructive && (!reason || reason.length < 30)) {
  return NextResponse.json({ error: 'reason 30자+ 필요' }, { status: 400 });
}
logAdminAction({
  action: 'DELETE',
  resource: 'catalog.user',
  resourceId: String(userId),
  details: { reason, targetEmail },
});

빈 문자열 또는 "삭제" 같은 2자 사유는 사후 분석이 불가능하므로 API 레벨에서 reject 하는 편이 낫습니다.

5. 감사 로그 뷰어 UI

  • 필터: resource · action · user_email · date_range
  • 정렬: created_at DESC
  • 페이지네이션: offset · keyset 둘 다 OK (대량 trace 는 keyset 이 유리)
  • 도메인별 뷰: resource ILIKE 'catalog.%' 쿼리를 고정하고 /admin/catalog/audit-log 같은 서브 뷰로 분기

6. 자주 걸리는 자리

시스템 액션의 user_id NULL — cron · 백업 · webhook 은 사람 주체가 없습니다. user_id NULL 허용 + user_email = 'system@internal' 같은 상수로 구분.

감사 테이블 과성장 — 1 년치 쌓이면 수백만 행. 파티셔닝 (created_at 월별) 또는 별도 아카이브 테이블로 분리 검토.

JSONB details 의 스키마 없음 — 자유도는 장점이지만 필수 키 (reason · previous_value · new_value) 는 코드 측 헬퍼로 강제하는 편이 검색에 유리.

감사 로그 자체의 삭제 — "감사 기록 삭제 기능" 은 두지 않습니다. 개인정보 삭제 요청 등 법적 의무가 생기면 user_email = NULL 로 익명화하는 쪽.

actor 주입 fallbackrequest 객체가 항상 주어지는 건 아닙니다 (Server Action · scheduled job). 쿠키 기반 fallback 이 없으면 user_id NOT NULL 제약 위반 사고가 생기므로 fallback 테스트를 회귀로 고정.

7. 운영 체크리스트

  • 모든 mutation API · Server Action 에 logAdminAction 호출
  • 파괴적 · PII 관련 작업은 reason 30자+ 강제
  • resource 는 도메인 접두사 규칙 준수
  • 일반 변경은 감사 장애를 격리하고 고위험 작업은 intent 실패 시 시작하지 않음
  • 결과 감사 실패는 적용 가능성과 재실행 금지를 별도 상태로 표시
  • 감사 로그 뷰어에 권한 (관리자 전용) 적용
  • 정기 아카이브 · 파티셔닝 계획

하고픈 말

감사로그는 사고 났을 때만 쓰는 기록이 아니라, 평소에 "이 사용자가 어제 이 포인트 왜 받았지?" 같은 질문의 1 차 답입니다. 사유 필드가 없으면 감사가 "언제 무엇을" 까지만 답하고 "왜" 는 영영 사라지니, 설계 초기부터 reason 을 필수 컬럼 취급하는 쪽이 유리합니다.

Next

  • api-handler-pattern
  • security/01-jwt-rotation

OWASP Audit Log Cheat Sheet · PostgreSQL JSONB · 개인정보보호위원회 — 개인정보 안전성 확보조치 기준 를 참고합니다.

이 글에서 만나는 용어

backend 카테고리의 다른 글

카테고리 전체 보기 →

이 글이 도움이 되었나요?