본문 바로가기
Spring 이해하기

[JPA] Field or Property access?

by simplify-len 2020. 12. 13.

원본 - thorben-janssen.com/access-strategies-in-jpa-and-hibernate/

 

Access Strategies in JPA and Hibernate - Which is better, field or property access? - Thorben Janssen

JPA and Hibernate support field-based and property-based access strategies. But what exactly is the difference and which one should you choose?

thorben-janssen.com

들어가기

JPA를 활용해 연관관계를 맺을 때, 나도 간과했던 부분이 하나 있었습니다.

Entity 의 attributes에 접근하는 방법을 정의할 수 있다. 이 방법에 따라 미묘하게 차이가 있다는 사실을 알았습니다.

`field-based` 과 `property-based` 접근이 가능하다.

field-based access

If you use field-based access, your JPA implementation uses reflection to read or write your entity attributes directly. It also expects that you place your mapping annotations on your entity attributes.

field 접근방식의 경우는 JPA 구현은 reflection 을 사용하여 Entity attributes directly 하게 읽고, 쓰고 할 수 있다. 이는 엔티티 속성에 @Access(Field) 애노테이션을 작성할 것을 기대합니다.

property-based access

If you use property-based access, you need to annotate the getter methods of your entity attributes with the required mapping annotations. Your JPA implementation then calls the getter and setter methods to access your entity attributes.

property 접근방식을 사용하는 경우 필요한 매핑 주석으로 엔티티 속성의 getter 메서드에 주석을 추가해야합니다. 그런 다음 JPA 구현은 getter및 setter 메소드를 호출하여 엔티티 속성에 액세스합니다.

이제 그 차이점을 찾아봅시다.

Access 전략을 지정하는 방법

Access 전략의 기본 구성

기본적으로 Primary Key Attribute 또는 해당 Getter 메서드에 @Id Annotation을 추가하여 액세스 전략을 암시적으로 지정할 수 있습니다. 속성 자체에 주석을 달면 Hibernate는 Field Access 사용합니다.

@Entity
@Getter
@NoArgsConstructor
public class Customer {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
  private Set<Order> orders = new HashSet<>();

  public Customer(String name) {
    this.name = name;
  }

  public void addOrder(Order order) {
    orders.add(order);
  }

  @Override
  public String toString() {
    return "Customer{" +
        "id=" + id +
        ", name='" + name + '\'' +
        ", orders=" + orders +
        '}';
  }
}

그리고 @Id Annotation으로 Getter 메소드에 어노테이션을 작성하면 Hibernate는 속성 기반 액세스를 사용하여이 엔티티의 속성을 설정하고 읽습니다.

import java.util.HashSet;
import java.util.Set;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
public class Customer {
  
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
  private Set<Order> orders = new HashSet<>();

  public Customer(String name) {
    this.name = name;
  }

  public void addOrder(Order order) {
    orders.add(order);
  }
  
  @Id
  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  @Override
  public String toString() {
    return "Customer{" +
        "id=" + id +
        ", name='" + name + '\'' +
        ", orders=" + orders +
        '}';
  }
}

 

기본 Access 전략을 재정의할 수 있다.

하나의 엔티티 또는 엔티티 계층 구조 내에서 두 액세스 전략을 혼합 하려면 @Access 주석 으로 기본 전략을 재정의해야합니다 . 그렇지 않으면 JPA 사양은 동작을 정의되지 않은 것으로 정의합니다.

Access 주석을 명시 적으로 지정하지 않고 엔터티 계층 구조 내에서 필드 및 속성에 대한 주석 배치를 혼합하는 애플리케이션의 동작은 정의되지 않습니다.
JSR 338 : JavaTM Persistence API, 버전 2.2

다음 예제에서 @Access  사용하여 버전 속성에 대한 액세스 전략을 속성 기반에서 필드 기반 액세스로 변경합니다.

필드 기반 액세스를 사용해야하는 5 가지 이유

 앞에서 설명한 것처럼 대부분의 경우 엔터티의 속성 또는 getter 메서드에 주석을 달아 액세스 전략을 지정합니다. 하지만 어떤 전략을 선택해야합니까? 그것이 진정한 차이를 만들까요?

