이 글은 3달 전쯤? 단순한 계기로 노션에 작성해둔 글을 수정해서 올려놓은 글입니다. 글 내용도 갑작스럽게 끝나는 느낌이 있어서 수정을 하는 와중에도 이렇게까지 다룰만한 내용인가, 올려놔도 되나? 하는 의구심도 들었는데요.  

 

그냥 이 사람은 그렇게 이해한 상태이구나 정도의 가벼운 마음으로 봐주시길 바랍니다. ㅎㅎ 

 

 

JPA에서는 특정 Column을 별도의 영역에 정의하는 2가지를 방법을 제공한다.

여러 column(field)을 하나의 embedded type으로 정의하여 객체에 포함하는 방법이고 다른 하나는 별도의 추상 클래스를 만들어 변수를 정의하고 @MappedSuperclass로 상속시키는 방법이다.

 

언급된 이 2가지 방식은 어떠한 차이가 있고 특정 column을 정의할 땐 어느 방법을 선택해야 할까?

 

??? : 이건 그냥 상속을 사용하는 것과 조합(위임)을 사용하는 것의 차이에요. 개발을 할 때에는 조합 방식을 쓰는 게 좋아요.

  • 이 질문에 대한 답변으로 가끔 나오는 내용이다. 이것만으로 사용 이유를 시원스럽게(?) 이해할 수 있을까?

 

우선 왜 저런 이야기가 나오는지 짚어보자.

 

 

 

객체지향 관점에서는 "상속보다는 조합을 사용하자"라는 이야기가 정설로 통한다.

이는

  • super type과 sub type이 강한 결합성을 가짐으로써 캡슐화가 깨지고 속한 객체들이 변경의 여파를 받게 된다는 것
  • 한 객체에서 정의된 상태나 행위에 대해서 관리하거나 확인해야 할 포인트(class)가 늘어난다는 것
  • sub type에서 메서드를 재정의하는 경우에 잘못된 이해로 특정 로직만 추가하고 super type의 메서드를 호출하고 있을 때 오동작할 수 있다는 것
  • super type의 메서드가 변경되어서 sub type의 메서드가 깨지거나 오동작할 수 있는 것

등의 문제점들을 기반으로 한다.

 

그렇기에 super type의 기능 혹은 다른 객체의 기능을 사용할 것이라면 이를 상속받는 것이 아니라 상태 값 즉 인스턴스 변수를 통해 기능을 사용하는 것을 추천하게 되는 것이다.

 

그럼 모든 column을 정의할 때 embedded type으로 묶어서 정의하는 것이 가장 좋을 것일까? 

  • createdDate, updatedDate, creator, updator와 같은 운영 상의 이유를 포함하는 Base column을 적용할 때에도 이 방식이 좋은 것일까?

위의 내용들을 이해했다면, 상속 방식 즉. MappedSuperclass를 사용해도 무관할 것 같은 상황이 있다고 느끼리라 생각한다.

 

 

 

우선 위에서 언급된 문제점을 바탕으로 생각해보자.

첫 번째는 위에서 나온 단점들은 객체의 캡슐화가 깨지는 것과 관리 포인트가 늘어난다는 점을 제외하고는 객체의 행위와 관련된 문제점들이라는 것을 알 수 있다. 즉 일반적으로 entity의 상태 변수를 상속하여서 발생하는 상황들은 아니다.

 

 

두 번째는 추상 클래스가 올바르게 사용되는 시점은 해당 type의 "공통된 속성"을 한 곳에 모아 관리할 때이며, 엔티티는 객체이기도 하지만, 데이터 영역에 접근하는 Adapter(JDBC API로 구현된 DAO, Repository)의 DTO. 즉 특정한 데이터 홀더라고 봐야 한다고 생각한다는 점이다. 나는 그렇게 생각한다.

 

여기서 말하고 싶은 것은 엔티티는 객체 지향 관점에서의 객체와는 어느 정도 거리가 있는 녀석이라는 것이다.

 

특히 entity의 공통된 속성들(Create, UpdateDate, creator, updator)의 경우 어떤 개념(객체)에 묶이는 것도 아니고 일반적으로 변경될만한 속성도 아니기에 (선택적으로 사용되는 경우에도 문제없다) 변경의 여파는 거의 발생하지 않을 것이다. 즉 각각의 엔티티의 상태로 조합하여 사용하는 것과 별반 차이가 없다.

 

이러한 데이터는 유연성을 고려할 필요도 없다. 사용하거나 하지 않거나 필요에 따라서 계층적으로 분리하여 사용할 수 있다.

 

 

 

이번엔 JPA Spec 기준으로 생각해보자.

첫 번째로 @MapperdSuperClass는 Spec에 적힌 대로 공통 정보를 상속하는 것에 목적을 두고 만들어진 방법이다.

부가적으로 내부에 정의된 상태를 별도로 나타내지 않고 상속받은 엔티티에서만 나타내기 때문에 결과적으로 완전한 상태의 엔티티를 바라보게 된다.

[2.11.2] Typically, the purpose of such a mapped superclass is to define state and mapping information that is common to multiple entity classes.

 

 

두 번째는 MapperdSuperClass를 사용하는 방식이 embedded type에 비해서 매우 간단하다는 점이다. 운영과 관련된 column의 경우 해당 방식을 통해 쉽게 적용하고 사용할 수 있다.

@MappedSuperclass 
public abstract class BaseEntity { 
    private LocalDate createDate; 
    private LocalDate updateDate; 
} 

@Entity 
@NoArgConstructor(access = AccessLevel.PROTECTED) 
public class PostEntity extend BaseEntity { //... }

 

위와 같은 상황에서 extend BaseEntity이라는 구문만을 추가함으로써 (상속의 간편함) 사용할 수 있으며, 자동으로 값을 생성하여 매핑해주는 @EntityListeners(AuditingEntityListener.class)와 같은 기능도 제공받을 수 있다.

 

 

세 번째로는 JPQL을 사용할 때인데, 엔티티에서 embedded type까지 쿼리 하기 위해서 type 명을 명시해야 하는 불편함이 존재한다.

 

이 부분은 영한님이 답변해주신 이 링크를 참고해보는 게 좋을 것 같다.

https://www.inflearn.com/questions/18578

 

 

갑자기 결론?!

 

결론

이 글에서 언급된 column들은 운영이나 통계 등의 도메인 외부의 요소를 위한 것들로 객체의 상태 관점보다는 데이터 그 자체로써 사용되는 의미가 짙다고 생각된다.

 

embedded type은 특정 의미(도메인, 비즈니스 성격)를 가진 column들이 중복적으로 정의되어 코드 수정 시 불편함이나 해당 값을 다루는 메서드를 한 곳에 응집시키기 위해 사용되는 방법이다. 대부분의 상황에서 이 방식을 고려하는 것이 좋겠지만 단순한 데이터를 다룰 때에는 단순한 방식을 선택하여 처리하는 것이 더 나은 경우도 있다. 

 

위에서 언급된 상황에서는 embedded type을 사용하는 것보단 MapperdSuperClass를 사용하는 것이 좋다고 생각한다. 

+ Recent posts