JPA

14장 컬렉션과 부가 기능

iksadnorth 2023. 9. 8. 14:37

해당 게시물은 책 '자바 ORM 표준 프로그래밍'을 읽고 작성했습니다.

👣 개요

해당 게시글에서는 JPA에서의 컬렉션, @Converter, 리스너, 엔티티 그래프에 대해서 배울 것이다.

1. JPA에서의 컬렉션
지연 로딩을 도와주는 컬렉션을 사용할 때, 의도하지 않은 시점에서의 지연 로딩을 알아야 한다.

2. @Converter
자바에서의 데이터값과 DB에서의 데이터값을 인코더와 디코더를 이용해서 
쉽게 변환해주는 장치

3. 리스너
엔티티 상태변화에 따른 리스너를 제공하고 있다.
이에 따라 상태변화 시, 로깅을 하거나 특수한 업무를 자동으로 처리할 수 있다.

4. 엔티티 그래프
연관 관계의 엔티티를 한 번에 가져오는 형태를 취해서 N+1문제를 일으키지 않는 장치.
JPQL의 fetch join과 같이 엔티티를 가져오지만 기존의 em.find() 메서드로도 호출할 수 있다.

 

👣 JPA에서의 컬렉션

// PersistentBag
@OneToMany
Collection<Member> collection = new ArrayList<>();
@OneToMany
List<Member> collection = new ArrayList<>();

// PersistentSet
@OneToMany
Set<Member> collection = new HashSet<>();

// PersistentList
@OneToMany @OrderColumn
List<Member> collection = new ArrayList<>();

PersistenceBag의 경우, 중복도 허용하고 순서 보관도 하지 않기 때문에
collection.add() 메서드를 호출하더라도 
굳이 지연 로딩을 수행할 필요없이 새로운 데이터를 담아내면 된다.
collection.get({아직 호출하지 않은 ID값}).getXxx() 메서드를 호출하면 그제서야
DB에 쿼리를 불러올 뿐이다. 이 때도 컬렉션의 모든 요소를 로드하는 것이 아니라
{아직 호출하지 않은 ID값}에 해당하는 요소만 부분적으로 로드할 뿐이다.
이것은 잘 이용한다면 불필요한 지연 로딩을 하지 않아도 되지만 
잘못 사용한다면 N+1문제를 일으킬 수도 있으니 유의해야 한다.

PersistenceSet의 경우, 중복을 허용하지 않기 때문에
collection.add() 메서드를 호출하면 
우선 1차 캐시에서 검색하고 없다면
지연 로딩된 해당 요소를 DB에서 부분적으로 초기화해야 한다.

PersistenceList의 경우, 순서를 고려하기 때문에
collection.add() 메서드를 호출하면 
순서에 영향받는 모든 요소를 조회해야 한다.

 

👣 @Converter

예를 들어, 성별 칼럼을 이용할 때, DB에서는 남자는 0, 여자는 1이라고 표기를 하고 있지만
Java에서 사용할 때는 남자는 'M', 여자는 'F'로 이용하고 싶다면 Encoder와 Decoder를 만들어서
사용해야 한다. 

이런 과정을 미리 @Converter 라는 것을 이용해 구현하기 쉽게 제공하고 있다.

@Entity
public class Member {
    @Id
    private Long id;
    
    @Convert(converter=BooleanToMFConverter.class)
    private String gender;
}
@Converter
public class BooleanToMFConverter implements AttributeConverter<String,Boolean> {
    @Override
    public String ConvertToEntityAttribute(Boolean attr) {
        return (attr != null && attr) ? "F" : "M";
    }
    
    @Override
    public Boolean ConvertToDatabaseColumn(String dbData) {
        return "F".equals(dbData);
    }
}

 

👣 리스너

엔티티는 영속성 컨텍스트와 DB에서의 상호작용 중에 수행해야 할 작업이 있을 수 있다.
예를 들어 엔티티의 상태를 추적하기 위해 각 리스너 메서드에 로깅 코드를 삽입할 수도 있다.
이와 같이 엔티티의 상태 변화에 따른 로직을 삽입하고 싶다면 해당 리스너를 사요하면 된다.

리스너는 3가지의 방법으로 등록할 수 있다.

1. 엔티티에 직접 적용
2. 별도의 리스너 등록
3. 기본 리스너 사용

1. 엔티티에 직접 적용

@Entity
public class Member {
    @Id
    private Long id;
    
    @PrePersist
    public void prePersist() {
        ...
    }
    
    @PostPersist
    public void postPersist() {
        ...
    }
}

2. 별도의 리스너 등록

@Entity
@EntityListeners(AuditingEntityListener.class)
public class Member {
    ...
}
public class AuditingEntityListener {
    private ObjectFactory<AuditingHandler> handler;

    @PrePersist
	public void touchForCreate(Object target) {
		if (handler != null) {
			AuditingHandler object = handler.getObject();
			if (object != null) {
				object.markCreated(target);
			}
		}
	}
}

여기서 @PrePersist의 메서드의
첫번째 파라미터는 추적 대상 엔티티 객체가 들어오고
두번째 파라미터는 EntityManager가 들어온다.

3. 기본 리스너 사용
모든 엔티티의 이벤트를 처리하고 싶으면 META-INF/orm.xml에 리스터를 지정하면 된다.

...

<persistence-unit-metadata>
    <persistence-unit-metadata>
        <entity-listeners>
            <entity-listener class="com.example.DefaultEntityListener"/>
        </entity-listeners>
    </persistence-unit-metadata>
</persistence-unit-metadata>

...

 

👣 엔티티 그래프

연관된 엔티티를 로드하기 위해선 2가지의 방법을 고려할 수 있다.

1. 글로벌 Fetch 옵션을 FetchType.EAGER로 설정
2. JPQL에서 Fetch Join을 사용하기

1번은 App 전체에 영향을 주기 때문에 불필요하게 엔티티를 가져오는 경우가 있을 수 있다.
그리고 2번은 JPQL에 같은 내용의 Join문을 번거롭게 연거푸 작성해야 하는 불편함이 존재한다.

엔티티 그래프를 이용하면 JPQL의 역할 중 연관관계의 엔티티 조회 기능을 따로 분리해서 
작성할 수 있고 이것은 유지 보수성을 크게 올릴 수 있다.

@NamedEntityGraph(name = "Post.withUser", attributeNodes = {
        @NamedAttributeNode("user")
})
@Entity @Table(name = "post")
public class Post {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY) @JoinColumn
    private User user;
}

위 코드는 사용할 엔티티 그래프를 미리 지정하는 코드다.

EntityGraph graph = em.getEntityGraph("User.withUser");

Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);

User entity = em.find(User.class, userId, hints);

위 코드는 엔티티 그래프를 실제로 활용하는 방법이다.

EntityGraph<?> graph = em.getEntityGraph("Post.withUser");
em.createQuery("select p from Post p where p.id = :postId")
        .setParameter("postId", 1L)
        .setHint("javax.persistence.fetchgraph", graph)
        .getResultList();

위 코드는 JPQL에서의 엔티티 그래프 활용 방법이다.

 

'JPA' 카테고리의 다른 글

16장 트랜잭션과 락, 2차 캐시  (0) 2023.09.12
15장 고급 주제와 성능 최적화  (0) 2023.09.09
13장 웹 애플리케이션과 영속성 관리  (0) 2023.09.07
12장 Spring Data JPA  (0) 2023.09.06
영속성 컨텍스트 vs JPQL  (0) 2023.09.06