다양한 연관관계 매핑

윤용운·2022년 4월 17일
1

JPA_스터디

목록 보기
6/9
post-thumbnail

6장. 다양한 연관관계 매핑

다대일

외래키는 항상 다쪽에 있으므로, 객체 양방향 관계에서 연관관계의 주인은 항상 "다"쪽이다.

다대일 단방향 [N:1]

// 회원 엔티티
@Entity
public class Member {
	
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")	// 외래키를 관리한다.
    private Team team;
    ...
}

// 팀 엔티티
@Entity
public class Team {
	
    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
    ...
}
  • 단방향 관계이므로 회원은 Member.team으로 팀 엔티티를 참조할 수 있지만, 팀에서 회원을 참조하는 필드가 없다. 따라서, 회원과 팀은 다대일 단방향 연관관계이다.

다대일 양방향 [N:1, 1:N]

// 회원 엔티티
@Entity
public class Member {
	
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")	// 외래키를 관리한다.
    private Team team;
    
    public void setTeam(Team team) {
    	this.team = team;
        
        // 무한루프에 빠지지 않도록 체크
        if (!team.getMembers().contains(this)) {
        	team.getMembers().add(this);
        }
    }
    ...
}

// 팀 엔티티
@Entity
public class Team {
	
    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
    
    public void addMember(Member mewmber) {
    	this.members.add(member)
        
        // 무한루프에 빠지지 않도록 체크
        if (member.getTeam() != this) {
        	member.setTeam(this);
        }
    }
    ...
}
  • 양방향은 외래키가 있는 쪽이 연관관계의 주인이다.
    일대다 연관관계는 항상 다(N)쪽에 외래키가 있으므로, Member.team이 연관관계의 주인이다. JPA에서는 외래키 관리시 연관관계의 주인만 사용하고, 주인이 아닌 Team.members는 조회를 위한 JPQL이나 객체 그래프 탐색시 사용한다.

  • 양방향 연관관계는 항상 서로를 참조해야 한다.
    어느 한쪽만 참조를 하게 되면 양방향 연관관계가 아니게 되므로 항상 참조해야 한다. 또한 항상 참조시 연관관계 편의 메소드(setTeam(), addMember())를 작성해주는 것이 좋은데, 이는 한곳에만 작성해도 되고 양쪽 다 작성해도 된다. 하지만 양쪽 다 작성시에는 무한루프에 빠질 수 있으므로 주의해야 한다.

일대다

다대일의 반대 방향이다. 일대다 관계는 엔티티를 하나 이상 참조할 수 있어야 하므로 자바 컬렉션(Collection, List, Set, Map)중 하나를 사용한다.

일대다 단방향 [1:N]

@Entity
public class Team {
	
    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
    
    @OneToMany
    @JoinColumn(name = "MEMBER_ID")
    private List<Member> members = new ArrayList<>();
    ...
}

@Entity
public class Member {
	
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username
    ...
}

보통 일대다 관계에서 다 쪽에 외래키가 있지만, 여기서는 다 쪽에 외래키를 매핑할 수 있는 참조 필드가 없다. 따라서, 반대편 테이블의 외래키를 관리하는 특이한 모습이 나타난다. 이렇기 때문에 연관관계 처리를 위해 UPDATE SQL을 추가로 실행해야 하는 단점이 생긴다. 따라서 일대다 단방향 매핑보다는, 다대일 양방향 매핑을 사용하는 것을 권장한다.

일대다 양방향 [1:N, N:1]

일대다 양방향 매핑은 존재하지 않는데(사실 다대일 양방향이랑 다를게 없기도 하다), 양방향 매핑에서 @OneToMany는 연관관계의 주인이 될 수 없기 때문이다. 물론 그렇게 보이게 할 수는 있지만, 일대다 단방향 매핑이 가지는 단점도 가지게 됨으로 전혀 쓸모가 없다. 될 수 있으면 다대일 양방향 매핑을 사용하는 것이 좋다.

일대일 [1:1]

양쪽이 서로 하나의 관계만을 가지는 관계이다. 일대일의 반대도 일대일 관계이고, 다대일과는 다르게 주 테이블이나 대상 테이블 둘 다 외래키를 가질 수 있다.

  • 주 테이블에 외래 키
    외래 키를 객체 참조와 비슷하게 사용할 수 있다. 주 테이블이 외래 키를 가지고 있으므로 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다.

  • 대상 테이블에 외래 키
    테이블 관계를 일대일에서 일대다로 변경할 떄 테이블 구조를 그대로 유지할 수 있다.

주 테이블에 외래 키

JPA에서는 주 테이블에 외래 키가 있으면 좀 더 편리하게 매핑할 수 있다고 한다.

  • 단방향
@Entity
public class Member {
	@Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @OneToOne
    @JoinColumn(name = "LOCKER_ID");
    private Locker locker
    ...
}

@Entity
public class Locker {
	@Id @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    
    private String name;
    ...
}

일대일 관계이므로 객체 매핑에 @OneToOne을 사용하고, LOCKER_ID 외래 키에 유니크 제약조건을 추가하였다.

  • 양방향
@Entity
public class Member {
	@Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @OneToOne
    @JoinColumn(name = "LOCKER_ID");
    private Locker locker
    ...
}

@Entity
public class Locker {
	@Id @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    
    private String name;
    
    @OneToOne(mappedBy = "locker")
    private Member member;
    ...
}

양방향이므로 Member.locker가 연관관계의 주인이 되고, Locker.membermappedBy를 선언하여 연관관계의 주인이 아니라고 설정하였다.

대상 테이블에 외래 키

  • 단방향
    일대일 관계 중 대상 테이블에 외래키가 있는 단방향 관계는 JPA에서는 지원하지 않는다.

  • 양방향

