본문 바로가기
Everyday Study

2024.09.11(수) { 지연 로딩&즉시 로딩, n+1 문제, 다이나믹 업데이트, 알아야할 것들 }

by xogns93 2024. 9. 11.

지연 로딩(Lazy Loading)즉시 로딩(Eager Loading)

 

지연 로딩 (Lazy Loading)

  • 설명: 필요할 때만 데이터를 불러오는 방식입니다. 즉, 처음에 데이터를 조회할 때 관련된 데이터를 바로 가져오지 않고, 나중에 그 데이터가 정말 필요할 때 데이터베이스에 쿼리를 보내서 불러옵니다.
  • 예시 상황:
    • UserOrder라는 두 개의 테이블이 있다고 가정해볼게요. 한 명의 사용자가 여러 주문을 했다고 할 때, 우리는 User 정보만 필요할 수도 있고, 주문 정보는 나중에 필요할 수 있습니다.
    • 이 경우, 지연 로딩을 사용하면 User를 조회할 때는 주문 정보(Order)를 불러오지 않고, 사용자가 Order에 접근할 때 그때서야 주문 정보를 가져오게 됩니다.

코드 예시:

@Entity
public class User {
    @OneToMany(fetch = FetchType.LAZY)
    private List<Order> orders;  // 주문 정보를 나중에 필요할 때만 로드
}
  • User를 가져올 때 주문 정보(Orders)는 아직 불러오지 않음.
  • 나중에 user.getOrders()를 호출하는 순간, 주문 정보를 DB에서 가져오게 됩니다.

즉시 로딩 (Eager Loading)

  • 설명: 한 번에 모든 데이터를 불러오는 방식입니다. 즉, 데이터를 처음 조회할 때 그와 관련된 다른 데이터도 미리 전부 가져옵니다. 이후 별도의 쿼리 없이 바로 데이터를 사용할 수 있습니다.
  • 예시 상황:
    • 만약 User 정보를 불러올 때 항상 그 사용자와 관련된 주문 정보(Order)도 필요하다면, 즉시 로딩을 사용하는 것이 좋습니다.
    • 이 경우에는 User를 불러오는 순간 주문 정보(Orders)도 함께 불러옵니다. 추가적인 쿼리 없이 User와 함께 Order 데이터를 사용할 수 있습니다.

코드 예시:

@Entity
public class User {
    @OneToMany(fetch = FetchType.EAGER)
    private List<Order> orders;  // 주문 정보를 즉시 로드
}
  • User를 가져오는 순간, 주문 정보(Orders)도 함께 가져옵니다.
  • 나중에 user.getOrders()를 호출할 때 별도의 쿼리가 실행되지 않음, 이미 데이터를 메모리에서 사용할 수 있습니다.

차이점 요약

구분 지연 로딩 (Lazy Loading) 즉시 로딩 (Eager Loading)
데이터 로드 시점 필요한 순간에 데이터를 가져옴 (지연) 처음 데이터를 조회할 때 관련 데이터를 모두 함께 가져옴
장점 불필요한 데이터를 로딩하지 않아서 성능 최적화 가능 한 번에 모든 데이터를 가져와서 추가적인 쿼리가 없음
단점 나중에 추가 쿼리가 발생할 수 있어 성능 저하 가능성 있음 불필요한 데이터를 미리 로드해서 메모리 낭비 및 성능 저하 가능
사용 예 관련 데이터를 항상 사용하지 않을 때 관련 데이터를 항상 사용하는 경우

 


N + 1 문제

 

N + 1 문제는 데이터베이스와 관련된 성능 문제 중 하나로, 1개의 쿼리를 실행한 후에 N개의 추가적인 쿼리가 발생하는 문제를 말합니다. 즉, 하나의 주 쿼리를 실행한 뒤, 그와 연관된 데이터를 조회하기 위해 추가적인 N번의 쿼리가 실행되면서 성능이 크게 저하될 수 있는 문제를 일으킵니다.

 

N + 1 문제

  • 게시글을 조회할 때, 게시글 1개에 대해 쿼리 1개가 실행됩니다.
  • 이후 게시글에 연관된 N개의 댓글을 조회할 때, 각 댓글에 대해 N번의 추가 쿼리가 실행됩니다.
  • 결과적으로 1개의 게시글을 조회하는데 N개의 추가 쿼리가 발생하여 성능이 저하되는 문제를 말합니다.

 

1. 1개의 쿼리: 예를 들어, 게시글을 조회하는 쿼리가 1번 실행됩니다.

SELECT * FROM board;

 

2. N개의 쿼리: 각 게시글에 대한 댓글을 가져오는 쿼리가 N번 실행됩니다. 즉, 게시글이 10개라면, 각 게시글에 대한 댓글을 가져오기 위해 10번의 추가적인 쿼리가 실행됩니다.

