개요
이전 글에서 Lazy와 Eager 중 Lazy를 사용해 성능을 튜닝해야 한다는 이야기를 했었다. 이번 포스팅에서는 JPA가 내부적으로 Lazy와 Eager 로딩을 어떻게 구분하는지, 그리고 이를 판단하는 주체가 누구인지, 그 주체가 어떻게 동작하는지 더 깊이 있게 파고들어 보자.
(hibernate 5.4.18.Final 기준)
연관 관계 어노테이션 처리 (@ManyToOne, @OneToOne, 등)
연관 관계와 Lazy인지 Eager인지를 판단하는 주체는 하이버네이트 기준 구현체 내부의 AnnotationBinder 클래스이다. AnnotationBinder 클래스 내부의 processElementAnnotations 메소드를 살펴보자
전체적인 메소드 흐름
processElementAnnotations 메서드는 엔티티의 프로퍼티에 적용된 다양한 어노테이션 분석하여 Hibernate의 매핑 메타데이터를 설정한다. 특히, 연관 관계 어노테이션(@OneToOne, @OneToMany, @ManyToOne, @ManyToMany)을 처리하면서 Lazy Loading과 Eager Loading 전략을 설정한다. 각 연관 관계 어노테이션이 코드 내에서 어떻게 처리되는지 상세히 살펴보겠다.
@OneToOne 어노테이션 처리
if (property.isAnnotationPresent(OneToOne.class)) {
OneToOne ann = (OneToOne)property.getAnnotation(OneToOne.class);
if (property.isAnnotationPresent(Column.class) || property.isAnnotationPresent(Columns.class)) {
throw new AnnotationException("@Column(s) not allowed on a @OneToOne property: " + BinderHelper.getPath(propertyHolder, inferredData));
}
isOverridden = property.isAnnotationPresent(PrimaryKeyJoinColumn.class) || property.isAnnotationPresent(PrimaryKeyJoinColumns.class);
Cascade hibernateCascade = (Cascade)property.getAnnotation(Cascade.class);
NotFound notFound = (NotFound)property.getAnnotation(NotFound.class);
ignoreNotFound = notFound != null && notFound.action().equals(NotFoundAction.IGNORE);
boolean mandatory = !ann.optional() || property.isAnnotationPresent(Id.class) || (property.isAnnotationPresent(MapsId.class) && !ignoreNotFound);
matchIgnoreNotFoundWithFetchType(propertyHolder.getEntityName(), property.getName(), ignoreNotFound, ann.fetch());
OnDelete onDeleteAnn = (OnDelete)property.getAnnotation(OnDelete.class);
ignoreNotFound = onDeleteAnn != null && OnDeleteAction.CASCADE.equals(onDeleteAnn.action());
JoinTable assocTable = propertyHolder.getJoinTable(property);
if (assocTable != null) {
Join join = propertyHolder.addJoin(assocTable, false);
for(Ejb3JoinColumn joinColumn : joinColumns) {
joinColumn.setExplicitTableName(join.getTable().getName());
}
}
bindOneToOne(
getCascadeStrategy(ann.cascade(), hibernateCascade, ann.orphanRemoval(), forcePersist),
joinColumns,
!mandatory,
getFetchMode(ann.fetch()),
ignoreNotFound,
ignoreNotFound,
ToOneBinder.getTargetEntity(inferredData, context),
propertyHolder,
inferredData,
ann.mappedBy(),
isOverridden,
isIdentifierMapper,
inSecondPass,
propertyBinder,
context
);
}
어노테이션 감지 및 초기 설정
- 현재 프로퍼티에 @OneToOne 어노테이션이 있는지 확인.
- 있다면, 해당 어노테이션 인스턴스를 ann 변수에 저장.
충돌 검사
- @OneToOne 관계에서는 @Column 또는 @Columns 어노테이션을 사용할 수 없으므로, 함께 사용 시 AnnotationException을 발생시킴.
오버라이드 확인
- @PrimaryKeyJoinColumn 또는 @PrimaryKeyJoinColumns 어노테이션이 있는지 확인하여 관계 매핑이 오버라이드되었는지 판단.
- 오버라이드된 경우 isOverridden를 true로 설정.
추가 어노테이션 처리
- @Cascade: 연관된 엔티티에 대한 캐스케이드 전략을 설정.
- @NotFound: 연관된 엔티티가 없을 때의 동작을 정의.
- NotFoundAction.IGNORE인 경우 ignoreNotFound를 true로 설정.
- @OnDelete: 삭제 시 연관된 엔티티의 동작을 정의.
- OnDeleteAction.CASCADE인 경우 ignoreNotFound를 true로 설정.
FetchType 결정
- @OneToOne의 fetch 속성을 통해 FetchType.LAZY인지 FetchType.EAGER인지 결정.
- matchIgnoreNotFoundWithFetchType 메서드를 통해 @NotFound 설정과 결합하여 최종 ignoreNotFound 값 조정.
조인 테이블 설정
- @JoinTable 어노테이션이 있는 경우, 조인 테이블 설정 및 조인 컬럼에 명시적 테이블 이름 할당.
Mandatory 플래그 설정
- 관계가 필수인지 결정.
- optional=false, @Id, 또는 @MapsId 어노테이션이 있는 경우 mandatory를 true로 설정.
바인딩 호출
- bindOneToOne 메서드를 호출하여 실제 매핑 수행.
- FetchMode 값과 기타 설정을 전달하여 로딩 전략 반영.
@OneToMany 어노테이션 처리
if (property.isAnnotationPresent(OneToMany.class)) {
OneToMany oneToManyAnn = (OneToMany)property.getAnnotation(OneToMany.class);
ManyToMany manyToManyAnn = (ManyToMany)property.getAnnotation(ManyToMany.class);
// ... 예외 처리 및 설정
// FetchType 결정
FetchType fetchType = oneToManyAnn.fetch();
collectionBinder.setFetchMode(getFetchMode(fetchType));
// mappedBy 설정
String mappedBy = oneToManyAnn.mappedBy();
collectionBinder.setMappedBy(mappedBy);
// Target Entity 설정
collectionBinder.setTargetEntity(context.getBootstrapContext().getReflectionManager().toXClass(oneToManyAnn.targetEntity()));
// Cascade 설정
collectionBinder.setCascadeStrategy(getCascadeStrategy(oneToManyAnn.cascade(), hibernateCascade, oneToManyAnn.orphanRemoval(), false));
// OneToMany 플래그 설정
collectionBinder.setOneToMany(true);
// 바인딩 호출
collectionBinder.bind();
}
어노테이션 감지 및 초기 설정
- 현재 프로퍼티에 @OneToMany 어노테이션이 있는지 확인.
- 있다면, 해당 어노테이션 인스턴스를 oneToManyAnn 변수에 저장.
추가 어노테이션 및 예외 처리
- @OneToMany와 @ManyToMany 어노테이션이 동시에 적용된 경우 예외 발생.
- 특정 상황에서 @OneToMany, @ManyToMany, @ElementCollection 등이 함께 사용될 경우 예외 처리.
FetchType 결정
- @OneToMany의 fetch 속성을 통해 FetchType.LAZY인지 FetchType.EAGER인지 결정.
- getFetchMode(fetchType)을 통해 Hibernate의 FetchMode로 변환하여 설정.
MappedBy 설정
- @OneToMany(mappedBy = "propertyName")를 통해 관계의 주체 정의.
- 이를 collectionBinder에 전달하여 반대편 엔티티의 필드와의 관계 설정.
Target Entity 설정
- @OneToMany의 targetEntity 속성을 통해 연관된 엔티티 클래스 설정.
- collectionBinder.setTargetEntity(...)을 통해 설정.
Cascade 설정
- @OneToMany의 cascade 속성과 @Cascade 어노테이션을 결합하여 캐스케이드 전략 설정.
- orphanRemoval 속성을 통해 고아 객체 제거 전략 설정 가능.
OneToMany 플래그 설정
- collectionBinder.setOneToMany(true)를 호출하여 해당 컬렉션이 @OneToMany 관계임을 설정.
바인딩 호출
- collectionBinder.bind()를 호출하여 실제 매핑 수행.
@ManyToOne 어노테이션 처리
if (property.isAnnotationPresent(ManyToOne.class)) {
ManyToOne ann = (ManyToOne)property.getAnnotation(ManyToOne.class);
if (property.isAnnotationPresent(Column.class) || property.isAnnotationPresent(Columns.class)) {
throw new AnnotationException("@Column(s) not allowed on a @ManyToOne property: " + BinderHelper.getPath(propertyHolder, inferredData));
}
Cascade hibernateCascade = (Cascade)property.getAnnotation(Cascade.class);
NotFound notFound = (NotFound)property.getAnnotation(NotFound.class);
lazy = notFound != null && notFound.action().equals(NotFoundAction.IGNORE);
matchIgnoreNotFoundWithFetchType(propertyHolder.getEntityName(), property.getName(), lazy, ann.fetch());
OnDelete onDeleteAnn = (OnDelete)property.getAnnotation(OnDelete.class);
ignoreNotFound = onDeleteAnn != null && OnDeleteAction.CASCADE.equals(onDeleteAnn.action());
JoinTable assocTable = propertyHolder.getJoinTable(property);
if (assocTable != null) {
Join join = propertyHolder.addJoin(assocTable, false);
for(Ejb3JoinColumn joinColumn : joinColumns) {
joinColumn.setExplicitTableName(join.getTable().getName());
}
}
boolean mandatory = !ann.optional() || property.isAnnotationPresent(Id.class) || (property.isAnnotationPresent(MapsId.class) && !lazy);
bindManyToOne(
getCascadeStrategy(ann.cascade(), hibernateCascade, false, forcePersist),
joinColumns,
!mandatory,
lazy,
ignoreNotFound,
ToOneBinder.getTargetEntity(inferredData, context),
propertyHolder,
inferredData,
false,
isIdentifierMapper,
inSecondPass,
propertyBinder,
context
);
}
어노테이션 감지 및 초기 설정
- 현재 프로퍼티에 @ManyToOne 어노테이션이 있는지 확인.
- 있다면, 해당 어노테이션 인스턴스를 ann 변수에 저장.
충돌 검사
- @ManyToOne 관계에서는 @Column 또는 @Columns 어노테이션을 사용할 수 없으므로, 함께 사용 시 예외 발생
추가 어노테이션 처리
- @Cascade: 연관된 엔티티에 대한 캐스케이드 전략을 설정.
- @NotFound: 연관된 엔티티가 없을 때의 동작을 정의.
- NotFoundAction.IGNORE인 경우 lazy를 true로 설정할 수 있음.
- @OnDelete: 삭제 시 연관된 엔티티의 동작을 정의.
- OnDeleteAction.CASCADE인 경우 ignoreNotFound를 true로 설정.
FetchType 결정
- @ManyToOne의 fetch 속성을 통해 FetchType.LAZY인지 FetchType.EAGER인지 결정.
- matchIgnoreNotFoundWithFetchType 메서드를 통해 @NotFound 설정과 결합하여 최종 lazy 값 조정.
조인 테이블 설정
- @JoinTable 어노테이션이 있는 경우, 조인 테이블 설정 및 조인 컬럼에 명시적 테이블 이름 할당.
Mandatory 플래그 설정
- 관계가 필수인지 결정.
- optional=false, @Id, @MapsId 어노테이션이 있는 경우 mandatory를 true로 설정.
바인딩 호출
- bindManyToOne 메서드를 호출하여 실제 매핑 수행.
- lazy 값과 기타 설정을 전달하여 로딩 전략 반영.
@ManyToMany 어노테이션 처리
if (property.isAnnotationPresent(ManyToMany.class)) {
ManyToMany manyToManyAnn = (ManyToMany)property.getAnnotation(ManyToMany.class);
// ... 예외 처리 및 설정
// FetchType 결정
FetchType fetchType = manyToManyAnn.fetch();
collectionBinder.setFetchMode(getFetchMode(fetchType));
// mappedBy 설정
String mappedBy = manyToManyAnn.mappedBy();
collectionBinder.setMappedBy(mappedBy);
// Target Entity 설정
collectionBinder.setTargetEntity(context.getBootstrapContext().getReflectionManager().toXClass(manyToManyAnn.targetEntity()));
// Cascade 설정
collectionBinder.setCascadeStrategy(getCascadeStrategy(manyToManyAnn.cascade(), hibernateCascade, false, false));
// OneToMany 플래그 설정 (ManyToMany는 OneToMany가 아님)
collectionBinder.setOneToMany(false);
// 바인딩 호출
collectionBinder.bind();
}
어노테이션 감지 및 초기 설정
- 현재 프로퍼티에 @ManyToMany 어노테이션이 있는지 확인.
- 있다면, 해당 어노테이션 인스턴스를 manyToManyAnn 변수에 저장.
추가 어노테이션 및 예외 처리
- @ManyToMany와 @OneToMany 또는 @ManyToMany와 다른 관계 어노테이션 동시에 적용된 경우 예외 발생.
- 특정 상황에서 @ManyToMany 어노테이션이 부적절하게 사용된 경우 예외 처리.
FetchType 결정
- @ManyToMany의 fetch 속성을 통해 FetchType.LAZY인지 FetchType.EAGER인지 결정.
- getFetchMode(fetchType)을 통해 Hibernate의 FetchMode로 변환하여 설정.
MappedBy 설정
- @ManyToMany(mappedBy = "propertyName")를 통해 관계의 주체 정의.
- 이를 collectionBinder에 전달하여 반대편 엔티티의 필드와의 관계 설정.
Target Entity 설정
- @ManyToMany의 targetEntity 속성을 통해 연관된 엔티티 클래스 설정.
- collectionBinder.setTargetEntity(...)을 통해 설정.
Cascade 설정
- @ManyToMany의 cascade 속성과 @Cascade 어노테이션을 결합하여 캐스케이드 전략 설정.
OneToMany 플래그 설정
- collectionBinder.setOneToMany(false)를 호출하여 해당 컬렉션이 @ManyToMany 관계임을 설정.
바인딩 호출
- collectionBinder.bind()를 호출하여 실제 매핑 수행.
공통 처리 요소
어노테이션 감지 및 읽기
- 각 연관 관계 어노테이션이 존재하는지 확인하고, 해당 어노테이션이 인스턴스를 추출.
예외 처리
- 연관 관계 설정 시 충돌하거나 잘못된 어노테이션 사용 시 예외 발생.
FetchType 결정
fetch 속성을 통해 FetchType.LAZY 또는 FetchType.EAGER를 결정하고, 이를 기반으로 Hibernate의 FetchMode 설정.
Cascade 설정
- cascade 속성과 @Cascade 어노테이션을 결합하여 캐스케이드 전략 설정.
MappedBy 설정
- 관계의 주체를 정의하기 위해 mappedBy 속성 설정. 이는 양방향 관계에서 필수적.
Target Entity 설정
- 연관된 엔티티 클래스를 설정하여, Hibernate가 어떤 엔티티와의 관계를 매핑할지 결정.
바인딩 호출
- 설정된 매핑 정보를 기반으로 적절한 바인딩 메서드(bindManyToOne, bindOneToOne, collectionBinder.bind()) 호출.
차별화된 처리 요소
- @OneToOne:
- @PrimaryKeyJoinColumn 또는 @PrimaryKeyJoinColumns 어노테이션을 통해 관계 매핑 오버라이드 확인.
- @OneToMany와 @ManyToMany
- 컬렉션 타입의 연관 관계를 처리하며, collectionBinder를 사용하여 매핑.
- @OneToMany는 setOneToMany(true)로 설정하고, @ManyToMany는 setOneToMany(false)로 설정하여 구분.
- @ManyToMany
- 양방향 다대다 관계를 처리하며, mappedBy 속성을 통해 관계의 주체 설정.
비교 요약
각 어노테이션 처리 방안을 보면 알겠지만 기본적으로 모두 비슷한 패턴을 띄고있다.
어노테이션 | FetchType 처리 | mappedBy 설정 | Target Entity 설정 | Cascade 설정 | OneToMany 플래그 | 예외 처리 및 추가 설정 |
@OneToOne | ann.fetch()을 통해 LAZY/EAGER 결정 | ann.mappedBy()을 통해 설정 | ToOneBinder.getTargetEntity()을 통해 설정 | @OneToOne의 cascade와 @Cascade 어노테이션 참조 | 해당 없음 | @Column과의 충돌 예외 발생, @PrimaryKeyJoinColumn 확인 |
@OneToMany | oneToManyAnn.fetch()을 통해 LAZY/EAGER 결정 | oneToManyAnn.mappedBy() 설정 | collectionBinder.setTargetEntity()을 통해 설정 | @OneToMany의 cascade와 @Cascade 어노테이션 참조 | true | @ManyToOne와의 충돌 예외 발생 등 |
@ManyToOne | ann.fetch()을 통해 LAZY/EAGER 결정 | 해당 없음 | ToOneBinder.getTargetEntity()을 통해 설정 | @ManyToOne의 cascade와 @Cascade 어노테이션 참조 | 해당 없음 | @Column과의 충돌 예외 발생 |
@ManyToMany | manyToManyAnn.fetch()을 통해 LAZY/EAGER 결정 | manyToManyAnn.mappedBy() 설정 | collectionBinder.setTargetEntity()을 통해 설정 | @ManyToMany의 cascade와 @Cascade 어노테이션 참조 | false | @OneToMany와의 충돌 예외 발생 등 |
'기술 탐구 > JPA' 카테고리의 다른 글
(JPA) hibernates 구현체는 어노테이션 메타데이터를 어떻게 관리할까? (feat: JavaXProperty, JavaXMember) (1) | 2024.12.27 |
---|---|
JPA 컬렉션 조회 최적화 (feat: hibernate.default_batch_fetch_size, @BatchSize, array_contains) (0) | 2024.12.25 |
JPA 연관 관계 조회 성능 최적화 (feat: One To One, Many To One, 지연 로딩, 패치 조인) (0) | 2024.12.25 |
(JPA) 영속성 컨텍스트에 대해 알아보자 (0) | 2024.07.14 |