06. 다양한 연관관계 매핑

zwundzwzig·2023년 3월 30일
0
post-thumbnail

다중성과 단방향, 양방향을 고려한 가능한 모든 연관관계를 하나씩 알아보자.

다대일

다대일 관계의 반대 방향은 항상 일대다 관계이고 일대다 관계의 반대 방향은 항상 다대일 관계이다.

DB 테이블의 일대다 관계에서 외래 키는 항상 다쪽에 있다. 따라서 객체 양방향 관계에서 연관관계의 주인은 항상 다쪽이다.

다대일 단방향


회원은 Member.team으로 팀 엔티티를 참조할 수 있지만 팀에는 회원을 참조할 수 있는 필드가 없다. 따라서 회원과 팀은 다대일 단방향 연관관계이다.

Member.team 필드로 회원 테이블의 team_id 외래 키를 관리한다.

다대일 양방향


그림에서 실선인 Team.members는 연관관계 주인이 아니다.

@Entity
public class Member {
	id; username; ...
    
    @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; name;
    ...
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
    
    public void addMember(Member member) {
    	this.members.add(member);
        // 무한 루프에 빠지지 않도록 체크
        if(member.getTeam() != this) {
        	member.setTeam(this);
        }
    }
}

일대다와 다대일 연관관계는 항상 다에 외래 키가 있다. 여기서 다쪽인 Member 테이블이 외래 키를 갖고 있으므로 Member.team이 주인이 되고, 주인이 아닌 Team.members는 조회를 위한 JPQL이나 객체 그래프를 탐색할 때 사용된다.

그리고, 양방향 연관관계는 항상 서로를 참조해야 한다. 항상 서로를 참조하게 하려면 setTeam(), addMember() 같은 연관관계 편의 메소드를 사용하는 게 좋다. 대신 편의 메소드를 양쪽 다 작성하면 무한루프에 빠지므로 주의해야 한다.

일대다

일대다 관계는 엔티티를 하나 이상 참조하므로 List, Collection, Set, Map 등의 컬렉션을 사용해야 한다.

일대다 단방향

하나의 팀은 여러 회원을 참조하는 관계를 일대다 관계라 한다. 그리고 팀은 회원을 참조하지만 반대는 아니라면 단방향 관계이다. JPA 2.0부터 지원된다.

그림 상에서 Team 엔티티의 members로 회원 테이블의 TEAM_ID 외래 키를 관리한다. 특이한 점은 자신이 매핑한 테이블의 외래키가 아닌, 반대 테이블에 있는 외래키를 관리한다는 점이다.

일대다에서 외래키는 다쪽 테이블에 있어야 하지만, 다쪽 엔티티에 외래키를 매핑할 수 있는 참조 필드가 없기 때문에 이런 현상이 나타난다.

@Entity
public class Team {
	...id; name;
    
    @OneToMany
    @JoinColumn(name = "TEAM_ID") // Member 테이블의 TEAM_ID (FK)
    private List<Member> members = new ArrayList<Member>();
}

@Entity
public class Member {
	...id; username;
}

일대다 단방향에선 @JoinColumn을 명시해야 한다. 그렇지 않으면 JPA는 JoinTable 전략을 취하기 때문.

매핑한 객체가 관리하는 외래 키가 다른 테이블에 있기 때문에, 연관관계 처리를 위해 불필요한 쿼리가 더 필요하는 등 성능&관리에 문제가 있다.

일대다 단방향 매핑보단, 다대일 양방향 매핑을 사용하자!!

다대일 양방향

@ManyToOne는 mappedBy 속성이 없는, 연관관계 주인이다.

일대일

일대일 관계는 양쪽이 서로 하나의 관계만 갖는, 서로 일대일 관계이다.

또한 주 테이블이나 대상 테이블 둘 중 어느 곳이나 외래 키를 가질 수 있다. 주든 대상이든 외래키 하나로 양쪽 조회가 가능한데, 그래서 누가 외래키를 가질지 잘 선택해야 한다.

