728x90

JPA(Java Persistence API)에서 엔티티 간의 관계를 매핑하는 방법에는 여러 가지가 있습니다. 이 중에서 JoinColumn과 MappedBy는 매우 중요한 개념입니다.

  • JoinColumn JoinColumn은 연관 관계를 맺고 있는 테이블에서 외래 키를 매핑할 때 사용합니다. 즉, 연관 관계의 주인(owning side)에서 외래 키를 관리하는 방법입니다.

예를 들어, 다음과 같은 두 개의 엔티티가 있다고 가정해보겠습니다.

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "author_id")
    private Author author;
    
    // ...
}

@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "author")
    private List<Book> books = new ArrayList<>();

    // ...
}

여기서 Book과 Author 엔티티는 ManyToOne 단방향 연관 관계를 맺고 있습니다. Book 엔티티에서는 @JoinColumn 어노테이션을 사용하여 author_id 외래 키를 매핑하고 있습니다. 이를 통해 Book 엔티티가 연관된 Author 엔티티의 id 값을 참조할 수 있습니다.

  • MappedBy MappedBy는 연관 관계의 주인이 아닌 엔티티에서 관계를 매핑할 때 사용합니다. 즉, 양방향 연관 관계에서 연관 관계의 주인이 아닌 엔티티에서 매핑 정보를 제공하는 방법입니다.

예를 들어, 위에서 사용한 Book과 Author 엔티티에서는 OneToMany 양방향 연관 관계를 맺고 있습니다. 이 때, Book 엔티티에서는 mappedBy 속성을 사용하여 Author 엔티티의 books 필드와 매핑할 수 있습니다.

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "author_id")
    private Author author;

    // ...
}

@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "author")
    private List<Book> books = new ArrayList<>();

    // ...
}

위의 코드에서는 Author 엔티티에서 mappedBy 속성을 사용하여 Book 엔티티의 author 필드와 매핑하고 있습니다. 이를 통해 양방향 연관 관계를 구현할 수 있습니다.

JoinColumn과 MappedBy 매핑 방식

JoinColumn vs MappedBy

JoinColumn과 MappedBy는 둘 다 엔티티 간의 연관 관계를 매핑하는 방식입니다. 하지만 다음과 같은 차이점이 있습니다.

  1. 연관 관계의 주인

JoinColumn은 연관 관계의 주인에서 사용되고, MappedBy는 연관 관계의 주인이 아닌 엔티티에서 사용됩니다.

  1. 매핑 방향

JoinColumn은 단방향 연관 관계에서 사용되며, 외래 키를 매핑합니다. MappedBy는 양방향 연관 관계에서 사용되며, 양쪽 엔티티 간에 서로를 참조할 수 있도록 매핑합니다.

  1. 관리하는 쪽의 변경 가능성

JoinColumn은 연관 관계의 주인에서 외래 키를 관리하므로, 외래 키 값이 변경될 때는 항상 관리하는 쪽에서 변경해야 합니다. MappedBy는 연관 관계의 주인이 아닌 엔티티에서 관계를 매핑하므로, 관리하는 쪽이 아닌 다른 쪽에서 연관 관계를 변경할 수 있습니다.

  1. 필드 이름

JoinColumn은 외래 키의 컬럼 이름을 지정하는 데 사용되므로, 필드 이름과 다를 수 있습니다. MappedBy는 필드 이름을 그대로 사용하므로, 필드 이름과 매핑되는 엔티티의 필드 이름이 같아야 합니다.

위에서 예시로 사용한 코드에서는 Book 엔티티에서 JoinColumn을 사용하여 ManyToOne 단방향 연관 관계를 매핑하고, Author 엔티티에서 MappedBy를 사용하여 OneToMany 양방향 연관 관계를 매핑했습니다. 이를 통해 JPA를 사용하여 데이터베이스의 테이블과 엔티티를 매핑할 때 JoinColumn과 MappedBy를 적절히 사용하여 연관 관계를 설정할 수 있습니다.

사용할 때 주의해야 할 점

@Entity
public class Department {
    @Id
    private Long id;

    // 하나의 부서에는 여러 명의 직원이 속해있다.
    @OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
    private List<Employee> employees = new ArrayList<>();

    // getter and setter methods
}

@Entity
public class Employee {
    @Id
    private Long id;

