본문 바로가기

메모

하이버네이트 공식 문서를 읽고 배운 점

728x90
Hibernate 6 Guide 문서를 읽고 정리한 내용입니다.

하이버네이트 공식 문서를 읽고 배운 점을 작성해 보자. 이미 알던 내용도 있고, 새로운 기능도 많아 한 번쯤 읽어봐도 좋을 것 같다.

ORM or SQL

A perennial question is: should I use ORM, or plain SQL? The answer is usually: use both.

지금은 아니지만, 처음 ORM 기술을 접하면서 ORM을 SQL을 완전히 대체할 수 있는 도구로 생각했다. 그러면서 당연하게도 이분법적인 사고를 가졌는데 하이버네이트 공식 문서에선 이 부분에 대해서 명확하게 전달하고 있다.

DAO에 대한 사과

더보기

Back in the dark days of Java EE 4, before the standardization of Hibernate, and subsequent ascendance of JPA in Java enterprise development, it was common to hand-code the messy JDBC interactions that Hibernate takes care of today. In those terrible times, a pattern arose that we used to call Data Access Objects (DAOs). A DAO gave you a place to put all that nasty JDBC code, leaving the important program logic cleaner.

When Hibernate arrived suddenly on the scene in 2001, developers loved it. But Hibernate implemented no specification, and many wished to reduce or at least localize the dependence of their project logic on Hibernate. An obvious solution was to keep the DAOs around, but to replace the JDBC code inside them with calls to the Hibernate Session.

We partly blame ourselves for what happened next.

Back in 2002 and 2003 this really seemed like a pretty reasonable thing to do. In fact, we contributed to the popularity of this approach by recommending—or at least not discouraging—the use of DAOs in Hibernate in Action. We hereby apologize for this mistake, and for taking much too long to recognize it.

Eventually, some folks came to believe that their DAOs shielded their program from depending in a hard way on ORM, allowing them to "swap out" Hibernate, and replace it with JDBC, or with something else. In fact, this was never really true—there’s quite a deep difference between the programming model of JDBC, where every interaction with the database is explicit and synchronous, and the programming model of stateful sessions in Hibernate, where updates are implicit, and SQL statements are executed asynchronously.

But then the whole landscape for persistence in Java changed in April 2006, when the final draft of JPA 1.0 was approved. Java now had a standard way to do ORM, with multiple high-quality implementations of the standard API. This was the end of the line for the DAOs, right?

Well, no. It wasn’t. DAOs were rebranded "repositories", and continue to enjoy a sort-of zombie afterlife as a front-end to JPA. But are they really pulling their weight, or are they just unnecessary extra complexity and bloat? An extra layer of indirection that makes stack traces harder to read and code harder to debug?

Our considered view is that they’re mostly just bloat. The JPA EntityManager is a "repository", and it’s a standard repository with a well-defined specification written by people who spend all day thinking about persistence. If these repository frameworks offered anything actually useful—and not obviously foot-shooty—over and above what EntityManager provides, we would have already added it to EntityManager decades ago.

공식 문서를 읽으며 흥미로웠던 점은 DAO와 Repository에 대한 입장이였는데, 과거 Hibernate in action에서 DAO를 지지했던 것에 대해, 명시적이고, 동기적인 JDBC 방식과 암시적이고 비동기적인 Hibernate 방식의 차이, 그리고 대부분의 Repository는 응집력이 매우 낮고, 의미가 있으려면 다양한 구현체를 구현하는 경우 쉽게 교체할 수 있지만, 이 경우도 드물고 또 Repository 특성 상 API와 매우 밀접하기 때문에 교체가 쉽지 않고, API가 좁은 경우에만 의미가 있기 때문이라고 설명한다.

Entity 선언

가끔 다른 분들의 만드신 프로젝트를 구경하다보면 entity class의 No-arg 생성자를 public으로 두는 경우가 종종 보였다. 이런 경우 높은 확률로 setter가 있지만, 이것은 다른 흐름이니 넘어가고, 공식 문서에서 Entity는 다음을 반드시 지켜야 한다고 명시한다.

  • be a nono-final class
  • with a non-private constructor with no parameters.

Entity class는 상속이 가능해야하며, No-arg 생성자의 경우 private이 아니여야 한다. 즉, private만 아니면 되는데, 최범균님의 책 "DDD Start!"에서는 protected로 선언하는 것을 추천하기에 나는 이 방식을 잘 실천하고 있다. 그리고 Entity class는 일반적인 class일 수 있으며, abstract class도 가능하다. 여기까진 이전 프로젝트에서도 잘 이용했기에 놀랍지 않았는데, static inner class로 가능하단 것은 새롭게 알게 되었다.

