Spring Framework/Spring

[JPA] 연관 관계

덴마크초코우유 2024. 9. 22. 02:44
반응형

JPA를 사용하면서 두 엔티티간 일다대 관계를 설정하는 경우가 많았다. 어쩔때는 중간 테이블이 생기고 어쩔때는 데이터베이스에 데이터가 저장이 안되고, 그럴때마다 그냥 mappedBy 추가하고 joinColumn 추가하고 넘어갔다. 특히 두 엔티티 관계를 설정할 때 연관관계의 주인이라는 개념을 잘 이해하지 못했다. 아직 제대로 개념을 익히지는 못했지만 두 엔티티 관계 설정을 해보며 어떤 변화가 생기는지 테스트해봤다.

기본 코드

아래의 Parent, Child 두 엔티티에 일대다 관계를 설정할 것이다. 각 엔티티에 @ManyToOne이나 @OneToMany를 추가해보며 테스트 코드를 실행했다.

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

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

테스트 코드는 아래처럼 Parent와 Child를 생성 후 서로 참조하도록 코드를 작성했다.

Parent parent = new Parent();
em.persist(parent);
em.flush();
Child child1 = new Child();
Child child2 = new Child();

// 경우에 따라 A, B 둘 중 한 코드만 추가했다.
// (A)
parent.getChildren().add(child1);
parent.getChildren().add(child2);
// (B)
child1.setParent(parent);
child2.setParent(parent);

em.persist(child1);
em.persist(child2);
em.flush();

연관 관계의 주인

데이터베이스에서는 테이블의 외래키로 두 테이블의 연관 관계를 관리한다. 하지만 객체에서는 참조를 통해 객체의 관계를 만들 수 있는데 이 때 두 객체가 서로 참조하는 양방향 관계를 만들 수도 있을 것이다. 여기서 데이터베이스와 객체의 패러다임 불일치가 발생한다.
두 객체가 서로 참조하는 상황에서 외래키를 관리하는 객체를 연관관계 주인이라고 한다.

  • @OneToMany, @ManyToOne 관계에서는 한쪽이 연관 관계의 주인이 된다.
  • 주인은 관계를 관리하고 데이터베이스의 외래 키를 실제로 업데이트하는 역할
  • JPA에서는 이 주인을 명시적으로 지정해야 하며 주인은 외래 키가 실제로 위치한 테이블을 관리하는 엔티티

연관관계의 주인으로 설정한 엔티티의 객체에 참조를 추가해야 데이터베이스에서 외래키 설정이 되는 것이다.

// Parent에 Child 참조를 추가한다.
parent.getChildren().add(child);

// Child에 Parent 참조를 추가한다.
child.setParent(parent);

Parent, Child에서 각각을 주인으로 설정한 다음 테스트해봤다.

Parent를 주인으로 설정

공부할 때 외래키를 가진 테이블과 매핑되는 엔티티만 연관 관계의 주인이 된다고 알고있었는데, 외래키를 가지지 않은 엔티티도 주인으로 설정할 수 있다.

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

    @OneToMany
    @JoinColumn(name = "parent_id")
    private List<Child> children = new ArrayList<>();
}

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

테스트 실행

Parent parent = new Parent();
em.persist(parent);
em.flush();
Child child1 = new Child();
Child child2 = new Child();
// Parent에 Child를 추가했다.
parent.getChildren().add(child1);
parent.getChildren().add(child2);
em.persist(child1);
em.persist(child2);
em.flush();

// 아래의 SQL이 실행된다.
insert into parent (id) values (default);
insert into child (id) values (default);
insert into child (id) values (default);
update child set parent_id=1 where id=1;
update child set parent_id=1 where id=2;

parent에만 child 관계를 설정했지만 UPDATE로 child의 parent_id를 지정한다.

Child를 주인으로 설정

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

    @OneToMany(mappedBy = "parent")
    private List<Child> children = new ArrayList<>();
}

@Entity
public class Child {

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

    @ManyToOne
    @Setter
    private Parent parent;
}
Parent parent = new Parent();
em.persist(parent);
em.flush();
Child child1 = new Child();
Child child2 = new Child();
// 각 Child에 Parent를 설정했다.
child1.setParent(parent);
child2.setParent(parent);
em.persist(child1);
em.persist(child2);
em.flush();

// 아래의 SQL이 실행된다.
insert into parent (id) values (default);
insert into child (parent_id,id) values (1,default);
insert into child (parent_id,id) values (1,default);

결론

  • 데이터베이스는 일대다 관계를 외래키로 관리하기 때문에 두 객체간 관계 설정을 위해서는 외래키를 설정해야한다.
  • 연관관계의 주인으로 설정한 엔티티에 다른 엔티티의 참조를 추가하고 객체를 할당해야 데이터베이스에서 외래키를 설정한다.

[번외] 주인 지정이 없다면?

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

    @OneToMany
    private List<Child> children = new ArrayList<>();
}

@Entity
public class Child {

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

    @ManyToOne
    private Parent parent;
}

중간에 두 엔티티를 매핑하는 테이블이 생성된다.

create table parent_children (
    parent_id bigint not null,
    children_id bigint not null
);
parent.getChildren().add(child1);
parent.getChildren().add(child2);
// child1.setParent(parent);
// child2.setParent(parent);
insert into parent (id) values (default);
insert into child (parent_id,id) values (1,default);
insert into child (parent_id,id) values (1,default);
insert into parent_children (parent_id,children_id) values (1,1);
insert into parent_children (parent_id,children_id) values (1,2);

아직 명확하게 알지 못하는 부분도 많고 포스팅의 문장도 매끄럽지 못한 것 같다.

  • 일대다 관계 설정에서 @JoinColumn은 언제 생략할 수 있는가?
  • 연관관계 주인 설정은 어떤 기준으로 결정해야 하는가?

그래도 언제 매핑 테이블이 생기는지, 연관 관계 주인에 참조를 추가해야 외래키가 업데이트된다 등을 알 수 있었다. 모두 이해하고 왜 이런지 설명할 수 있도록 JPA에 대해 더 공부해야겠다.

Ref

반응형