Problem and Solution Guide/JPA / / 2024. 9. 23. 09:12

JPA 핵심 문제 풀이: 실무에서 자주 마주하는 이슈와 해결법

문제 1: 단방향 연관관계 매핑 - ManyToOne

 

문제:

OrderCustomer 엔티티가 있다. 각 주문은 하나의 고객에게만 속하지만, 한 고객은 여러 개의 주문을 할 수 있다. 이를 단방향 연관관계로 매핑하고, Order 엔티티에 고객과의 연관관계를 설정하는 코드를 작성해보세요.

 

풀이:

단방향 연관관계에서, Order 엔티티가 Customer 엔티티를 참조하도록 설정하면 돼.

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

    private String orderDate;

    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;

    // getters and setters
}

 

해설:

@ManyToOne을 사용해 여러 주문이 하나의 고객에게 속하는 관계를 표현했어. @JoinColumn(name = "customer_id")를 통해 외래 키를 설정하고, 각 OrderCustomer에 대한 참조를 가지게 돼. 이 방식은 단방향이므로 CustomerOrder를 알지 못해.

 

문제 2: 양방향 연관관계 매핑 - OneToMany, ManyToOne

 

문제:

OrderCustomer 엔티티 간의 관계를 양방향으로 설정하고, Customer 엔티티가 자신의 주문들을 참조할 수 있도록 하려면 어떻게 해야 할까? 양방향 연관관계를 설정하는 코드를 작성해보세요.

 

풀이:

양방향 관계에서는 두 엔티티가 서로를 참조할 수 있어. Customer는 자신이 주문한 Order 리스트를 가지고, Order는 어느 고객에 속해 있는지 알아야 해.

@Entity
public class Customer {

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

    private String name;

    @OneToMany(mappedBy = "customer")
    private List<Order> orders = new ArrayList<>();

    // getters and setters
}

@Entity
public class Order {

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

    private String orderDate;

    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;

    // getters and setters
}

 

해설:

양방향 연관관계를 설정할 때는 한 쪽이 “주인” 역할을 하고, 다른 쪽은 “주인”을 참조하는 역할을 해. 여기서 Order 엔티티가 연관관계의 주인이고, @JoinColumn을 통해 외래 키를 관리해. 반면 Customer 엔티티는 mappedBy 속성을 사용해 Order가 자신을 참조하고 있음을 나타내.

 

문제 3: 영속성 전이 (Cascade)와 고아 객체 제거

 

문제:

Parent 엔티티와 Child 엔티티가 있다. 부모는 여러 자식을 가질 수 있고, 부모가 삭제되면 자식들도 함께 삭제된다. 또한, 부모에서 자식을 제거하면 그 자식도 데이터베이스에서 삭제되도록 설정하라. 필요한 설정을 추가하는 코드를 작성해보세요.

 

풀이:

CascadeType.ALLorphanRemoval = true를 사용해 부모-자식 관계에서 부모가 자식을 삭제하면 자식이 고아 객체로 남지 않도록 설정할 수 있어.

@Entity
public class Parent {

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

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Child> children = new ArrayList<>();

    // getters and setters
}

@Entity
public class Child {

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

    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;

    // getters and setters
}

 

해설:

cascade = CascadeType.ALL을 설정하면 부모와 자식 간의 영속성 전이가 일어나서 부모를 저장, 업데이트, 삭제할 때 자식들도 함께 처리돼. orphanRemoval = true를 사용하면 부모로부터 자식이 제거될 때 그 자식은 고아 객체가 되고, 자동으로 데이터베이스에서도 삭제돼.

 

문제 4: 상속 매핑 - Joined Table 전략

 

문제:

Payment라는 상위 클래스와 CreditCardPayment, BankTransferPayment라는 하위 클래스가 있다. 이 상속 구조를 Joined Table 전략으로 매핑하는 코드를 작성하라.

 

풀이:

Joined Table 전략에서는 상위 클래스와 하위 클래스가 각각 테이블을 가지며, 상위 클래스의 기본 키가 하위 클래스의 기본 키로 사용돼.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Payment {

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

    private double amount;

    // getters and setters
}

@Entity
public class CreditCardPayment extends Payment {

    private String cardNumber;

    // getters and setters
}

@Entity
public class BankTransferPayment extends Payment {

    private String bankAccount;

    // getters and setters
}

 

해설:

Joined Table 전략에서는 상위 클래스 Payment와 하위 클래스 CreditCardPayment, BankTransferPayment가 각각 테이블을 가지며, Payment 테이블의 기본 키가 하위 테이블들의 기본 키로 사용돼. 이 방식은 상속 구조를 명확하게 표현할 수 있지만, 성능상 단점은 여러 테이블을 조인해서 조회해야 한다는 점이야.

 

문제 5: FetchType.LAZY vs FetchType.EAGER

 

문제:

ProductCategory 엔티티는 다대일 관계를 가지고 있다. 기본적으로 연관관계는 FetchType.LAZY로 설정되어 있지만, 이를 FetchType.EAGER로 변경하면 어떤 차이가 생길까? 두 가지 방식의 차이를 설명하고, 각각의 장단점을 설명하세요.

 

풀이:

 

FetchType.LAZY는 지연 로딩 방식으로, 엔티티를 조회할 때 관련된 엔티티는 실제로 필요할 때 로딩된다. 즉, Product를 조회할 때 Category는 실제로 사용되는 순간까지 로딩되지 않아.

FetchType.EAGER는 즉시 로딩 방식으로, 엔티티를 조회할 때 연관된 엔티티도 즉시 함께 조회된다. Product를 조회하면 Category도 즉시 로딩돼.

 

해설:

 

FetchType.LAZY의 장점: 연관된 엔티티를 실제로 사용할 때만 데이터베이스에서 가져오므로 불필요한 쿼리를 줄일 수 있어. 그러나 연관된 데이터를 사용할 때 추가 쿼리가 발생할 수 있어서 N+1 문제가 발생할 가능성이 있어.

FetchType.EAGER의 장점: 한 번의 쿼리로 연관된 엔티티까지 모두 가져오기 때문에 쿼리의 복잡성을 줄일 수 있어. 하지만 연관된 데이터가 많거나 복잡한 경우 불필요한 데이터를 함께 가져오면서 성능에 영향을 미칠 수 있어.

 

문제 6: N+1 문제 해결 - EntityGraph 사용

 

문제:

AuthorBook 엔티티는 1:N 관계를 가지고 있다. Author 리스트를 조회할 때 각 Book에 대한 쿼리가 N+1 문제를 일으키고 있다. 이를 해결하기 위해 @EntityGraph를 사용해 쿼리를 최적화하는 코드를 작성해보세요.

 

풀이:

@EntityGraph를 사용하면 연관된 엔티티를 미리 로딩할 수 있어.

@Entity
public class Author {

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

    private String name;

    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
    private List<Book> books = new ArrayList<>();
    
    // getters and setters
}

@Entity
public class Book {

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

    private String title;

    @ManyToOne
    @JoinColumn(name = "author_id")
    private Author author;
    
    // getters and setters
}

@Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {

    @EntityGraph(attributePaths = {"books"})
    List<Author> findAllWithBooks();
}

 

해설:

@EntityGraph(attributePaths = {"books"})를 통해 Author 리스트를 조회할 때 Book도 함께 가져오도록 했어. 이렇게 하면 N+1 문제를 피할 수 있어. 기본적으로 @EntityGraphLEFT OUTER JOIN을 사용해서 필요한 데이터를 한 번에 가져오므로 성능을 최적화할 수 있어.

이 방법은 복잡한 관계에서도 효과적이지만, 너무 많은 연관 엔티티를 미리 로딩하게 되면 성능상 부담이 될 수 있으니 상황에 맞게 사용하는 게 중요해.

 