    // 여러 명의 직원은 하나의 부서에 속한다.
    @ManyToOne
    @JoinColumn(name = "department_id")
    private Department department;

    // getter and setter methods
}
  • JoinColumn을 사용할 때 주의할 점:
    • @JoinColumn은 일대다, 다대일 관계에서 사용합니다.
    • JoinColumn을 사용하여 연관관계의 주인을 정의합니다. 따라서 OneToMany 또는 ManyToOne의 주인이 됩니다.
    • name 속성을 반드시 지정해야 합니다.
    • referencedColumnName 속성을 생략할 경우 기본값은 참조하는 엔티티의 기본키입니다.
  • MappedBy를 사용할 때 주의할 점:
    • @OneToMany, @OneToOne에서 사용합니다.
    • mappedBy 속성을 사용하여 양방향 연관관계를 설정할 수 있습니다. mappedBy 속성에는 연관관계의 주인 필드 이름을 지정합니다.
    • 연관관계의 주인은 외래 키를 관리합니다. 연관관계의 주인이 아닌 쪽에서는 읽기 전용으로 설정해야 합니다.
    • mappedBy를 사용하는 엔티티는 데이터베이스 테이블에 외래 키를 생성하지 않습니다.

따라서 위 예제에서는 Department 엔티티에서 mappedBy 속성을 사용하여 Employee 엔티티와의 양방향 연관관계를 설정하고, Employee 엔티티에서 JoinColumn을 사용하여 Department 엔티티와의 일대다 관계에서 외래 키를 설정합니다.


내저장소 바로가기 luxury515

728x90

JPA 의 N+1 이란?

JPA(N+1) 문제는 JPA(Java Persistence API)에서 발생할 수 있는 성능 문제 중 하나입니다. 이 문제는 연관 관계가 있는 엔티티를 쿼리할 때 발생할 수 있습니다.

예를 들어, 하나의 엔티티 A와 그와 관계된 다수의 엔티티 B가 있을 경우, A와 B를 함께 조회하는 쿼리를 실행하면 A와 관련된 모든 B 엔티티를 가져옵니다. 하지만 이후에 B 엔티티를 사용하는 다른 코드에서는 각 B 엔티티마다 추가적인 쿼리가 실행됩니다. 이 때문에 총 N+1번의 쿼리가 실행되어 성능 문제가 발생할 수 있습니다.

이러한 문제를 해결하기 위해서는 JPA에서 제공하는 fetch 조인이나 엔티티 그래프 기능을 사용하면 됩니다. 이를 통해 한 번의 쿼리로 모든 관련 엔티티를 함께 가져올 수 있습니다. 또한, 지연 로딩을 사용하여 필요한 시점에만 엔티티를 가져오도록 설정할 수도 있습니다. 이러한 방법을 사용하면 JPA(N+1) 문제를 해결할 수 있습니다.

어떤 경우 발생하는지? 아뉘?

JPA는 객체-관계 매핑(Object-Relational Mapping, ORM)을 사용하는 프레임워크로, 객체와 데이터베이스 간의 불일치를 해결하기 위해 여러 기술을 제공한다. 하지만 JPA를 사용하다보면 N+1 문제가 발생할 수 있다. N+1 문제란, 쿼리 한 번으로 가져와야 할 데이터를 N 번 쿼리하는 문제를 말한다.

예를 들어, 게시글 리스트를 가져올 때 해당 게시글 작성자의 정보를 함께 가져와야 한다면, 게시글을 한 번 조회하고 작성자 정보를 가져오기 위해 작성자별로 N 번 쿼리해야 할 수 있다. 이는 많은 데이터를 조회할 때 성능 저하를 일으킬 수 있다.

왜 발생하는지

N+1 문제는 JPA의 지연 로딩(Lazy Loading)과 관련이 있다. JPA는 연관된 엔티티를 필요할 때 가져오는 지연 로딩 기능을 제공한다. 하지만 이때 연관된 엔티티가 많아지면, 필요한 엔티티를 가져오기 위해 N 번의 추가 쿼리가 발생하게 된다.

어떤 해결 방안이 있는지 ?

N+1 문제를 해결하는 방법은 크게 두 가지이다.

  • 첫 번째는 페치 조인(Fetch Join)을 사용하는 방법이다. 페치 조인은 연관된 엔티티를 함께 조회하는 방법으로, 쿼리 한 번으로 모든 데이터를 가져올 수 있다.
  • 두 번째는 엔티티 그래프(Entity Graph)를 사용하는 방법이다. 엔티티 그래프는 필요한 엔티티를 미리 지정해 놓는 방법으로, 페치 조인보다 더 세밀하게 데이터를 가져올 수 있다.

