기능개발요구서에 stamp 관련 요구가 있어서 구현했습니다. 효율성을 위해 Spring Data JPA Auditing 기능을 도입하였고, 소프트 삭제(Soft Delete) 기능도 함께 구현해보았습니다.
Stamp 요구사항:
엔티티 생성 및 수정 시점에 자동으로 생성자, 수정자, 생성/수정 일시를 기록해야 합니다.
또한 삭제 시 실제 삭제가 아닌 소프트 삭제로 기록하기 위해 삭제 일시(deletedAt)와 삭제자(deletedBy)도 기록해야 합니다.
효율성 고려:
@CreatedDate, @LastModifiedDate, @CreatedBy, @LastModifiedBy 어노테이션을 통해 반복되는 코드를 줄이고, 코드의 일관성을 유지할 수 있습니다.status)를 "DELETED"로 변경하여 감사(audit)와 복구가 가능하도록 구현하는 소프트 삭제 방식을 도입합니다.고민 포인트:
Spring Security를 사용하여 현재 인증된 사용자의 username을 반환하는 AuditorAware 구현체를 작성합니다.
package com.sprta.samsike.config;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.empty();
}
return Optional.of(authentication.getName());
}
}
AuditorAware 구현체를 등록하고, JPA Auditing을 활성화하는 설정 클래스입니다.
package com.sprta.samsike.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorAwareImpl")
public class JpaConfig {
// 추가 설정이 필요하면 작성
}
Stamp 관련 필드를 포함하는 상위 엔티티에 @CreatedDate, @CreatedBy, @LastModifiedDate, @LastModifiedBy 어노테이션을 추가합니다.
package com.sprta.samsike.domain;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Getter
@Setter // 리스너나 서비스에서 값을 수정할 수 있도록 setter가 필요합니다.
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Stamped {
@CreatedDate
@Column(nullable = false, updatable = false)
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime createdAt;
@CreatedBy // AuditorAware를 통해 자동 할당됨
@Column(updatable = false)
private String createdBy;
@LastModifiedDate
@Column
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime updatedAt;
@LastModifiedBy // 수정자도 자동 할당을 원한다면 추가합니다.
private String updatedBy;
// 소프트 삭제 관련 필드
private LocalDateTime deletedAt;
private String deletedBy;
}
주의:
- 최초 회원가입 시에는 인증된 사용자가 없으므로, 서비스 레이어에서
createdBy를 수동으로 설정해야 합니다.
소프트 삭제는 실제 데이터를 삭제하지 않고, 삭제된 것으로 표시하기 위해 엔티티의 상태를 업데이트합니다.
회원 엔티티(Member)는 status 필드로 상태를 관리하며, "DELETED"로 변경 시 소프트 삭제로 간주합니다.
package com.sprta.samsike.domain.member;
import com.sprta.samsike.domain.Stamped;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(catalog = "samsike", name = "p_user")
public class Member extends Stamped {
@Id
@Column(length = 50, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String status;
@Column(nullable = false)
private String role = "CUSTOMER";
public Member() {
}
public Member(String username, String password, String name, String email) {
this.username = username;
this.password = password;
this.name = name;
this.email = email;
this.role = "CUSTOMER";
this.status = "ACTIVE";
}
// 소프트 삭제를 수행하는 메서드
public void softDelete() {
this.status = "DELETED";
}
}
삭제 시 엔티티의 상태가 "DELETED"로 변경되면, 엔티티 리스너를 통해 deletedAt과 deletedBy 필드를 자동으로 설정합니다.
package com.sprta.samsike.domain;
import jakarta.persistence.PreUpdate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.time.LocalDateTime;
public class SoftDeleteListener {
/**
* 엔티티 업데이트 전에 호출됩니다.
* 여기서 삭제 상태("DELETED")로 변경된 경우, deletedAt과 deletedBy를 설정합니다.
*/
@PreUpdate
public void preUpdate(Stamped entity) {
// 예시: Member 엔티티에서 status가 "DELETED"로 변경된 경우 처리
if (entity instanceof com.sprta.samsike.domain.member.Member) {
com.sprta.samsike.domain.member.Member member = (com.sprta.samsike.domain.member.Member) entity;
if (member.getDeletedAt() == null && "DELETED".equals(member.getStatus())) {
member.setDeletedAt(LocalDateTime.now());
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
member.setDeletedBy(auth.getName());
} else {
member.setDeletedBy("SYSTEM");
}
}
}
}
}
주의:
- 엔티티 리스너가 동작하려면
Stamped클래스의@EntityListeners에SoftDeleteListener.class를 추가해야 합니다.- 조회 시 소프트 삭제된 데이터(예: status가 "DELETED" 또는 deletedAt이 설정된 데이터)를 제외하는 조건을 추가하는 것도 고려해야 합니다.
회원가입 시에는 아직 인증된 사용자가 없으므로, 서비스 레이어에서 수동으로 createdBy를 설정합니다.
그러나 Spring Data JPA Auditing이 동작하는 과정에서, 엔티티가 저장되기 전에 AuditingEntityListener가 AuditorAware를 호출하여 createdBy 값을 세팅합니다.
따라서, 회원가입 로직에서 수동으로 member.setCreatedBy(username)을 호출하더라도, 엔티티가 persist되면서 AuditorAware에서 반환하는 값(예를 들어, "unanimoususer" 또는 "SYSTEM")으로 덮어쓰게 됩니다.
즉,
setCreatedBy를 호출해도, 엔티티 저장 시점에 AuditingEntityListener가 자동으로 AuditorAware.getCurrentAuditor()를 호출하여 createdBy 필드를 업데이트합니다.이를 해결하기 위한 두 가지 접근 방법이 있습니다.
회원가입 시점에 사용자는 아직 인증되지 않은 상태입니다. 그래서 AuditorAware 구현체가 기본값(예: "unanimoususer" 또는 "SYSTEM")을 반환하게 됩니다.
이를 방지하기 위해, 회원가입 로직에서 임시로 인증 객체(Authentication)를 SecurityContext에 설정하는 방법이 있습니다.
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
// 회원가입 서비스 내에서
public void signup(@Valid SignupRequestDTO requestDto) {
String username = requestDto.getUsername();
String password = passwordEncoder.encode(requestDto.getPassword());
// 회원 중복 및 이메일 중복 확인 (생략)
Member member = new Member(username, password, requestDto.getName(), requestDto.getEmail());
// 임시 인증 객체 생성 및 SecurityContext 설정
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
username,
null,
List.of(new SimpleGrantedAuthority("ROLE_CUSTOMER")));
SecurityContextHolder.getContext().setAuthentication(authToken);
// AuditorAware가 이 시점의 username을 반환하여 createdBy에 반영함
memberRepository.save(member);
// 필요에 따라 SecurityContext를 초기화(클리어)할 수 있음
SecurityContextHolder.clearContext();
}
장점:
주의:
JPA Auditing 도입:
@CreatedDate, @LastModifiedDate, @CreatedBy, @LastModifiedBy 어노테이션을 사용해 자동으로 감사 정보를 기록소프트 삭제 구현:
status 필드를 통해 삭제 여부를 관리하고, 삭제 시 "DELETED"로 변경 @PreUpdate 시점에 deletedAt과 deletedBy를 자동으로 설정 특수 상황(회원가입) 처리:
createdBy 필드를 설정 AuditorAware.getCurrentAuditor()를 호출하여 값을 덮어쓸 수 있으므로,이와 같이 구현함으로써 기능 요구사항에 맞추어 엔티티 생성/수정/삭제 시 감사 정보를 효율적으로 관리할 수 있게 되었으며, 소프트 삭제 방식으로 데이터의 무결성과 복구 가능성을 확보할 수 있었습니다.