Files
project-auth-server/docs/examples/db/audit-columns.md
T

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은 별도 주제로 분리되어 있다.