728x90

 

📋 즉시 로딩과 지연 로딩

 

즉시로딩(Immediate Loading)과 지연로딩(Lazy Loading)은 데이터베이스에서 데이터를 조회하는 방식 중의 하나로, 객체 간의 연관관계를 어떻게 로딩하고 관리할 것인지에 대한 개념입니다.

  1. 즉시로딩(Immediate Loading) 즉시로딩은 엔티티를 조회할 때 해당 엔티티와 연관된 모든 엔티티를 동시에 조회하는 방식입니다.
    예를 들어, A 엔티티와 B 엔티티가 연관되어 있을 때 A를 조회하면 B도 함께 조회됩니다. 이로 인해 객체 간의 관계를 필요한 시점에 바로 사용할 수 있습니다.
    하지만 조인 등의 복잡한 쿼리가 생성될 수 있고, 불필요한 데이터 로딩으로 인해 성능 문제가 발생할 수도 있습니다.

  2. 지연로딩(Lazy Loading) : 객체를 Proxy로 가져온 후 실제 해당 객체를 사용하는 시점에 초기화 지연로딩은 연관된 엔티티를 처음에는 조회하지 않고, 실제로 해당 엔티티가 필요한 시점에 조회하는 방식입니다.
    예를 들어, A 엔티티를 조회해도 B 엔티티는 초기에 조회되지 않고, B 엔티티를 실제로 사용할 때 데이터베이스에서 조회됩니다. 이를 통해 쿼리의 최적화와 성능 향상을 이룰 수 있습니다.
    그러나 연관 엔티티를 사용하는 과정에서 데이터베이스 쿼리가 추가적으로 발생하게 될 수 있습니다.

지연로딩은 대부분의 JPA 구현체에서 지원되는 기능이며, 엔티티 클래스의 연관 관계 필드에 @ManyToOne, @OneToMany, @OneToOne, @ManyToMany와 같은 어노테이션을 사용할 때
fetch 속성을 지정하여 조절할 수 있습니다. 지연로딩을 사용하려면 데이터베이스 트랜잭션 내에서 연관된 엔티티에 접근해야만 데이터베이스 조회가 일어납니다.

각각의 상황과 필요에 따라 선택하여 전략적으로 사용할 수 있습니다.

즉시로딩(Immediate Loading)을 선택하는 이유

  1. 객체 간의 관계를 활용하기 편리: 즉시로딩을 사용하면 객체를 조회할 때 연관된 모든 객체가 한 번에 로딩되므로 객체 간의 관계를 편리하게 활용할 수 있습니다. 모든 연관된 데이터가 이미 로딩되어 있으므로 어떤 객체를 사용할 때 별다른 데이터베이스 조회 없이 객체 그래프를 따라 이동할 수 있습니다.

  2. 복잡한 조회를 단순화: 데이터베이스에서 조인을 사용하여 복잡한 연관관계를 해결할 필요 없이, 즉시로딩으로 모든 데이터를 한 번에 가져올 수 있습니다.

 

지연로딩(Lazy Loading)을 선택하는 이유

  1. 성능 최적화: 즉시로딩은 모든 연관된 데이터를 한 번에 가져오기 때문에, 필요하지 않은 데이터까지 불필요하게 로딩될 수 있습니다. 이는 성능 저하를 야기할 수 있습니다. 지연로딩은 필요한 시점에만 데이터를 로딩하기 때문에 성능을 최적화할 수 있습니다.

  2. 데이터 접근을 최적화: 사용자가 실제로 해당 데이터를 사용할 때만 로딩하므로 데이터베이스 접근이 최적화됩니다. 따라서 시스템 전체적으로 데이터 로딩에 대한 부하가 분산될 수 있습니다.

  3. 순환 참조 방지: 지연로딩을 사용하면 객체 간의 연관관계에서 순환 참조가 발생할 확률이 줄어듭니다. 객체를 조회할 때 실제로 필요한 데이터만 로딩되므로 무한한 순환 참조를 방지할 수 있습니다.

  4. 메모리 사용 최적화: 즉시로딩은 연관된 모든 데이터를 로딩하기 때문에 메모리를 많이 사용할 수 있습니다. 지연로딩은 필요한 데이터만 로딩하기 때문에 메모리 사용을 최적화할 수 있습니다.

즉시로딩과 지연로딩은 각각의 상황에 따라 선택되며, 객체의 연관관계가 어떻게 사용되는지, 어떤 데이터가 실제로 필요한지 등을 고려하여 결정해야 합니다.
JPA를 사용하는 경우에는 애플리케이션의 성능, 데이터 접근 패턴, 메모리 사용 등을 고려하여 최적의 로딩 전략을 선택하는 것이 중요합니다.

 

