HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
✍🏻
Learnary (learn - diary)
/
[JPA] - @OneToOne 관계의 N+1문제

[JPA] - @OneToOne 관계의 N+1문제

progress
Done
Tags
Spring
@OneToOne 관계에서 N + 1이 발생하는 이유Thumbnail Image 테이블에서 file_id 외래키를 가지고 있을 때 왜 N+1이 발생 할까?그러면 왜 1:N관계에서는 Lazy Loading이 적용될까?Thumbnail Image에서는 왜 지연로딩이 잘 적용이 될까?
 

@OneToOne 관계에서 N + 1이 발생하는 이유

양방향 관계에서 발생하게 된다.
결론적으로 프록시의 한계로 인해 발생하는 기술적 문제이지만, 왜 발생하는지는 조금있다가 보겠다.
 
 
 

Thumbnail Image 테이블에서 file_id 외래키를 가지고 있을 때

[Entity 측면에서 Thumbnail Image 연관관계의 주인, file은 가짜 주인]
notion image
 
[code]
@Entity public class File { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long Id; @OneToOne(mappedBy = "file", fetch = FetchType.LAZY, cascade = CascadeType.ALL) private ThumbnailImage thumbnailImage; public void setThumbnailImage(ThumbnaulImage thumbnailImage) { this.thumbnailImage = thumbnailImage; thumbnailImage.setFile(this); } } @Entity public class ThumbnailImage { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long Id; @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "file_id") private File file; } @RequiredArgsConstructor @Service public class FileService { private final FileRepository fileRepository; public List<FileResponseDto> getFiles() { return fileRepository.findAll().stream() .map(FileResponseDto::from) .collect(Collectors.toList()); } } ================================== 결과 ================================== Hibernate: select file0_.id as id1_0_0_, file0_.file_size as file_siz2_0_0_, file0_.filename as filename3_0_0_, file0_.thumbnail_image_id as thumbnai4_0_0_ from file file0_ where file0_.id=? Hibernate: insert into thumbnail_image (thumbnail_image_name, thumbnail_image_size) values (?, ?) Hibernate: update file set file_size=?, filename=?, thumbnail_image_id=? where id=?
 
파일 객체를 조회하는 서비스를 테스트해보면 N+1이 발생한다.
file 한 객체들을 조회함과 동시에 연관되있는 thumbnail 까지 같이 조회된다.
@RequiredArgsConstructor @Service public class FileService { private final FileRepository fileRepository; public List<FileResponseDto> getFiles() { return fileRepository.findAll().stream() .map(FileResponseDto::from) .collect(Collectors.toList()); } } ========================= 결과 ==================================== Hibernate: select file0_.id as id1_0_, file0_.file_size as file_siz2_0_, file0_.filename as filename3_0_ from file file0_ Hibernate: select thumbnaili0_.id as id1_1_0_, thumbnaili0_.file_id as file_id4_1_0_, thumbnaili0_.thumbnail_image_name as thumbnai2_1_0_, thumbnaili0_.thumbnail_image_size as thumbnai3_1_0_ from thumbnail_image thumbnaili0_ where thumbnaili0_.file_id=? Hibernate: select thumbnaili0_.id as id1_1_0_, thumbnaili0_.file_id as file_id4_1_0_, thumbnaili0_.thumbnail_image_name as thumbnai2_1_0_, thumbnaili0_.thumbnail_image_size as thumbnai3_1_0_ from thumbnail_image thumbnaili0_ where thumbnaili0_.file_id=? Hibernate: select thumbnaili0_.id as id1_1_0_, thumbnaili0_.file_id as file_id4_1_0_, thumbnaili0_.thumbnail_image_name as thumbnai2_1_0_, thumbnaili0_.thumbnail_image_size as thumbnai3_1_0_ from thumbnail_image thumbnaili0_ where thumbnaili0_.file_id=? Hibernate: select thumbnaili0_.id as id1_1_0_, thumbnaili0_.file_id as file_id4_1_0_, thumbnaili0_.thumbnail_image_name as thumbnai2_1_0_, thumbnaili0_.thumbnail_image_size as thumbnai3_1_0_ from thumbnail_image thumbnaili0_ where thumbnaili0_.file_id=? Hibernate: select thumbnaili0_.id as id1_1_0_, thumbnaili0_.file_id as file_id4_1_0_, thumbnaili0_.thumbnail_image_name as thumbnai2_1_0_, thumbnaili0_.thumbnail_image_size as thumbnai3_1_0_ from thumbnail_image thumbnaili0_ where thumbnaili0_.file_id=?
 

왜 N+1이 발생 할까?

JPA 객체 관점 메타 확인
File Entity는 mappedBy 속성을 가진 연관관계의 주인이 아니며 그리고 지연로딩(LAZY)이 적용되어있다.
 
테이블 관점 메타 확인
테이블 관점에서 보면 File 테이블(외래키가 없는 테이블)에서 Thumbnail Image(외래키가 있는 테이블)을 조회할 수 있다는 특징 (JPA의 양방향 매핑의 편리함)
 
다시 코드를 살펴보면 ThumbnailImage Entity는 외래키를 가지고 있는 테이블과 연결되어 있는 엔티티이기 때문에 객체의 관점에서도 연관관계의 주인이 된다. 객체관점에서(애노테이션 없이) 서로 주인 처럼 보인다는 것이다.
@Entity publicclassThumbnailImage { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String thumbnailImageName; private String thumbnailImageSize; @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) private File file; }
 
즉 @OneToOne 관계에서는 연관관계 주인이 아닌 곳에서 조회할 때 N + 1 문제가 발생한다.
notion image
 
