Backend/SpringBoot 이론 부분

Entity 심화

Dean83 2025. 9. 30. 19:11

왠만한 Entity에 대한 설명은 https://dean83.tistory.com/277 이곳에 작성해 두었다. 이어서, Entitiy 심화 내용을 정리하고자 한다.

 

 

N+1 문제

N+1 문제는 JPA를 이용할 경우 상황에 따라서 쿼리문을 N+1번 날린다는 뜻이다.

상황을 설명하자면, 

 

  • A, B 테이블이 있다. 
  • B 테이블은 A 테이블의 외래키를 갖고 있고, 1:N 관계이다. 
  • 다수의 B 항목들을 조회하면 (이때 쿼리 1번 발생), JPA에 의해 List 형태로 리턴을 받게 된다.
  • List 를 반복문으로 돌면서 A 항목을 조회하게 되면, B의 개수 만큼 쿼리문이 발생한다. (N번)

이는 join을 사용하지 않고 JPA가 동작하기 때문에 발생하는 문제이다. 

위의 경우를 JPA 를 사용했을때를 생각해 본다면 다음과 같을 것이다. 

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)   // 다(N) -> 일(1)
    @JoinColumn(name = "team_id")
    private Team team;
}

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team", fetch = FetchType.LAZY) // 일(1) -> 다(N)
    private List<Member> members = new ArrayList<>();
}

------

@Service
public class MemberService
{

....

    // 모든 회원 조회
    List<Member> members = memberRepository.findAll();

    // 각 회원이 속한 팀 이름을 출력
    for (Member m : members) {
        System.out.println(m.getTeam().getName()); 
    }
....

}

 

  • 이를 해결하기 위해서는 @Query 어노테이션에서 Fetch join을 활용하면 된다.
    • 역방향 조회를 해야 한다.
      • Fetch join을 활용하나, N에 해당하는 곳에서 조회를 역으로 한다.
      • 역으로 조회를 함으로서 쿼리 결과를 최적화 하여 받는것을 말한다. 
public interface MemberRepository extends JpaRepository<Member, Long> {

    @Query("select m from Member m join fetch m.team")
    List<Member> findAllWithTeam();
}
  • N+1 문제가 있는지 여부를 판단하기 위해선 다음의 조건을 모두 만족할때로 요약할 수 있다.
    • 엔티티 관계가 1:N 일때
    • N 쪽에서 findAll 등 단건이 아닌 다수의 데이터를 조회 할때 (단건은 해당 안됨)
    • 다수의 데이터에서 반복문을 통해 맴버에 접근 할 때
      • 반복문은 for 문 같은 명시적인 반복문도 있지만, stream 을 통한 반복 코드도 생각해야함.
  • 즉 요약하면, N 에 해당하는 엔티티의 Service 에서 findAll, findByxxx 를 호출하는 코드들을 확인하면 된다.

 

  • Eager loading
    • Eager loading은 엔티티 조회시, 연관되어 있는 엔티티들을 바로 같이 불러 오는 경우이다.
    • @ManyToOne, @OneToOne 일 경우 Eager loading이 기본값이다. 
    • N+1 문제의 주요 원인이기도 하다. 
    • 실제 사용할 일이 잘 없으므로 lazy loading을 하는것이 좋다. 
    • 따라서 위의 관계일 경우 명시적으로 lazy loading을 설정해 줘도 Eager loading으로 동작 할 수 있다.(양방향일 경우)


  • Lazy loading
    • 필요할때 쿼리를 날려 조회를 한다. 
    • 모든 관계에서 이것을 기본으로 설정 해야 한다.
      • 예 : @ManyToOne(fetch = FetchType.LAZY)
    • 트랜잭션이 종료된 후 해당 컬럼을 조회하려 하면, lazy initialization 오류가 발생하니, 조심해야 한다 
      • 예를들어, entityA, entityB가 있고, 양방향 관계를 맺고 있다. (OneToOne, OneToMany 등)
      • 해당 관계는 Lazy 로딩으로 설정되어 있다.
      • Service 에서 entityA를 조회한 후에 Controller 로 리턴을 해줬다.
      • Controller 에서 entityA 에 속한 entityB 를 get 하려고 할때 오류가 발생한다. 
      • 이를 해결하기 위해 application.yaml 의 jpa 설정에서 open-in-view: true 를 설정할 수도 있다. (https://dean83.tistory.com/277 상단부 설정 부분 참조)
        • 그러나, N+1 오류를 미리 알아야 하므로, 개발단에서는 false로, 실서버에는 true로 하는게 좋다.
      • 되도록이면 서비스에서 DTO로 치환하여 Controller로 변환 해주면 문제가 해결된다. 

 

Cascade, orphanRemoval 옵션

  • Cascade 옵션은 , 부모 객체가 삭제될 때 자식객체를 어떻게 관리할지에 대한 설정
  • orphanRemoval 은 부모 객체가 삭제되지 않은 상황에서, 부모에서 자식을 삭제할때 실제로 자식 객체를 DB에서 삭제하는 옵션
    • 즉, A 라는 클래스에서 맴버변수로 List<B> bList를 가졌다고 생각할때, bList에서 항목을 삭제하게 되면 DB에서 어떻게 처리할지 하는 옵션이다. 

'Backend > SpringBoot 이론 부분' 카테고리의 다른 글

Filter 및 SecurityFilterChain  (0) 2025.12.15
Spring boot의 요청 처리 흐름  (0) 2025.12.15
SpringBoot 에서 트랜잭션  (0) 2025.10.01
Proxy  (0) 2025.09.30
Spring Data JPA  (0) 2025.09.29