JPA

컬렉션과 부가기능

리승자이 2022. 8. 6. 00:08
728x90

코드는 모두 깃허브에 있음.

일단 자바의 컬렉션 인터페이스들의 특징부터 나열한다.

컬렉션

  • Collection
    • 자바가 제공하는 최상위 컬렉현, Hibernate는 중복을 허용하고, 순서를 보장하지 않는다고 가정
  • Set
    • 중복을 허용하지 않고, 순서도 보장하지 않는다.
  • List
    • 순서가 있는 컬렉션아며 중복을 허용한다.
  • Map
    • Key, Value 구조로 되어있는 컬렉션이다.

JPA와 Collection

Hibernate는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 Hibernate

준비한 컬렉션으로 감싸서 사용한다.

다음 예시를 보자

@Entity
public class Team {
    @Id
    @GeneratedValue
    private Long id;

    @OneToMany
    private Collection<Member> members = new ArrayList<>();
}

@DataJpaTest
class TeamTest {

    @PersistenceUnit
    EntityManagerFactory emf;

    EntityManager em;

    EntityTransaction tx;

    @BeforeEach
    void setUp() {
        em = emf.createEntityManager();
        tx = em.getTransaction();
        tx.begin();
    }


    @Test
    void 컬렉션_테스트() {
        Team team = new Team();
        System.out.println(team.getMembers().getClass());
        em.persist(team);
        System.out.println(team.getMembers().getClass());
    }
}

테스트코드 지만 단순히 이 결과를 확인하기 위해서 콘솔 출력을 진행하였다.

결과는 이렇게 나온다.

image
{: text-center}

처음 객체를 포장할때는 Team 엔티티 클래스에서 명시한 ArrayList로 포장을 하는데 엔티티를 영속상태로 바꿔주는 순간 PersistentBag 으로 변경된다.

Hibernate는 컬렉션을 효율적으로 사용하려고 영속상태로 만들때 원본의 컬렉션을
감싼 내장 컬렉션을 생성하여 이 감싼 내장 컬렉션을 사용하도록 참조를 변경한다.

그렇기 때문에 컬렉션을 사용하려면 즉시 초기화를 해주고 사용하는걸 권장한다.

다음은 Hibernate의 내장 컬렉션들과 특징이다.

컬렉션 내장컬렉션 중복 순서
Collection, List PersistentBag O X
Set PersistentSet X X
List + @OrderColumn PersistentList O O

Collection, List

CollectionList는 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고

왜❓ - 중복을 허용하기 때문

단순히 저장만 하면 된다. 그렇기 때문에 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화 하지 않는다.

Set

Set은 엔티티를 추가할 때 중복값을 확인하기 때문에 서로 비교를 해야한다.

그렇기 때문에 지연 로딩된 컬렉션을 초기화 한다.

List + @OrderColumn

@OrderColumn은 DB에 순서값을 저장해서 조회할 때 사용한다는 의미

순서가 있기에 DB에 순서값도 관리하는데

단점이 있어 사용하지 않는다고 한다.

순서값을 DB가 가지고 있기 때문에 하나를 지운다고 가정하면 삭제된 List의 번호에는 null이 저장된다.

NullPointerException우려

@OrderBy

책에서 나온것처럼 특정 칼럼에 @OrderBy를 주는 법도 있겠지만 이렇게 하지않고 대부분 Auditing 기능 오버라이드 하여 한다고 한다.

@Converter

컨버터는 단어 그대로 형 변환을 해주는 것이다.

예를들어 boolean 타입은 DB에 저장될 때 0과 1로 저장이 된다. 대신에 Y나 N으로 저장하고 싶다면

컨버터를 사용하면 된다.

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

    @Override
    public String convertToDatabaseColumn(Boolean attribute) {
        return (attribute != null && attribute) ? "Y" : "N";
    }

    @Override
    public Boolean convertToEntityAttribute(String dbData) {
        return "Y".equals(dbData);
    }
}

이렇게 AttributeConverter를 구현해주고 @Converter를 명시해준다.

//방법 1
@Convert(converter = BooleanYNConverter.class, attributeName = "적용할 변수")
public class Test {
    //방법 2
    @Convert(converter = BooleanYNConverter.class)
    private boolean 변수명;
}

이렇게 있다. 그리고 추가로 모든 boolean에 대해서 적용을 시켜준다면

