본문 바로가기

개발/쉽게 이해하고 사용하는 JPA

JPA 관계와 그 사용법에 대해 (단방향)

데이타베이스의 관계

앞서 관계형 데이타베이스의 데이타를 객체로 표현해 관리하는 것이 orm 의 기본 방향이라고 이야기 드렸습니다. 하지만 객체로 표현하기 어려운 데이타간의 관계를 위해 jpa 에서 기능을 제공하고 있는데 크게 단방향양방향의 관계로 나뉘어 집니다. 단방향양방향을 구분짓는 차이는 데이타를 사용하는 관점에서 누가 주도권을 가지는 것인가에 대한 차이이며 둘다 동일한 데이타베이스 테이블간의 관계를 의미합니다. 이글에서는 단방향에 대해서 먼저 다루겠습니다.

데이타베이스에서 사용하는 관계는 OneToOne(1:1), OneToMany(1:n), ManyToOne(n:1), ManyToMany(n:m) 가 있습니다. 이중 ManyToMany(n:m) 는 데이타베이스에서 물리적으로 지원하진 않지만 jpa 상에서 논리적으로 지원합니다.

데이타베이스에서 관계를 맺기 위해선 부모 테이블의 PK (Primary Key) 와 자식 테이블의 FK (Foreign key) 를 연결하여 서로가 서로에게 의미가 있는 관계임을 지정하게 됩니다. 이를테면 부모 parent 테이블에서 PK 가 id 이면 자식 child 테이블에서는 FK parent_id 를 통해 서로 연결을 하게 되는 것 입니다. FK 를 정의할때 컬럼명은 부모 테이블명 + _ + 부모 테이블 PK 로 구성하는게 일반적 입니다. 이제 관계를 지원하는 기능에 대해서 하나씩 알아봅시다.

 

@OneToOne

@OneToOne 관계는 데이타베이스 상의 1:1 관계를 의미합니다. 부모 테이블과 자식 테이블의 레코드가 각각 하나씩 연결되어 의미를 가진다는 이야기입니다. 하지만 1:1 관계를 지정하기에 앞서 이것이 꼭 물리적으로 테이블이 분리되어야 하는지에 대해 생각해 봐야 합니다. 1:1 관계로 구성 한다는 것은 결국 하나의 목적에 부합되는 공통된 데이타를 관리한다고 볼 수 있으며 이것은 하나의 테이블에서 관리 할 수 있는 데이타일 가능성이 높다는 의미입니다.

JPA 에서 관계를 정의할때 부모 Entity 객체에 정의된 PK 와 자식 Entity 객체에 정의된 FK 를 기준으로 정의할 수 있는데 @OneToOne 어노테이션이 위치하는 곳은 기본적으로 FK 가 있는쪽 즉 자식 Entity 객체쪽이 됩니다.

@Entity(name = "parent")
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; //여기는 parent 의 id
}

@Entity(name = "child")
public class Child {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; //여기는 child 의 id 
    
    @OneToOne
    @JoinColumn(name = "parent_id") //child 에 지정되어 있는 FK parent_id 기준으로 parent 조회 
    private Parent parent;
}

 

OneToOne 관계를 맺었을때 디폴트 설정으로 FetchTypeEAGER 로 설정되어 있어 자식 Entity 를 조회 했을때 자동으로 부모Entity 를 조회해 옵니다. 이때 바로 부모 Entity 를 사용할 필요가 없다면 속도를 위해 FetchTypeLAZY 로 설정할 수 있습니다. FetchTypeLAZY 로 지정하면 지정된 Entity 객체를 미리 가지고 있는 것이 아니라 Entity 객체를 사용하려고 할때 그 즉시 데이타를 데이타베이스에서 가지고 옵니다.

@Entity(name = "parent")
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; //여기는 parent 의 id
}

@Entity(name = "child")
public class Child {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; //여기는 child 의 id 
    
    @OneToOne(fetch = FetchType.LAZY) //사용시점에 조회가 됨
    @JoinColumn(name = "parent_id") //child 에 지정되어 있는 FK parent_id 기준으로 parent 조회 
    private Parent parent;
}

 

@SecondaryTable

1:1 관계중 자식 Entity 객체의 모든 값을 사용할 필요 없이 필요한 자식 Entity 의 값을 부모 Entity 에서 사용하게 하는 방법을 @SecondaryTable 이 제공합니다. @SecondaryTable 이 정의된 Entity 를 조회할때 내부적으로 데이타를 join 해서 parent 와 child 의 데이타를 합쳐서 가지고 오기 때문에 속도면에서도 조금은 유리하다고 볼 수 있습니다. 하지만 이렇게 Entity 의 값을 혼용해서 사용한다면 Entity Domain 의 정의가 모호해지고 의미가 퇴색될 수 있기 때문에 꼭 사용이 필요한지 확인을 해야 합니다.