장단점?

페치 조인과 엔티티 그래프는 각각 장단점이 있다.

  • 페치 조인은 한 번의 쿼리로 모든 데이터를 가져올 수 있기 때문에 성능이 우수하다는 장점이 있다. 그러나 필요한 데이터만 가져오기 어렵기 때문에, 쿼리의 복잡성이 증가할 수 있다는 단점이 있다. 특히, 여러 개의 연관된 엔티티를 함께 가져와야 할 때는 쿼리의 복잡성이 급격히 증가할 수 있다.
  • 엔티티 그래프는 필요한 데이터만 가져올 수 있기 때문에, 페치 조인보다 더 세밀하게 데이터를 가져올 수 있다는 장점이 있다. 하지만 지정한 그래프만 가져오기 때문에, 모든 데이터를 가져오려면 여러 그래프를 지정해야 할 수 있다는 단점이 있다.

따라서 어떤 방법을 선택할지는 데이터의 복잡성과 성능 등 여러 요인을 고려해 결정해야 한다. 예를 들어, 데이터의 양이 많을 때는 페치 조인을 사용하면 쿼리의 복잡성이 증가하기 때문에 성능 저하가 발생할 수 있다. 그러나 필요한 데이터가 적을 경우에는 엔티티 그래프를 사용하면 불필요한 데이터를 가져오지 않아 성능이 개선될 수 있다.

예시 코드 살펴보자!

  • 페치 조인 예시
@Entity
public class Post {
    @Id
    private Long id;
    
    private String title;
    
    @ManyToOne(fetch = FetchType.LAZY)
    private User author;
    
    // getters, setters
}

@Entity
public class User {
    @Id
    private Long id;
    
    private String username;
    
    // getters, setters
}

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
    @Query("SELECT p FROM Post p JOIN FETCH p.author")
    List<Post> findAllWithAuthor();
}

위 코드에서 JOIN FETCH p.author 부분이 페치 조인을 사용한 부분이다. 이를 통해 게시글과 작성자를 함께 조회할 수 있다.

  • 엔티티 그래프 예시
@Entity
public class Post {
    @Id
    private Long id;
    
    private String title;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @EntityGraph(attributePaths = {"author"})
    private User author;
    
    // getters, setters
}

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
    @EntityGraph(attributePaths = {"author"})
    List<Post> findAll();
}

위 코드에서 @EntityGraph 부분이 엔티티 그래프를 사용한 부분이다. attributePaths 속성을 사용해 연관된 엔티티를 미리 지정해 놓을 수 있다. 이를 통해 게시글과 작성자를 함께 조회할 수 있다.

결론적으로

JPA(N+1) 문제는 연관 관계가 있는 엔티티를 쿼리할 때 발생하는 성능 문제입니다. 이 문제를 해결하기 위해서는 fetch 조인이나 엔티티 그래프를 사용하여 한 번의 쿼리로 모든 관련 엔티티를 함께 가져오거나, 지연 로딩을 사용하여 필요한 시점에만 엔티티를 가져오도록 설정할 수 있습니다. 이를 통해 총 N+1번의 쿼리 실행을 줄여 성능 문제를 해결할 수 있습니다.

이 중 어떤 방법을 선택할지는 데이터의 복잡성과 성능 등 여러 요인을 고려해 결정해야 한다.


내저장소 바로가기 luxury515

728x90

왜 QueryDSL 인가?

JPA QueryDSL을 사용하면 문자열 기반 쿼리 대신에 Java 코드로 쿼리를 작성할 수 있으므로, 컴파일 시점에서 오류를 검출할 수 있고 IDE에서 코드 자동완성 등의 기능을 사용할 수 있다는 장점이 있다.

JPA QueryDSL의 세팅 방법은 다음과 같다.

  1. 의존성 추가: JPA QueryDSL 라이브러리를 사용하기 위해서는 라이브러리의 의존성을 추가해야 한다. 예를 들어, Maven을 사용하는 경우에는 pom.xml 파일에 다음과 같이 의존성을 추가할 수 있다.
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
    <version>{version}</version>
