티스토리 뷰

JPA 에서 엔티티간에 연관관계를 맺을시 연관관계의 주인이 되는 엔티티에는 @JoinColumn 애노테이션을 이용해 FK 를 설정한다. @JoinColumn 에는 여러 프로퍼티들이 있는데 이중 name 에는 해당 엔티티의 컬럼명을, referencedColumnName 에는 상대방 엔티티의 컬럼명을 적는다. referencedColumnName 을 명시적으로 넣어주지 않으면 디폴트 옵션으로 상대방의 primary key 에 해당하는 컬럼을 이용하게된다.

 

@Entity
@Table(name = "person")
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Person {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "person_id")
    private Long id;
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "child_id")
    private Child child;
    @Column(name = "person_name")
    private String name;
}

@Entity
@Table(name = "child")
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Child {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "child_id")
    private Long id;
    @Column(name = "child_name")
    private String name;
}

Person 과 Child 는 아주 간단한 엔티티들이며 두 엔티티는 1:1 관계를 갖고있고, (명시적으로 설정되지는 않았지만) FK 는 child_id 를 이용하고있다.

public interface PersonRepository extends JpaRepository<Person, Long> {
    @Override
    @EntityGraph(attributePaths = "child")
    List<Person> findAll();
}

@DataJpaTest
public class PersonRepositoryTest {
    @Autowired
    private PersonRepository personRepository;
    @Autowired
    private ChildRepository childRepository;
    @Autowired
    private EntityManager em;

    @Test
    void test() {
        Child c = new Child(null, "child");
        Person p = new Person(null, c, "person");

        childRepository.save(c);
        personRepository.save(p);

        em.flush();
        em.clear();

        personRepository.findAll();
    }
}

join fetch 를 이용하도록 @EntityGraph 를 이용했고, 테스트코드에서 찍히는 SQL 로그를 보면 join 을 이용한 쿼리가 실행되는걸 볼 수 있다. 즉 아무런 문제가 없다.

@Entity
@Table(name = "person")
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Person {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "person_id")
    private Long id;
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "person_code", referencedColumnName = "child_code")
    private Child child;
    @Column(name = "person_name")
    private String name;
}

@Entity
@Table(name = "child")
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Child {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "child_id")
    private Long id;
    @Column(name = "child_name")
    private String name;
    @Column(name = "child_code")
    private String code;
}

child_id 를 이용해 맺었던 연관관계에서, code 를 이용해 연관관계를 맺도록 변경했다. code 는 테이블 내에서 unique 하지만 특정 테이블의 기본키는 아니다.

 

두 엔티티의 연관관계만 변경해주고 테스트코드를 그대로 실행시키면 에러가 발생하게된다.

class com.yong.firstspring.sample.Child cannot be cast to class java.io.Serializable

사실 에러메세지가 너무 간단하고 명확해 문제를 해결하기는 쉽다. 연관관계의 대상이 되는 Child 에 Serializable 인터페이스를 구현하도록하면 된다.

 

문제를 해결하는건 어렵지않은데 왜 Serializable 을 구현해야하는지 궁금했다. 꽤 다양한 정보를 찾아봤다고 생각하고있는데, 일단 JPA 와 하이버네이트 스펙상 Entity 에 Serializable 을 구현하는건 필수가 아니다. 다만 선택적으로 필요한 순간에는 구현하도록 되어있는데 그 필요한 순간이라는게 상당히 추상적이다. "원격으로 사용되는 경우 Serializable 을 구현하라" 정도로 요약할 수 있는데 이건 친절한 문서라고 보기 어렵지 않나 라는 생각이 든다. 결국 "직렬화해야할때는 Serializable 을 구현하세요" 정도의 매우 당연한 말이기때문이다.

 

- JPA document 발췌
If an entity instance is passed by value as a detached object, such as through a session bean’s remote business interface, the class must implement the Serializable interface.

- Hibernate document 발췌
If an entity instance is to be used remotely as a detached object, the entity class must implement the Serializable interface.

 

그러다가 그나마 공식적인걸 하나 찾을 수 있었는데 hibernate document ( https://docs.jboss.org/hibernate/stable/annotations/reference/en/html/entity.html#entity-mapping-association )의 2.2.5.1 절에서 찾았다.

 

A Customer is linked to a Passport, with a foreign key column named passport_fk in the Customer table. The join column is declared with the @JoinColumn annotation which looks like the @Column annotation. It has one more parameters named referencedColumnName. This parameter declares the column in the targeted entity that will be used to the join. Note that when using referencedColumnName to a non primary key column, the associated class has to be Serializable. Also note that the referencedColumnName to a non primary key column has to be mapped to a property having a single column (other cases might not work).

 

다만 여기도 왜 해야하는지까지는 나와있지 않다. 문서에 누락됐거나 내가 못찾은게 아니라면 JPA/Hibernate 문서와 조합해볼때 referencedColumnName 을 이용하는 경우엔 Hibernate 내부에서 직렬화를 하도록 구현되어있는게 아닐까 추측해볼 수 있다.

 

이걸 찾다가 느낀건데 JPA 나 Hibernate 는 문서보기가 참 힘들다. 위에 referencedColumnName 관련 문서도 여기저기 뒤지다가 우연히 발견한거지 Hibernate 공식 페이지에서 저 문서에 접근하는 법은 못찾았다;;

 

처음엔 이유가 궁금했던 것도 있고, 혹시 Hibernate 버그가 아닌가 싶어서 찾아봤던건데 여튼 몇시간 찾아본 결과 공식문서에 나와있는 구문을 발견했으니 기본키가 아닌 컬럼을 이용할때는 Serializable 을 구현하도록 해야겠다.

 

참고자료

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

https://vladmihalcea.com/how-to-map-a-manytoone-association-using-a-non-primary-key-column/

https://docs.oracle.com/javaee/6/tutorial/doc/bnbqa.html

https://docs.jboss.org/hibernate/orm/5.5/userguide/html_single/Hibernate_User_Guide.html#domain-model

https://docs.jboss.org/hibernate/stable/annotations/reference/en/html/entity.html#entity-mapping-association

댓글
댓글쓰기 폼