@SecondaryTablename 속성은 자식 테이블의 이름의 의미하며 pkJoinColumns 속성은 부모 테이블과 자식 테이블간의 join 설정을 합니다. 이때 @PrimaryKeyJoinColumn 어노테이션을 이용해 name 속성에 자식 테이블의 FK 이름을 referencedColumnName 속성에 부모 테이블의 PK 이름을 명시합니다.

@Entity(name = "parent")
@SecondaryTable(
    name = "child",
    pkJoinColumns = @PrimaryKeyJoinColumn(name="parent_id", referencedColumnName="id")
)
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; //여기는 parent 의 id
    
    @Column(name = "name", table = "child")
	private String childName;
}

 

필요하다면 @SecondaryTable 을 1:1 로서 하나의 자식 Entity 와 관계를 맺을 수 있는 것이 아니라 다중 Entity 와도 관계를 맺을 수 있습니다.

@Entity(name = "parent")
@SecondaryTables({
    @SecondaryTable(
        name = "child",
        pkJoinColumns = 
        	@PrimaryKeyJoinColumn(name="parent_id", referencedColumnName="id")
    ),
    @SecondaryTable(
        name = "detail",
        pkJoinColumns = 
        	@PrimaryKeyJoinColumn(name="parent_id", referencedColumnName="id")
    )
})
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; //여기는 parent 의 id
    
    @Column(name = "name", table = "child")
	private String childName;
    
    @Column(name = "name", table = "detail")
	private String detailName;
}

 

@ManyToOne

@ManyToOne 관계는 자식 Entity 객체에서 부모 Entity 객체를 바라볼때 사용하는 어노테이션 입니다. @OneToOne 과 다른점은 동일한 부모 Entity 데이타를 가지는 자식 Entity 데이타가 여러개가 있을 수 있다는 점 입니다. @ManyToOne 어노테이션이 위치하는 곳은 FK 가 있는 자식 Entity 객체쪽 입니다.

@ManyToOneFetchType 의 디폴트 값은 EAGER 로서 자식 Entity 가 조회됨과 동시에 부모 Entity 를 조회해 옵니다. 먄약 부모 Entity 를 바로 사용하지 않는다면 속도를 위해 FetchTypeLAZY 로 설정할 수 있습니다.

@Entity(name = "parent")
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
}

@Entity(name = "child")
public class Child {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

 

@OneToMany

Entity 간의 관계를 지정할때 아마 가장 많이 사용하는 관계가 @OneToMany 일 것 입니다. @OneToMany 는 데이타를 바라보는 주체가 부모 Entity 이며 하나의 부모 Entity 데이타와 연관이 있는 여러개의 자식 Entity 데이타를 사용 하겠다는 의미입니다. 이때 부모 Entity 에서 @OneToMany 어노테이션을 지정하게 되며 JPA 관계 중 유일하게 FK 가 위치한 자식 Entity 가 아닌 부모 Entity 에 어노테이션이 위치 하게 됩니다. 또한 속도를 위해 기본적으로 FetchType 설정이 LAZY 로 설정되어 있습니다.

@Entity(name = "parent")
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @OneToMany
    @JoinColumn(name = "parent_id") //child 테이블에 있는 parent_id FK 
    private List<Child> childList;
}

@Entity(name = "child")
public class Child {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
}

 

@ElementCollection

@ElementCollection 어노테이션은 OneToMany 관계에서 자식 Entity 에 있는 모든 값을 가지고 오는 것이 아니라 필요한 속성의 값만 가지고와서 Collection 으로 구성할 수 있는 기능을 제공합니다. 또한 @Embeddable 로 지정된 Value 객체를 이용해 값을 가지고 올 수 도 있습니다.

@ElementCollection 을 사용하기 위해 @CollectionTable 어노테이션의 name 속성에 자식 테이블의 이름을 명시하고 joinColumns 속성에 @JoinColumn 으로 정의한 자식 테이블의 FK 를 지정합니다. 이때 @Column 어노테이션의 name 속성에 지정되는 이름은 자식 테이블의 컬럼명이 됩니다.

@Entity(name = "parent")
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ElementCollection
    @CollectionTable(
        name = "child",
        joinColumns = @JoinColumn(name = "parent_id")
    )
    @OrderColumn(name = "id")
    @Column(name = "name")
    private List<String> childNameList;
}

 

@ElementCollection 을 통해 정의된 값을 가지는 List 형태의 Collection 이 아니라 Map 형태의 Collection 을 가지고 올 수도 있습니다. 차이점은 @MapKeyColumn 어노테이션을 이용해 Map 의 Key 에 해당되는 컬럼을 @Column 을 이용해 Map 의 Value 에 해당되는 컬럼을 지정하며 Map Collection 에 값을 가지고 온다는 점 입니다.

@Entity(name = "parent")
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ElementCollection
    @CollectionTable(
        name = "child",
        joinColumns = @JoinColumn(name = "parent_id")
    )
    @MapKeyColumn(name = "id")
    @Column(name = "name")
    private Map<Long, String> childNameMap;
}

 

@ManyToMany