@Entity
public class Member {
	@Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @OneToOne(mappedBy = "member")
    private Locker locker
    ...
}

@Entity
public class Locker {
	@Id @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    
    private String name;
    
    @OneToOne
    @JoinColumn(name = "MEMBER_ID");
    private Member member;
    ...
}

이렇게 엔티티를 매핑하게 되면, 나중에 멤버 한명이 여러 라커를 소유하게 되는 경우가 생기더라도 테이블 구조를 바꾸지 않게 된다(LOCKER의 MEMBER_ID에서 유니크 제약조건만 없애주면 된다).

다대다

관계형 데이터베이스에서는 정규화된 테이블 두개로는 다대다 관계를 표현할 수 없다. 따라서 다대다 관계는 일대다, 다대일 관계로 풀어서 연결하는 방법을 사용하게 된다. 하지만, 객체에서는 다대다 관계를 컬렉션을 이용하여 만들 수 있다.

다대다 : 단방향

@ManyTomany@JoinTable을 사용하여 바로 매핑이 가능하다.

  • @JoinTable.name : 연결 테이블 지정
  • @JoinTable.joinColumns : 현재 방향인 회원과 매핑할 조인 컬럼 정보를 지정한다.
  • @JoinTable.inverseJoinColumns : 반대 방향인 상품과 매핑할 조인 컬럼 정보를 지정한다.

@ManyToMany를 사용하여 조회하게 되면 알아서 JOIN하여 연관된 데이터를 조회한다.

다대다 : 양방향

mappedBy를 사용하여 연관관계의 주인을 지정할 수 있다.

다대다 : 매핑의 한계와 극복, 연결 엔티티 사용


@ManyToMany 사용 시 연결테이블을 자동으로 처리해주므로 편할 수는 있지만, 실무에서 사용하기에는 한계가 있다. 보통 연결 테이블에 추가 컬럼이 필요한데, @ManyToMany에서는 이런 컬럼들을 매핑을 못하기 때문이다. 따라서, 다대다 관계를 매핑할 떄는 위의 그림처럼 연결 엔티티를 만들고, 이곳에 추가한 컬럼들을 다대일, 일대다 관계로 매핑해야 한다.

@Entity
public class Member {
	@Id @Column(name = "member")
    private String id;
    
    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts;
    ...
}

@Entity
public class Product {
	@Id @Column(name = "PRODUCT_ID")
    private String id;
    
    // 상품->회원상품 엔티티로 객체 그래프 탐색 기능이 필요하지 않다고 판단되어 연관관계를 만들지 않았다.
    private String name;
}

그리고, 회원상품 엔티티와 회원상품 식별자 클래스를 생성하였다.

@Entity
@IdClass(MemberProductId.class)
public class MemberProduct {
	
    @Id @ManyToOne
    @JoinColum(name = "MEMBER_ID")	// @Id와 @JoinColumn을 동시에 사용해 기본키 + 외래키 한번에 매핑
    private Member member;		// MemberProductId.member와 연결
    
    @Id @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;	// MemberProductId.product와 연결
    
    private int orderAmount;
}

public class MemberProductId implements Serializable {
	
    private String member;
    private String product;
    
    @Override
    public boolean equals(Object o) {...}
    
    @Override
    public int hashCode() {...}
}
  • 복합 기본 키
    JPA에서 다음과 같이 복합 기본 키를 생성하려면 별도의 식별자 클래스를 만들어야 한다. 이후, @IdClass를 사용해서 식별자 클래스를 지정해주면 된다
    • 복합키는 별도의 식별자 클래스를 만들어야 한다.
    • Serializable을 구현해야 한다.
    • equalshashCode 메소드를 구현해야 한다.
    • 기본 생성자가 있어야 한다.
    • 식별자 클래스는 public이어야 한다.
    • @IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있다.
  • 식별 관계
    부모 테이블의 기본 키를 받아서 자신의 기본 키 + 외래 키로 사용하는 것을 데이터베이스 용어로 식별 관계라 한다.

다대다 : 새로운 기본 키 사용

위처럼 복합 키를 사용하는 방법도 있지만, 추천하는 기본 키 생성 전략을 통해 대리키를 사용하는 방법이 있다. 이는 간편하고(복합 키 사용시에는 해야할 것들이 너무 많다...), 영구적으로 쓸 수 있으며, 비즈니스에 의존하지도 않게 된다. 또한, ORM 매핑 시 복합 키를 만들지 않아도 되므로 간단히 매핑을 완성할 수 있다.


다음과 같이, ORDER_ID라는 새로운 키를 하나 만들고, MEMBER_ID, PRODUCT_ID 컬럼은 외래키로만 사용한다.

@Entity
public class Order {
	@Id @GeneratedValue
    @Column(name = "ORDER_ID")
    private Long Id;
    
    @ManyToOne
    @JoinColum(name = "MEMBER_ID")
    private Member member;
    
    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;
    
    private int orderAmount;
    ...
}

다음처럼 대리 키를 사용함으로서 복합키를 사용하는 것보다 매핑이 단순하고 이해하기 쉬워졌다.

다대다 연관관계 정리

  • 식별 관계
    받아온 식별자를 기본 키 + 외래 키로 사용
  • 비식별 관계
    받아온 식별자는 외래 키로만 사용 + 새로운 식별자 추가

식별 관계보다는 비식별 관계가 복합키를 위한 식별자 클래스를 생성하지 않아도 되고, 단순하고 편리하게 ORM 매핑을 할 수 있으므로 단순하고 편리하게 ORM 매핑을 할 수 있다. 따라서, 식별 관계보다는 비식별 관계를 추천한다.

Reference

  • 자바 ORM 표준 JPA 프로그래밍 (김영한)

0개의 댓글