[JPA] 엔티티 설계시 주의점

atdawn·2024년 6월 3일

SPRING BOOT+JPA

목록 보기
21/49

참고 : 인프런 < 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 김영한 >


엔티티에서는 Setter를 사용하지 말자.

강의에서는 예제를 보이기 위해 Setter을 사용하였지만,
실무에서는 추후 유지보수가 어렵기 때문에 가급적 사용하지 않는다.


모든 연관관계는 지연로딩(LAZY)으로 설정 (중요)

  • 즉시로딩( EAGER ) : 엔티티를 조회할 때 연관된 엔티티도 함께 조회

    • 즉시로딩은 예측이 어렵과 추적하기가 어렵다.
    • 과도한 로딩으로 성능 저하 발생
    • PQL을 실행할 때 N+1 문제가 자주 발생
  • 지연로딩( LAZY ) : 엔티티를 조회할 때 연관된 엔티티를 즉시 로드하지 않고, 해당 엔티티가 실제로 접근될 때까지 로딩을 지연

    • 즉, 기본 엔티티를 가져올 때 연관된 엔티티는 프록시 객체로 유지되고, 실제 데이터는 필요할 때 조회
    • 필요할 때만 데이터를 로드하여 메모리 사용량을 줄이고, 성능을 최적화
  • 연관된 엔티티를 함께 DB에서 조회해야 하면, fetch join 또는 엔티티 그래프 기능을 사용

  • @XToOne(OneToOne, ManyToOne) 관계는 기본이 즉시로딩이므로 직접 지연로딩으로 설정

설정 방법
연관된 엔티티의 필드에 @OneToMany, @ManyToOne, @OneToOne, @ManyToMany 어노테이션을 사용하면서 fetch 속성을 FetchType.XXX 설정

@OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "address_id")
    private Address address;
  • 앞서 생성한 엔티티들의 @XToOne 어노테이션에도 LAZY로 속성을 설정해주자.

컬렉션은 필드에서 초기화

컬렉션을 초기화하는 방법은 2가지 이다.

  1. 필드에서 초기화
private List<Order> orders = new ArrayList<>();
  1. 생성자에서 초기화
public Member(){
	orders=new ArrayList<>();
}

컬렉션은 필드에서 바로 초기화 하는 것이 안전하다.

  • null 문제에서 안전
  • 하이버네이트는 엔티티를 영속화 할 때, 컬랙션을 감싸서 하이버네이트가 제공하는 내장 컬렉션으로 변경한다. 만 약 getOrders() 처럼 임의의 메서드에서 컬력션을 잘못 생성하면 하이버네이트 내부 메커니즘에 문제가 발생 할 수 있다. 따라서 필드레벨에서 생성하는 것이 가장 안전하고, 코드도 간결하다.
Member member = new Member();
 System.out.println(member.getOrders().getClass());
 em.persist(member);
 System.out.println(member.getOrders().getClass());
//출력 결과
 class java.util.ArrayList
 class org.hibernate.collection.internal.PersistentBag

테이블, 컬럼명 생성 전략

스프링 부트에서 하이버네이트 기본 매핑 전략을 변경해서 실제 테이블 필드명은 다르다.

  • 하이버네이트 기존 구현: 엔티티의 필드명을 그대로 테이블의 컬럼명으로 사용

스프링 부트 신규 설정 (엔티티(필드) 테이블(컬럼))
1. 카멜 케이스 언더스코어(memberPoint memberpoint)
2. .(점)
(언더스코어)
3. 대문자 소문자

적용 2 단계
1. 논리명 생성: 명시적으로 컬럼, 테이블명을 직접 적지 않으면 ImplicitNamingStrategy 사용
spring.jpa.hibernate.naming.implicit-strategy : 테이블이나, 컬럼명을 명시하지 않을 때 논리명 적 용,
2. 물리명 적용:
spring.jpa.hibernate.naming.physical-strategy : 모든 논리명에 적용됨, 실제 테이블에 적용 (username usernm 등으로 회사 룰로 바꿀 수 있음)