가장 마지막으로 볼 관계는 @ManyToMany 다대다 관계입니다. 이는 앞서 이야기 드린것과 같이 데이타베이스상에 표현할 수 없는 관계이며 JPA 상에서 논리적으로만 표현할 수 있습니다. 이를 위해 데이타베이스 중간에 @ManyToMany 를 표현할 수 있는 맵핑 테이블을 생성해 서로의 PK 가지고 관계를 설정 하게 되며 중간 맵핑 테이블은 JPA 상에서 숨겨져서 표현이 됩니다.

@ManyToMany 를 데이타베이스상에서 사용하지 않는 이유는 데이타간의 관계가 복잡해지기 때문이며 @ManyToMany 를 사용하기에 앞서 이것이 꼭 필요한 관계 설정인지를 확인해야 합니다. 왜냐하면 대부분의 경우 @ManyToMany 가 아닌 다른 형태로 Entity 를 설계한다든지 다른 형태로 값을 가지고 온다든지 여러 형태로 대신 할 수 있기 때문입니다.

@ManyToMany 를 사용하기 위해서 @JoinTable 어노테이션의 name 속성으로 중간 맵핑 테이블을 정의합니다. 이 중간 맵핑 테이블로 부모 Entity 와 자식 Entity 간의 관계를 알 수 있으며 다른 관계와는 다르게 부모와 자식 Entity 가 동등한 위치이기 때문에 자식 Entity 의 FK 는 존재하지 않으며 중간 맵핑 테이블에 부모 Entity 의 PK 와 자식 Entity 의 PK 가 둘다 존재하게 됩니다.

@Entity(name = "parent")
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToMany
    @JoinTable(
        name = "parent_child",
        joinColumns = @JoinColumn(name = "parent_id"),
        inverseJoinColumns = @JoinColumn(name = "child_id")
    )
    private List<Child> childList;
}

 

다음글에서는 Entity 의 양방향 관계에 대해서 알아 보겠습니다.


  • 궁금 2020.04.13 17:17

    안녕하세요? 글 잘 봤습니다.
    한 가지 궁금한 점이 있는데 양방향 매핑은 어떤 경우에 사용을 하나요?

    관계가 설정되어 있는 두 객체 간에 FK 값을 한 쪽에서만 업데이트를 하고
    한 쪽은 업데이트가 되면 안 되는 경우에 사용하나요?

    예를 들어 회원과 주문내역이 있으면 주문내역에서만 회원ID 가 변경되면 안 되니
    이건 회원 엔티티를 통해서만 수정할 수 있는 권한을 주겠다.

    이렇게 이해해도 괜찮을까요?

    • 조금 jogeum 2020.04.13 17:30 신고

      데이터의 소유가 누구에게 있는지에 따라 달라지게 됩니다.

      단방향의 경우 명확하게 부모가 부모에 속한 자식의 데이터를 사용하는 경우 (혹은 그 반대도 성립이 되겠죠.) 를 의미합니다.

      하지만 양방향의 경우 부모와 자식 모두 데이터를 소유할때 사용이 되는데요.

      이를테면 부서/사원 관계에서 부서에 속한 사원을 찾거나 값을 변경하는 경우와 사원이 소속되어 있는 부서를 찾거나 값을 변경하는 경우에 사용할 수 있습니다.

    • 궁금 2020.04.13 17:45

      답변 감사드립니다.

      그런데 댓글로 말씀해주신 부분 중에서도 이해가 되지 않는 부분이 있는데요.

      단방향의 경우 명확하게 부모가 부모에 속한 자식의 데이터를 사용하는 경우 (혹은 그 반대도 성립이 되겠죠.) 를 의미합니다.

      이 경우에도 결국 양방향을 사용해야 하는 것 아닌가요?

      부모가 자식의 데이터를 사용할 수도 있고 자식이 부모의 데이터를 사용할 수도 있는 걸로 보이는데 아닌가요? ㅠㅠ

    • 조금 jogeum 2020.04.24 02:15 신고

      넵 단방향이라고 이라고 하는 것은 데이타의 주도권을 가진 부모 도메인 Entity 개체가 자식 도메인 Entity 를 바라보는 경우를 말합니다.

      @OneToMany 인 경우를 이야기 해볼께요. A 가 부모이고 B 가 자식이면 A 에서는 B 에 대해 접근이 가능하지만 B 에서는 A 로 접근이 되지 않습니다.

      B 도메인 Entity 에서는 A 에 대한 정보가 없기 때문이죠.

      하지만 DataBase 상에 있는 Table 에는 A Table 과 B Table 간에 FK 가 걸려 있는 것은 단방향이나 양방향이나 변하지 않아요.

      단지 A 에서 B 로 혹은 B 에서 A 로 접근 할 수 있느냐에 따라 단방향과 양방향이 나뉘어 집니다. 한쪽 방향으로만 접근이 된다면 단방향이죠.

      즉 DB 에 구성되어 있는 관계는 그대로 이지만 JPA 에서 이를 어떻게 논리적인 구성을 하느냐에 따라 달라진다고 보시면 됩니다. :)