2023-06-21-TIL
2023-06-21-TIL
Today I Learned
UCI
한국저작권위원회 설명에 따르면 국가콘텐츠식별체계(UCI, Universal Content Identifier)는 식별 가능한 자원의 유통과 활용을 위해 개별 자원에 유일한 코드를 부여하고 관리하며, 서로 다른 식별 체계를 연계하는 표준 체계다. 책의 ISBN이나 논문의 DOI처럼 콘텐츠마다 고유한 번호를 붙이는 것으로, 위원회는 UCI가 2008년 방송통신표준으로 채택되었고 2012년부터 위원회가 총괄 기구를 맡고 있다고 소개한다. 음원 분야에서는 곡마다 식별 코드를 붙여 두면 권리 정보 조회와 정산에서 같은 곡을 정확히 가리킬 수 있다. K-POP 음악 검색 사이트는 이 체계로 등록된 음원 정보를 조회하는 곳이다.
- https://www.uci.or.kr/?menuno=1
- http://uci.k-pop.or.kr/search/music
- https://www.copyright.or.kr/business/uci/index.do
JPA에서 엔티티 저장과 새 엔티티 판단
Spring Data JPA의 save()는 엔티티가 새것이면 EntityManager.persist()를, 아니면 merge()를 호출한다. 문서에 따르면 새것인지 판단하는 기본 전략은 이렇다. 래퍼 타입의 @Version 필드가 있으면 그 값이 null일 때 새 엔티티로 보고, 없으면 식별자가 null일 때 새 엔티티로 본다. 기본형 int 버전 필드는 0이 첫 버전이라 판단에 쓸 수 없다.
문제는 ID를 직접 할당하는 엔티티다. 저장 전에 이미 ID가 채워져 있으므로 Spring Data는 기존 엔티티로 보고 merge()를 호출하고, Hibernate는 병합하기 위해 먼저 SELECT를 실행한다. 새 행을 넣을 뿐인데 조회 쿼리가 하나 더 나가는 것이다. 문서는 이런 경우 다음 절의 Persistable을 쓰라고 안내한다.
- https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.entity-persistence.saving-entites
커스텀 ID 생성 방법
자동 증가 키를 쓸 수 없거나 ORD-20230621-0001처럼 의미 있는 형식의 ID가 필요할 때 선택지가 몇 가지 있다.
- Hibernate 커스텀 생성기: Thorben Janssen의 글은
SequenceStyleGenerator를 상속해configure()에서 파라미터를 읽고generate()에서 시퀀스 값에 접두사나 날짜를 붙이는 방법을 보여 주고,@GenericGenerator로 엔티티 ID에 연결한다. DB 시퀀스의 최적화를 그대로 쓰면서 형식만 입힐 수 있다. - Spring Data JDBC: Spring 블로그 글은 ID를 직접 할당하는 방법과
BeforeConvertCallback빈에서 새 애그리거트에만 ID를 생성해 넣는 방법을 소개한다. 비즈니스 코드에 ID 생성 로직을 두고 싶지 않을 때 콜백이 낫다고 설명한다. 애플리케이션에서 생성: UUID나 시간 기반 ID를 생성자에서 만들어 넣는다. 이 경우 앞 절의 새 엔티티 판단 문제가 생긴다.
- https://spring.io/blog/2021/09/09/spring-data-jdbc-how-to-use-custom-id-generation
- https://github.com/spring-projects/spring-data-examples/tree/main/jdbc/howto/idgeneration
- https://thorben-janssen.com/custom-sequence-based-idgenerator/
- https://yoonbing9.tistory.com/39
Persistable
Persistable<ID>를 구현하면 Spring Data가 새 엔티티 판단을 엔티티의 isNew()에 맡긴다. Spring Data JPA 문서가 제시하는 흔한 패턴은 @Transient boolean isNew = true 플래그를 두고, @PostPersist와 @PostLoad 콜백에서 false로 바꾸는 공통 기반 클래스다. 이렇게 하면 직접 할당한 ID를 가진 엔티티도 처음 save()할 때는 persist()가 호출되어 불필요한 SELECT가 사라진다. 다른 방법으로 생성 시각 필드(@CreatedDate)가 null인지로 isNew()를 판단하기도 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@MappedSuperclass
public abstract class AssignedIdEntity implements Persistable<String> {
@Transient
private boolean isNew = true;
@Override
public boolean isNew() {
return isNew;
}
@PostPersist
@PostLoad
void markNotNew() {
this.isNew = false;
}
}
- https://stackoverflow.com/questions/26313044/preferred-way-to-implement-persistable-isnew-for-entity-with-predefined-id-in-sp
- https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Persistable.html
- https://leegicheol.github.io/jpa/jpa-is-new/
- https://catsbi.oopy.io/69a3e741-f165-4046-a6eb-2002143e5dae
- https://neate.dev/technology/java/2020/04/15/using-persistable-for-jpa-performance-improvements.html
- https://thorben-janssen.com/spring-data-jpa-state-detection/
- https://jaime-note.tistory.com/65
SharedSessionContractImplementor
Hibernate의 IdentifierGenerator.generate()를 구현하면 첫 번째 인자로 SharedSessionContractImplementor가 들어온다. 이는 Session과 StatelessSession이 공유하는 내부 계약(SPI) 인터페이스로, 생성기 안에서 현재 세션의 JDBC 연결이나 쿼리, 저장 프로시저 호출에 접근할 수 있게 해 준다. 패키지 이름(org.hibernate.engine.spi)이 말해 주듯 애플리케이션 코드가 아니라 확장 구현용 API라서 Hibernate 버전에 따라 시그니처가 바뀔 수 있다. 링크한 Javadoc이 5.2, 5.4, 최신 버전으로 나뉘어 있는 것도 그래서 버전별로 확인하기 위해서다.
- https://docs.jboss.org/hibernate/orm/5.2/javadocs/org/hibernate/engine/spi/SharedSessionContractImplementor.html
- https://docs.jboss.org/hibernate/orm/current/javadocs/org/hibernate/engine/spi/SharedSessionContractImplementor.html
- https://javadoc.io/static/org.hibernate/hibernate-core/5.4.9.Final/org/hibernate/engine/spi/SharedSessionContractImplementor.html
@Transient
JPA의 @Transient는 필드를 영속 대상에서 제외한다. 테이블 컬럼에 매핑되지 않고 저장, 조회에서 무시되므로 위 Persistable 예제의 isNew 플래그나 계산된 값을 담는 데 쓴다. Java 키워드 transient는 직렬화에서 제외한다는 뜻이고 JPA도 이를 영속 제외로 취급하지만, 직렬화(예: 세션 복제, 캐시)에는 포함하면서 DB에서만 빼고 싶을 때는 애너테이션이 필요하다. JPA가 별도 애너테이션을 둔 이유가 이 구분이다. 또 Jackson의 @JsonIgnore와는 무관하므로 JSON 응답에서 빼려면 따로 처리해야 한다.
- https://gmoon92.github.io/jpa/2019/09/29/what-is-the-transient-annotation-used-for-in-jpa.html
- https://stackoverflow.com/questions/2154622/why-does-jpa-have-a-transient-annotation
- https://www.baeldung.com/jpa-transient-ignore-field
IdentifierGenerator
우아한형제들 글은 기존 시스템이 저장 프로시저로 기본 키를 발급하고 있어 자동 증가 값을 쓸 수 없던 상황에서 Hibernate의 IdentifierGenerator와 Configurable을 구현한 사례다. generate()에서 세션의 createStoredProcedureCall()로 기존 프로시저를 호출해 키를 받고, configure()에서는 @GenericGenerator의 parameters로 넘긴 값을 읽는다. 생성기 클래스에는 public 기본 생성자가 있어야 한다는 점도 짚는다.
- https://techblog.woowahan.com/2607/
엔티티에 스프링 빈 주입하기
엔티티 자체가 빈으로 등록되는게 아니므로 강제로 DB 커넥션에 대한 세션을 얻어와서 쿼리를 실행시켜야 한다. 하지만 이는 JPA의 영속계층에 대한 보호를 받지 못한다. 따라서 엔티티 안에서는 쿼리를 사용하지 않는 것이 좋다.
엔티티는 JPA가 리플렉션으로 생성하는 객체라 스프링 컨테이너가 의존성을 주입하지 않는다. Baeldung 글은 @Configurable과 AspectJ 위빙으로 컨테이너 밖 객체에 빈을 주입하는 방법을 소개하지만, 설정이 무겁고 엔티티가 인프라에 의존하게 된다. 엔티티에 필요한 값은 서비스 계층에서 계산해 메서드 인자로 넘기는 편이 테스트하기도 쉽다. 함께 저장한 질문처럼 저장 전에 ID를 알아야 하는 경우라면, 앞에서 본 것처럼 ID를 애플리케이션에서 생성해 할당하는 방식이 깔끔하다.
- https://www.baeldung.com/spring-inject-bean-into-unmanaged-objects
- https://stackoverflow.com/questions/12742826/how-do-i-know-the-id-before-saving-an-object-in-jpa
댓글
아직 댓글이 없습니다