JPA 다양한 연관관계

연관관계 매핑시 고려사항

다중성

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

다대다의 경우 실무에서 사용을 지양한다.

단방향, 양방향

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

연관관계의 주인

  • 테이블은 외래키 하나로 두 테이블이 연관관계를 맺는다.
  • 객체 양방향 관계는 A->B, B->A 처럼 참조가 두 개인 것이다. 그래서 둘 중 테이블의 외래키를 관리할 곳을 지정해야 한다(연관관계의 주인 설정).

다대일

가장 많이 사용하는 연관관계. DB 테이블의 N:1 관계에서 외래키는 항상 N쪽에 존재한다. 객체의 양방향 관계에서 연관관계의 주인은 항상 N쪽(외래키 존재로 관리가 편함).

다대일 단방향 [N:1]

@Entity
public class Member {
    ...

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

    ...
}
@Entity
public class Team {
    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

    ...
}

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

객체의 양방향 관계에서 실선이 연관관계의 주인, 점선은 연관관계의 주인이 아님을 표시한다.

@Entity
public class Member {
    ...

    @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 {
    ...

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

    // 편의 메서드
    public void addMember(Member member) {
        this.members.add(member);

        //무한 루프에 빠지지 않도록 체크
        if(member.getTeam() != this) {
            member.setTeam(this);
        }
    }
    ...
}

일대다

다대일 관계의 반대 방향 연관관계. 엔티티를 하나 이상 참조할 수 있으므로 자바 컬렉션을 사용한다.

일대다 단방향 [1:N]

  • 1:N에서 1이 연관관계의 주인이 된다. 외래키는 N 쪽에 존재한다.
  • 객체와 테이블의 차이 때문에 반대편 테이블의 외래키를 관리하는 특이한 구조가 형성된다.
  • @JoinColumn을 사용해야한다. 그렇지 않으면 조인 테이블 방식을 사용하여 중간에 연결 테이블이 추가된다.
@Entity
public class Team {
    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    ...

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

    ...
}
@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;

    private String username;

    ...
}

일대다 단방향 매핑은 매핑한 객체가 관리하는 외래키가 다른 테이블에 있다. 즉, Team 엔티티에서 members로 연관관계를 관리한다는 말이다. 이 때 UPDATE SQL 추가 실행이 필요하므로 성능, 관리 측면에서 좋지 않다. 이를 해결하기 위한 가장 좋은 해결방법은 일대다 단방향 매핑을 다대일 양방향 매핑으로 수정하는 것이다.

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

  • 양방향 매핑에서 @OneToMany는 연관관계의 주인이 될 수 없다. 관계형 데이터베이스 특성상 N쪽에 외래키가 존재하며 항상 N쪽에 @ManyToOne을 사용하기 때문이다. 그래서 이런 매핑방법은 공식적으로 존재하지 않는다.
  • 일대다 양방향 매핑이 완전히 불가능하지는 않다. 다대일 단방향에 @JoinColumn(insertable = false, updatable = false)(읽기 전용 속성)을 설정하여 양방향처럼 사용하는 방법이 있다. 그래도 가능한 다대일 양방향을 사용하는 것이 좋다.
@Entity
public class Team {
    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

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

    ...
}
@Entity
public class Member {
    ...

    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
    private Team team;

    ...
}

일대일 [1:1]

  • 테이블 관계에서 일대다, 다대일은 항상 N쪽에 외래키를 가진다. 그런데 일대일 관계에서는 주테이블, 대상테이블 어느 곳에서든 외래키를 가질 수 있다.
  • 외래키에 DB 유니크(UNI) 제약조건을 추가해야 한다.

주 테이블 외래키

  • 주테이블에 외래키를 두고 대상 테이블을 참조한다. 외래키를 객체 참조와 비슷하게 사용할 수 있다.
  • 주 테이블이 외래키를 가짐으로써 주테이블을 통해 대상 테이블 관계를 확인할 수 있다는 장점을 가진다.
주 테이블 외래키 단방향

다대일 단방향(@ManyToOne) 매핑과 유사하다.

@Entity
public class Member {
    ...

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;

    ...
}
@Entity
public class Locker {
    @Id
    @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    
    private String name;
    ...
}
주 테이블 외래키 양방향

다대일 양방향 매핑처럼 외래키가 있는 곳이 연관관계의 주인이다. 반대편은 mappedBy 속성의 값으로 주인을 지정한다.

@Entity
public class Member {
    ...

    @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;

    ...
}

대상 테이블 외래키

테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있다는 장점을 가진다.

대상 테이블 외래키 단방향

일대일 관계에서 대상 테이블에 외래키가 있는 단방향 관계는 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;

    ...
}

다대다 [N:N]

객체는 컬렉션을 사용하여 다대다 관계를 표현할 수 있지만, RDB에서는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 연결 테이블을 추가하여 일대다, 다대일 관계로 형성해야 한다.

@Entity
public class Member {
    @Id
    @Column(name = "MEMBER_ID");
    private String id;

    private String username;

    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT",	
            joinColumns = @JoinColumn(name = "MEMBER_ID"),
            inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
    private List<Product> products = new ArrayList<>();

    ...
}		
@Entity
public class Product {
    @Id
    @Column(name = "PRODUCT_ID")
    private String id;

    private String name;

	...
}

@JoinTable

속성 기능
name 조인 테이블 명
joinColumns 현재 엔티티를 참조하는 외래키
inverseJoinColumns 반대방향 엔티티를 참조하는 외래키

다대다 매핑 한계

@ManyToMany로 연결테이블을 처리하면 편리해보이지만 실무에서 사용하기 어렵다. 연결 테이블에 단순히 식별자만 담는 것이 아닌 다른 컬럼들이 필요하게 될 수도 있기 때문이다.

다대다 매핑 한계 극복

연결 테이블용 엔티티를 추가하여 @ManyToMany@ManyToOne, @OneToMany로 분리하여 해결할 수 있다.

  • 식별관계 : 받아온 식별자를 복합키로 설정
  • 비식별관계 : 받아온 식별자는 외래키로만 사용하고, 새로운 식별자를 추가

비식별관계를 사용하면 복합키 없이 단순하고 편리한 ORM 매핑이 가능하다는 장점이 있다. 그러나 아래 예제는 복합키 설정방법을 나타내고자 식별관계를 사용했다.

@Entity
public class Member {
    @Id
    @Column(name = "MEMBER_ID")
    private String id;		

    //역방향
    @OneTomany(mappedBy = "member")
    private List<MemberProduct> memberProducts;

    ...
}
@Entity
public class Product {
    @Id
    @Column(name = "PRODUCT_ID")
    private String id;

    ...
}
@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와 연결
	
    private int orderAmount;

    ...
}
// 복합키 식별자 클래스
public class MemberProductId implements Serializable {
    private String member;
    private String product;

    ...
}

복합키 식별자 클래스

JPA에서 복합키를 사용하려면 별도 식별자 클래스를 생성해야한다.

  • Serializable 구현
  • equals와 hashCode 메서드를 구현
  • 기본 생성자 필요
  • public 클래스
  • @IdClass or @EmbeddedId 애니테이션으로 식별자 클래스를 지정

Categories:

Updated:

Comments