본문 바로가기

JPA

[JPA] 연관관계 매핑

단방향 연관관계

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    public void setTeam(Team team) {
    	this.team = team;
    }
    
}
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Team {
    
    @Id
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
    
}

 

데이터베이스 테이블 관점에서 봤을때 회원 테이블과 팀 테이블은 TEAM_ID를 외래키로 양방향 되어있지만 객체의 관점에서 보면 위 코드에서 Member 클래스와 Team 클래스는 단방향으로 다대일(N : 1) 매핑 되어 있다. 회원은 Member.team 필드를 통해서 팀을 알 수 있지만 반대로 팀은 회원을 알 수 없다. 이런 경우를 단방향 매핑 되어 있다고 한다.

단방향 연관관계 매핑에 쓰인 어노테이션

@ManyToOne

이름 그대로 다대일 (N : 1) 관계라는 매핑 정보이다. 각각의 Member는 하나의 Team에 소속되어 있고 하나의 Team은 여러 Member를 포함할 수 있기 때문에 다대일 관계로 매핑하였다. 이 밖에 OneToOne(일대일), OneToMany(일대다), ManyToMany(다대다) 연관관계 매핑이 존재한다.

 

@JoinColumn(name = "TEAM_ID")

@JoinColumn 어노테이션은 외래키를 매핑할 때 사용하며 name 속성에는 매핑할 외래키 이름을 지정한다. 참조하는 테이블의 ID 컬럼명을 기입한다.

양방향 연관관계

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    public void setTeam(Team team) {
    
        // 기존 팀과의 관계 제거
        if (this.team != null) {
            this.team.getMembers().remove(this);
        }
        
        this.team = team;
        team.getMembers().add(this);
    }

}
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Team {
    
    @Id
    @Column(name = "TEAM_ID")
    private Long id;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
    
    private String name;
    
}

 

단방향 매핑된 상태에서 Team 클래스에 팀에 소속된 Member를 담을 Members 리스트를 추가하고 일대다 관계를 매핑하기 위해 @OneToMany 어노테이션과 mappedBy 속성을 사용하여 양방향 매핑 관계에서 주인을 정하였다. 그리고 만약 기존에 소속된 팀이 있었다면 관계를 제거해 주고 양방향 연관관계 양쪽 모두 관계를 맺어 주기 위해 setTeam() 메소드에서 Member 클래스의 team을 할당해주고 파라미터로 들어온 Team 클래스에서도 Members 리스트에 Member를 추가해주었다.

연관 관계의 주인

객체 관점에서 보면 사실 양방향 매핑이라는 것은 존재하지 않는다. 위 코드에서도 양방향 매핑이라고 관계지었지만 사실은 Member에서 Team, Team에서 Member로 단방향 매핑을 두번 설정한 것이다. 그렇기 때문에 연관관계는 두개 이지만 외래키는 TEAM_ID 하나 이므로 두 단방향 매핑 중 테이블의 외래키를 관리할 연관관계의 주인을 정해주어야 하는데 이를 mappedBy 속성을 이용해서 정할 수 있다.

 

그렇다면 주인은 어떻게 정할까? 먼저 mappedBy는 다음과 같은 특징을 가지고 있다.

  • 주인은 mappedBy 속성을 사용하지 않는다. (mappedBy 속성을 쓰고 있는 Team은 연관관계의 주인이 아니다.)
  • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다. (연관관계의 주인인 Member 클래스에서 외래키 역할을 하는 Team 클래스 변수명인 team을 입력한다.)

연관관계의 주인을 정한다는 것은 외래키의 관리자를 정한다는 것이다. 그렇기 때문에 연관관계의 주인은 테이블에 외래키가 있는 곳으로 정해야 하는데 위의 ERD를 보면 MEMBER 테이블이 연관관계 외래키인 TEAM_ID를 갖고 있기 때문에 연관관계의 주인이 된다. 참고로 다대일 일대다 양방향 관계에서는 항상 다 쪽이 외래키를 가진다.

 

양방향 매핑은 Member 에서 Team으로 Team 에서 Member로 양쪽 방향 모두 객체 그래프 탐색이 가능하다는 장점이 있지만 양방향 매핑을 구현하는 것이 까다롭기 때문에 우선 단방향 매핑을 사용하고 만약 반대방향으로도 객체 그래프 탐색이 필요한 시점이 오면 그때 양방향 매핑을 해주는 것이 좋다.

연관관계 매핑의 종류

다대일 (@ManyToOne)

  • 다대일 관계의 반대 방향은 항상 일대다 관계이고 일대다 관계의 반대 방향은 항상 다대일 관계이다.
  • 데이터베이스 테이블의 다대일, 일대다 관계에서 외래키는 항상 다쪽에 있다.
  • 다대일 관계의 단방향 매핑과 양방향 매핑은 위에서 설명했던 단방향 매핑, 양방향 매핑 예시 코드와 같다.