SELECT * FROM comment WHERE board_id = 1;
SELECT * FROM comment WHERE board_id = 2;
...
SELECT * FROM comment WHERE board_id = 10;

예시 상황

게시판 애플리케이션에서 게시글과 댓글이 있다고 가정해봅시다. 게시글 하나는 여러 개의 댓글을 가질 수 있는 1

관계입니다.

  • 예를 들어, 10개의 게시글이 있다고 했을 때, 이 게시글들을 조회하려면 1번의 쿼리가 필요합니다.
  • 그 다음에 각 게시글에 달린 댓글을 조회하는데, 게시글마다 댓글을 조회하는 N번의 추가적인 쿼리가 실행됩니다.

즉, 게시글 10개를 조회하면 댓글을 불러오기 위해 10번의 추가 쿼리가 발생합니다. 결과적으로, 게시글과 댓글을 조회하기 위해 총 1번의 게시글 조회 쿼리 + 10번의 댓글 조회 쿼리 = 11개의 쿼리가 실행됩니다.

 

 

N + 1 문제가 발생하면 어떻게 되나?

  • 성능 저하: 만약 게시글이 수백 개 또는 수천 개라면, 댓글을 불러오는 쿼리가 엄청나게 많이 발생합니다. 예를 들어, 게시글 1,000개를 조회하면 댓글을 조회하는 추가적인 쿼리도 1,000번 발생하게 됩니다. 이런 방식은 쿼리 실행에 대한 네트워크 비용 데이터베이스 부하를 크게 증가시켜, 성능에 매우 부정적인 영향을 미칩니다.
  • 비효율적인 데이터베이스 사용: 데이터베이스에서 한 번에 처리할 수 있는 작업을 여러 번 쪼개서 실행하게 되므로, 불필요한 데이터베이스 요청이 증가하게 됩니다. 이는 데이터베이스 자원의 낭비를 초래합니다.

 

N + 1 문제 해결 방법

fetch join 사용:

  • fetch join을 사용하면 관련된 데이터를 한 번의 쿼리로 모두 가져올 수 있습니다. 즉, 게시글과 댓글을 한 번에 불러오는 쿼리로 N + 1 문제를 해결할 수 있습니다.
String jpql = "SELECT b FROM Board b JOIN FETCH b.comments WHERE b.id = :boardId";
List<Board> boards = entityManager.createQuery(jpql, Board.class)
                                  .setParameter("boardId", 1L)
                                  .getResultList();

 

 


다이나믹 업데이트(Dynamic Update)

 

다이나믹 업데이트(Dynamic Update)는 JPA에서 엔티티를 업데이트할 때, 변경된 필드만 SQL UPDATE 쿼리로 반영하는 기능을 말합니다. 기본적으로 JPA는 엔티티의 변경 사항을 저장할 때, 엔티티의 모든 필드를 업데이트하는 쿼리를 생성합니다. 하지만 다이나믹 업데이트를 사용하면 실제로 변경된 필드만 업데이트 쿼리에 포함되어, 불필요한 데이터베이스 작업을 줄일 수 있습니다.

 

기본 동작 (다이나믹 업데이트 미사용)

기본적으로 JPA는 엔티티의 필드가 변경되지 않더라도, 모든 필드를 포함한 UPDATE 쿼리를 생성합니다.

@Entity
public class User {
    @Id
    private Long id;
    private String name;
    private String email;
}
User user = entityManager.find(User.class, 1L);  // 기존 엔티티 조회
user.setName("newName");  // 이름만 변경
entityManager.getTransaction().commit();  // 트랜잭션 커밋

 

 

이 경우, name 필드만 변경되었지만, JPA는 기본적으로 모든 필드를 업데이트하는 SQL 쿼리를 생성합니다:

UPDATE User SET name = 'newName', email = 'existingEmail' WHERE id = 1;

 

 

다이나믹 업데이트(Dynamic Update)

다이나믹 업데이트를 사용하면, 변경된 필드만 업데이트 쿼리에 포함됩니다. JPA에서는 @DynamicUpdate 어노테이션을 엔티티 클래스에 추가하여 이 기능을 사용할 수 있습니다.

다이나믹 업데이트 적용 예시:

@Entity
@DynamicUpdate  // 다이나믹 업데이트 활성화
public class User {
    @Id
    private Long id;
    private String name;
    private String email;
}
User user = entityManager.find(User.class, 1L);  // 기존 엔티티 조회
user.setName("newName");  // 이름만 변경
entityManager.getTransaction().commit();  // 트랜잭션 커밋

 

 

이 경우, 다이나믹 업데이트가 활성화되어, 변경된 필드만 포함된 UPDATE 쿼리가 생성됩니다:

UPDATE User SET name = 'newName' WHERE id = 1;

 

 

