17 KiB
Audit Columns 기준
1. 목적
이 문서는 PostgreSQL 테이블에서 생성 시각, 수정 시각, 생성 주체, 수정 주체 같은 감사 컬럼을 어떤 기준으로 두고 채울지 정의한다.
이 문서의 목표는 다음과 같다.
- 감사 컬럼을 테이블마다 제멋대로 두지 않고 공통 규칙으로 표준화한다
- 시간 컬럼과 주체 컬럼의 의미를 분리한다
- DB 기본값, trigger, Spring Data JPA auditing, Hibernate timestamp 기능 중 무엇을 어디까지 맡길지 정한다
- soft delete, 전체 변경 이력(audit log), version/concurrency 컬럼과 역할을 섞지 않는다
PostgreSQL은 컬럼 기본값으로 CURRENT_TIMESTAMP 같은 표현식을 둘 수 있고, 그 식은 row 삽입 시 평가된다고 설명한다. Spring Data JPA는 @CreatedDate, @LastModifiedDate, @CreatedBy, @LastModifiedBy를 제공하고, Hibernate는 @CreationTimestamp, @UpdateTimestamp를 제공한다. 즉 시간과 주체를 채우는 기술 수단은 여러 개가 있지만, 어떤 컬럼을 왜 두는지는 프로젝트 표준이 먼저 정해야 한다.
2. 근거 수준
- Official: PostgreSQL / Spring Data JPA / Hibernate 공식 문서에서 직접 확인되는 내용
- Official + Practice: 공식 기능 위에 일반적인 실무 best practice를 결합한 내용
- Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙
이번 문서는 PostgreSQL의 default value, date/time function, trigger, system information 함수 문서와 Spring Data JPA auditing, Hibernate @CreationTimestamp / @UpdateTimestamp 문서를 기준으로 작성한다. PostgreSQL은 CURRENT_TIMESTAMP와 now()가 transaction start time 의미를 갖는다고 설명하고, trigger는 NEW row를 수정해 반환할 수 있다고 설명한다. Spring Data JPA는 현재 principal을 AuditorAware<T>로 제공하도록 정의하고, Hibernate는 timestamp 값을 JVM 메모리 또는 DB에서 생성하도록 선택할 수 있다고 설명한다.
3. 기본 원칙
3.1 감사 컬럼은 “변경 이력 저장소”가 아니라 “현재 row의 메타데이터”다
created_at, updated_at, created_by, updated_by는 현재 row가 언제 누구에 의해 만들어지고 마지막으로 바뀌었는지를 담는 컬럼이다. 전체 변경 히스토리를 남기는 audit table, CDC, outbox, event log와는 역할이 다르다. PostgreSQL 공식 문서도 CURRENT_TIMESTAMP를 row insertion 시각 같은 기본값 예시로 설명하고, Spring Data JPA auditing 역시 “누가/언제 만들고 바꿨는지”를 엔티티 메타데이터로 다룬다.
3.2 시간 감사 컬럼과 주체 감사 컬럼은 분리해서 설계한다
시간 컬럼은 “언제”를, 주체 컬럼은 “누가”를 표현한다. 시간만 필요한 테이블도 있고, 주체까지 필요한 테이블도 있다. Spring Data JPA도 날짜 계열만 추적하는 경우 AuditorAware가 필요 없다고 설명한다. 따라서 프로젝트에서는 created_at/updated_at을 기본 세트로 보고, created_by/updated_by는 실제 운영·보안·추적 요구가 있을 때 추가한다.
3.3 한 컬럼은 한 가지 권위 있는 채움 방식만 가진다
updated_at을 DB trigger도 갱신하고 애플리케이션 auditing도 갱신하면, 어떤 값이 source of truth인지 흐려진다. Hibernate의 @CreationTimestamp / @UpdateTimestamp는 기본적으로 JVM 메모리에서 값을 만들 수 있고, PostgreSQL의 CURRENT_TIMESTAMP는 DB에서 생성된다. 따라서 같은 컬럼을 두 체계가 동시에 관리하게 두지 않는다. 프로젝트 기본 원칙은 한 감사 컬럼당 하나의 authoritative writer다.
4. 표준 감사 컬럼 세트
4.1 기본 필수 세트는 created_at, updated_at
프로젝트 기본 감사 컬럼 세트는 다음 두 컬럼이다.
created_atupdated_at
이 둘은 대부분의 비즈니스 테이블에서 현재 row의 생성 시점과 마지막 수정 시점을 설명하는 최소 메타데이터다. PostgreSQL은 timestamp 기본값에 CURRENT_TIMESTAMP를 쓰는 것을 대표 예시로 제시하고, Spring Data JPA도 @CreatedDate, @LastModifiedDate를 기본적인 auditing metadata로 제시한다.
4.2 선택 세트는 created_by, updated_by
다음 컬럼은 운영/보안/추적 요구가 있을 때 선택적으로 둔다.
created_byupdated_by
Spring Data JPA는 @CreatedBy, @LastModifiedBy를 통해 “누가 생성·수정했는지”를 저장할 수 있고, 이를 위해 AuditorAware<T>가 현재 principal을 제공해야 한다고 설명한다. 프로젝트에서는 이 요구가 있는 도메인에만 주체 컬럼을 추가한다. 모든 테이블에 기계적으로 붙이지는 않는다.
4.3 다음 컬럼들은 이 문서의 기본 감사 컬럼 범위에 넣지 않는다
deleted_at/deleted_by: soft delete 문서에서 별도로 다룬다version: 낙관적 락 / concurrency 문서에서 다룬다- 변경 사유, 변경 요청 id, 전체 이전값/이후값: audit log / domain event / outbox 범위다
이 구분은 PostgreSQL 기능 차이라기보다 프로젝트 문서 경계에 대한 규칙이다. 현재 문서 순서에서도 soft delete는 별도 주제로 분리되어 있다.
5. 시간 감사 컬럼 기준
5.1 시간 감사 컬럼 타입은 timestamp with time zone
PostgreSQL은 date/time 타입을 제공하고, CURRENT_TIMESTAMP 같은 함수는 timestamp with time zone 의미로 동작한다. 프로젝트의 이전 column-types 기준과도 일치하게, 감사 시각 컬럼은 로컬 시각이 아니라 절대 시점을 표현해야 하므로 timestamp with time zone을 기본으로 한다.
5.2 created_at은 NOT NULL + DB 기본값을 기본으로 한다
PostgreSQL은 default value 식이 row 삽입 시 평가되며, CURRENT_TIMESTAMP를 timestamp column default의 전형적 예시로 설명한다. 프로젝트 기본값은 created_at timestamp with time zone not null default current_timestamp다. 생성 시각은 삽입 시점 메타데이터이므로, 애플리케이션이 매번 수동으로 채우게 두기보다 DB 기본값으로 닫는 편이 더 안정적이다.
5.3 updated_at은 insert 시점과 update 시점을 모두 고려해 채운다
updated_at은 insert 때도 값이 있어야 하고, 이후 row가 바뀔 때마다 갱신되어야 한다. PostgreSQL의 default value는 insert 시점에만 적용되므로, updated_at default current_timestamp만으로는 update 반영까지 해결되지 않는다. 따라서 updated_at은 insert 초기값은 default로 두되, update 시점 갱신은 DB trigger 또는 애플리케이션 auditing 중 하나로 별도 책임을 둬야 한다.
5.4 CURRENT_TIMESTAMP는 “실제 벽시계 현재 시각”이 아니라 transaction start time이다
PostgreSQL은 transaction_timestamp()가 CURRENT_TIMESTAMP와 같고, now()도 전통적 동등어라고 설명한다. 반면 statement_timestamp()는 현재 statement 시작 시각, clock_timestamp()는 실제 현재 시각으로 statement 안에서도 변할 수 있다고 설명한다. 프로젝트에서는 일반 감사 컬럼의 기본 의미를 트랜잭션 기준 시각으로 두고, 특별히 벽시계 실시간이 필요하지 않다면 CURRENT_TIMESTAMP를 사용한다.
5.5 default에서는 TIMESTAMP 'now' 같은 literal 형태를 사용하지 않는다
PostgreSQL은 later evaluation이 필요한 DEFAULT 절에서 TIMESTAMP 'now' 형태를 쓰지 말라고 명시한다. 이 형태는 상수가 파싱될 때 고정되어, 실제로는 table creation 시점 값이 들어갈 수 있기 때문이다. 프로젝트에서도 default에는 CURRENT_TIMESTAMP 또는 now() 같은 함수형 표현만 허용한다.
6. 주체 감사 컬럼 기준
6.1 created_by / updated_by는 기본적으로 “애플리케이션 principal”을 저장한다
Spring Data JPA는 AuditorAware<T>가 “현재 애플리케이션과 상호작용하는 current user or system”을 제공하도록 정의한다. 따라서 웹 애플리케이션에서 주체 감사 컬럼은 보통 DB role 이름이 아니라, 서비스가 인지하는 사용자/시스템 actor 식별자를 저장하는 것이 맞다. 프로젝트 기본값도 이 해석을 따른다.
6.2 current_user / session_user를 애플리케이션 사용자 식별자로 기본 사용하지 않는다
PostgreSQL은 session_user가 DB 연결을 시작한 사용자이고, current_user는 권한 검사에 쓰이는 사용자이며 SET ROLE이나 SECURITY DEFINER로 바뀔 수 있다고 설명한다. 이 값들은 DB 세션/권한 맥락에는 유용하지만, 일반적인 애플리케이션 사용자 principal과는 다를 수 있다. 특히 connection pool을 쓰는 서비스에서는 “DB 계정”과 “최종 사용자”가 거의 항상 다르다. 그래서 프로젝트에서는 created_by / updated_by를 DB의 current_user에 기본적으로 매핑하지 않는다.
6.3 주체 컬럼 타입은 도메인 식별자 규칙에 맞춘다
주체 컬럼은 문자열, 숫자, UUID 중 어떤 것이든 가능하지만, 중요한 것은 현재 애플리케이션 actor를 안정적으로 식별할 수 있는 값이어야 한다는 점이다. Spring Data JPA의 AuditorAware<T>도 제네릭 타입 T로 주체 컬럼 타입을 자유롭게 정할 수 있게 설계되어 있다. 프로젝트에서는 보통 사용자 PK나 시스템 actor code 같은 내부 식별자를 저장하고, 표시용 이름은 감사 컬럼에 넣지 않는다.
7. 값을 채우는 방식 기준
7.1 created_at 기본값은 DB가 채우는 것을 기본으로 한다
PostgreSQL이 default value를 row insertion 시 평가해 주므로, created_at은 DB 기본값으로 채우는 것이 가장 단순하고 신뢰도가 높다. 이 값은 insert 경로가 JPA이든 native SQL이든 배치든 일관되게 적용된다. 프로젝트 기본값은 created_at을 애플리케이션 코드에서 직접 세팅하지 않고, DB 기본값으로 닫는 것이다.
7.2 updated_at의 프로젝트 기본값은 DB trigger다
PostgreSQL의 BEFORE ROW trigger는 NEW row를 수정해서 반환할 수 있고, UPDATE 전용 trigger를 만들 수 있으며, WHEN (OLD.* IS DISTINCT FROM NEW.*) 같은 조건도 줄 수 있다고 설명한다. 프로젝트에서는 updated_at을 DB trigger로 유지하는 것을 기본 권장안으로 둔다. 이유는 이 방식이 JPA 엔티티 저장, native SQL, 운영 스크립트, 배치 업데이트처럼 여러 write path를 가장 일관되게 커버하기 때문이다. 이것은 공식 기능 위에 얹는 프로젝트 best practice다.
7.3 단, ORM auditing을 쓴다면 DB trigger와 섞지 않는다
Spring Data JPA auditing은 @CreatedDate, @LastModifiedDate, @CreatedBy, @LastModifiedBy를 제공하고, Hibernate @CreationTimestamp / @UpdateTimestamp도 timestamp 생성을 지원한다. 다만 Hibernate는 두 annotation의 기본 source가 VM(in memory)라고 설명하고, Spring Data auditing의 시간 공급자도 기본적으로 애플리케이션 쪽 CurrentDateTimeProvider다. 따라서 ORM auditing을 채택하면, 같은 컬럼을 DB trigger가 다시 덮어쓰지 않도록 한쪽만 authoritative writer로 선택해야 한다.
7.4 클러스터/다중 writer 환경의 시간 일관성이 중요하면 DB time source를 우선한다
Hibernate는 timestamp source를 VM 또는 DB로 선택할 수 있다고 설명한다. 프로젝트에서는 여러 애플리케이션 인스턴스, 여러 write path, 배치/운영 SQL까지 함께 고려해야 하는 감사 컬럼이라면, 시간 source는 DB가 더 보수적이고 일관된 기본값이다. 따라서 시간 감사 컬럼은 DB source, 주체 감사 컬럼은 애플리케이션 principal source로 나누는 구성이 기본 권장안이다. 이는 공식 기능 조합 위에 얹는 프로젝트 best practice다.
8. 변경 의미 기준
8.1 created_at / created_by는 삽입 후 불변이다
생성 감사 컬럼은 row가 처음 만들어진 사실을 설명하므로, 이후 update에서 바뀌면 안 된다. Hibernate도 @CreationTimestamp는 insert 시 한 번만 생성된다고 설명한다. 프로젝트에서는 생성 감사 컬럼을 비즈니스 코드에서 수정하지 않으며, 필요하면 DB 권한/trigger/ORM 매핑으로 보호한다.
8.2 updated_at / updated_by는 마지막 유효 변경을 반영한다
수정 감사 컬럼은 row가 마지막으로 바뀐 시점을 나타내므로, update 시 갱신되어야 한다. Hibernate @UpdateTimestamp는 row update마다 재생성된다고 설명하고, PostgreSQL trigger는 변경된 NEW row를 반환해 저장 row를 바꿀 수 있다. 프로젝트에서는 “마지막 유효 변경”의 기준을 명확히 두고, updated_at을 사람이 임의로 세팅하는 방식은 금지한다.
8.3 no-op update에까지 updated_at을 바꿀지 여부는 명시적으로 정한다
PostgreSQL CREATE TRIGGER 문서는 WHEN (OLD.* IS DISTINCT FROM NEW.*) 또는 특정 컬럼이 실제 바뀐 경우에만 trigger를 실행하는 예시를 보여 준다. 프로젝트 기본값은 실제 row 값이 바뀐 경우에만 updated_at을 바꾸는 것이다. 단순 재저장이나 동일값 overwrite까지 모두 “수정”으로 간주할지 여부는 팀 규칙으로 명시해야 한다.
9. 프로젝트 권장안
9.1 기본 권장 조합
프로젝트 기본 권장 조합은 다음과 같다.
created_at timestamptz not null default current_timestampupdated_at timestamptz not null default current_timestampupdated_at은 BEFORE UPDATE row trigger로 갱신created_by/updated_by는 필요할 때만 추가- 주체 컬럼은
AuditorAware등으로 애플리케이션 principal을 채움 - 시간 컬럼과 주체 컬럼은 서로 다른 source를 가져도 되지만, 같은 컬럼에 이중 writer를 두지 않음
이 조합은 PostgreSQL의 default/trigger 기능과 Spring Data JPA의 auditing principal 모델을 가장 안정적으로 결합하는 프로젝트 권장안이다.
9.2 DB role 이름을 감사 주체로 저장하는 것은 예외적이다
DB 내부 배치, 직접 SQL 운영 도구, 보안성 높은 DB 중심 워크플로처럼 DB 세션 주체 자체가 의미 있는 시스템이 아니라면, current_user / session_user를 row 감사 주체의 기본값으로 삼지 않는다. PostgreSQL은 이 값들이 DB 연결/권한 문맥을 나타낸다고 설명하고, Spring Data JPA는 애플리케이션 principal을 AuditorAware로 공급하도록 설계한다. 프로젝트 기본값은 애플리케이션 actor 식별자다.
10. 문서 경계
이 문서는 현재 row 수준의 감사 컬럼만 다룬다.
다음 내용은 별도 문서에서 확장한다.
deleted_at/deleted_by와 soft delete- 전체 변경 이력 테이블
- CDC / outbox / event log
- 낙관적 락용 version
- 운영 감사 로그와 DB session audit
현재 문서 체계에서도 soft delete는 별도 주제로 남아 있다.
11. 금지 규칙
다음은 기본 금지다.
created_at/updated_at없이 테이블마다 제각각 감사 컬럼을 두는 것- 시간 감사 컬럼을 로컬 시각 문자열로 저장하는 것
DEFAULT TIMESTAMP 'now'같은 literal 형태를 사용하는 것- 같은
updated_at컬럼을 DB trigger와 ORM auditing이 동시에 관리하는 것 created_by/updated_by를 애플리케이션 principal 대신 DBcurrent_user에 기본 매핑하는 것- 생성 감사 컬럼을 business code가 임의로 수정하는 것
updated_at의미를 정하지 않은 채 no-op update와 실제 변경을 섞어 쓰는 것deleted_at이나version을 기본 감사 컬럼과 같은 범주로 취급하는 것
이 금지 규칙은 PostgreSQL default/trigger/current_user semantics와 Spring Data JPA / Hibernate auditing semantics를 운영 규칙으로 압축한 것이다.
12. 체크리스트
다음 질문에 “예”로 답할 수 있어야 한다.
- 이 테이블의 최소 감사 컬럼이
created_at,updated_at으로 표준화되어 있는가? - 시간 감사 컬럼 타입이 절대 시점을 표현하는 타입인가?
created_at은 DB default로 안전하게 채워지는가?updated_at의 authoritative writer가 하나로 정해져 있는가?created_by/updated_by가 필요하다면 actor source가 애플리케이션 principal로 정의되어 있는가?- DB role 이름과 애플리케이션 사용자 식별자를 혼동하지 않는가?
- 생성 감사 컬럼은 삽입 후 불변으로 취급되는가?
- no-op update에 대한
updated_at정책이 팀 규칙으로 정해져 있는가? - soft delete / version / full audit log와 문서 경계가 섞이지 않는가?