8.5 KiB
Audit Columns 예시
좋은 예시
예시 1. 최소 감사 컬럼은 created_at, updated_at을 공통으로 둔다
CREATE TABLE auth.users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL,
status text NOT NULL,
created_at timestamp with time zone NOT NULL DEFAULT current_timestamp,
updated_at timestamp with time zone NOT NULL DEFAULT current_timestamp
);
좋은 이유:
- 생성/수정 시각을 공통 규칙으로 표준화한다
- 두 컬럼 모두 절대 시점 타입을 사용한다
- insert 시점 기본값을 DB가 일관되게 채운다
PostgreSQL은 CURRENT_TIMESTAMP를 timestamp column default의 대표 예시로 설명하고, default expression은 row 삽입 시 평가된다고 설명한다. CURRENT_TIMESTAMP/now()는 transaction start time 의미를 갖는다.
예시 2. updated_at은 DB trigger 하나만 source of truth로 둔다
CREATE OR REPLACE FUNCTION common_set_updated_at()
RETURNS trigger AS $$
BEGIN
NEW.updated_at := current_timestamp;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_users_set_updated_at
BEFORE UPDATE ON auth.users
FOR EACH ROW
WHEN (OLD.* IS DISTINCT FROM NEW.*)
EXECUTE FUNCTION common_set_updated_at();
좋은 이유:
updated_at갱신 책임이 DB에 명확하게 모인다- BEFORE ROW trigger가 NEW를 수정해 반환하는 PostgreSQL 공식 모델과 맞다
WHEN (OLD.* IS DISTINCT FROM NEW.*)로 실제 변경이 있을 때만 갱신하게 만들 수 있다
PostgreSQL은 BEFORE row trigger가 NEW를 수정하고 반환할 수 있다고 설명하고, WHEN (OLD.* IS DISTINCT FROM NEW.*) 예시도 공식 문서에 제공한다.
예시 3. 주체 컬럼은 애플리케이션 principal 식별자로 둔다
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "users", schema = "auth")
public class User {
@Id
private Long id;
@CreatedBy
@Column(name = "created_by")
private Long createdBy;
@LastModifiedBy
@Column(name = "updated_by")
private Long updatedBy;
}
좋은 이유:
created_by,updated_by를 DB 연결 계정이 아니라 애플리케이션 actor 식별자로 다룬다- Spring Data JPA의 표준 auditing metadata를 사용한다
- 시간 컬럼과 주체 컬럼의 source를 분리할 수 있다
Spring Data JPA는 @CreatedBy, @LastModifiedBy를 제공하고, AuditorAware<T>가 현재 애플리케이션 principal을 제공하도록 설명한다.
예시 4. 시간과 주체를 필요한 만큼만 선택적으로 둔다
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "login_failures", schema = "auth")
public class LoginFailure {
@Id
private Long id;
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
}
좋은 이유:
- 시간 정보만 필요하면 날짜 감사 컬럼만 써도 된다
- Spring Data JPA는 시간 추적만 하는 경우
AuditorAware가 필수가 아니라고 설명한다 - 모든 테이블에 주체 컬럼을 기계적으로 강제하지 않는다
Spring Data JPA는 생성/수정 날짜만 추적하는 경우 AuditorAware가 필요 없다고 설명한다.
예시 5. Hibernate timestamp를 쓴다면 source를 명시적으로 선택한다
@Entity
@Table(name = "sessions", schema = "auth")
public class Session {
@Id
private Long id;
@CreationTimestamp(source = SourceType.DB)
@Column(name = "created_at", nullable = false, updatable = false)
private OffsetDateTime createdAt;
@UpdateTimestamp(source = SourceType.DB)
@Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt;
}
좋은 이유:
- Hibernate의 기본 VM source 대신 DB source를 명시적으로 선택한다
- 다중 인스턴스 환경에서 시간 source를 DB로 맞추고 싶다는 의도가 드러난다
- 같은 컬럼을 또 다른 trigger가 동시에 갱신하지만 않는다면 일관성이 높다
Hibernate는 @CreationTimestamp, @UpdateTimestamp가 기본적으로 VM(in memory)에서 생성되지만, source()로 변경할 수 있고 SourceType.DB는 DB가 값을 생성함을 뜻한다고 설명한다.
나쁜 예시
예시 1. 감사 컬럼 없이 테이블마다 임의 이름을 쓴다
CREATE TABLE auth.users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL,
reg_dt timestamp,
mod_ymd text
);
나쁜 이유:
- 공통 규칙이 없어 해석과 조회가 어려워진다
- 수정 시각을 문자열로 저장해 타입 의미가 무너진다
- 생성/수정 메타데이터가 표준화되지 않는다
PostgreSQL은 timestamp/date/time 타입을 제공하고, CURRENT_TIMESTAMP 같은 기본 시간 함수도 제공한다. 감사 컬럼은 이런 표준 타입 위에서 일관되게 두는 편이 안전하다.
예시 2. default에 TIMESTAMP 'now' literal을 쓴다
CREATE TABLE auth.users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
created_at timestamp with time zone NOT NULL DEFAULT TIMESTAMP 'now'
);
나쁜 이유:
- PostgreSQL이 default 절에서는 이 형태를 쓰지 말라고 명시한다
- row 삽입 시점이 아니라 table creation 시점으로 고정될 수 있다
CURRENT_TIMESTAMP또는now()같은 함수형 표현이 맞다
PostgreSQL은 later evaluation이 필요한 DEFAULT 절에서 TIMESTAMP 'now'를 사용하지 말라고 설명한다.
예시 3. 애플리케이션 사용자 식별자를 current_user에 맡긴다
CREATE TABLE auth.users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
created_by text NOT NULL DEFAULT current_user,
updated_by text NOT NULL DEFAULT current_user
);
나쁜 이유:
current_user는 DB 권한 검사에 쓰이는 사용자다- connection pool,
SET ROLE,SECURITY DEFINER환경에서는 최종 애플리케이션 사용자와 다를 수 있다 - 일반 웹 애플리케이션의 actor 추적 컬럼으로는 부적절하다
PostgreSQL은 current_user가 권한 검사에 쓰이는 사용자이고, SET ROLE이나 SECURITY DEFINER에 의해 바뀔 수 있다고 설명한다. Spring Data JPA는 이런 경우 현재 principal을 AuditorAware<T>로 제공하도록 설계한다.
예시 4. 같은 updated_at을 DB trigger와 ORM이 동시에 관리한다
@Entity
@Table(name = "users", schema = "auth")
public class User {
@UpdateTimestamp
@Column(name = "updated_at")
private OffsetDateTime updatedAt;
}
CREATE TRIGGER trg_users_set_updated_at
BEFORE UPDATE ON auth.users
FOR EACH ROW
EXECUTE FUNCTION common_set_updated_at();
나쁜 이유:
- 같은 컬럼에 두 개의 writer가 생긴다
- VM source와 DB source가 섞일 수 있다
- 어떤 값이 권위 있는지 흐려지고 디버깅이 어려워진다
Hibernate는 @UpdateTimestamp의 기본 source가 VM이라고 설명하고, PostgreSQL trigger는 NEW row를 수정해 저장값을 바꿀 수 있다고 설명한다. 둘을 같은 컬럼에 동시에 쓰면 source of truth가 모호해진다.
예시 5. created_at을 business code가 직접 덮어쓴다
user.setCreatedAt(OffsetDateTime.now());
user.setUpdatedAt(OffsetDateTime.now());
나쁜 이유:
- 생성 감사 컬럼이 도메인 로직에서 임의로 바뀔 수 있다
- DB default나 프레임워크 auditing 의미를 깨뜨린다
- 생성 메타데이터의 불변성이 사라진다
Hibernate는 @CreationTimestamp 필드는 애플리케이션이 직접 설정할 수 없다고 설명한다. 프로젝트도 같은 철학으로, 생성 감사 컬럼을 business code가 임의로 다루지 않게 한다.
예시 6. deleted_at과 version을 기본 감사 컬럼처럼 섞어 둔다
CREATE TABLE auth.users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
deleted_at timestamp with time zone,
version bigint NOT NULL
);
나쁜 이유:
deleted_at은 soft delete 정책의 일부이고,version은 optimistic locking 메타데이터다- 현재 row 감사 컬럼과 다른 목적의 컬럼을 한 범주로 섞는다
- 문서 경계와 코드 책임이 흐려진다
현재 문서 체계에서도 soft delete와 concurrency/version은 별도 주제로 분리되어 있다.