들어가며
애플리케이션을 개발하다 보면 대부분의 테이블에 생성 시간(created_at), 수정 시간(updated_at) 컬럼을 추가하게 됩니다. 이러한 메타 정보는 데이터 추적, 장애 대응, 감사 목적으로 필수적입니다.
문제는 모든 Entity에 동일한 필드를 반복적으로 선언해야 한다는 점입니다. Entity가 10개, 20개로 늘어나면 코드 중복이 발생하고, 수정 시점에 updatedAt을 갱신하는 코드를 누락하는 실수도 생깁니다. 심지어 created_by, updated_by 라는 사용자 컬럼이라면 더 어려울 수 있습니다.
@Entity
public class User {
@Id
private Long id;
private String name;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
// 수정 시 직접 갱신해야 함
user.setName("변경된이름");
user.setUpdatedAt(LocalDateTime.now()); // 누락하기 쉬움
userRepository.save(user);
Spring Data JPA의 auditing support를 사용하면 이러한 문제를 해결할 수 있습니다. @MappedSuperclass를 활용한 공통 클래스와 JPA Auditing을 조합하여 Audit 필드를 자동으로 관리하는 방법을 살펴보겠습니다.
@MappedSuperclass를 통한 공통 필드 관리
@MappedSuperclass란
@MappedSuperclass는 JPA에서 제공하는 상속 매핑 전략입니다. 부모 클래스의 매핑 정보를 자식 Entity에게 전달하되, 부모 클래스 자체는 Entity가 아닙니다.
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
public LocalDateTime getCreatedAt() {
return createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
}
@MappedSuperclass로 선언된 BaseEntity는 독립적인 테이블을 생성하지 않습니다. 대신 이를 상속받는 각 Entity의 테이블에 필드가 컬럼으로 추가됩니다.
Spring Data JPA Auditing 동작 원리
JPA Entity Lifecycle Callback
JPA는 Entity의 생명주기 이벤트를 감지하는 콜백 기능을 제공합니다. 주요 콜백 이벤트는 다음과 같습니다.
- @PrePersist: Entity가 영속화되기 직전
- @PreUpdate: Entity가 수정되기 직전
- @PostPersist: Entity가 영속화된 직후
- @PostUpdate: Entity가 수정된 직후
AuditingEntityListener의 역할
Spring Data JPA는 AuditingEntityListener를 통해 이러한 콜백을 활용합니다. 이 리스너는 @PrePersist, @PreUpdate 시점에 Audit 필드를 자동으로 설정합니다.
구현 방법
1. BaseEntity 정의
package com.example.entity;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
}
주요 설정:
- updatable = false: createdAt은 최초 생성 시에만 설정되고 이후 변경되어선 안됩니다.
- nullable = false: 필수 값으로 지정합니다.
- abstract class: 일반적으로 BaseEntity는 추상 클래스로 선언합니다. JPA Auditing 스펙 상 필수는 아니지만 직접 인스턴스화를 방지하기 위한 관례입니다.
2. JPA Auditing 활성화
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@SpringBootApplication
@EnableJpaAuditing
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@EnableJpaAuditing 어노테이션이 없으면 Auditing 기능이 동작하지 않습니다. (PrePersist 등 어노테이션을 사용해도 됩니다.)
3. Entity에서 상속
package com.example.domain;
import com.example.entity.BaseEntity;
import jakarta.persistence.*;
@Entity
@Table(name = "users")
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
// createdAt, updatedAt은 BaseEntity가 관리
}
4. 동작 확인
ㅈ@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void createAndUpdate() {
// 생성
User user = new User();
user.setName("홍길동");
userRepository.save(user);
// createdAt: 2025-11-21 10:00:00
// updatedAt: 2025-11-21 10:00:00
// 수정
user.setName("김철수");
// createdAt: 2025-11-21 10:00:00 (변경 없음)
// updatedAt: 2025-11-21 10:05:00 (자동 갱신)
}
}
실제 실행되는 SQL:
-- INSERT
INSERT INTO users (name, created_at, updated_at)
VALUES ('홍길동', '2025-11-21 10:00:00', '2025-11-21 10:00:00');
-- UPDATE
UPDATE users
SET name = '김철수',
updated_at = '2025-11-21 10:05:00'
WHERE id = 1;
작성자 정보 추가
Spring Security와 연동하면 작성자, 수정자 정보도 자동으로 기록할 수 있습니다.
BaseEntity 확장
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@CreatedBy
@Column(name = "created_by", updatable = false, length = 50)
private String createdBy;
@LastModifiedBy
@Column(name = "last_modified_by", length = 50)
private String lastModifiedBy;
}
AuditorAware 구현
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> {
Authentication authentication = SecurityContextHolder
.getContext()
.getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.of("SYSTEM");
}
return Optional.of(authentication.getName());
};
}
}
AuditorAware 인터페이스를 구현하여 현재 사용자를 제공하면, @CreatedBy와 @LastModifiedBy 필드가 자동으로 채워집니다.
시간대 처리
LocalDateTime의 한계
LocalDateTime은 시간대 정보를 포함하지 않습니다. 글로벌 서비스에서는 UTC 기준으로 저장하고 표시할 때 지역 시간으로 변환하는 것이 일반적입니다.
Instant 사용
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(name = "created_at", updatable = false)
private Instant createdAt; // UTC 기준
@LastModifiedDate
@Column(name = "updated_at")
private Instant updatedAt; // UTC 기준
}
Instant는 UTC 시간대를 기준으로 동작하며, 애플리케이션 레벨에서 지역 시간으로 변환하여 사용할 수 있습니다.
ZonedDateTime 사용
시간대 정보까지 저장해야 한다면 ZonedDateTime을 사용할 수 있습니다.
@CreatedDate
private ZonedDateTime createdAt; // 시간대 정보 포함
다만 이에 맞춰 컬럼 타입을 설정해야 하므로 실제 요구사항에 맞게 선택해야 합니다.
적용 시 체크리스트
- @EnableJpaAuditing 어노테이션 추가 확인
- BaseEntity에 @EntityListeners(AuditingEntityListener.class) 선언
- createdAt 필드에 updatable = false 설정
- 데이터베이스 컬럼 타입 확인 (TIMESTAMP 권장)
- 시간대 처리 전략 수립 (LocalDateTime vs Instant)
- 필요시 @CreatedBy, @LastModifiedBy와 AuditorAware 구현
마치며
@MappedSuperclass를 활용한 공통 클래스와 JPA Auditing은 실무에서 거의 표준처럼 사용되는 기법입니다. 프로젝트 초기에 한 번만 설정해두면 이후 모든 Entity에서 자동으로 Audit 정보가 관리되므로, 개발 생산성과 데이터 품질을 동시에 향상시킬 수 있습니다.
특히 운영 환경에서 데이터 추적이 필요한 상황이나 장애 대응 시 생성/수정 시간 정보는 매우 유용합니다. 프로젝트 시작 단계에서 적용하는 것을 권장합니다.
참조
- Spring Data JPA 공식 문서 - Auditing
- JPA Specification - @MappedSuperclass
- Baeldung - Hibernate Inheritance Mapping
- Baeldung - Auditing with JPA, Hibernate, and Spring Data JPA
- Baeldung - JPA Entity Lifecycle Events
- Vlad Mihalcea - How to inherit properties from a base class entity using @MappedSuperclass
'설계 전략' 카테고리의 다른 글
| Spring Data JPA에서 NPE를 방지하는 Optional 활용법 (0) | 2026.01.28 |
|---|---|
| JPA 영속성 컨텍스트 이해하기 (0) | 2025.11.20 |