✅ 지연로딩의 예제

Member Entity 내부에 Team 객체가 연관관계 매핑이 되어 있는 경우, Member만 조회하고자 할 때, Member 내부의 Team 객체에 지연로딩을 설정하여
해당 엔티티가 실제로 사용되는 경우에 초기화가 발생하며 쿼리문을 호출할 수 있도록 합니다.

    Team team = new Team();
                team.setName("teamA");
                em.persist(team);

                Member member1 = new Member();
                member1.setUsername("Hello1");
                member1.setTeam(team);
                em.persist(member1);

                em.flush();
                em.clear();

                Member m = em.find(Member.class, member1.getId());

                System.out.println("m = " + m.getTeam().getClass());

                System.out.println("==============");
                m.getTeam().getName();
                System.out.println("==============");


                tx.commit();

 

해당 코드 출력 결과 먼저 Team 객체를 사용하기 전엔 Member m = em.find(Member.class, member1.getId()); m 변수에 저장된 getTeam().getClass()가 proxy로 반환 되는걸 확인할 수 있습니다.

 

 

📋 지연 로딩의 활용 예제

  • Member와 Team 객체가 자주 함께 사용 → 즉시 로딩
  • Member와 Order는 가끔 사용 → 지연 로딩
  • Order와 Product는 자주 함께 사용 → 즉시 로딩