문제 7: JPA의 고아 객체 제거와 CascadeType.REMOVE의 차이점

 

문제:

ParentChild 엔티티가 있고, 부모는 여러 자식을 가질 수 있다. 부모가 삭제되면 자식도 삭제되도록 하려면 CascadeType.REMOVE를 사용하면 되지만, 자식을 직접 삭제하는 상황에서 고아 객체 제거 기능이 왜 필요할까? CascadeType.REMOVEorphanRemoval=true의 차이점을 설명하고, 두 기능이 모두 필요한 예시를 설명하세요.

 

풀이:

CascadeType.REMOVE는 부모 엔티티가 삭제될 때 자식 엔티티도 삭제되는 것을 보장해줘. 반면 orphanRemoval=true는 부모 엔티티가 자식과의 관계에서 자식을 제거할 때, 데이터베이스에서도 해당 자식 엔티티가 삭제되도록 하는 기능이야.

@Entity
public class Parent {

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

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE, orphanRemoval = true)
    private List<Child> children = new ArrayList<>();
    
    // getters and setters
}

 

해설:

 

**CascadeType.REMOVE**는 부모 엔티티가 삭제될 때 자식 엔티티도 함께 삭제되지만, 부모에서 자식을 리스트에서 제거하는 것만으로는 자식이 삭제되지 않아.

**orphanRemoval=true**는 부모가 자식을 리스트에서 제거할 때, 자식이 고아 객체가 되고 자동으로 삭제돼.

이 두 기능을 동시에 사용하면, 부모가 삭제될 때 자식도 삭제되고, 부모에서 자식을 관계에서 제거하면 데이터베이스에서도 자식이 삭제되는 두 가지 동작을 모두 구현할 수 있어.

 

문제 8: 고급 조회 기술 - JPQL

 

문제:

JPQL을 사용해서 특정 조건에 맞는 Employee 엔티티를 조회하려 한다. 모든 직원 중에서 급여가 5000 이상인 직원들의 이름과 급여를 조회하는 JPQL 쿼리를 작성하라.

 

풀이:

JPQL은 객체 중심의 쿼리 언어야. 테이블 대신 엔티티 이름을 사용하고, SQL과 비슷하게 쿼리를 작성할 수 있어.

public List<Object[]> findHighSalaryEmployees(EntityManager em) {
    String jpql = "SELECT e.name, e.salary FROM Employee e WHERE e.salary >= 5000";
    return em.createQuery(jpql, Object[].class).getResultList();
}

 

해설:

이 JPQL 쿼리는 Employee 엔티티의 namesalary를 선택하고, 급여가 5000 이상인 직원만을 조회해. SQL과 비슷한 문법을 사용하지만, 엔티티 이름을 사용하고 객체 필드에 접근하는 점이 다르지. 결과는 배열로 반환되는데, 배열의 첫 번째 요소는 이름, 두 번째 요소는 급여야.

 

문제 9: Native Query 사용

 

문제:

JPA에서 Native Query를 사용해 실제 SQL 쿼리를 실행할 수 있다. 모든 Product 엔티티의 이름과 가격을 가져오는 Native Query를 작성하라.

 

풀이:

Native Query는 실제 SQL 쿼리를 그대로 사용할 수 있는 기능이야. JPA에서 제공하는 표준 문법이 아닌, 데이터베이스에 맞는 SQL 쿼리를 작성할 수 있어.

public List<Object[]> findAllProducts(EntityManager em) {
    String sql = "SELECT name, price FROM product";
    return em.createNativeQuery(sql).getResultList();
}

 

해설:

createNativeQuery() 메서드를 사용하면 실제 SQL 쿼리를 실행할 수 있어. 위 코드에서는 product 테이블에서 nameprice 컬럼을 선택하고, 결과를 배열로 반환하도록 했어. Native Query는 데이터베이스에 맞춘 최적화 쿼리를 실행해야 할 때 유용하지만, JPA의 장점인 객체 매핑을 활용하지 못한다는 단점이 있어.

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유