포스트

2023-05-12-TIL

2023-05-12-TIL

Today I Learned

Spring Boot에서 동적으로 작업 스케줄링하기

@Scheduled는 주기와 cron 식이 코드나 설정에 고정되어 있어서, 관리자 화면에서 작업을 등록하거나 주기를 바꾸는 요구에는 맞지 않는다. 이때는 TaskScheduler를 직접 쓴다. Spring 문서에 나오듯 TaskScheduler는 schedule(task, trigger), scheduleAtFixedRate, scheduleWithFixedDelay 메서드를 제공하고 ScheduledFuture를 돌려준다. 이 ScheduledFuture를 작업 ID별로 맵에 보관해 두면 cancel()로 중지하고 새 CronTrigger로 다시 등록하는 식으로 실행 중에 스케줄을 바꿀 수 있다. SchedulingConfigurer를 구현하면 @EnableScheduling이 쓰는 레지스트라에 트리거를 직접 등록할 수도 있다. 등록한 스케줄은 메모리에만 있으므로 재시작 후에도 유지하려면 DB에 저장해 두고 시작 시 다시 등록해야 한다.


Spring Scheduler, Quartz, Spring Batch의 차이

세 가지는 경쟁 관계가 아니라 역할이 다르다.

  • Spring Scheduler(@Scheduled, TaskScheduler): 언제 실행할지를 정하는 가벼운 스케줄러다. 상태를 저장하지 않으므로 클러스터 환경에서 중복 실행을 막으려면 별도 장치가 필요하다.
  • Quartz: 작업(Job)과 트리거를 DB(JobStore)에 저장할 수 있는 스케줄러다. 클러스터 모드에서 한 노드만 실행하게 하거나, 재시작 후 놓친 실행(misfire)을 처리하는 기능이 있다.
  • Spring Batch: 무엇을 어떻게 처리할지를 다루는 배치 처리 프레임워크다. 청크 단위 읽기, 처리, 쓰기와 실행 이력, 재시작, 건너뛰기를 제공한다. Spring Batch 문서도 스스로를 스케줄링 프레임워크가 아니라고 밝히고, Quartz 같은 스케줄러와 함께 쓰라고 안내한다.

그래서 흔한 조합은 스케줄러(또는 Jenkins, Kubernetes CronJob 같은 외부 스케줄러)가 Spring Batch 잡을 실행하는 형태다.

  • https://www.linkedin.com/advice/0/what-pros-cons-using-spring-batch-vs-quartz-processing
  • https://stackoverflow.com/questions/33188368/spring-batch-vs-quartz-jobs
  • https://yongku.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98Spring-Batch%EC%99%80-Quartz
  • https://azderica.github.io/01-spring-batch/
  • https://sabarada.tistory.com/113
  • Spring Batch Introduction

JPA의 “Found shared references to a collection” 오류

Hibernate는 엔티티의 컬렉션 필드를 자신이 관리하는 컬렉션 래퍼(PersistentBag, PersistentSet 등)로 바꿔 넣고, 각 래퍼가 어느 엔티티의 어느 필드에 속하는지 추적한다. 그런데 두 엔티티가 같은 컬렉션 인스턴스를 참조하면 flush 시점에 소유자를 하나로 정할 수 없어 이 예외가 발생한다. 흔한 원인은 엔티티를 복사하면서 copy.setTags(origin.getTags())처럼 컬렉션 참조를 그대로 넘기는 코드다. 해결은 참조 대신 새 컬렉션을 만들어 요소를 복사하는 것이다(new ArrayList<>(origin.getTags())). 연관관계가 @ManyToMany라면 요소 엔티티는 공유해도 되지만 컬렉션 객체 자체는 엔티티마다 따로 있어야 한다.

  • https://stackoverflow.com/questions/61077311/spring-data-jpa-found-shared-references-to-a-collection-org-hibernate-hibernat
  • https://jforj.tistory.com/270
  • https://coderanch.com/t/531381/databases/Complete-Reproducing-shared-references-collection
  • http://www.java2s.com/Questions_And_Answers/JPA/Collection/HibernateException.htm
  • https://stackoverflow.com/questions/1692871/found-shared-references-to-a-collection-org-hibernate-hibernateexception
  • https://slothink.tistory.com/92
  • https://forum.hibernate.org/viewtopic.php?p=2269506
  • https://forum.hibernate.org/viewtopic.php?p=2475920
  • https://forum.hibernate.org/viewtopic.php?p=2288359
  • https://forum.hibernate.org/viewtopic.php?p=2410857
  • https://forum.hibernate.org/viewtopic.php?p=2273553
  • https://m.blog.naver.com/ndrsgrwh4/221731486601

Hibernate를 깊게 다루는 사람들

JPA와 Hibernate 문제를 검색하면 반복해서 나오는 두 사람의 블로그를 정리해 두었다. Vlad Mihalcea는 스스로를 Hibernate ORM의 주요 커미터 중 한 명으로 소개하는 JPA 전문가로, 연관관계 매핑과 성능 문제를 SQL 로그 수준까지 보여 주는 글이 많다. Thorben Janssen은 Hibernate 팁과 모범 사례를 짧은 글로 꾸준히 정리한다. 영상 링크도 함께 저장해 두었다.

  • https://www.youtube.com/watch?v=pc6QIwx0EL0
  • https://vladmihalcea.com/
  • https://thorben-janssen.com/

JPA 프로젝션으로 Java Record 사용하기

record는 엔티티로는 쓸 수 없지만 조회 결과를 담는 DTO로는 잘 맞는다. Spring Data JPA 문서도 record가 값 의미론을 따르므로 DTO 타입을 정의하기에 적합하다고 설명한다. 리포지토리 메서드의 반환 타입을 record로 두면 클래스 기반 프로젝션으로 동작하고, JPQL에서는 생성자 표현식 select new com.example.MemberSummary(m.id, m.name) from Member m으로 바로 만들 수 있다. 필요한 컬럼만 조회하고 영속성 컨텍스트에 엔티티를 올리지 않으므로 읽기 전용 화면에서 유리하다.


Study

UI/UX 상에서 부정어 보다는 평서문으로 피드백을 주는 것이 더 직관적이다.

예를 들어 “비밀번호는 8자 미만일 수 없습니다”보다 “비밀번호는 8자 이상이어야 합니다”가 사용자가 해야 할 행동을 바로 알려 준다. 부정문은 읽는 사람이 한 번 더 뒤집어 해석해야 하고, 이중 부정이 섞이면 의미를 잘못 받아들이기 쉽다. API 오류 메시지를 작성할 때도 같은 원칙이 적용된다.

이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다