실제 엔티티의 상태에 따라 동적으로 생성하도록 하여 성능상 이점을 얻기 위해 사용하는 어노테이션 !
2️⃣ @DynamicInsert와 @DynamicUpdate가 어떻게 작동되는 지 ?
기존 방식
JPA (정확히는 하이버네이트와 같은 JPA 구현체)는 기본적으로 엔티티의 영속성 상태 변화를 감지하여 자동으로 SQL 쿼리를 생성하고 실행한다.이 방식은 쿼리 생성 로직이 단순하다는 장점이 있지만, 불필요하게 많은 컬럼을 포함시키는 단점이 있다.
예시
INSERT: 기본적으로 모든 컬럼을 INSERT 문에 포함시킴
UPDATE: 업데이트 가능한 모든 컬럼을 UPDATE 문의 SET 절에 포함시킴.
@DynamicInsert, @DynamicUpdate 적용
1. 엔티티 클래스 레벨에 @DynamicInsert 를 붙이면 적용된다.
예시
INSERT: null이 아닌 필드들만 INSERT 문에 포함시킴
UPDATE: 엔티티가 컨텍스트에 로딩된 후 실제로 값이 변경된 필드들만 SET절에 포함시킴
3️⃣ 적용 시 장단점
구분
기존
@DynamicInsert/Update
장점
쿼리 생성 로직이 단순하여 하이버네이트 내부 처리 부하가 적음
- 쿼리 문자열이 짧아지기 때문에 네트워크 부하가 줄어듦 - 불필요한 컬럼의 삽입/갱신을 막아 데이터베이스 부하를 줄일 수 있음 - null 값 삽입시 DB의 DEFAULT값이 적용되도록 유도할 수 있음. - 실제 변경된 컬럼만 갱신하여 동시성 충돌 가능성을 줄일 수 있음
단점
- 불필요하게 많은 컬럼을 쿼리에 포함시켜 쿼리 문자열이 길어지고 네트워크 부하가 증가할 수 있음 - 데이터베이스의 DEFAULT 값이 아닌 명시적인 NULL이 삽입될 수 있음 (@DynamicInsert 시 기대하는 동작과 다를 수 있음) - 값이 변경되지 않은 컬럼도 다시 갱신하여 불필요한 쓰기 작업이 발생할 수 있음.
어떤 컬럼을 쿼리에 포함시킬지 결정하는 추가적인 내부처리가 필요하여 하이버네이트 내부 처리 부하가 소폭 증가할 수 있음
4️⃣ 언제 적용하면 좋을까 ?
엔티티의 컬럼 수가 매우 많은 경우
컬럼 수가 많을 수록 불필요하게 포함되는 컬럼이 많아져 쿼리 문자열이 길어지고 네트워크 및 데이터베이스 부하가 커진다.
INSERT 시 대부분의 컬럼이 null이거나 DB의 DEFAULT 값을 사용하고 싶은 경우
@DynamicInsert → 명시적으로 null을 삽입하는 대신 DB의 기본값을 사용하도록 유도
UPDATE 시 변경되는 컬럼의 수가 적은 경우
전체 컬럼 중 실제로 몇 개만 변경되는 경우가 빈번하다면, 변경된 컬럼만 갱신함으로써 데이터베이스 쓰기 부하를 줄이고 쿼리 크기를 최적화할 수 있음.
성능 최적화가 필요한 와이드(Wide) 테이블 (컬럼이 많은 테이블): 대규모 데이터 처리에서 쿼리 크기나 불필요한 쓰기 작업이 병목이 될 수 있다면 고려해볼 수 있음.
🚨 주의사항
@DynamicInsert와 @DynamicUpdate는 하이버네이트의 확장 기능이며 표준 JPA 기능은 아니다.
성능 측정 없이는 명확한 효과를 단정하기 어려움. 때로는 기본 동작이 더 나은 성능을 보이기도 함.
단순히 컬럼 수가 적고 변경이 잦은 테이블에는 큰 이점이 없을 수 있으며, 오히려 내부 처리 부하 때문에 미미하게나마 성능 저하가 있을 수 있음.
작년에 node.js로 프로젝트 할 때는 node-cron으로 자동 삭제 기능을 구현했던 기억이 있다.
보통 데이터는 바로 hard-delete하는 방식이 아니라
is_deleted 값을 true로 바꾸는 soft-delete 방식으로 구현되기 때문에
soft-delete된 아이들 중 특정 시간이 지난(1년, 30일 등등..) 아이들은 서버가 실행될 때 자동으로 삭제 되는 방식으로 구현한다.
이번 에디슨 프로젝트는 SpringBoot로 구현하고 있어서 스프링으로는 어떻게 하는 지 정리해두려고 한다.
구현 목표 : Spring의 @Scheduled을 활용하여 일정 간격으로 30일 지난 버블을 삭제
Config
@Configuration
@EnableScheduling
public class SchedulingConfig {
// 스케줄링 활성화
}
BubbleServiceImpl
@Override
@Scheduled(cron = "0 0 0 * * ?") // 매일 새벽 0시에 실행
public void deleteExpiredBubble() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime expiryDate = now.minusDays(30);
List<Bubble> expiredBubbles = bubbleRepository.findAllByUpdatedAtBeforeAndIsDeletedTrue(expiryDate);
if (!expiredBubbles.isEmpty()) {
bubbleRepository.deleteAll(expiredBubbles);
log.info("Deleted {} expired bubbles", expiredBubbles.size());
} else {
log.info("No expired bubbles found for deletion");
}
}
BubbleRepository
@Query("SELECT b FROM Bubble b WHERE b.updatedAt < :expiryDate AND b.isDeleted = true")
List<Bubble> findAllByUpdatedAtBeforeAndIsDeletedTrue(@Param("expiryDate") LocalDateTime expiryDate);
@Scheduled을 사용하여 매일 특정 시간에 자동으로 30일 지난 삭제된 버블을 삭제한다.
updatedAt기준으로 30일이 지난isDeleted = true상태의 버블 (휴지통에 있는 버블) 을 찾아 삭제한다.
결과
- 휴지통에 만료된 버블이 없을 때
- 휴지통에 만료된 버블이 있을 때
+ 수정 : Error Shooting
위에서 한 것처럼 간단하게 구현하니.. 자동삭제 될 때, fk constraint 관련 에러가 발생했다.
삭제하기 전 관련 아이디를 참조하는 값을 null로 바꿔주는 로직을 추가해서 해결하였읍니다 :>