| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- html #css #부트스트랩 #웹사이트 #개발 #초보 #til #내일배움캠프 #스파르타코딩클럽
- java
- sql #내일배움캠프 #스파르타코딩클럽
- sql #부트캠프 #내일배움캠프 #웹관리자 #도전 #학습
- java #문법
- Today
- Total
Hyeok의 웹 개발 블로그
<2025.05.07> 연관관계 N+1 본문
✅단방향 연관관계
- 단방향 연관관계는 아래와 같이 연관관계에 있는 두 객체 사이에 한 방향으로의 참조가 존재하는 상태
- Team -> Member
- Member -> Team
Team 입장에서는 Member 와 1:N 연관관계를 가지지만, Member 입장에서는 Team 과 N:1 연관관계이고 어떤 방향이든 두 객체 사이의 한 방향의 참조만 존재한다면 모두 단방향 연관관계라 할 수 있다.
✔ @OneToOne
- @OneToOne 연관관계는 두 테이블 / 객체가 1:1 연관관계일 때 사용
- 1:1연관관계에 있는 객체간 참조 방향을 어떻게할지 잘 결정해야함.
- 물리적으로 분리되어야하는 테이블인지 고민 (데이터의 생명주기 혹은 사용 패턴을 토대로 고민 가능)
✔ @ManyToOne
- 테이블상의 외래키 위치와 객체상의 참조 위치가 동일해 헷갈리지 않음
- 별다른 설정 없이도 우리가 생각한대로 SQL 쿼리가 잘 동작한다.
@Entity
public class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
private String name;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
private String name;
private int age;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
private LocalDateTime createdAt;
}
✔ @OneToMany
- RDB 의 외래키의 위치와 객체 참조의 위치가 다르다.
- 1번 문제로 인해 INSERT 쿼리 요청 시, 불필요한 UPDATE 쿼리가 발생.
- 단방향 연관관계로 사용할 때, @JoinColum을 반드시 사용해야함. 사용 안했을땐, 불필요한 연결 테이블이 생성될 가능성있음
- 연관관계에 있는 Entity(member)를 save() 해주지 않으면 제대로 저장 X
이런 경우 반드시 cascade 옵션 중 PERSIST 를 사용해야함.
- 단방향 연관관계의 방향으로 @OneToMany 를 사용했을 때 주의해야할 것들이 존재할 뿐이지 명확한 단점이 있는 것은 아니라고 생각한다.
- @OneToMany 연관관계는 사용하면 안돼! 가 아니라 이런 포인트에서의 주의해야할 부분이 있으니 Trade-Off 하자! 로 접근하는게 더 좋을 것 같다.
- 도메인을 정의할 때 더 자연스러운 방향으로 설계하는게 비즈니스 로직을 개발하는 과정에서 더 자연스럽고 더 잘 읽히는 코드를 만들 수 있다.
- 항상 같이 조회되는 상황에서는 @OneToMany 로 조회했을 때 코드가 자연스러워 진다.
- Post - Hashtag 처럼 Hashtag → Post 로 참조할 일이 없고 항상 Post → Hashtag 로만 참조하는 조회패턴을 가진다면 @OneToMany 를 적극 고려할 수 있다.
✅ 양방향 연관 관계
- 객체 사이에는 "양방향"이라는 개념 X
Team -> Member, Member -> Team 두 단방향 연관관계가 존재하는 상황을 양방향 연관관계라고 함.
✔ 양방향 연관관계를 지양해야하는 이유
- 양방향 연관관계를 지양해야한다는 말은 많이 들어봤을거라 생각한다! 그럼 왜 그럴까?
- 불필요한 연관관계로 인해 설계의 복잡도가 증가한다.
- 양방향 의존성은 순환참조 및 데이터 동기화 등에서 문제가 생길 가능성이 존재한다.
- 객체 사이의 과도한 결합으로 인해 서로 변경을 전파하게되어 변경에 유연하지 못한 구조가 된다. ”OOP 의 의존성” 에 대한 이야기!
- 객체의 독립성이 저하되어 테스트 코드를 작성하기 어렵다.
✔ 단점만 있는 기술은 없다!
- 양방향 연관관계는 그럼 절대 쓰지말아야하는 걸까?
- 그렇지 않다! 그럼 어떤 상황에서 양방향 연관관계를 고려해볼 수 있을까?
- 이미 @ManyToOne 연관관계를 사용하고 있는 상황에서 @OneToMany 방향으로의 조회(객체 참조)패턴이 많이 필요한 상황 ⇒ “단순 조회 목적!”
- @OneToMany 단방향 연관관계 매핑을 사용할 때 발생하는 “INSERT 쿼리시 연관관계 처리를 위한 UPDATE 쿼리가 함께 발생하는 문제” 를 해소하기 위한 상황 (대안 존재함!)
- 양방향 연관관계는 “필수” 가 아니라 “필요” 에 의해서 추가되어야한다는 것을 꼭 기억하자!
- 우선 하나의 단방향 연관관계로 시작하자!! 이후에 반대 방향의 객체 참조가 필요하다면 양방향 연관관계를 추가해도 테이블에 영향을 주지 않기 때문에 충분히 유연하다
✅ N+1
- Todo 와 Comment 는 1:N 연관관계를 가지는 테이블이다.
- Todo 와 Comment 객체는 양방향 연관관계 매핑을 맺고 있다.
- 양방향 연관관계를 사용한 이유는 @OneToMany, @ManyToOne 에 대한 N+1 문제를 하나의 예제코드에서 모두 살펴보기 위함이다!!
- 두 연관관계 매핑에서 모두 FetchType.EAGER 를 적용했다.
@Entity
public class Todo {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "todo_id")
private Long id;
private String contents;
private LocalDate date = LocalDate.now();
private boolean isComplete = false;
@OneToMany(mappedBy = "todo", fetch = FetchType.EAGER)
public List<Comment> comments = new ArrayList<>();
}
@Entity
public class Comment {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "comment_id")
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "todo_id")
private Todo todo;
private String contents;
}
✔ N+1 문제
N+1 문제는 JPA 를 이용해 연관관계에 있는 두 테이블의 데이터를 조회할 때 JOIN 을 이용한 SELECT 쿼리 1개가 발생할거라 기대했지만, JOIN 을 이용하지 않은 1개의 SELECT 쿼리 + 연관관계에 있는 데이터를 조회하기 위한 N개의 SELECT 쿼리로 총 N+1 개의 SELECT 쿼리가 발생하는 문제.
▶ ORM 이 가지는 고질적인 문제인 N+1 문제 발생 이유?
- JPA 에서 연관관계에 있는 두 테이블 JOIN 해서 가지고오고 싶은지 확신 X
- "연관관계 = JOIN" 이라고 가정, 너무 많은 연관관계 그래프가 한번에 조회 될 가능성 Up!!
- 어떤 시점에 어떤 연관관계 그래프를 한번에 조회해야하는지 JPA 입장에서 알 수 없다.
N+1 문제가 발생하고, Fetch Join, Entity Graph와 같은걸 통해 명시적으로 알려줌.
- N+1 문제는 JPA가 객체인 Entity 기반으로 쿼리를 자동으로 만들어주기 때문에 발생하는 문제.
✔ N+1 문제가 발생하는 이유는 EAGER ?
- 예제 코드에서 Todo, Comment 는 서로에 대한 연관관계 Fetch 전략을 모두 EAGER 로 설정했다.
- 경우에 따라 Lazy Loading 이 발생하지 않는 로직일 경우, 즉 연관관계를 참조하지 않는 경우 N개의 쿼리가 생략될 수는 있다.
- 하지만, 연관관계에 있는 Entity 를 사용하는 시점에 결국 N개의 SELECT 쿼리가 발생해 N+1 문제가 여전히 발생하는걸 알 수 있다.
- 즉, LAZY Fetch 전략을 통해 N+1 문제를 완전히 해결할 수는 없다!
✔ N+1 문제 해결
- Batch Size 조절
- Fetch Join
- EntityGraph
- Batch Size
- Batch Size 를 조절하는 것은 근본적인 해결 방법 X
- N개의 쿼리가 많아 특정 개수씩 묶어 In Query를 날려서 쿼리 개수를 줄임.
- Fetch Join
- Fetch Join은 JPA에서 지원하는 JOIN 방식
- Fetch Join을 사용할 때, 연관관계에 있는 Entity 까지 한번에 조회해 영속화 해줌.
- Fetch Join은 JPQL로 JOIN FETCH 구문 사용
@Repository
public interface TodoRepository extends JpaRepository<Todo, Long> {
@Query("SELECT todo FROM Todo todo JOIN FETCH todo.comments")
List<Todo> findAllWithFetchJoin();
}
- EntityGraph
- @EntityGraph 를 이용해 해당 Entity의 연관관계에 대한 정보를 넣어주는 방식
@Repository
public interface TodoRepository extends JpaRepository<Todo, Long> {
@EntityGraph(attributePaths = ["comments"])
@Query("SELECT todo FROM Todo todo")
List<Todo> findAllWithEntityGraph();
}
'TIL > Spring' 카테고리의 다른 글
| <2025.05.19> 페이지네이션, 무한스크롤 (0) | 2025.05.19 |
|---|---|
| <2025.05.08> TestCode - 단위 테스트 (0) | 2025.05.08 |
| <2025.05.01> 영속성 컨텍스트 (2) | 2025.05.01 |
| <2025.04.22> 배달 기능 프로젝트 피드백 (1) | 2025.04.22 |
| <2025.04.21> [Spring - advanced] 코드 개선 & 트러블 슈팅 (1) | 2025.04.21 |