728x90

JPA에서 Specification 인터페이스를 사용하면 동적 쿼리를 작성할 수 있습니다. 동적 쿼리란, 실행 시점에 조건을 추가하여 쿼리를 생성하는 방식입니다. Specification 인터페이스를 사용하면 쿼리를 작성할 때마다 조건을 추가할 필요 없이, 메서드 호출만으로 조건을 추가할 수 있습니다.

다음은 Specification 인터페이스를 사용한 JPA 코드입니다.

예를 들어, Customer 엔티티를 검색할 때, 이름과 이메일 주소로 검색하는 기능을 구현해 보겠습니다.

public interface CustomerSpecification {
    static Specification<Customer> hasName(String name) {
        return (root, query, builder) -> builder.like(root.get("name"), "%" + name + "%");
    }

    static Specification<Customer> hasEmail(String email) {
        return (root, query, builder) -> builder.like(root.get("email"), "%" + email + "%");
    }

    static Specification<Customer> hasNameOrEmail(String name, String email) {
        return hasName(name).or(hasEmail(email));
    }
}

위 코드에서 CustomerSpecification 인터페이스는 Specification<Customer>를 반환하는 hasName, hasEmail, hasNameOrEmail 메서드를 정의하고 있습니다. 이 메서드들은 각각 이름, 이메일, 이름 또는 이메일로 검색하는 Specification을 반환합니다.

다음은 CustomerRepository 인터페이스에서 CustomerSpecification 인터페이스를 사용하는 코드입니다.

@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long>, JpaSpecificationExecutor<Customer> {
    List<Customer> findAll(Specification<Customer> specification);
}

위 코드에서 CustomerRepository 인터페이스는 JPA의 JpaRepository<Customer, Long>와 JpaSpecificationExecutor<Customer>를 상속받고 있습니다. JpaSpecificationExecutor 인터페이스는 Specification을 사용하여 동적 쿼리를 작성할 수 있도록 해줍니다.

다음은 CustomerService 클래스에서 CustomerRepository 인터페이스를 사용하여 Customer 엔티티를 검색하는 코드입니다.

@Service
public class CustomerService {
    private final CustomerRepository customerRepository;

    @Autowired
    public CustomerService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    public List<Customer> search(String name, String email) {
        Specification<Customer> specification = CustomerSpecification.hasNameOrEmail(name, email);
        return customerRepository.findAll(specification);
    }
}

위 코드에서 CustomerService 클래스는 CustomerRepository 인터페이스를 사용하여 Customer 엔티티를 검색하는 search 메서드를 정의하고 있습니다. 이 메서드는 CustomerSpecification 인터페이스에서 반환하는 Specification을 사용하여 동적 쿼리를 작성합니다.


내저장소 바로가기 luxury515

728x90

들어가면서…

  1. 복합키 사용하는 케이스
  • 여러 컬럼을 하나의 식별자로 사용해야 할 때(ex: 학생과 과목을 조인한 결과를 저장하는 테이블에서, 학생과 과목을 기준으로 각각 조회해야 할 때)
  1. 왜 복합키 사용해야 하는 지
  • 기본키로 단일 컬럼을 사용할 경우, 하나의 컬럼으로는 식별할 수 없는 경우가 있기 때문에 복합키를 사용해야 합니다.
  1. 복합키 사용하기 위한 사전 설정
  • @Embeddable 어노테이션을 사용하여 복합키 클래스를 정의합니다.
  • 복합키 클래스 내부에는 @Embeddable 어노테이션을 사용하지 않고 @Column 어노테이션으로 각 컬럼을 정의합니다.
  • 해당 엔티티 클래스의 @EmbeddedId 어노테이션을 사용하여 복합키 클래스를 매핑합니다.

코드 예시

  • 복합키 클래스 정의
@Embeddable
public class StudentSubjectId implements Serializable {
    private Long studentId;
    private Long subjectId;
    
    //생성자, getter, setter, equals, hashCode 생략
}
  • 엔티티 클래스 정의
@Entity
public class StudentSubject {
    @EmbeddedId
    private StudentSubjectId id;
    
    @ManyToOne
    @MapsId("studentId")
    private Student student;
    
    @ManyToOne
    @MapsId("subjectId")
    private Subject subject;
    
    //생성자, getter, setter 생략
}

복합키 장단점

장점:

  • 복합키를 사용하면 하나의 컬럼으로는 식별할 수 없는 경우에도 식별할 수 있습니다.
  • 복합키를 사용하면 여러 컬럼을 하나의 식별자로 사용할 수 있으므로, 테이블 간 조인 시 효율적인 쿼리를 작성할 수 있습니다.

단점:

  • 복합키를 사용하면 코드가 복잡해질 수 있습니다.
  • @IdClass 방식은 복합키를 구성하는 각각의 식별자 컬럼을 별도로 정의해야 하므로 유지보수성이 떨어질 수 있습니다.

결론

따라서 복합키를 사용할 때는 상황에 맞게 적절한 방법을 선택하고, 유지보수성을 고려하여 구현해야 합니다. ^^


내저장소 바로가기 luxury515

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

+ Recent posts