</dependency>
  1. Q 클래스 생성: JPA QueryDSL을 사용하려면 Q 클래스를 생성해야 한다. Q 클래스는 엔티티 클래스의 필드를 정의한 클래스로, 쿼리 작성 시에 사용된다. Q 클래스는 JPA QueryDSL의 코드 생성 플러그인을 사용하여 자동으로 생성할 수 있다.
  1. JPAQueryFactory 생성: JPAQueryFactory는 쿼리를 실행할 때 사용하는 객체로, EntityManager를 인자로 받아 생성된다. JPAQueryFactory는 스프링 프레임워크에서는 @Bean 어노테이션을 사용하여 빈으로 등록할 수 있다.

JPA QueryDSL의 사용법은 다음과 같다.

  1. JPAQueryFactory 객체 생성: EntityManager를 인자로 받아 JPAQueryFactory 객체를 생성한다.
@Autowired
private EntityManager entityManager;

private JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
  1. 쿼리 작성: Q 클래스를 사용하여 타입 안전한 쿼리를 작성한다.

예를 들어, 다음은 엔티티 클래스인 User와 Q 클래스인 QUser를 사용하여 이름이 "John"인 유저를 조회하는 쿼리이다.

QUser qUser = QUser.user;

List<User> userList = queryFactory
    .selectFrom(qUser)
    .where(qUser.name.eq("John"))
    .fetch();

아래 코드는 JPA의 다중 조회 결과를 활용한 예시입니다. JPA에서 다중 조회는 서브 쿼리를 사용하거나 여러 개의 쿼리를 작성하여 수행할 수 있습니다. 하지만 QueryDSL을 사용하면 더 간편하고 직관적인 코드를 작성할 수 있습니다.

QUser user = QUser.user;
QOrder order = QOrder.order;
QOrderItem orderItem = QOrderItem.orderItem;

List<User> users = queryFactory
    .selectFrom(user)
    .where(user.name.like("%" + name + "%"))
    .fetch();

List<Order> orders = queryFactory
    .selectFrom(order)
    .where(order.user.in(users))
    .fetch();

List<OrderItem> orderItems = queryFactory
    .selectFrom(orderItem)
    .where(orderItem.order.in(orders))
    .fetch();

Map<Order, List<OrderItem>> orderItemMap = orderItems.stream()
    .collect(Collectors.groupingBy(OrderItem::getOrder));

List<OrderDto> result = orders.stream()
    .map(order -> new OrderDto(order, orderItemMap.get(order)))
    .collect(Collectors.toList());

위 코드에서는 먼저 QUser, QOrder, QOrderItem 클래스를 사용하여 QueryDSL에서 사용할 쿼리 객체를 정의합니다. 그 후 queryFactory 객체를 사용하여 각각의 쿼리를 작성합니다. selectFrom() 메서드로 조회할 엔티티를 지정하고, where() 메서드로 검색 조건을 설정합니다. 마지막으로 fetch() 메서드를 호출하여 쿼리를 수행하고 결과를 반환합니다.

위 코드에서는 다중 조회 결과를 처리하기 위해 Java 8의 Stream API를 사용합니다. 먼저 ordersorderItems를 조회한 후, orderItemMap에 각 Order 객체와 그에 대한 OrderItem 객체들을 매핑합니다. 그리고 ordersorderItemMap을 사용하여 OrderDto 객체를 생성하고 이를 result 리스트에 추가합니다.

이처럼 QueryDSL을 사용하면 JPA에서 지원하는 복잡한 쿼리도 간편하게 작성할 수 있으며, 코드의 가독성과 유지보수성을 높일 수 있습니다.

JPA QueryDSL의 장단점은?

장점:

  • 타입 안전한 쿼리 작성이 가능하여 컴파일 시점에서 오류를 검출할 수 있다.
  • 코드 자동완성 등의 기능을 사용할 수 있어 개발 생산성이 향상된다.
  • 문자열 기반 쿼리 대신에 Java 코드로 쿼리를 작성하므로, 쿼리의 가독성이 향상된다.

단점:

추가적인코드 생성 플러그인을 사용하여 Q 클래스를 생성해야 하므로, 프로젝트 설정이 번거로울 수 있다.

  • 복잡한 쿼리를 작성하는 경우에는 코드의 복잡도가 증가할 수 있다.
  • JPA QueryDSL을 학습하고 익히는데 시간이 필요할 수 있다.

