티스토리 뷰

이전 글: https://gojs.tistory.com/57

 

영속성 관리 전략

트랜잭션 범위의 영속성 컨텍스트스프링 컨테이너는 기본적으로 트랜잭션 범위의 영속성 컨텍스트 전략을 가진다. 트랜잭션이 시작되면 영속성 컨텍스트가 생성되고, 트랜잭션이 끝나면 영속

gojs.tistory.com

 

JPA는 Java에서 기본으로 제공하는 Collection, Set, List, Map 등을 지원한다.

 

@Entity
public class Team {
    @Id
    private String id;
    
    @OneToMany
    @JoinColumn
    private Collection<Member> members = new ArrayList<>();
    
    ...
}
System.out.println("before persist: " + team.getMembers().getClass());
em.persist(team);
System.out.println("after persist: " + team.getMembers().getClass());
before persist: java.util.ArrayList
after persist: org.hibernate.collection.internal.PersistentBag

Hibernate는 위와 같이 컬렉션을 효울적으로 관리하기 위해 엔티티를 영속 상태로 만들 때 원본 컬렉션을 감싸고 있는 Wrapper 컬렉션을 생성한다.

 

Collection<Member> members = new ArrayList<>();

따라서 컬렉션을 사용할 때 즉시 초기화해서 사용하는 것을 권장한다.

 

하이버네이트 내장 컬렉션 특징

Interface Wrapper Collection Duplicate Allowed Order Guaranteed
Collection, List PersistentBag O X
Set PersistentSet X X
List + @OrderColumn PersistentList O O

 

Collection, List

Collection, List 인터페이스는 중복을 허용하는 컬렉션이다. Wrapper Collection으로 PersistentBag을 사용한다.

    ...
    
    @OneToMany
    @JoinColumn
    private Collection<CollectionChild> collection = new ArrayList<>();
    
    @OneToMany
    @JoinColumn
    private List<ListChild> list = new ArrayList<>();
    
    ...

중복을 허용하기 때문에 add(...) 메서드를 호출하면 항상 true를 반환한다. 자료구조 내에 동일한 엔티티가 있는지 찾거나 삭제할 때 equals(...) 메서드를 사용한다.

 

Set

Set은 중복을 허용하지 않는 컬렉션이다. Wrapper Collection으로 PersistentSet을 사용한다.

    ...
    
    @OneToMany
    @JoinColumn
    private Set<SetChild> set = new HashSet<>();
    
    ...

중복을 허용하지 않기 때문에 add(...) 메서드를 호출할 때마다 equals(...) 메서드를 활용해 동일한 객체가 있는지 찾아서 결과를 반환한다. HashSet의 경우에는 hashcode() 메서드도 함께 사용한다.

그리고 Set은 엔티티를 추가할 때 중복 데이터 체크를 해야하기 때문에 지연 로딩된 컬렉션을 초기화한다.

 

List + @OrderColumn

List에 @OrderColumn을 추가하면 순서가 있는 특수한 컬렉션으로 인식한다. Wrapper Collection으로 PersistentList를 사용한다.

    ...
    
    @OneToMany(mappedBy = "board")
    @OrderColumn(name = "POSITION")
    private List<Comment> comments = new ArrayList<>();
    
    ...

위 코드와 같이 @OrderColumn을 사용해서 comments 필드를 순서가 있는 컬렉션으로 인식한다.

어노테이션의 name 속성에 "POSITON"이라는 값을 설정함으로써 JPA는 List의 위치값을 테이블의 POSITION 컬럼에 저장한다.

@OrderColumn이 포함된 연관관계 테이블

@OrderColumn은 여러 가지 단점을 가지고 있어 자주 사용되지는 않는다.

POSITION 값을 알 수 없으며, List를 변경하면 많은 위치 값을 한번에 변경해야한다.

또한 연속되지 않게 저장되면 조회 시 NPE가 발생할 수 있다.

 

@OrderBy

엔티티의 연관관계 필드에 @OrderBy를 설정한 경우 데이터베이스의 order by 구문을 활용하여 연관관계 엔티티를 로딩한다.

@Entity
public class Team {

    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    @OrderBy("username desc, id asc")
    private Set<Member> members = new HashSet<>();
    
    ...
}

위와 같이 설정하게 되면 순서를 보장하는 LinkedHashSet를 사용하게된다. 

 

@Converter

엔티티의 데이터를 변환해서 데이터베이스에 저장하거나, 조회한 데이터를 변환해서 엔티티에 값을 세팅할 수 있다.

@Converter
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {

    @Override
    public String convertToDatabaseColumn(Boolean attribute) {
        return attribute == null || !attribute ? "N" : "Y";
    }
    
    @Override
    public Boolean convertToEntityAttribute(String dbData) {
        return "Y".equals(dbData);
    }
}
@Entity
public class Member {

    @Id
    private String id;
    
    private String username;
    
    @Convert(converter = BooleanToYNConverter.class)
    private boolean vip;
    
    ...
}

 

엔티티 이벤트 리스너

엔티티 생명주기 중 발생하는 동작마다 이벤트가 함께 발생한다.

