영속성 전이
영속성 전이는 JPA에서 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속성 상태로 만들고 싶을 때 사용한다. 영속 상태의 Entity에서 수행되는 작업들이 연관된 Entity 까지 전파되는 상황을 의미. Cascade / orphan
Cascade 종류
- ALL
- PERSIST
- 부모 엔티티가 저장될 때 자식 엔티티도 함께 저장
- REMOVE
- 부모 엔티티가 삭제될 때 자식 엔티티도 함께 삭제
- MERGE
- 부모 엔티티가 merge될 때 자식 엔티티도 함께 merge
- REFRESH
- DETACH
주의사항
완전히 종속일 때만 사용하는 것이 좋다. 만약 child를 다른 곳에서 알게 되면 사용하지 않는 것이 좋다.
⇒ 단일 엔티티에 완전히 종속적일 때 사용한다.
- 라이프 사이클이 거의 유사할 때
- 단일 소유자일 때
orphanRemove는 ManyToOne에는 존재하지 않는다. ⇒ 삭제되는 entity가 다른 곳에서 참조될 가능성이 높기 때문이다.
JPA Auditing
spring audit docs
생성일자, 수정일자, 식별자와 같은 관계형 데이터베이스에서 테이블에 매핑할 때 도메인들이 공통적으로 갖고 있는 필드 또는 칼럼들을 관리해주는 기능을 말한다.
Spring Data Jpa에서 시간에 대해서 자동으로 값을 넣어주는 기능
아래의 두 어노테이션을 설정해서 사용할 수 있다.
@EnableJpaAuditing@EntityListners(AuditingEntityListener.class)
AuditorAware
@CreatedBy 또는 @LastModifiedBy 를 사용하기 위해서는 audit infrastructure에서 현재 principal을 인식해야할 필요가 있다. ⇒ 그것을 위해 사용하는 것이 AuditAware<T>
SpringSecurity 에서 Audit User 찾기
class SpringSecurityAuditorAware implements AuditorAware<User> {
@Override
public Optional<User> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}Spring Security에서 제공하는 Authentication object에서 접근해서 UserDetails 인스턴스를 조회해서 알게 된다.
Request에서 Audit User 찾기
public class RequestUserJpaAuditConfig implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
ServletRequestAttributes attributes
= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String userId = null;
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String userIdHeader = request.getHeader(X_USER_ID);
if (userIdHeader != null) {
userId = userIdHeader;
}
}
return Optional.ofNullable(userId);
}
}- 요청 컨텍스트 조회
RequestContextHolder에서 현재 요청 정보(ServletRequestAttributes)를 가져옵니다.
→ 주의: 웹 컨텍스트가 아닌 경우(예: 배치 작업)attributes가null일 수 있습니다. - HTTP 요청 객체 추출
웹 요청이 존재하는 경우(attributes != null),HttpServletRequest를 가져옵니다. - 헤더에서 사용자 ID 추출
X_USER_ID헤더 값을 읽어userId변수에 저장합니다.
→ 헤더가 없는 경우userId는null이 됩니다. - 결과 반환
userId를Optional로 감싸서 반환한다. →userId가null이면Optional.empty()가 반환된다.
문제 가능성
Batch Job과 같이 웹 요청이 아닌 경우에는 null일 수 있다.
null인 경우에는 기본값이나 Annonymous와 같이 처리가 가능할 수 있다.
Audit을 기록하기 위한 상위 클래스를 만드는 방법
예제
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@Column(name = "created_at", updatable = false)
@CreatedDate
protected LocalDateTime createdAt;
@CreatedBy
@Column(updatable = false)
protected String createdBy;
@Column(name = "updated_at")
@LastModifiedDate
protected LocalDateTime updatedAt;
@LastModifiedBy
protected String updatedBy;
}MappedSuperclass
- 이 클래스는 JPA의 매핑된 슈퍼클래스로 정의됩니다.
- 즉, 이 클래스를 상속받는 엔티티 클래스들은 이 클래스의 필드들을 자신의 테이블에 포함하게 됩니다.
- 하지만
BaseEntity자체는 테이블로 매핑되지 않습니다.
EntityListeners()
- Spring Data JPA의 Audit 기능을 활성화하기 위해 사용됩니다.
AuditingEntityListener는 엔티티가 저장되거나 업데이트될 때, 자동으로 생성자와 수정자 정보를 기록합니다.- 이를 통해
@CreatedDate,@CreatedBy,@LastModifiedDate,@LastModifiedBy와 같은 어노테이션이 동작하게 됩니다.
Embedded와 Embeddable이란
Embeddable 정의
@Embeddable은 값 타입(VO, Value Object) 클래스를 정의할 때 사용하는 어노테이션이다.
이 어노테이션이 붙은 클래스는 엔티티의 일부(속성)로 저장되며, 자체적으로 테이블을 만들지 않고, 엔티티의 테이블에 컬럼으로 포함이 된다.
예시
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
// 생성자, getter 등
}Embedded 정의
@Embedded는 엔티티의 필드에 붙여, 해당 필드가 @Embeddable로 정의된 값 타입임을 명시한다.
이 필드는 엔티티의 테이블에 컬럼으로 포함되어 저장됩니다
예시
Address의 필드(city, street, zipcode)가 User 테이블에 컬럼으로 표시되어서 저장된다.
@Entity
public class User {
@Id
private Long id;
private String name;
@Embedded
private Address address;
}
특징
- 불변 객체 권장
- 값 타입은 불변(immutable)으로 설계하는 것이 좋으며, setter를 두지 않는 것이 일반적입니다
- 임베디드 값의 내부 필드 변경만으로는 JPA가 변경을 감지하지 못할 수 있다.
- 값 객체는 불변으로 만들고 새 객체로 교체하는 방식이 권장됩니다
@NoArgsConstructor(access = AccessLevel.PROTECTED)
- JPA Entity 요구사항: JPA에서 엔티티 클래스를 사용할 때 기본 생성자가 반드시 필요하다
- 다만 무분별한 객체의 생성을 방지하기 위해서
PROTECTED로 선언해준다.
Jpa를 사용해서 row 삭제
삭제 방법
메소드 사용
// deleteById 메소드 예시
repository.deleteById(1L);
// deleteAllByIdIn 메소드 예시 List<Long> ids = Arrays.asList(1L, 2L, 3L);
repository.deleteAllByIdIn(ids); @Query 어노테이션 사용
// @Query 어노테이션을 활용한 삭제 예시
@Query("delete from Member m where m.name = ?1") @Modifying void deleteByName(String name);발생한 문제
- 연관된 자식 엔티티가 삭제되지 않아서 삭제 쿼리가 날라가지 않는 현상 발생
예제
- 아래와 같이 User와 Board Entity가 있다고 가정하면
@Entity
public class User {
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Board> boards = new ArrayList<>();
}@Entity
public class Board {
@ManyToOne(fetch = FetchType.LAZY)
private User user;
}원인
- 연관된 Board들이 User의 boards 리스트에 그대로 남아 있기 때문이다.
- JPA는 Board들이
고아(orphan)가 아니라고 판단해서 삭제하지 않음.
해결
- 아래와 같이 삭제를 진행하면 된다.
user.getBoards().clear(); // 양방향 연관관계를 끊음 → orphanRemoval 작동
userRepository.delete(ids);