file 테이블에는 thumbnail_id 라는 외래키가 없기 때문에 File Entity 입장에서는 File에 연결되어 있는 Thumbnail Image가 null 인지 아닌지를 조회해보기 전까지는 알 수 없다.
또한 LAZY 로딩이어서 프록시 객체를 사용할 것처럼 보이지만, 실제로는 Proxy 객체를 사용하지 않고 있다.
그 이유는 Proxy 객체를 만들기 위해서는 Thumbnail Image 객체가 null인지 값이 있는지를 알아야 하는데, File Entity 객체 관점으로는 알 수 없기 때문이다.
그래서 Thumbnail Image를 조회하는 쿼리들이 실행되는 것이다.
이렇게 쿼리들을 실제로 조회를 하면 영속성 컨텍스트에 엔티티들이 올라오기 때문에 프록시 객체를 사용할 이유가 없어져서 LAZY 로딩으로 설정하여도 즉시 로딩처럼 동작하는 것이다.
 
JPA 문서 부분
지연 로딩을 설정하여도 즉시 로딩으로 동작하는 이유는 JPA의 구현체인 Hibernate 에서 프록시 기능의 한계로 지연 로딩을 지원하지 못하기 때문에 발생한다. bytecode instrumentation을 사용하면 해결할 수 있다.
Reference: JPA ORM 프로그래밍
 
 
해당 링크를 보면 즉, 연관관계 주인이 아닌 테이블에서는 프록시로 만들 객체가 null 인지 아닌지 알 수 없기 때문에 조회하는 쿼리가 실행되는 것입니다.라고 하고 있다.
The reason for this is that owner entity MUST know whether association property should contain a proxy object or NULL and it can't determine that by looking at its base table's columns due to one-to-one normally being mapped via shared PK, so it has to be eagerly fetched anyway making proxy pointless.
[자세한 설명]
How can I make a JPA OneToOne relation lazy
In this application we are developing, we noticed that a view was particularly slow. I profiled the view and noticed that there was one query executed by hibernate which took 10 seconds even if there
How can I make a JPA OneToOne relation lazy
https://stackoverflow.com/questions/1444227/how-can-i-make-a-jpa-onetoone-relation-lazy
How can I make a JPA OneToOne relation lazy
 
 

그러면 왜 1:N관계에서는 Lazy Loading이 적용될까?

그러면 @OneToMany 관계에서도 연관관계 주인이 아니기 때문에 똑같이 Proxy가 적용되지 않아야 맞는거 아닐까? 라는 생각을 했다.
 
@Entity publicclassUser { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String nickname; private String part; @OneToMany(mappedBy = "user") private List<Post> posts = new ArrayList<>(); }
@OneToMany 경우라면 위와 같이 List 형태로 참조하고 있을 것이다.
@OneToMany는 @OneToOne과 다르게 Lazy Loading이 적용이 잘 되고 있다. 그 이유는?
 
위에서 말했던 링크 에서 답이 나와 있다.
many-to-one associations (and one-to-many, obviously) do not suffer from this issue. Owner entity can easily check its own FK (and in case of one-to-many, empty collection proxy is created initially and populated on demand), so the association can be lazy.
요약하자면, @OneToMany 관계는 빈 컬렉션이 초기화될 때(new ArrayList<>() 할 때) Proxy가 생긴다. 이다.
다시 말하면 posts 자체는 null이 아니고 size 자체가 0일 수 있는 것이기 때문에 @OneToMany 관계는 @OneToOne과 다르게 Lazy Loading이 가능했던 것이였다.
 
간략하게 설명하면 초기화(연관 객체의 여부 판단을 위한 행위)를 진행할 때 컬렉션이 초기화 되는 것인가 혹은 객체가 초기화 되는 것이다. 이다.
 
 

Thumbnail Image에서는 왜 지연로딩이 잘 적용이 될까?

@Entity publicclassThumbnailImage { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String thumbnailImageName; private String thumbnailImageSize; @OneToOne(fetch = FetchType.LAZY) private File file; }
Thumbnail Image Entity를 보면 연관 관계의 주인이다.
즉, Thumbnail Image 테이블에서 file_id 외래키를 가지고 있기 때문에 Thumbnail Image 객체 입장에서 굳이 File Entity를 조회해보지 않아도 File Entity가 존재하는지 안하는지를 알 수 있다.
그렇기에 프록시 객체도 만들 수 있어서 Thumbnail Image를 통해서 File을 조회했을 때 지연로딩이 적용될 수 있는 것이다
 
 
[JPA] @OneToOne, 일대일[1:1] 관계
일대일 [1:1]일대일 관계는 그 반대도 일대일이다.일대일 관계는 특이하게 주 테이블이나 대상 테이블 중에 외래 키를 넣을 테이블을 선택 가능하다.주 테이블에 외래 키 저장대상 테이블에 외래 키 저장외래 키에 데이터베이스 유니크 제약조건 추가되어야 일대일 관계가 된다.일대일 - 주 테이블에 외래 키 단방향회원이 딱 하나의 락커를 가지고 있는 상황이다. 반대로 락커도 회원 한명만 할당 받을 수 있는 비즈니스 적인 룰이 있고, 이때, 둘의 관계는 일대일 관계이다.이 경우 멤버를 주 테이블로 보고 주 테이블 또는 대상 테이블에 외래 키를 저장할 수 있다. 단, 유니크 제약조건을 추가한 상태에서만.다대일[N:1] 단방향 관계 매핑과 JPA 어노테이션만 달라지고, 거의 유사하다.일대일 - 주 테이블에 외래 키 양..
[JPA] @OneToOne, 일대일[1:1] 관계
https://ict-nroo.tistory.com/126
[JPA] @OneToOne, 일대일[1:1] 관계