장점:

  • 성능 최적화: 불필요한 필드 업데이트를 방지하고, 데이터베이스에 대한 부하를 줄입니다.
  • 데이터 무결성: 일부 필드가 트리거나 다른 로직에 의해 관리되는 경우, 불필요한 필드 업데이트를 방지하여 예상치 못한 문제를 피할 수 있습니다.

단점:

  • 추가적인 트래킹 비용: JPA는 변경된 필드를 추적해야 하기 때문에, 기본 설정보다 약간의 성능 오버헤드가 발생할 수 있습니다.
  • 복잡성 증가: 변경된 필드만 업데이트하므로, 어떤 필드가 실제로 업데이트되는지 추적해야 할 때, 코드가 조금 더 복잡해질 수 있습니다.

 

언제 사용하는가?

  • 대규모 엔티티에서 일부 필드만 자주 변경되는 경우.
  • 불필요한 필드 업데이트를 줄여야 하는 성능 민감한 애플리케이션에서 유용합니다.
  • 다이나믹 업데이트는 엔티티가 업데이트될 때 실제로 변경된 필드만 SQL 쿼리로 반영함으로써 성능을 최적화하는 JPA 기능입니다.

Spring Data JPA 쓰면 엔티티매니저 안써도됨
Spring Data JPA 가 구체를 알아서 만들어버림
QueryDSL 쓰면 잘못된거 컴파일 때 다 걸러지기 때문에 훨씬 좋음

 

Spring Data JPA와 EntityManager

  • Spring Data JPA는 JPA 리포지토리 인터페이스를 제공하여, 엔티티의 CRUD(Create, Read, Update, Delete)작업을 자동으로 처리해 줍니다.
  • 개발자는 기본적인 CRUD 작업을 위해 EntityManager를 사용할 필요 없이, JpaRepository 인터페이스를 상속받아 쉽게 사용할 수 있습니다.
  • Spring Data JPA가 내부적으로 EntityManager를 관리하고, 구체적인 구현을 자동으로 만들어주기 때문에 직접 코딩할 필요가 없습니다.
public interface UserRepository extends JpaRepository<User, Long> {
    // 기본 CRUD 메서드를 제공
}

위와 같이 JpaRepository를 상속받으면, Spring Data JPA가 자동으로 구현체를 생성해 주고, 필요한 메서드들을 제공해줍니다.

개발자는 save(), findAll(), delete() 같은 기본 메서드를 바로 사용할 수 있습니다.

 

QueryDSL과 타입 안전성

  • QueryDSL 타입 안전한 방식으로 동적 쿼리를 생성할 수 있는 훌륭한 라이브러리입니다.
  • 보통 JPQL이나 네이티브 쿼리를 사용하면, 쿼리에 오타나 잘못된 필드 이름이 있을 때 런타임에만 오류가 발생하지만, QueryDSL을 사용하면 컴파일 시점에 잘못된 쿼리를 감지할 수 있어, 더 안전합니다.
  • 즉, 동적 쿼리를 작성할 때, 쿼리의 잘못된 부분이 컴파일 타임에 바로 걸러지기 때문에 런타임 오류를 줄일 수 있습니다.

사용방법

  • Maven 설정에서 QueryDSL 관련 의존성 및 플러그인을 추가합니다.
  • 빌드 시 자동으로 Q 클래스가 생성되며, 이 클래스를 통해 동적 쿼리를 작성할 수 있습니다.
  • 설정이 완료되면 QueryDSL을 사용하여 타입 안전한 동적 쿼리를 쉽게 작성할 수 있습니다.

엔티티를 삭제하려면 먼저 삭제 대상 엔티티를 조회해야한다.

 

JPA에서는 엔티티를 삭제하기 위해서는 먼저 삭제할 대상 엔티티를 조회해야 합니다. JPA는 엔티티가 영속성 컨텍스트에 있어야만 삭제 작업을 수행할 수 있기 때문에, 엔티티를 삭제하려면 우선 삭제하려는 엔티티를 조회하여 영속성 컨텍스트에 올려야 합니다.

삭제 작업의 일반적인 흐름:

  1. 삭제할 엔티티 조회: 먼저 삭제하려는 엔티티를 **find()**나 getReference() 등을 통해 조회해야 합니다. 이때 엔티티는 영속 상태가 됩니다.
  2. 삭제 작업 실행: remove() 메서드를 사용하여 해당 엔티티를 삭제합니다.
  3. 트랜잭션 커밋: 트랜잭션이 커밋될 때 실제로 데이터베이스에서 삭제 쿼리가 실행됩니다.
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();

// 1. 삭제할 엔티티 조회
User user = em.find(User.class, 1L);

// 2. 엔티티 삭제
if (user != null) {
    em.remove(user);
}

// 3. 트랜잭션 커밋
em.getTransaction().commit();

 

