조금 평범한 개발 이야기

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

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

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

jogeum 2018. 10. 9. 15:56

데이타베이스의 관계

앞서 관계형 데이타베이스의 데이타를 객체로 표현해 관리하는 것이 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 의 양방향 관계에 대해서 알아 보겠습니다.


Comments