약간 차이가 있습니다. 나는 항상 field Access 를 추천합니다. 

Reason 1: 코드의 가독성 향상(Better readability of your code)

 읽을 수있는 코드를 작성하는 것이 중요합니다. 특정 구현이 코드에 시각적으로 어떤 영향을 미치는지 항상 고려해야합니다. 그리고 액세스 전략은 매핑 주석을 배치 할 수있는 위치를 정의하기 때문에 가독성에 상당한 영향을 미칩니다.

필드 기반 액세스를 사용하는 경우 매핑 주석으로 엔터티 속성에 주석을 답니다. 모든 엔터티 속성의 정의를 클래스 맨 위에 배치하면 모든 속성과 해당 매핑을 비교적 간결하게 볼 수 있습니다.

@Entity
public class Review {
  
    @Id
    protected Long id;
  
    @Enumerated
    private Rating rating;
  
    private ZonedDateTime postedAt;
  
    @Version
    private int version;
 
    ...
}

 getter 메서드의 구현이 여러 줄에 걸쳐 있기 때문에 속성 기반 액세스로는 이를 달성 할 수 없습니다. 이렇게 하려면 더 많은 코드를 스크롤해야하므로 엔터티와 해당 매핑에 대한 개요를 얻기가 훨씬 더 어려워집니다.

@Entity
public class Review {
  
    ...
 
    @Id
    public Long getId() {
        return id;
    }
     
    public void setId(Long id) {
        this.id = id;
    }
     
    @Version
    public int getVersion() {
        return version;
    }
     
    public void setVersion(int version) {
        this.version = version;
    }
     
    @Enumerated
    public Rating getRating() {
        return rating;
    }
 
    public void setRating(Rating rating) {
        this.rating = rating;
    }
 
    public ZonedDateTime getPostedAt() {
        return postedAt;
    }
 
    public void setPostedAt(ZonedDateTime postedAt) {
        this.postedAt = postedAt;
    }
 
    @Override
    public String toString() {
        return "BaseReview [id=" + id + ", rating=" + rating + ", postedAt="
                + postedAt + ", version=" + version + "]";
    }
}

Reason 2: 애플리케이션에서 호출하면 안되는 getter 또는 setter 메서드를 생략합니다.(Omit getter or setter methods that shouldn’t be called by your application)

 필드 기반 액세스의 또 다른 장점은 Hibernate 또는 EclipseLink와 같은 지속성 공급자가 엔티티 속성의 getter 및 setter 메서드를 사용하지 않는다는 것입니다. 즉, 비즈니스 코드에서 사용해서는 안되는 방법을 제공 할 필요가 없습니다. 생성 된 기본 키 속성 또는 버전 열의 setter 메서드에 대한 경우가 가장 많습니다 . 지속성 공급자는 이러한 특성의 값을 관리하므로 프로그래밍 방식으로 설정해서는 안됩니다.

@Entity
public class Review {
  
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "review_seq")
    @SequenceGenerator(name = "review_seq", sequenceName = "review_seq")
    protected Long id;
  
    @Enumerated
    private Rating rating;
  
    private ZonedDateTime postedAt;
  
    @Version
    private int version;
 
 
    public Long getId() {
        return id;
    }
 
    public int getVersion() {
        return version;
    }
 
    ...
}

또한 대다 연관에서 요소를 추가 또는 제거하고 기본 List 또는 Set에 대한 직접 액세스를 방지 할 수있는 유틸리티 메소드를 제공 할 수도 있습니다 . 이렇게하면 비즈니스 코드를보다 편안하게 구현할 수 있으며 양방향 다 대다 연결에 대한 일반적인 모범 사례입니다 .

@Entity
public class Book {
 
    @ManyToMany
    Set<Author> authors;
  
    public void addAuthor(Author a) {
        this.authors.add(a);
        a.getBooks.add(this);
    }
  
    public void removeAuthor(Author a) {
        this.authors.remove(a);
        a.getBooks().remove(this);
    }
  
    …
}

보시다시피 필드 기반 액세스는 비즈니스 코드에서 사용해야하는 getter 및 setter 메서드 만 구현할 수있는 유연성을 제공합니다. 그러면 엔티티를 훨씬 쉽게 사용할 수 있고 버그를 방지 할 수 있습니다.