삭제 과정 설명:

  1. em.find()를 통해 삭제할 엔티티를 조회하여 영속성 컨텍스트에 올립니다.
  2. em.remove()를 호출하여 해당 엔티티를 삭제하도록 표시합니다. 그러나 실제 삭제 쿼리는 트랜잭션이 커밋될 때 실행됩니다.
  3. 트랜잭션을 커밋하면 JPA는 삭제 쿼리를 데이터베이스로 전달하여 해당 엔티티를 삭제합니다.

EnumType.Ordinal (Ordinal은 Integer, 정수란 뜻) -> 이거 안좋음

 

EnumType.ORDINAL은 JPA에서 enum 값을 데이터베이스에 저장할 때, 해당 enum의 순서(ordinal) 값(정수)을 저장하는 방식입니다. 그러나 이 방식은 나중에 enum 값이 변경되거나 새 값이 추가될 때 문제가 발생할 수 있습니다.

 

EnumType.ORDINAL 설명:

  • ORDINAL은 enum이 선언된 순서에 따라 데이터베이스에 정수 값으로 저장됩니다.
  • 예를 들어, 아래와 같은 enum이 있다고 가정합니다.
public enum Status {
    ACTIVE,  // 0
    INACTIVE // 1
}

이 경우, ACTIVE는 0, INACTIVE는 1로 저장됩니다. 하지만 enum 값의 순서가 바뀌거나 새로운 값이 추가되면, 그 값의 순서(ordinal)가 바뀌어 버려 기존 데이터와의 불일치 문제가 발생할 수 있습니다.

 

public enum Status {
    ACTIVE, 
    PENDING,  // 새로 추가된 값
    INACTIVE
}

기존에 INACTIVE가 1로 저장되어 있었지만, PENDING이 추가되면서 INACTIVE의 값이 2로 변경됩니다. 이로 인해 기존 데이터베이스에 저장된 INACTIVE 값(1)은 더 이상 유효하지 않고, 잘못된 의미로 해석됩니다.

 

해결 방법: EnumType.STRING 사용:

  • 이 문제를 방지하기 위해서는 EnumType.STRING을 사용하는 것이 좋습니다.
  • EnumType.STRING은 enum 값을 문자열로 저장합니다. 이 방식은 enum 값의 순서가 변경되더라도, 데이터베이스의 기존 데이터에 영향이 없습니다.
@Entity
public class User {
    @Enumerated(EnumType.STRING)
    private Status status;
}

이 경우, enum 값이 데이터베이스에 ACTIVE 또는 **INACTIVE**라는 문자열로 저장됩니다. 이 방식은 enum 값이 변경되더라도 순서와 상관없이 안전하게 저장할 수 있습니다.

정리:

  • EnumType.ORDINAL은 enum 값이 정수로 저장되며, enum에 값이 추가되거나 순서가 변경될 때 데이터 불일치 문제가 발생할 수 있어 비추천됩니다.
  • EnumType.STRING을 사용하면 enum 값이 문자열로 저장되어, enum 값이 변경되거나 추가되더라도 데이터의 일관성을 유지할 수 있으므로, 권장됩니다.

유니크 제약조건

 

유니크 제약조건(Unique Constraint)은 데이터베이스에서 특정 열(컬럼) 또는 컬럼의 조합에 대해 중복된 값을 허용하지 않도록 설정하는 제약조건입니다. 즉, 테이블 내에서 특정 필드나 필드 조합이 중복되지 않도록 보장합니다.

단일 컬럼에 유니크 제약조건 적용

유니크 제약조건 개의 컬럼에 대해 유니크 제약조건을 설정하면, 해당 컬럼에 저장된 값이 테이블 내에서 중복되지 않도록 합니다.

CREATE TABLE users (
    id BIGINT AUTO_INCREMENT,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    email VARCHAR(100) UNIQUE, -- email에 유니크 제약조건 설정
    PRIMARY KEY (id)
);

 

다중 컬럼(복합 컬럼)에 유니크 제약조건 적용

유니크 제약조건은 두 개 이상의 컬럼에 대해 동시에 설정할 수 있습니다. 이렇게 하면 개별 컬럼의 값이 중복되는 것은 허용되지만, 두 컬럼의 조합이 중복되는 것은 허용되지 않습니다.

예를 들어, first name last name의 조합이 유일해야 하는 경우:

CREATE TABLE users (
    id BIGINT AUTO_INCREMENT,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    email VARCHAR(100),
    PRIMARY KEY (id),
    UNIQUE (first_name, last_name)  -- first_name과 last_name의 조합에 유니크 제약조건 설정
);

위의 경우:

  • ("John", "Doe")라는 데이터가   , 동일한 first name last name의 조합인 ("John", "Doe")를 다시 저장할 수 없습니다.
  • 하지만 ("John", "Smith")와 같은 다른 조합은 저장할 수 있습니다.