DEV ℧ Developer Diary

[JPA] 다양한 연관관계 매핑 (1)

해당 포스트는 인프런 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 듣고 정리한 글입니다.

다양한 연관관계 매핑 (1)

연관관계는 매핑시 3가지를 고려해서 설계해야한다.

  1. 다중성
  2. 단방향, 양방향
  3. 연관관계의 주인

종류

JPA의 연관관계의 대표적인 다중성 매핑방법은 아래와 같다.

  • 다대일 : @ManyToOne
  • 일대다 : @OneToMany
  • 일대일 : @OneToOne
  • 다대다 : @ManyToMany

보통 설계에서는 다대일을 주로 사용하고, 다대일을 통해 대부분의 문제를 해결할 수 있다. 하지만 실무에서는 다대다의 관계는 쓰면 안된다.

단방향, 양방향

  • 테이블
    • 외래 키 하나로 양쪽 조인 가능
    • 사실 방향이라는 개념이 없음
  • 객체
    • 참조용 필드가 있는 쪽으로만 참조 가능
    • 한쪽만 참조하면 단방향
    • 양쪽이 서로 참조하면 양방향

연관관계의 주인

  • 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺음
  • 객체 양방향 관계는 A->B, B->A 처럼 참조가 2군데 하지만 테이블은 참조가 하나이다.
  • 객체 양방향 관계는 참조가 2군데 있음. 둘중 테이블의 외래 키를 관리할 곳을 지정해야함
  • 연관관계의 주인(mappedBy) : 외래 키를 관리하는 참조
  • 주인의 반대편 : 외래 키에 영향을 주지 않음, 단순 조회만 가능

다대일 (N:1)

무조건 외래키는 다의 관계를 가진 곳에 외래키나 참조를 관리한다.

다대일

기존에 작성한 예시의 @ManyToOne이 다대일에 해당한다.

@Entity
@Getter @Setter
public class Member {

    @Id @GeneratedValue
    private long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

@Entity
@Getter @Setter
public class Team {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public void addMember(Member member) {
        members.add(member);
        member.setTeam(this);
    }
}

다대일 정리

  • 가장 많이 사용하는 연관관계
  • 다대일의 반대는 일대다
  • 다수쪽에 보통 외래키의 참조를 넣는다.

일대다 (1:N)

1인 Entity애서 외래키를 관리하겠다는 뜻이다.

먼저 사족을 붙이자면 실무에서는 해당 모델은 추천하지 않는다.

일대다 단방향

일대다

해당 그림에 대해 객체의 입장에서는 일대다의 구조가 자주 나올 수 있다.

하지만 DB의 입장에서는 다의 관계에 무조건 외래키가 들어가므로, 해당 구조가 나올 수가 없다.

해당 구조는 Team에 있는 Member의 정보를 변경하면, Member의 Entity를 변경해야하는 요상한 설계가 들어간다.

/* Member */
@Entity
@Getter @Setter
public class Member {

  @Id @GeneratedValue
  private long id;

  @Column(name = "USERNAME")
  private String username;

}
/* Team */
@Entity
@Getter @Setter
public class Team {

  @Id @GeneratedValue
  private Long id;

  private String name;

  @OneToMany
  @JoinColumn(name = "TEAM_ID")
  private List<Member> members = new ArrayList<>();
}

private List<Member> members = new ArrayList<>();@OneToMany을 선언하고 Member Entity와 일대다 구조로 매핑하였다. 해당 구조를 간단한 예시를 통해 들어보자.

Member member = new Member();
member.setUsername("member1");
entityManager.persist(member);

Team team = new Team();
team.setName("TeamA");
team.getMembers().add(member);
entityManager.persist(team);

entityTransaction.commit();

해당 소스를 실행하면 이상한 일이 일어난다.

/* Member Insert */
    insert
    into
        Member
        (USERNAME, id)
    values
        (?, ?)

/* Team Insert */
    insert
    into
        Team
        (name, id)
    values
        (?, ?)

/* Member update */
update
    Member
set
    TEAM_ID=?
where
    id=?

각각 Member와 Team에 데이터를 Insert를 해준뒤 @OneToMany를 선언한 테이블은 Team Entity 지만 Member 테이블에 데이터를 update하는 것을 볼 수 있다.

그리고 결과를 살펴보자.

일대다

해당 관계는 객체에서는 올바르지만 DB에서는 다의 관계에서 외래키를 관리하므로 Member에 들어갈 수 밖에 없다. 그래서 Team Entity에 연관관계를 선언 해도 Member에 update를 날려서 해당 데이터를 관리할 수 밖에 없다.

해당 구조는 연관관계의 주인인 Team이 아닌 연관관계를 매핑하고 있는 Member에서 엉뚱하게 update를 주기때문에 혼란을 줄 수가 있다. 실제 테이블이 수십개가 되는 운영에서 해당 구조를 쓸 경우 혼란을 야기할 수 있다.

일대다 단방향 정리

  • 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인
  • 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있음
  • 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조
  • @JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블 방식을 사용함(중간에 테이블을 하나 추가한다.)

일대다 단방향 매핑의 단점

  • 엔티티가 관리하는 외래 키가 다른 테이블에 있다. DB의 입장에서는 다의 관계에서 외래키를 관리하기 때문이다.
  • 연관관계 관리를 위해 추가로 UPDATE SQL 실행

일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자.

일대다 양방향 정리

  • 이런 매핑은 공식적으로는 존재하지 않는다.
  • @JoinColumn(insertable=false, updateble=false)
  • 읽기 전용 필드를 사용해서 양방향 처럼 사용하는 방법
  • 차라리 다대일 양방향을 사용하자.
@Entity
@Getter @Setter
public class Member {

    @Id @GeneratedValue
    private long id;

    @Column(name = "USERNAME")
    private String username;

    /* insert와 update를 막아서 읽기전용 필드를 생성한다. */
    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
    private Team team;
}

결론

정리하자면 해당 구조는 객체지향에서는 맞지만 DB에서는 어긋나는 설계이고, 직접 건드리는 테이블외에 다른 테이블에 영향이 갈 수가있다.

최대한 일대다를 지양하고 다대일 양방향을 사용하도록 하자.