Reason 3: getter 및 setter 메서드의 유연한 구현(Flexible implementation of getter and setter methods)

Persistence provider가 getter 및 setter 메서드를 호출하지 않기 때문에 외부 요구 사항을 충족하지 않아도됩니다. 원하는 방식으로 이러한 메서드를 구현할 수 있습니다. 이를 통해 비즈니스 별 유효성 검사 규칙을 구현하고 추가 비즈니스 논리를 트리거하거나 엔터티 속성을 다른 데이터 유형으로 변환 할 수 있습니다.

다음 예제 에서이를 사용하여 Java Optional 선택적 연결  래핑합니다 . 그럼에도 불구하고 JPA와 Hibernate는 Optional 을 속성 유형으로 지원하지 않으며 , 필드 기반 액세스를 통해 getter 메소드의 리턴 유형으로 사용할 수 있습니다.

@Entity
public class Book {
 
    @ManyToOne
    Publisher publisher;
  
    public void setPublisher(Publisher p) {
        this.publisher = p;
    }
  
    public Optional<Publisher> getPublisher() {
        return Optional<Publisher>.ofNullable(this.publisher);
    }
  
    …
}

 

Reason 4: 유틸리티 메서드를 @Transient 로 표시 할 필요가 없습니다.(No need to mark utility methods as @Transient)

 필드 기반 액세스 전략의 또 다른 이점은 @Transient로 유틸리티 메서드에 주석을 달 필요가 없다는 것 입니다. 이 주석은 Presistence Provider 에게 method 또는 attributes이 엔티티 지속성 상태의 일부가 아님을 알려줍니다. 필드 유형 액세스를 사용하면 지속적 상태가 엔티티의 속성에 의해 정의되기 때문에 JPA 구현은 엔티티의 모든 메소드를 무시합니다.

Reason 5: 프록시 작업시 버그 방지(Avoid bugs when working with proxies)

Hibernate uses proxies for lazily fetched to-one associations so that it can control the initialization of these associations. That approach works fine in almost all situations. But it introduces a dangerous pitfall if you use property-based access.

 

Entity Mappings: Introduction to JPA FetchTypes

FetchTypes define when Hibernate shall fetch related entities from the database and are an important element for a high-performance persistence tier.

thorben-janssen.com

Hibernate는 이러한 연관의 초기화를 제어 할 수 있도록 지연 가져 오기 된 대일 연관에 프록시를 사용합니다. 이 접근 방식은 거의 모든 상황에서 잘 작동합니다. 그러나 Property-based access를 사용하면 위험한 함정이 발생합니다.

 Property-based access를 사용하는 경우 Hibernate는 getter 메서드를 호출 할 때 프록시 개체의 속성을 초기화합니다. 비즈니스 코드에서 프록시 개체를 사용하는 경우 항상 그렇습니다. 그러나 상당수의 equals 및 hashCode 구현 은 속성에 직접 액세스합니다. 프록시 속성에 처음 액세스하는 경우 이러한 속성은 여전히 ​​초기화되지 않습니다.

@Entity
public class Book {
 
    @NaturalId
    String isbn;
 
    ...
  
    @Override
    public int hashCode() {
    return Objects.hashCode(isbn);
    }
 
    @Override
    public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    Book other = (Book) obj;
    return Objects.equals(isbn, other.isbn);
    }
}

Property-based access 전략을 사용하면 이러한 함정을 쉽게 피할 수 있습니다.

결론

Hibernate 및 EclipseLink와 같은 모든 JPA 구현은 지속성 공급자가 엔티티의 속성을 읽고 설정하는 방법을 정의하는 2 개의 액세스 전략을 지원합니다. 필드 기반 액세스 전략은 리플렉션을 사용하고 속성 기반 액세스 전략은 엔터티의 getter 및 setter 메서드를 사용합니다.

이것은 약간의 내부 차이처럼 보일 수 있습니다. 그러나이 기사에서 논의했듯이 액세스 전략은 엔티티를 구현하는 방법에 영향을 미치며 버그를 방지하는 데 도움이 될 수도 있습니다. 그렇기 때문에 항상 현장 기반 액세스 전략을 사용하는 것이 좋습니다.

댓글