객체지향은 주테이블에, DB 지향은 대상 테이블에 외래키가 있는 것을 선호한다. 대상 테이블에 외래키가 있는 단방향 관계는 JPA에서 지원하지 않는다.

다대다

OOP는 RDBMS와 다르게 중간 테이블 없이 엔티티 두 개로 다대다를 편리하게 매핑할 수 있다.

단방향

@Entity
public class Member {
	...id; username;
    
    @ManyToMany
    @JoinTable(
    	name = "MEMBER_PRODUCT",
    	joinColumns = @JoinColumn(name = "MEMBER_ID"),
        inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID")
    )
    private List<Product> products = new ArrayList<Product>();
}

@Entity
public class Product {
	...id;name
}

@JoinTable

  • name 으로 임시적인 연결 테이블 생성하고,
  • joinColumns로 현재 방향인 회원과 매핑할 조인 컬럼 정보를 지정하고,
  • inverseJoinColumns로 반대 방향인 상품과 매핑할 조인 컬럼 정보를 지정한다.

양방향

양방향 연관관계는 연관관계 편의 메소드를 추가해 관리하는 게 편하다.

member.getProducts().add(product); 대신,
member.addProduct(product);처럼 말이다.

이를 위해선,
public void addProduct (Product product) {
	products.add(product);
    product.getMembers().add(this);
}
와 같은 메소드 정의가 필요하겠다.

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

@ManyToMany를 사용하면 연결 테이블을 자동으로 처리해 도메인 모델이 단순해지는 장점이 있음에도 실무에 도입하기엔 한계가 있다.

RDBMS 상 연결 테이블에 부가적인 컬럼이 더 필요할 때가 많기 때문이다.

그래서 연결해주는 엔티티를 만들어야 한다.

@Entity
public class Member {
	// id 있다 쳐
	@OneToMany(mappedBy = "member")
	private List<MemberProduct> memberProducts;
}

@Entity
@IdClass(MemberProductId.class)
public class MemberProduct {
	@Id @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member; // MemberProductId.member와 연결
    
    @Id @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product; // MemberProductId.product와 연결
}

public class MemberProductId implements Serializable {
	// member, product 있다 쳐
    
    @Override
    public boolean equals(Object o) {...}
    
    @Override
    public int hashCode() {...}
}

중간 테이블인 MemberProduct에서 @Id, @JoinColumn 그리고 @IdClass를 사용해 복합 기본 키를 매핑했다. (혹은 @EmbeddedId가 필요하다.)

JPA에서 복합 기본 키를 만들려면 MemberProductId와 같은 별도의 식별자 클래스가 필요하다. 그리고 식별자 클래스에선 equals, hashCode 도 구현해야 한다.

MemberProduct에서 Member와 Product의 기본키를 받아 자신의 기본키 + 외래키로 사용하는 것을 데이터 베이스 용어로 식별 관계 Identifying Relationship라고 한다.

새로운 기본 키 사용

복합키를 사용하지 않고 간단히 다대다 관계를 구성하는 방법은 데이터베이스에서 자동으로 생성해주는 대리 키를 Long 값으로 사용하는 것이다.

@Entity
public class MemberProduct {
	@Id @GeneratedValue
    @Column(name = "MemberProduct_ID")
    private Long id;

	@Id @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member; // MemberProductId.member와 연결
    
    @Id @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product; // MemberProductId.product와 연결
}

이를 통해 식별자 클래스를 사용하지 않고 간결하게 코드를 작성할 수 있다.

다대다 연관관계 정리

다대다 관계를 일대다 다대일 관계로 풀려고 연결 테이블을 만들 때 식별자를 어떻게 구성할지 선택해야 한다.

  • 식별 관계 : 받아온 식별자를 기본 키 + 외래 키로 사용한다.
  • 비식별 관계 : 받아온 식별자를 외래 키로만 사용하고 새로운 식별자를 추가한다.

DB 설계에서와 달리 객체 입장에선 단순한 비식별 관계를 추천한다.

🧷 참조 교재

  • 김영한, 『자바 ORM 표준 JPA 프로그래밍』 에이콘(2015)
profile
개발이란?

0개의 댓글