클래스최상단에 @Converter(autoApply = true)를 주면 된다.

리스너

JPA 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트 처리 가능

image
{: text-center}

이벤트의 종류와 발생 시점은 위의 이미지와 같다.

  1. PostLoad : 엔티티가 영속성 컨텍스트에 조회된 후 또는 refresh 호출한 후(2차 캐시에 저장되어 있어도 호출).
  2. PrePersist : persist() 를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에. 식별자 생성전략을 사용한 (이하 @GeneratedValue) 경우 엔티티에 식별자는 아직 존재하지 않는다. 또한 새로운 인스턴스를 merge할 때.
  3. PreUpdate : flushcommit을 호출해서 엔티티를 DB에 수정하기 직전
  4. PreRemove : remove()를 호출해 엔티티를 영속성 컨텍스트에서 삭제하기 직전. 영속성 전이가 일어날 때, orphanRemoval(고아객체 관련)에 대해선 flushcommit시에
  5. PostPersist : flushcommit을 호출해서 엔티티를 DB에 저장한 직후 호출. 식별자 항상 존재함. 생성전략이 IDENTITY면 식별자를 생성하기 위해 persist()를 호출한 직후 바로 호출.
  6. PostUpdate : flushcommit을 호출해서 엔티티를 DB에 수정한 직후
  7. PostRemove : flushcommit을 호출 엔티티를 DB에 삭제한 직후

이벤트 적용위치

적용 위치는 3가지이다.

  • 엔티티에 직접 적용
  • 별도의 리스너 등록
  • 기본 리스너 사용

엔티티에 직접 적용

@Entity
public class Entity {
    @Id @GeneratedValue
    private Long id;

    ...

    //아래로 쭉 구현
    @PrePersist
    public void prePersist() {
        ...
    }

    @PostPersist
    public void postPersist() {
        ...
    }
    ...

}

별도의 리스너 등록

이거는 JPA Auditing 생각해보면 될거같다. 결국 AuditingEntityListener 이 리스너도 안에 어노테이션으로 아래와 같이 구현되어있다.

@Configurable
public class AuditingEntityListener {

    private @Nullable ObjectFactory<AuditingHandler> handler;

    public void setAuditingHandler(ObjectFactory<AuditingHandler> auditingHandler) {

        Assert.notNull(auditingHandler, "AuditingHandler must not be null!");
        this.handler = auditingHandler;
    }

    @PrePersist
    public void touchForCreate(Object target) {

        Assert.notNull(target, "Entity must not be null!");

        if (handler != null) {

            AuditingHandler object = handler.getObject();
            if (object != null) {
                object.markCreated(target);
            }
        }
    }

    @PreUpdate
    public void touchForUpdate(Object target) {

        Assert.notNull(target, "Entity must not be null!");

        if (handler != null) {

            AuditingHandler object = handler.getObject();
            if (object != null) {
                object.markModified(target);
            }
        }
    }
}

여러개의 리스너를 등록했을 때 호출순서는

  1. 기본 리스너
  2. 부모 클래스 리스너
  3. 리스너
  4. 엔티티

와 같다.

엔티티 그래프

엔티티 그래프는 엔티티를 조회하는 시점에 연관된 엔티티들을 함께 조회하는 기능이다.
Named 엔티티 그래프는 Named쿼리 자체의 빈도수가 낮기때문에 다루지 않겠다.

EntityGraph<Team> graph = em.createEntityGraph(Team.class);

graph.addAttributeNodes("속성");

JPAQuery<Emp> query = queryFactory.selectFrom(Q클래스).where(조건);

query = query.setHint("javax.persistence.fetchgraph", graph);

query.fetchOne();

이렇게 엔티티 그래프를 정의하고 Hint로 그래프를 넣어주면 되는 방식이다.

정리

엔티티 그래프는 항상 조회하는 엔티티의 ROOT경로에서 시작해야 한다.

만약 Member엔티티에 Team이 포함되어 있다면 Member조회 후 Team으로 가야되는데 역으로 갈 수는 없다.

영속성 컨텍스트에 엔티티가 이미 로딩되어 있다면 엔티티 그래프 적용 ❌

fetchgraph와 loadgraph의 차이는 loadgraph는 엔티티 그래프의 설정한 속성과 함께 글로벌 페치전략이 FetchType.EAGER 인 관계들도 전부 포함해서 함께 조회한다.

728x90