일대다 (@OneToMany)

  • 일대다 관계는 다대일 관계의 반대방향이다.
  • 일대다 관계는 엔티티를 하나 이상 참조할 수 있으므로 자바 컬렉션은 Collection, List, Set, Map 중에 하나를 사용해야 한다.
  • 일대다 단방향 매핑 예시
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Team {
  
    @Id 
    @GeneratedValue    // 기본키 매핑 전략, 기본 값은 AUTO이다.
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
    
    @OneToMany
    @JoinColumn(name = "TEAM_ID")    // MEMBER 테이블의 TEAM_ID (FK)
    private List<Member> members = new ArrayList<>();
    
}
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
}

일대다 매핑이 특이한 점은 다대일 매핑에서 언급했듯이 보통 외래키는 다 쪽에 있다. 하지만 다 쪽인 Member 클래스에 외래키를 매핑할 수 있는 참조 필드가 없다. 일대다 매핑의 단점은 매핑한 객체가 관리하는 외래키가 다른 테이블에 있다는 점이다. 위 예시에서 매핑한 객체(Team)가 관리하는 외래키(TEAM_ID)가 Team 테이블에 없고 다른 테이블(Member)에 있는데 이런 경우에 연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야 하기 때문에 성능상 문제도 있고 관리하기가 까다롭다. 그래서 되도록이면 일대다 단방향 매핑은 사용하지 않고 다대일 양방향 매핑을 권장한다.

 

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

일대다 양방향 매핑은 연관관계의 주인이 어느쪽이냐에 따라 다르게 지칭하는것 뿐이지 사실 다대일 양방향 매핑과 다르지 않다. 다만 데이터베이스의 특성상 외래키는 항상 다쪽에 존재하기 때문에 일대다 양방향 매핑은 사실상 존재하지 않는다. 

양방향 매핑이 완전히 불가능한 것은 아니지만 일대다 단방향 매핑과 같은 문제가 생기기 때문에 여기서도 다대일 양방향 매핑을 더 권장한다.

일대일 (OneToOne)

  • 일대일 관계는 그 반대도 일대일 관계이다.
  • 일대일 관계는 주 테이블이나 대상 테이블 둘 중 어느 곳이나 외래키를 가질 수 있다.

주 테이블 단방향

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;

}
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Locker {

    @Id @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    
    private String name;

}

 

주 테이블 양방향

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Locker {

    @Id @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    
    private String name;
    
    @OneToOne(mappedBy = "locker")
    private Member member;

}

 

Member 클래스는 단방향 매핑과 같고 Locker 클래스에 @OneToOne 어노테이션을 선언해주고 mappedBy 속성을 추가해서 연관관계의 주인이 아니라고 설정했다. 

 

주 테이블이 외래키를 가질 때 장점

주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다.

객체지향 개발자들이 선호한다.

 

대상 테이블 단방향

JPA에서 대상테이블 일대일 단방향 매핑은 지원하지 않는다.

 

대상 테이블 양방향

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @OneToOne(mappedBy = "member")
    private Locker locker;

}
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Locker {

    @Id @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    
    private String name;
    
    @OneToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

}

 

대상 테이블이 외래키를 가질 때 장점

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

데이터베이스 개발자들이 선호한다.

다대다 (@ManyToMany)

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 그렇기 때문에 중간에 연결테이블을 생성하고 일대다 다대일로 풀어서 설계하는데 객체는 테이블과 다르게 객체 2개로 @ManyToMany 어노테이션을 사용해서 다대다 관계를 만들 수 있다. 이렇게 말하면 다대다 매핑이 굉장히 편리한 연관관계로 보일 수 있겠지만 사실 다대다 매핑은 실무에서는 잘 사용하지 않는다.

 

그 이유는 연결 테이블에 외래키가 아닌 다른 컬럼이 추가되면 기존 객체에서는 추가한 컬럼들을 매핑할 방법이 없기 때문에 @ManyToMany를 사용할 수 없기 때문이다. 대신 관계형 데이터베이스와 마찬가지로 객체를 설계할때도 중간에 연결 객체를 추가해서 일대다 다대일 연관관계로 풀어서 매핑하는 방법을 사용하고 있다.

 

아래와 같은 조건에서 다대다 연관관계 매핑을 실습해보자

  • 회원 테이블과 상품 테이블이 존재한다.
  • 회원은 여러 상품을 주문할 수 있고 상품은 여러 회원이 주문할 수 있다. (다대다 연관관계)
  • 회원과 상품 사이에 회원-상품 테이블을 추가해서 일대다 다대일로 풀어서 매핑해 보자

회원 클래스

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProduct;

}

 

상품 클래스

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Product {

    @Id @GeneratedValue
    @Column(name = "PRODUCT_ID")
    private Long id;
    
    private String name;

}

 

회원-상품 클래스

@Entity
@Getter
@Setter
@NoArgsConstructor
public class MemberProduct {

    @Id @GeneratedValue
    @Column(name = "MEMBERPRODUCT_ID")
    private Long id;  
    
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
    
    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;
    
    private int orderAmount;
    
}