API/JPA

컬렉션과 부가 기능

시뻘건 튼튼발자 2024. 9. 14. 11:41
반응형

 

컬렉션

[그림 1] 자바 컬렉션 구조

자바는 기본으로 Collection, List, Set, Map 컬렉션을 지원한다. 이 컬렉션은 JPA에서 아래와 같이 활용할 수 있다.

  • @OneToMany, @ManyToMany를 사용하여 일대다, 다대다 관계를 매핑할 때
  • @ElementCollection을 사용하여 값 타입을 하나 이상 보관할 때

(각 자바의 컬렉션의 특징은 자바의 기본적인 사항이므로 넘어간다.)

JPA와 컬렉션

하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 "내장 컬렉션"으로 감싸서 사용한다. "래퍼 컬렉션"이라고도 부른다.

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    
    @OneToMany
    @JoinColumn
    private Collection<Order> orders = new ArrayList<>();
    
    //...
}

// ...

Member member = new Member();
System.out.println(member.getOrders().getClass()) // class java.util.ArrayList
em.persist(member);
System.out.println(member.getOrders().getClass()) // class org.hibernate.collection.internal.PersistentBag

하이버네이트의 내장 컬렉션은 아래와 같다.

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

Collection, List

Collection과 List는 중복이 허용되므로 add() 메소드는 내부적으로 비교를 하지 않고 항상 true를 리턴한다. 같은 엔티티가 있는지 찾거나 삭제할 때는 equals() 메소드를 사용한다. 따라서 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않는다.

Set

Set은 중복이 허용되지 않으므로 add() 메소드로 객체를 추가할 때마다 equals() 메소드로 같은 객체가 있는지 비교하고 이미 있으면 false를 리턴한다. HashSet은 해시 알고리즘을 사용하므로 hashcode()도 함께 사용한다. 따라서 엔티티를 추가할 때마다 지연 로딩된 컬렉션을 초기화한다.

List + @OrderColumn

List와 @OrderColumn를 사용하면 순서가 있는 특수한 컬렉션으로 인식한다. 즉, 데이터베이스에 ㅅ ㅜㄴ서 값을 저장해서 조회할 때 사용한다는 의미다.

@Entity
public class Member {
    
    @Id @GeneratedValue
    private Long id;
    
    @OrderColumn(name = "POSITION")
    private List<Address> addresses = new ArrayList<>();
}

@OrderColumn는 아래와 같은 단점으로인해 현업에서는 잘 사용하지 않는다.

  • 컬렉션의 중간에 값을 삽입하거나 삭제하는 경우 모든 순서 정보가 변경되어 해당 행들을 모두 업데이트해야 한다. 이로 인해 추가적인 업데이트 쿼리가 발생하여 성능이 저하될 수 있다.
  • 컬렉션이 수정되면 순서 정보도 같이 업데이트되는데, 이 과정에서 순서가 어긋나거나 잘못 저장될 위험이 있다. 예를 들어 수동으로 컬럼의 값을 변경하거나 데이터베이스에서 직접 순서값을 수정하면 무결성 문제가 발생할 수 있다.

@OrderBy

@OrderColumn를 대체할 수 있는 @OrderBy를 사용하는 것을 권장한다.

@OrderBy는 JPA에서 컬렉션이나 관계의 엔티티를 정렬된 상태로 조회하기 위해 사용하는 어노테이션이다. 컬렉션에 포함된 엔티티를 특정 필드의 값을 기준으로 자동으로 정렬할 수 있다.

참고로 Set에 @OrderBy를 적용하면 순서 유지를 위해 HashSet 대신에 LinkedHashSet을 내부적으로 사용한다.

@Entity
public class Member {
    
    @Id @GeneratedValue
    private Long id;
    
    private String name;

    @OneToMany(mappedBy = "member")
    @OrderBy("street ASC")
    private List<Address> addresses = new ArrayList<>();
}

@Entity
public class Address {
    
    @Id @GeneratedValue
    private Long id;
    
    private String street;
    private String city;

    @ManyToOne
    private Member member;
}

@Converter

@Converter는 JPA에서 사용자 정의 타입 변환기를 구현하기 위해 사용하는 어노테이션이다. 이를 통해 엔티티 속성 값을 데이터베이스와 매핑할 때 특정 변환 작업을 수행할 수 있다. @Converter를 사용하면 엔티티 필드의 값을 데이터베이스에 저장할 때나, 데이터베이스에서 값을 읽어올 때 자동으로 변환할 수 있다.

@Converter는 주로 아래와 같은 상황에서 유용하다.

  • 복잡한 객체 타입을 데이터베이스에 적합한 형식으로 변환해야 할 때.
  • enum과 같은 자바의 특수한 타입을 데이터베이스에 문자열 또는 숫자 형태로 저장할 때.
  • 날짜, 시간, 로컬 값 같은 특정 값을 변환할 때.
// 글로벌 설정
@Converter(autoApply = true)
public class BooleanToYNConverter 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);
    }
}

// 컬럼 설정
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;
    @Convert(converter = BooleanToYNConverter.class)
    private Boolean active;
}

// 클래스 설정
@Entity
@Convert(converter = BooleanToYNConverter.class, attributeName = "active")
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private Boolean active;  // 이 필드에 BooleanToYNConverter 적용됨
    private Boolean verified; // 다른 필드에는 적용되지 않음
}

리스너

