sol 개발 블로그 로고
Published on

Spring JPA entity mapping

Authors
  • avatar
    Name
    Chan Sol OH
    Twitter

목차

프로젝트를 하면서 일대 다 연관관계를 위해 설정하는 mappedBy 방법과 joincolumn에 대한 방법의 차이점과 어떤 상황에서 각각을 사용해야하는지 정리합니다.

결론

관계의 주인은 외래키를 관리하고 DB에서 물리적으로 관계를 가지고 있습니다. 하지만, 다른 쪽은 외래키가 따로 없이 단순히 관계를 참조만합니다.

보통 JoinColumn은 관계의 주인쪽이 직접 외래키를 관리하기 위해서 사용하고, mappedBy는 주인이 아닌쪽이 관계를 참조만할 때 사용합니다. 하지만 " JoinColumn은 주인이고 mappedBy는 주인이 아니여야한다."라는 의미는 아닙니다. 정확하게는 양방향 관계일 경우라는 조건이 붙어야합니다.

일대다 단방향 관계일 경우 OneToManyJoinColumn는 같이 쓰여야합니다.

JoinColumn

OneToMany 관계에서 외래키를 관리하는 쪽은 당연히 Many가 됩니다. 왜냐하면, One이 수 많은 외래키를 관리하기 매우 어렵기 때문입니다. 자연스럽게 외래키를 관리하는 Many가 관계의 주인이 됩니다. JPA는 @JoinColumn 어노테이션을 통해 관계의 주인이 가진 외래키를 표현합니다.

java
@Entity
public class Email {

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "employee_id")
    private Employee employee;

    // ...

}

mappedBy

DB 자체는 주인만 외래키를 가지고 있어서 One쪽은 Many를 참조하기 위한 아무런 데이터를 가지고 있지 않습니다. 다만, JPA는 양방향 관계를 위해 Many쪽 데이터를 참조할 수 있도록 합니다. 이 때, 연관관계의 주인쪽 속성 값을 mappedBy의 value로 지정하게 됩니다.

java
@Entity
public class Employee {

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

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "employee")
    private List<Email> emails;
    
    // ...
}

위처럼 연관관계의 주인을 참조하면 Hibernate가 fetch할 때 자동으로 연관관계의 주인도 가져오게 할 수 있습니다.

만약 둘 다에 JoinColumn을 작성하면 어떻게 될까?

실제 프로젝트에서 실수로 아래처럼 작성했었습니다. 연관관계의 주인이 아닌 One쪽인 Employee에 JoinColumn을 써버린 겁니다. 빌드를 했을 때 에러가 발생했을까요? DB 속 Employee 테이블에도 employee_id 속성이 생겼을까요?

java
@Entity
public class Employee {

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

    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(value = "employee_id")
    private List<Email> emails;
    
    // ...
}

답은 아닙니다. 빌드도 잘되고 런타임 에러도 없고, 심지어 employee_id 속성도 Employee 테이블에 생기지 않았습니다. 위에 설명 속 " JoinColumn은 주인이고 mappedBy는 주인이 아니여야한다."라는 설명은 틀린 설명인겁니다. 그렇다면 JPA가 mappedBy와 JoinColum을 어떻게 구현하길래 괜찮은 걸까요?

JPA 코드 까보기 - JoinColumn

JPA의 JoinColumn 어노테이션을 까보면 아래 같은 주석과 설명이 있습니다. 정리하면 JoinColumn은 특정 Column을 어떤 entity나 element collection과 연관시킨다고 합니다.

Specifies a column for joining an entity association or element collection. If the JoinColumn annotation itself is defaulted, a single join column is assumed and the default values apply

위 내용을 방증하듯 연관관계의 주인이 아닌 @OneToMany와 함께 사용한 것을 예시로 보여줬습니다.

// Example:
 
@ManyToOne
@JoinColumn(name="ADDR_ID")
public Address getAddress() { return address; }


Example: unidirectional one-to-many association using a foreign key mapping

// In Customer class
@OneToMany
@JoinColumn(name="CUST_ID") // join column is in table for Order
public Set<Order> getOrders() {return orders;}

JPA 코드 까보기 - mappedBy

Jpa의 @OneToMany 어노테이션을 까보면 아래 같은 설명과 예시가 있습니다.

정리하면 양방향에서는 mappedBy로 연관관계 주인쪽 속성을 적어줘야하고, 단방향 관계에서는 JoinColumn을 통해 외래키를 지정해주는겁니다. 양방향 관계에서 mappedBy를 지정하지 않는다면 관계의 주인은 없어지고, 서로 다른 두 단방향 관계가 생기게되는 겁니다.

if the relationship is bidirectional, the mappedBy element must be used to specify the relationship field or property of the entity that is the owner of the relationship.

java

// Example 1: One-to-Many association using generics

// In Customer class:

@OneToMany(cascade=ALL, mappedBy="customer")
public Set<Order> getOrders() { return orders; }

In Order class:

@ManyToOne
@JoinColumn(name="CUST_ID", nullable=false)
public Customer getCustomer() { return customer; }
 
 ....

// Example 3: Unidirectional One-to-Many association using a foreign key mapping
 
// In Customer class:

@OneToMany(orphanRemoval=true)
@JoinColumn(name="CUST_ID") // join column is in table for Order
public Set<Order> getOrders() {return orders;}

프로젝트에서 동작 (못) 했던 이유 - 착각

프로젝트에서 일대다 관계에서 둘 다 JoinColumn을 붙였을 때 동작은 가능했던 이유는 일대다 관계, 다대일 관계인 서로 다른 두 단방향 관계가 생성되었고, 이를 통해 조작했기 때문에 가능했었습니다.

만약 일대다 양방향이라고 착각하고 One 쪽에 element collection을 넣고 cascade all을 하더라도 Many쪽은 저장되지 않습니다.

빌드나 런타임 에러가 발생하지 않고 단지 Cascade 되지 않아 저장 자체가 되지 않습니다. 그렇기에 mappedBy가 더욱 중요하다고 생각합니다. 서로 다른 두 관계를 하나의 양방향 관계로 묶어주는 역할이기 때문입니다.

정리

  • OneToMany 단방향일 경우
    • One 쪽에 @JoinColumn(value = "{외래키 이름}")
  • ManyToOne 단방향일 경우
    • May 쪽에 @JoinColumn(value = "{외래키 이름}")
  • 양방향일 경우
    • Many(관계의 주인) 쪽에 @JoinColumn(value = "{외래키 이름}")
    • One 쪽에 @OneTo---(mappedBy = "{주인쪽 속성 이름}")

참조

  1. Difference Between @JoinColumn and mappedBy
  2. @JoinColumn Annotation Explained