하지만 실무에서는 모든 연관관계에 지연로딩을 사용해야 합니다 ! (이론적으로만 분류) 즉시 로딩 XX / JPQL fetch 조인이나, 엔티티 그래프 기능을 사용

 

	

 ============ Member 내에서의 Team과의 연관관계가 Lazy =============
  @ManyToOne(fetch = FetchType.LAZY) //지연로딩으로 설정하게되면 Team 객체를 proxy 객체로 조회하게 됩니다.
	@JoinColumn(name = "TEAM_ID") //어떤 컬럼과 조인할 지 (Team과 Member 테이블의 TEAM_ID(FK)를 매핑)
	private Team team;



	try {
		
					Team team = new Team();
					team.setName("team");
					em.persist(team);
		
					Team teamA = new Team();
					teamA.setName("teamB");
					em.persist(teamA);
		
					Member member1 = new Member();
					member1.setUsername("Hello1");
					member1.setTeam(team);
					em.persist(member1);
		
					Member member2 = new Member();
					member2.setUsername("Hello2");
					member2.setTeam(teamA);
					em.persist(member2);
		
					em.flush();
					em.clear();
		
					//Member m = em.find(Member.class, member1.getId());
		
					->JPQL 문에 join fetch 설정
					List<Member> members = em.createQuery("select m from Member m join fetch m.team", Member.class)
							.getResultList();
		
		
		
			SQL : select * from Member가 일단 먼저 DB에 전송되는데 이 때, Team 엔티티와의 연관관계가 있으며,
		  로딩이 즉시로딩 (EAGER)로 설정되어 있기 때문에 Member 반환 시 Team이 바로 조회될 수 있도록
			SQL : select * from Team where TEAM_ID = member에 있는 Team과 관련된 데이터를 불러옴

 

Member와 Team의 연관관계가 지연로딩으로 맺어져있지만, JPQL 쿼리문에서 join fetch 설정을 통해 한번에 연관된 쿼리문을 함께 조회가 가능합니다.

 

 

✅ 즉시 로딩의 예제

비즈니스 로직 상 엔티티들끼리 연관관계가 맺어져 있기때문에 , 두 개의 엔티티가 함께 사용되는 경우가 많습니다. 이 때 즉시로딩을 사용하여 연관되어 있는 모든 엔티티들을 한번에 조회하여 편리한 활용이 가능하지만, 성능이 저하될 수 있는 단점이 있습니다.

 

    ===== Member 엔티티의 Team 엔티티와의 연관관계 설정====
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID") //어떤 컬럼과 조인할 지 (Team과 Member 테이블의 TEAM_ID(FK)를 매핑)
    private Team team;



    try {

                Team team = new Team();
                team.setName("teamA");
                em.persist(team);

                Member member1 = new Member();
                member1.setUsername("Hello1");
                member1.setTeam(team);
                em.persist(member1);

                em.flush();
                em.clear();

                Member m = em.find(Member.class, member1.getId());

                System.out.println("m = " + m.getTeam().getClass());

                System.out.println("==============");
                m.getTeam().getName();
                System.out.println("==============");


                tx.commit();

 

 

즉시로딩으로 설정 후 실행하면 Member와 Team을 조인을 한 쿼리를 한번에 DB에서 조회하고, 이 때 Team의 클래스 타입을 출력하면 Proxy 객체가 아닌 실제 Team 객체가 출력됩니다.

JPA 구현는 가능하면 조인을 사용해서 SQL을 한번에 함께 조회합니다.

 

💥 Proxy와 즉시 로딩에서의 주의점!

가급적 실무에서는 지연 로딩만 사용하는 것이 좋습니다. 즉시 로딩을 적용하게 되면 예상하지 못한 SQL이 발생할 수 있습니다.

  1. N+1 쿼리 문제 : 즉시로딩을 사용하면 부모 객체를 가져올 때 연관된 모든 자식 객체들도 함께 가져오게 됩니다. 이로 인해 부모 객체의 수만큼 추가 쿼리가 발생할 수 있으며, 이를 N+1 쿼리 문제라고 합니다. 이는 데이터베이스 부하와 성능 저하를 초래할 수 있습니다. 이러한 상황을 피하기 위해서는 지연로딩(Lazy Loading)을 사용하거나, 페치 조인(fetch join) 등을 활용하여 쿼리를 최적화해야 합니다.
  2. 메모리 부하 : 즉시로딩을 사용하면 연관된 모든 자식 객체들도 함께 메모리에 로딩됩니다. 이는 메모리 부하를 증가시킬 수 있습니다. 필요한 경우에만 로딩되도록 지연로딩을 고려하거나 페치 조인을 활용하여 필요한 데이터만 가져오도록 할 수 있습니다.
  3. 데이터 무결성 : 프록시를 사용한 즉시로딩은 객체의 연관 관계를 보장하며, 데이터베이스의 무결성을 유지하기 위해 자식 객체들도 부모 객체와 함께 관리되어야 합니다. 만약 부모 객체와 자식 객체 간의 데이터 무결성이 깨진다면, 프록시 즉시로딩을 사용하는 것보다는 지연로딩과 명시적인 트랜잭션 처리를 고려해야 합니다.
  4. 데이터베이스 성능 : 즉시로딩은 데이터베이스에서 모든 필요한 데이터를 한 번에 가져오기 때문에 성능에 영향을 줄 수 있습니다. 데이터베이스의 쿼리 성능을 고려하여 필요한 경우 쿼리 최적화를 수행하고 인덱스를 활용하는 등의 작업을 진행해야 합니다.

 

✍️ N+1 문제

N+1 문제는 데이터베이스에서 발생하는 성능 이슈 중 하나로, 주로 ORM(Object-Relational Mapping) 기술을 사용하는 애플리케이션에서 발생하는 문제입니다. 이 문제는 데이터베이스 쿼리 실행 횟수가 데이터의 개수에 비례하여 증가하게 되는 상황을 의미합니다.

N+1 문제는 다음과 같은 상황에서 발생합니다:

  1. 부모 객체 조회: 부모 객체(예: 게시물)를 한 번 조회합니다(N 번).
  2. 자식 객체들 조회: 부모 객체들에 연관된 모든 자식 객체(예: 댓글)를 각각 별도의 쿼리로 추가 조회합니다(1 번씩 총 N 번)

이런 방식으로 데이터를 가져오게 되면 총 N+1번의 쿼리 실행이 발생하게 됩니다. 이로 인해 데이터베이스에 불필요한 부하가 발생하며, 애플리케이션의 성능이 저하될 수 있습니다. 또한 자식 객체들이 많은 경우에는 많은 쿼리가 발생하게 되므로 성능 이슈가 더욱 악화될 수 있습니다.

N+1 문제를 해결하는 방법 중 하나는 페치 조인(Fetch Join)을 사용하는 것입니다. 페치 조인은 연관된 데이터를 한 번의 쿼리로 모두 가져오는 방식으로, 데이터베이스의 성능을 향상시키고 N+1 문제를 방지할 수 있습니다. 페치 조인을 사용하면 부모 객체와 연관된 모든 자식 객체를 함께 가져오므로 추가적인 쿼리 호출 없이도 필요한 데이터를 한 번에 가져올 수 있습니다.

N+1 문제를 방지하기 위해 ORM 사용 시 페치 조인을 고려하고, 필요한 데이터만 적절하게 가져오는 쿼리 최적화를 신중하게 진행하는 것이 중요합니다.

728x90
반응형

'[ BACKEND] > Spring' 카테고리의 다른 글

[SPRING] 기본 값 타입  (0) 2023.08.20
[SPRING] 영속성 전이 & 고아객체  (0) 2023.08.19
[SPRING] ✅ Proxy  (0) 2023.08.17
[SPRING] 고급 매핑 - 구현 클래스 , 조인 전략  (0) 2023.08.16
[SPRING] 연관관계 고급 매핑  (0) 2023.08.15

+ Recent posts