JPA 리스너는 엔티티의 생명주기에서 특정 이벤트가 발생할 때 실행되는 메소드를 정의하여 특정 작업을 자동으로 수행할 수 있도록 해주는 기능이다. 엔티티의 상태가 변경되거나 특정 액션이 발생할 때마다 JPA는 이를 감지하고, 리스너를 통해 미리 정의된 작업을 실행할 수 있다. 이를 통해 비즈니스 로직이나 데이터 검증, 로그 기록 등을 일관성 있게 처리할 수 있다.

 

  • @PrePersist: 엔티티가 영속성 컨텍스트에 저장되기 전에 호출된다.
  • @PostPersist: 엔티티가 영속성 컨텍스트에 저장된 후에 호출된다.
  • @PreRemove: 엔티티가 삭제되기 전에 호출된다.
  • @PostRemove: 엔티티가 삭제된 후에 호출된다.
  • @PreUpdate: 엔티티가 업데이트되기 전에 호출된다.
  • @PostUpdate: 엔티티가 업데이트된 후에 호출된다.
  • @PostLoad: 엔티티가 데이터베이스에서 로드된 후에 호출된다.

리스너 메소드는 일반적으로 엔티티 클래스 내에 선언하거나 외부 리스너 클래스를 통해 정의할 수 있다. 각 메소드에는 해당 이벤트를 처리하기 위한 어노테이션을 추가한다.

// 엔티티 클래스 내에 선언
@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    
    private String name;

    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;

    @PrePersist
    public void prePersist() {
        this.createdAt = LocalDateTime.now();
        System.out.println("Before persisting: " + this.name);
    }

    @PreUpdate
    public void preUpdate() {
        this.updatedAt = LocalDateTime.now();
        System.out.println("Before updating: " + this.name);
    }

    @PostLoad
    public void postLoad() {
        System.out.println("Entity loaded: " + this.name);
    }
}

// 외부 리스너 클래스를 통해 정의
public class MemberEntityListener {

    @PrePersist
    public void prePersist(Member member) {
        member.setCreatedAt(LocalDateTime.now());
        System.out.println("Before persisting: " + member.getName());
    }

    @PreUpdate
    public void preUpdate(Member member) {
        member.setUpdatedAt(LocalDateTime.now());
        System.out.println("Before updating: " + member.getName());
    }
}

@Entity
@EntityListeners(MemberEntityListener.class)
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

 

엔티티 그래프

엔티티 그래프는 엔티티의 연관 관계를 정의하고, 특정 필드 또는 연관 엔티티를 함께 조회할지 선택할 수 있는 기능이다. 이는 JPA에서 제공하는 기본 패치 전략을 무시하고 즉시 로딩을 사용할 수 있도록 도와준다.

@Entity
public class Member {
    
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne(fetch = FetchType.LAZY)
    private Team team;

    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
    private List<Order> orders;
}

// ...

EntityGraph<Member> entityGraph = em.createEntityGraph(Member.class);
entityGraph.addAttributeNodes("team");

Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.fetchgraph", entityGraph);

Member member = em.find(Member.class, memberId, hints);

Named 엔티티 그래프

Named 엔티티 그래프는 자주 사용하는 엔티티 그래프를 이름으로 정의하고, 필요할 때마다 호출할 수 있는 방법이다. 주로 반복적으로 사용되는 엔티티 그래프를 미리 정의하여 관리할 때 유용하다.

@Entity
@NamedEntityGraph(
    name = "Member.withTeam",
    attributeNodes = @NamedAttributeNode("team")
)
public class Member {
    
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne(fetch = FetchType.LAZY)
    private Team team;
    
    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
    private List<Order> orders;
}

// ...

EntityGraph<?> entityGraph = em.getEntityGraph("Member.withTeam");

Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.fetchgraph", entityGraph);
// javax.persistence.fetchgraph: 명시된 필드만 즉시 로딩.
// javax.persistence.loadgraph: 명시된 필드는 즉시 로딩, 나머지는 기본 패치 전략을 따름.

Member member = em.find(Member.class, memberId, hints);

subgraph

서브그래프는 엔티티 그래프에서 연관된 엔티티의 속성에 대해서도 그래프를 정의할 수 있는 기능이다. 즉, 엔티티 그래프 내에서 또 다른 엔티티의 필드를 세부적으로 설정할 수 있다.

@Entity
@NamedEntityGraph(
    name = "Member.withTeamAndDepartment",
    attributeNodes = @NamedAttributeNode(value = "team", subgraph = "teamWithDepartment"),
    subgraphs = @NamedSubgraph(
        name = "teamWithDepartment",
        attributeNodes = @NamedAttributeNode("department")
    )
)
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @ManyToOne(fetch = FetchType.LAZY)
    private Team team;
}

동적 엔티티 그래프

런타임 시점에 프로그램의 조건에 따라 엔티티 그래프를 동적으로 생성하여 사용할 수 있는 기능이다. 정적으로 정의된 엔티티 그래프를 사용하는 것과 달리, 특정 조건에 따라 그래프를 동적으로 생성해 유연하게 사용할 수 있다.

EntityGraph<Member> entityGraph = em.createEntityGraph(Member.class);

if (fetchTeam) {
    entityGraph.addAttributeNodes("team");
}
if (fetchOrders) {
    entityGraph.addAttributeNodes("orders");
}

Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.fetchgraph", entityGraph);

Member member = em.find(Member.class, memberId, hints);
반응형