Recent Posts
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
Tags
- k8s
- 자원부족
- mp4fpsmod
- 코루틴 컨텍스트
- JanusWebRTC
- 오블완
- tolerated
- 달인막창
- terminal
- JanusGateway
- k8s #kubernetes #쿠버네티스
- 헥사고날아키텍처 #육각형아키텍처 #유스케이스
- PersistenceContext
- JanusWebRTCGateway
- pytest
- JanusWebRTCServer
- kotlin
- taint
- 코루틴 빌더
- PessimisticLock
- 개성국밥
- vfr video
- python
- OptimisticLock
- Spring Batch
- 겨울 부산
- 티스토리챌린지
- Kubernetes
- 깡돼후
- preemption #
Archives
너와 나의 스토리
[SpringBoot] JPA 양뱡향 매핑은 불필요한 것인가? Fetch 전략과 N+1 문제 본문
반응형
본 포스팅은 "[NHN Cloud 2019] Spring JPA의 사실과 오해" 강연 내용을 정리한 글입니다.
연관관계 매핑 - 단방향 vs 양방향
- 사실상 단방향 매핑만으로 연관관계 매핑은 이미 완료
- 어차피 연관관계 매핑은 내부적으로 foreign key를 이용하게 되는데, foreign key는 결국 하나이기 때문에
- 양방향 매핑은 양쪽에서 서로에 대한 설정을 해줘야 하기 때문에 복잡해짐.
- 그에 비해 반대쪽 방향으로 객체 그래프 탐색 기능 추가된 게 유일한 이점임.
- 결론
- 대개의 경우 단방향 매핑이면 충분하다
- 우선은 단방향 매핑을 사용하고 반대 방향으로의 객체 그래프 탐색이 필요할 때 양방향을 사용
다대일(N:1) 단방향 연관관계 매핑
@Entity
public class Member {
@Id
@Column(name ="member_id")
private Long memberId;
...
}
@Entity
public class MemberDetail {
@Id
@Column(name ="member_detail_id")
private Long memberDetailId;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "member_id")
private Member member;
...
}
Cascade: 영속성 전이
- Entity의 영속성 상태 변화를 연관된 Entity에도 함께 적용하는 것
- Cascade Type
- PERSIST
- Entity를 영속 객체로 추가할 때 연관된 Entity도 함께 영속 객체로 추가한다.
- REMOVE
- Entity를 삭제할 때 연관된 Entity도 함께 삭제한다.
- DETACH
- Entity를 영속성 컨텍스트에서 분리할 때 연관된 Entity도 함께 분리 상태로 만든다.
- REFRESH
- Entity를 데이터베이스에서 다시 읽어올 때 연관된 Entity도 함께 다시 읽어온다.
- MERGE
- Entity를 준영속 상태에서 다시 영속 상태로 변경할 때 연관된 Entity도 함께 변경한다.
- ALL
- 모든 상태 변화에 대해 연관된 Entity에 함께 적용한다.
- PERSIST
Case1: 연속성 전이를 통한 insert
@Transactional
public void createMemberWithDetails() {
Member member = new Member("member1", LocalDateTime.now());
MemberDetail memberDetail1 = new MemberDetail(member, "type1", "description1");
MemberDetail memberDetail2 = new MemberDetail(member, "type2", "description2");
membserDetailRepository.saveall(Arrays.asList(memberDetail1, memberDetail2));
}
- 실제 수행 결과
- members 테이블에 member INSERT
- member_details 테이블에 member_details1 INSERT
- member_details 테이블에 member_details2 INSERT
일대다(1:N) 단방향 연관관계 매핑
@Entity
public class Member {
@Id
@Column(name ="member_id")
private Long memberId;
@OneToMany(casecade = CascadeType.ALL)
@JoinColumn(name = "member_id")
private List<MemberDetail> details;
...
}
@Entity
public class MemberDetail {
@Id
@Column(name ="member_detail_id")
private Long memberDetailId;
...
}
Case2: 연속성 전이를 통한 insert
@Transactional
public void createMemberWithDetails() {
Member member = new Member("member1", LocalDateTime.now());
Member savedMember = memberRepository.save(member);
MemberDetail memberDetail1 = new MemberDetail(member, "type1", "description1");
MemberDetail memberDetail2 = new MemberDetail(member, "type2", "description2");
member.getDetails().add(memberDetail1);
member.getDetails().add(memberDetail2);
}
- 실제 수행 결과
- members 테이블에 member INSERT
- member_details 테이블에 member_details1 INSERT
- member_details 테이블에 member_details2 INSERT
- member_details 테이블에서 member_id UPDATE
- member_details 테이블에서 member_id UPDATE
- 추가적으로 업데이트 쿼리가 추가됨
- 즉, 일대다(1:N) 단방향 연관관계 매핑에서 연속성 전이를 통해 insert를 하게 되면
- 일대다 관계의 외래 키(FK) 지정을 위해 추가적인 update 쿼리가 발생하는 문제가 생김
- 이 경우에는 오히려 일대다 양방향 연관관계로 변경하면 추가적인 update 쿼리가 없어짐.
일대다(1:N) 양방향 매핑
@Entity
public class Member {
@Id
@Column(name ="member_id")
private Long memberId;
@OneToMany(casecade = CascadeType.ALL, mappedBy = "member")
private List<MemberDetail> details;
...
}
@Entity
public class MemberDetail {
@EmbeddedId
private PK pk;
private String description;
@ManyToOne
@MapsId("memberId") // 복합키의 memberId를 member 연관관계와 매핑 -> pk.memberId와 member.memberId는 항상 동일한 값이 됨.
private Member member;
@Embeddable
public static class Pk implements Serializable {
@Column(name = "member_id")
private Long memberId;
private String type;
}
...
}
- 양방향으로 설정하는 경우, 연관관계의 주인을 설정하는 게 중요하다. 내부적으로는 FK를 사용하기 때문이다.
- 결과적으로 FK를 가지고 있는쪽이 연관관계의 주인이 된다.
- 예제에서 보면 MemberDetail에서 MapsId
- Member는 연관관계의 주인이 아니기 때문에 join column을 쓰면 안 된다.
- @JoinColumn: 이 필드가 FK임을 명시.
- mappedBy: "나는 주인이 아니다"라는 선언
- @MapsId: 자식 엔티티의 PK와 FK를 공유할 때 사용
- 즉, member_detail의 PK는 (member_id, type) 복합키이고, 그 중 member_id는 member 테이블의 FK 역할을 동시에 한다.
- 이렇게 구현 후, 아까 예제를 수행하면, 업데이트 쿼리가 추가적으로 발생하지 않게 된다.
그렇다면 항상 ManyToOne 단방향만 쓰는 게 정답일까?
| 상황 | 추천 방식 |
| 대부분 MemberDetail 중심으로 접근하고, Member에서 details를 잘 안 쓰는 경우 | ManyToOne 단방향 |
| Member에서 details를 자주 조회해야하는 경우 | 양방향 (mappedBy 설정) |
| OneToMany 단방향 | 비추 (쿼리 비효율적) |
- FK는 항상 "다"쪽(@ManyToOne)에서 관리하는 게 정석이고, "일"쪽(@OneToMany)는 필요할 때만 컬렉션으로 접근할 수 있게 양방향을 열어주는 게 가장 실용적이다.
Fetch 전략
- 전략
- FetchType.EAGER
- 하나의 entity를 가져올 때, 연관관계에 있는 entity를 즉시 가져오는 것
- @OneToOne, @ManyToOne
- FetchType.LAZY
- 실제 참조가 이뤄졌을 때 연관관계에 있는 entity 값을 가져오는 것
- @OneToMany, @ManyToMany
- FetchType.EAGER
N+1 문제
- 연관된 entity를 가지고 올 때, 우리가 의도한 것과 달리 추가적으로 쿼리를 N번 추가적으로 수행하는 문제
- 해결 방법
- Fetch Join
- Entity Graph
오해1: N+1 문제는 EAGER Fetch 전략 때문에 발생하는가?
- Fetch 전략을 LAZY로 설정했더라도 연관 Entity를 참조하면 그 순간 추가적인 쿼리가 수행됨.
- Fetch 전략은 시점 차이지, 똑같이 문제 발생.
오해2: findAll() 메서드 N+1 문제가 발생하지 않는가?
- fetch 전략을 적용해서 연관 entity를 가져오는 것은 오직 단일 레코드에서만 적용
- 단일 레코드 조회가 아닌 경우 해당 JPQL을 먼저 수행하고 반환된 레코드 하나 하나에 대해 entity에 설정된 fetch 전략을 적용해서 연관 entity 가져옴.
- 예를 들어, findAll()로 Member 목록을 가져온 직후, JPA는 단순히 Member 객체들만 영속성 컨텍스트에 올린다.
- 이후, member.getDetails()를 호출하면 그때, fetch 전략을 수행함.
- 즉, 연관 entity를 조회하는 시점에서 결국 N+1이 발생.
- 그렇기 때문에 findAll() 메서드 호출도 역시 이 과정에서 N+1 문제 발생 가능
N+1 해결을 위해 Fetch Join 사용
- 흔히 하는 실수 1: Pagination + Fetch JOIN
- Pagination 쿼리에 Fetch Join을 적용하면 실제로는 모든 레코드르 가져와서 조인한 후에 Pagination처리가 됨.
- 즉, 분리해서 실행해야한다.
반응형
'개발 > Spring Boot' 카테고리의 다른 글
| Spring JPA 트랜잭션 관리 - 낙관적 락, 비관적 락 (0) | 2025.09.28 |
|---|---|
| Dapr란? 마이크로서비스 운영 최적화하기 - 개념, 기능, 사용 방법 (1) | 2025.03.10 |
| 설정 파일(*.properties, *.yml)에 있는 값들을 자바 클래스로 바인딩해서 사용하기 - @ConfigurationProperties (0) | 2023.07.29 |
| Spring Boot 버전 업그레이드에 따른 변경 사항 / Spring Cloud, Gradle 등 (0) | 2023.03.29 |
| [SpringBoot] JPA Converter 적용 / Converter null 처리 (1) | 2023.03.05 |
Comments