접근 타입

Entity property에 접근하는 일반적인 방법은 필드 접근, 프로퍼티 접근 방식이 있다. 하이버네이트의 경우 @Id annotation의 위치를 기준으로 자동으로 결정하는데, 필드에 붙어있는 경우 필드 접근, getter에 붙어있다면 프로퍼티 접근으로 인식한다. 과거 하이버네이트의 경우 프로퍼티 접근이 더 일반적이였지만, 현재는 필드 접근이 더 일반적으로 사용되는 방식이다. 만약 이런 접근 방식을 명시적으로 지정하려면 @Access annotation을 이용하면 되지만, 권장되지는 않는다.

NaturalId

사이드 프로젝트 수준에서 경험한 JPA에선 대부분 entity를 식별하는 것은 @Id annotation으로 충분했다. 하지만 @Id의 식별자는 데이터베이스에서 행에 대한 식별자이다. 만약 그 외에서 객체를 식별할 수 있는 키가 있다면 @NaturalId를 권장하고 있다. 더 나아가 @NaturalId를 사용한다면 이를 Foreign key로 사용하는 것을 권장하는데, 이 방식은 이후 data model을 더 쉽게 변경할 수 있다. (하나 더 성능적인 효과도 있지만 그것은 아래에...)

Not null

프로젝트를 진행하면서 NOT NULL 제약을 명시하기 위해 주로 @Column(nullable = false)를 사용했다. 그런데 또 다른 방법이 있는데, 바로 @Basic(optional = false)이다. 이 두 개의 annotation의 차이는 공식 문서에 명시되어 있는데, 바로 Logical layer인지, Mapping layer인지로 구분될 수 있다. @Entity, @Id, @Basic은 logical layer로 분류되고, @Table, @Column은 Mapping layer로 분류된다. 즉 Mapping layer의 @Column(nullable = false)는 스키마 생성에 영향을 미치며, Logical layer인 @Basic(optional = false)는 데이터베이스에 쓰기 전 하이버네이트에서 확인한다. 마지막으로 더 좋은 방법으로 bean validation의 @NotNull을 권장하고 있다.

PostgreSQL의 열거형

JPA에서 열거형 데이터는 의례 @Enumerated(String)을 사용하곤 한다. 하지만 조금 특수한 사례로 PostgreSQL의 열거형 타입은 PostgreSQL JDBC 드라이버에서 잘 지원되지 않는 모양이다. 따라서 @JdbcTypeCode(SqlTypes.NAMED_ENUM)또는 변환기를 사용하도록 제안하고 있다.

변환기

변환기(converter)는 데이터베이스에서 쓰거나, 읽기 전 데이터에 대한 전처리를 지원한다. 공식 문서에서는 EnumSet<DayOfWeek>을 Integer로 또는 그 반대의 경우를 변환하는 변환기 예제가 있다. 변환기는 @AttributeConvert를 이용해 특정 Entity의 필드에 적용하는 방법과 @ConverterRegistration으로 해당하는 모든 Entity의 필드에 자동으로 적용하는 방법이 있다.

임베디드

임베디드도 프로젝트를 하다보면 자주 사용하게되는 부분이라 익숙했지만, Java의 record class와 함께 사용하는 예제가 조금 색다로웠다. 하지만 여전히 @EmbeddedId는 지원하지 않기 때문에 주의하자.

💀

Collection

나는 @OneToMany의 경우 List보단, Set을 선호한다. 왜냐하면 List의 경우 중복된 Entity가 저장될 수 있기 때문에 명시적인 Set을 선호한다. 하지만 대부분의 경우 List가 마음 편한 이유가 있는데, 내가 본 거의 모든 Entity 선언은 @GeneratedValue(IDENTITY)를 사용하고 있었다. 즉, 식별자는 데이터베이스에서 받아와서 쓰는 용도이다 보니, 저장되기 전에는 식별자가 없다(정확하게 말하자면 NULL 또는 0). 그리고 Entity의 경우 hashCode, equals는 식별자를 기준으로 분류하는데, 저장되지 않은 Entity의 경우 식별자가 NULL or 0이라 Set이 제대로 동작하지 않는다.

 

그리고 @OneToMany의 경우 mappedBy를 지정해야하는데, 이때 대부분의 코드에서 문자열을 사용한다. 하지만 문서에서는 Metamodel generator를 통해 생성된 정보를 이용해 좀 더 type safety하게 동작하도록 사용하는 것을 권장하고 있다.