스프링 부트 기본 설정
spring.jpa.hibernate.naming.implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy spring.jpa.hibernate.naming.physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy


영속성 전이(cascading) 옵션

cascade = CascadeType.ALL

  • JPA(Java Persistence API)에서 엔티티 간의 연관 관계에 대한 영속성 전이(cascading) 옵션을 설정하는 데 사용
  • 이를 통해 부모 엔티티에 대한 작업이 자식 엔티티에도 동일하게 적용되도록 설정

예제

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

    private String name;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "address_id")
    private Address address;

    // Getters and Setters
}

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

    private String street;
    private String city;
    private String state;

    // Getters and Setters
}
  • Student 엔티티는 Address 엔티티와 일대일 관계
  • cascade = CascadeType.ALL을 설정하여 Student 엔티티에 대한 모든 영속성 작업이 Address 엔티티로 전이
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("example-unit");
        EntityManager em = emf.createEntityManager();

        em.getTransaction().begin();

        // Address 생성
        Address address = new Address();
        address.setStreet("123 Main St");
        address.setCity("Anytown");
        address.setState("Anystate");

        // Student 생성
        Student student = new Student();
        student.setName("John Doe");
        student.setAddress(address);

        // Student 저장 (Address도 함께 저장됨)
        em.persist(student);

        em.getTransaction().commit();

        em.getTransaction().begin();

        // Student 조회
        Student foundStudent = em.find(Student.class, student.getId());
        System.out.println("Student Name: " + foundStudent.getName());
        System.out.println("Student Address: " + foundStudent.getAddress().getStreet());

        // Student 삭제 (Address도 함께 삭제됨)
        em.remove(foundStudent);

        em.getTransaction().commit();

        em.close();
        emf.close();
    }
}
  • 저장: em.persist(student)를 호출하면 Student 엔티티와 연관된 Address 엔티티도 함께 저장
  • 조회: em.find(Student.class, student.getId())를 통해 Student 엔티티를 조회할 때 연관된 Address 엔티티도 함께 로드
  • 삭제: em.remove(foundStudent)를 호출하면 Student 엔티티와 연관된 Address 엔티티도 함께 삭제

연관 관계 메서드

  • JPA에서는 양방향 관계를 사용할 때 두 엔티티 간의 연관성을 각각의 엔티티에서 관리
  • 만약 이를 적절히 관리하지 않으면 데이터의 일관성이 깨짐
  • 연관 관계 메서드는 이러한 일관성을 유지하기 위해 사용
@Entity
@Table(name="orders")
public class Order {

    @Id @GeneratedValue
    @Column(name="order_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="member_id")
    private Member member;

    @OneToMany(mappedBy = "order",cascade = CascadeType.ALL)
    private List<OrderItem> orderItems;

    @OneToOne(fetch = FetchType.LAZY,cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    private LocalDateTime orderDate;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    //==연관관계 메서드==//

    // 회원 정보 설정 및 양방향 관계 설정
    public void setMember(Member member){
        this.member=member;
        member.getOrders().add(this); // 회원 엔티티의 orders에 현재 주문을 추가
    }

    // 주문 상품 정보 추가 및 양방향 관계 설정
    public void addOrderItem(OrderItem orderItem){
        orderItems.add(orderItem); // 현재 주문에 주문 상품 추가
        orderItem.setOrder(this); // 주문 상품 엔티티에 현재 주문 설정
    }

    // 배송 정보 설정 및 양방향 관계 설정
    public void setDelivery(Delivery delivery){
        this.delivery=delivery; // 현재 주문에 배송 정보 설정
        delivery.setOrder(this); // 배송 정보 엔티티에 현재 주문 설정
    }
}
  • 일반적으로 연관 관계의 주인(Owner) 엔티티에 선언하는 것이 좋음.
profile
복습 복습 복습

0개의 댓글