JPA의 상속과 구성

소개



«반복하지 마세요» 또는 «DRY». 개발자는 소프트웨어 개발 중에 이 원칙을 고수하려고 합니다. 중복 코드 작성을 방지하고 결과적으로 향후 유지 관리를 단순화합니다. 그러나 JPA 세계에서 이 원칙을 달성하는 방법은 무엇입니까?

상속과 구성의 두 가지 접근 방식이 있습니다. 둘 다 장단점이 있습니다. "실제 세계"는 아니지만 대표적인 예에서 그것들이 무엇인지 알아 봅시다.

주제 도메인



우리 모델에는 Article, Author 및 Spectator의 세 가지 엔터티가 있습니다. 각 엔터티에는 감사를 위한 필드(createdDate, createdBy, modifiedDate 및 modifedBy)가 있습니다. 저자와 관람자는 주소(국가, 도시, 거리, 건물)에 대한 필드도 있습니다.



상속: MappedSuperclass



DRY 원칙을 준수하기 위해 중복 필드를 별도의 매핑된 슈퍼클래스로 가져갑시다. 우리는 그들로부터 엔티티를 물려받을 것입니다. 모든 엔터티에는 감사를 위한 필드가 있어야 하므로 BaseEntityAudit 클래스부터 시작하겠습니다. 주소 필드가 있는 엔터티에 대해 "BaseEntityAuditAddress"클래스를 만들고 BaseEntityAudit 클래스에서 상속합니다.



참고: 이 문서에 제시된 모든 접근 방식은 GitHub의 이 저장소에서 구현되고 사용할 수 있습니다.

@MappedSuperclass 
public class BaseEntityAuditAddress extends BaseEntityAudit { 
  @Column(name = "country") 
  private String country; 

  @Column(name = "city") 
  private String city; 

  @Column(name = "street") 
  private String street; 

  @Column(name = "building") 
  private String building;
  //...
}

@Entity 
@Table(name = "spectator") 
public class Spectator extends BaseEntityAuditAddress {
  //...
}


엔터티의 계층 구조는 더 이상 반복되지 않도록 구현됩니다. 미션 완료. 하지만 만약에...

계층 구조 깨기



그러나 모델의 초기 요구 사항이 약간 변경되면 어떻게 될까요? 예를 들어 기사에는 감사 필드만 필요하고 관람자는 주소 필드만 필요하며 작성자는 둘 다 필요하다고 생각하십시오. 이 경우 상속 전략에 따라 Java에는 클래스에 대한 다중 상속이 없기 때문에 어쨌든 DRY 원칙을 무시해야 합니다. 즉, 우리의 계층 구조는 아래 다이어그램처럼 보일 것이며 Java에서는 구현이 불가능합니다.



이전에 만든 두 개의 슈퍼클래스와 관중 전용 주소 필드가 있는 슈퍼클래스를 남겨두어야 합니다. 따라서 주소 필드는 두 엔터티에서 반복됩니다. DRY 원칙을 준수하려면 컴포지션을 대신 사용합시다.



구성: @Embeddable 및 인터페이스



getBaseEntityAudit() 또는 getBaseEntityAddress() 메서드 하나만 있는 인터페이스를 통해 컴포지션을 구현해 보겠습니다. 짐작할 수 있듯이 해당 필드를 포함하는 포함 가능한 엔터티를 반환합니다. 엔티티에서 이러한 메소드를 구현하면 @Embedded 필드에 대한 getter가 대체됩니다.



@Embeddable 
public class BaseEntityAudit { 
  @Column(name = "created_date", nullable = false, updatable = false) 
  @CreatedDate 
  private long createdDate; 

  @Column(name = "created_by") 
  @CreatedBy 
  private String createdBy; 

  @Column(name = "modified_date") 
  @LastModifiedDate 
  private long modifiedDate; 

  @Column(name = "modified_by") 
  @LastModifiedBy 
  private String modifiedBy;
  // ...
}

public interface EntityWithAuditFields { 
  BaseEntityAudit getBaseEntityAudit(); 
}


이제 우리는 모든 엔티티에서 이러한 인터페이스를 자유롭게 사용할 수 있습니다. 인터페이스 메소드를 구현하려면 @Embedded 속성과 이에 대한 getter를 추가해야 합니다.

@Entity 
@Table(name = "author") 
public class Author implements EntityWithAuditFields, EntityWithAddressFields { 
//...

@Embedded 
private BaseEntityAudit baseEntityAudit; 

@Embedded 
private BaseEntityAddress baseEntityAddress; 

public BaseEntityAddress getBaseEntityAddress() {
  return baseEntityAddress; 
} 

public BaseEntityAudit getBaseEntityAudit() { 
  return baseEntityAudit; 
}
//... 
}


다형성: 상위 클래스로 업캐스트



엔터티 코드에서 DRY를 달성했지만 이러한 엔터티와 함께 ​​작동하는 비즈니스 코드는 어떻습니까? 엔터티 목록에서 국가 목록을 반환하는 메서드가 필요하다고 상상해 봅시다. 상속이 있는 예제에서는 BaseEntityAuditAddress 유형이 매개변수로 포함된 목록을 전달해야 합니다. 그리고 우리는 이 방법을 작성자와 관중 모두에게 사용할 수 있습니다.

public class Business { 
  public List<String> getCountries(List<BaseEntityAuditAddress> entitiesList) { 
    if (entitiesList == null || entitiesList.isEmpty()) { 
      return Collections.emptyList(); 
    } 
    return entitiesList.stream() 
      .map(BaseEntityAuditAddress::getCountry) 
      .distinct() 
      .collect(Collectors.toList()); 
    } 
}


사용법은 다음과 같습니다.

List<BaseEntityAuditAddress> authors = new ArrayList<>();

//add authors to the list

List<String> countries = new Business().getCountries(authors);


그러나 접근 방식을 변경해도 아무 것도 변경되지 않습니다. 변경해야 할 모든 것은 BaseEntityAuditAddress를 EntityWithAddressFields로 바꾸는 것입니다.

public class Business { 
  public List<String> getCountries(List<EntityWithAddressFields> entitiesList) { 
    if (entitiesList == null || entitiesList.isEmpty()) { 
      return Collections.emptyList(); 
    } 
    return entitiesList.stream() 
      .map(EntityWithAddressFields::getBaseEntityAddress) 
      .map(BaseEntityAddress::getCountry) 
      .distinct() 
      .collect(Collectors.toList());
    }
}


아마도 주소와 감사 필드가 모두 아닌 주소만 있는 엔터티를 명시적으로 참조하기 때문에 메서드를 읽기가 더 쉬워졌을 것입니다.

결론



결국 컴포지션은 더 유연한 사용 사례를 가진 것 같습니다. 그러나 상속을 사용하기로 결정하더라도(가능한 이유 중 하나: 그러한 유연성을 의도적으로 제한하기 위해) 선택한 접근 방식에 관계없이 JPA Buddy이 도움이 될 것입니다. 이 기사의 짧은 비디오 버전에서 확인하십시오.

좋은 웹페이지 즐겨찾기