엔티티 생명주기

  1. PostLoad: 엔티티가 조회된 직후, refresh를 호출한 직후
  2. PrePersist: persist() 메서드를 호출하기 직전, 새로운 인스턴스를 merge할 때
  3. PreUpdate: flush나 commit을 호출하여 수정하기 직전
  4. PreRemove: remove 메서드를 호출하여 엔티티를 삭제하기 직전, 영속성 전이로 flush나 commit 될 때
  5. PostPersist: flush나 commit을 호출하여 저장한 직후 (항상 식별자 존재)
  6. PostUpdate: flush나 commit을 호출하여 수정한 직후
  7. PostRemove: flush나 commit을 호출하여 삭제한 직후

이처럼 이벤트가 발생하는 시점에 동작하는 로직을 이벤트 리스너를 구현하여 처리할 수 있다.

 

엔티티에 직접 적용

@Entity
public class Duck {

    @Id @GeneratedValue
    public Long id;
    
    private String name;
    
    @PrePersist
    public void prePersist() {
        System.out.println("duck prePersist id =" + id);
    }
    
    @PostLoad
    public void postLoad() {
        ...
    }
    
    ...
}

위와 같이 각 이벤트에 해당하는 어노테이션을 엔티티에 적용함으로써 처리 로직을 구현할 수 있다.

 

별도 리스너 등록

@Entity
@EntityListeners(DuckListener.class)
public class Duck {
    ...
}
public class DuckListener {
    
    @PrePersist
    private void prePersist(Obejct obj) {
        System.out.println("DuckListener prePersist obj = " + obj.toString());
    }
    
    ...
}

위와 같이 별도 리스너 클래스를 구현하고 이를 엔티티에 등록해서 사용할 수도 있다.

리스너에서 정의되는 메서드들은 파라미터로 엔티티를 받아서 사용한다. (반환 타입은 void)

 

기본 리스너 등록

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings ...>
    <persistence-unit-metadata>
        <persistence-unit-defaults>
            <entity-listeners>
                <entity-listener class="jpabook.jpashop.domain.test.listener.DefaultListener" />
            </entity-listeners>
        </persistence-unit-defaults>
    </persistence-unit-metadata>
</entity-mappings>

모든 엔티티의 동작을 처리할 기본 리스너를 위와 같이 orm.xml에 등록할 수 있다.

 

엔티티 그래프

일반적인 연관관계 설정으로 전역 페치 전략을 지연 로딩으로 설정하고, 즉시 로딩이 필요한 경우에만 JPQL fetch join을 사용한다.

하나의 엔티티가 여러 연관관계를 가지는 경우에는 연관관계마다 fetch join 쿼리를 일일히 작성해주어야한다.

이러한 불편함을 해소하기 위해 엔티티 그래프를 설정해서 필요한 경우에 알맞은 페치 전략을 사용할 수 있다.

 

Named 엔티티 그래프

@NamedEntityGraph(
    name = "Order.withMember",
    attributeNodes = { @NamedAttributeNode("member") }
)
@Entity
@Table(name = "Orders")
public class Order {

    @Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "member_id")
    private Member member;
    
    ...
}
Order order = em.find(
    Order.class, 
    orderId, 
    Map.of("javax.persistence.fetchgraph", em.getEntityGraph("Order.withMember"))
);

위와 같이 즉시 로딩으로 조회하기 위해 사용될 엔티티 그래프 이름을 "Order.withMember"로 설정해두고 엔티티 그래프 이름을 파라미터로 전달해서 즉시 로딩할 수 있다.

 

Subgraph

@NamedEntityGraph(
    name = "Order.withMember",
    attributeNodes = {
        @NamedAttributeNode("member"),
        @NamedAttributeNode(
            value = "orderItems", 
            subgraph = "orderItems"
        )
    },
    subgraph = @NamedSubgraph(
        name = "orderItems",
        attributeNodes = { @NamedAttributeNode("item") }
    )
)
@Entity
@Table(name = "Orders")
public class Order {

    @Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "member_id")
    private Member member;
    
    @OneToMany(mapptedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();
    
    ...
}

위 코드에서는 Order를 조회할 때 member 필드와 orderItems 필드를 같이 조회하도록 엔티티 그래프를 설정했다.

그리고 orderItems를 조회할 때는 그 내부의 item이라는 연관관계도 함께 탐색하도록 Subgraph를 설정하였다.

 

JPQL에서 엔티티 그래프 힌트 사용하기

List<Order> resultList = em
    .createQuery("select o from Order o where o.id = :orderId", Order.class)
    .setParameter("orderId", orderId)
    .setHint("javax.persistence.fetchgraph", em.getEntityGraph("Order.withMember"))
    .getResultList();

위와 같은 JPQL을 실행할 때 setHint(...) 메서드를 사용해서 엔티티 그래프 힌트를 설정해서 즉시 로딩한다.

 

동적 엔티티 그래프 구성

EntityGraph<Order> graph = em.createEntityGraph(Order.class);
graph.addAttributeNodes("member");

Subgraph<OrderItem> orderItems = graph.addSubgraph("orderItems");
orderItems.addAttributeNodes("item");

Order order = em.find(
    Order.class,
    orderId,
    Map.of("javax.persistence.fetchgraph", graph)
);

위와 같이 조회 시 엔티티 그래프를 동적으로 구성해서 즉시 로딩할 수 있다.

 

다음 글: https://gojs.tistory.com/59

 

JPA 표준 예외 처리

JPA의 표준 예외는 javax.persistence.PersistenceException의 자식 클래스이다.이 PersistenceException은 RuntimeException의 자식 클래스로 unchecked exception이다. 이와 같은 JPA 표준 예외는 크게 두 가지로 구분할 수

gojs.tistory.com

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/08   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함