총평하자면, JPA QueryDSL은 JPA를 사용하는 애플리케이션에서 쿼리 작성을 타입 안전하게 할 수 있도록 도와주는 라이브러리이다. 사용하면서 발생할 수 있는 설정의 어려움과 복잡도, 그리고 학습에 대한 시간 투자를 고려하여 사용 여부를 결정해야 한다.


내저장소 바로가기 luxury515

728x90

몇 가지 방법은 다음과 같다.

  • Entity Manager를 이용한 벌크 insert: Entity Manager를 사용하여 데이터를 한 번에 여러 개의 엔티티로 삽입합니다.
  • Named Query를 이용한 벌크 insert: JPA Named Query를 사용하여 여러 개의 엔티티를 삽입합니다.
  • Spring Batch를 이용한 벌크 insert: Spring Batch를 사용하여 대량의 데이터를 처리하고 insert합니다.
  • JPA Batch Insertion API를 이용한 벌크 insert: JPA Batch Insertion API를 사용하여 대량의 데이터를 처리하고 insert합니다.

시간은 데이터베이스와 환경 설정에 따라 다르다고 하지만 가장 효율적이라는 JPA Batch Insertion API를 사용한 방법을 한번 알아보자.

JPA Batch Insertion API 장점.

  • JPA Batch Insertion API는 일괄 처리 방식을 사용하여 대량의 데이터를 처리합니다. 이를 통해 일반적인 방식보다 더 빠른 처리 속도를 보입니다.
  • JPA Batch Insertion API는 데이터를 처리할 때 JDBC의 addBatch()와 executeBatch() 메서드를 사용합니다. 이는 데이터베이스 쿼리 수행 속도를 높일 수 있습니다.
  • JPA Batch Insertion API는 영속성 컨텍스트를 플러시하지 않습니다. 이로 인해 영속성 컨텍스트에서 관리되는 객체의 수가 줄어들어 성능을 향상시킵니다.

예시 코드

@Entity
@Table(name = "employee")
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    @Column(name = "salary")
    private Double salary;

    // 생성자, Getter/Setter 메서드 생략
}

application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/example_db?characterEncoding=UTF-8&serverTimezone=UTC
    username: [username]
    password: [password]
  jpa:
    properties:
      hibernate:
        jdbc.batch_size: 50
        order_inserts: true
        order_updates: true
@Repository
public class EmployeeRepository {

    @PersistenceContext
    private EntityManager entityManager;

    public void saveAll(List<Employee> employees) {
        Session session = entityManager.unwrap(Session.class);
        session.setJdbcBatchSize(50); // 설정 파일에서 설정한 값과 같음.

        for (int i = 0; i < employees.size(); i++) {
            session.persist(employees.get(i));
            if (i % 50 == 0 || i == employees.size() - 1) {
                session.flush();
                session.clear();
            }
        }
    }
}

위 코드에서 JPA Batch Insertion API를 사용하여 대량의 데이터를 처리하는 방법은 다음과 같습니다:

  • EntityManager를 통해 Session 객체를 가져옵니다.
  • Session.setJdbcBatchSize() 메서드를 사용하여 배치 크기를 설정합니다. 이 값은 application.yml 파일에서 설정한 hibernate.jdbc.batch_size 값과 같아야 합니다.
  • Session.persist() 메서드를 사용하여 데이터를 삽입합니다.
  • 일정한 배치 크기(위 예시에서는 50)에 도달하거나 마지막 데이터까지 삽입한 후, Session.flush()와 Session.clear() 메서드를 사용하여 데이터를 플러시하고 영속성 컨텍스트를 클리어합니다.

위 코드는 대량의 데이터를 처리하는 방법 중 하나로, 배치 크기와 영속성 컨텍스트 관리를 통해 성능을 최적화합니다. 또한 설정 파일에서 JDBC 배치 크기를 지정하여 더욱 성능을 향상시킬 수 있습니다.

결론으로

JPA Batch Insertion API를 사용하여 대량의 데이터를 처리하는 것이 가장 효율적입니다. 이 방법을 사용하면 JDBC 배치 삽입을 수행할 때와 같이 대량의 데이터를 한 번에 처리하여 더욱 성능을 향상시킬 수 있습니다.

따라서 대량의 데이터를 처리해야 하는 경우 이 방법을 사용하는 것이 좋을것 같네요~!


내저장소 바로가기 luxury515

+ Recent posts