JPA 연관관계 매핑 2

윤현우·2023년 6월 20일
0
post-thumbnail

일대다 단방향 연관관계

일대다 관계는 위에서 봤던 @OneToMany다. 하지만 만약 @OneToMany로 단방향 관계를 맺는다면 어떻게 될까?

다시 말해 @OneToMany 어노테이션이 있는 필드에 @JoinColumn을 아래 코드처럼 건다면 어떻게 될까?

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

해당 엔티티는 일대다 단방향 연관관계 매핑을 한 것이다.

하지만, 해당 외래키는 다쪽인 Member에 생긴다.

애당초 @OneToMany 어노테이션은 외래키 생성을 할 수 없다.

그래서 일대다 단방향 연관관계는 권장되지 않는다.

왜냐하면 연관관계 주인 엔티티에서 외래키를 관리하지않고 반대편 엔티티에서 외래키를 관리하기 때문에(데이터베이스 설계에서 1:M관계에서 외래키는 M에 존재하기 때문에) 관리가 부담스럽다.


일대다 양방향 연관관계

일대다 양방향 연관관계는 애초에 다대일 양방향 연관관계와 같기 때문에 존재하지 않는다.


일대일 단방향 연관관계

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

일대일 관계에서는 외래키가 어디에 있든 상관이 없다.

Member 엔티티와 Locker 엔티티가 있고, 1:1 관계라 하자.

외래키는 Member 또는 Locker 엔티티에 존재할 수 있다.

연관관계 주인을 Member로 한 1:1 단방향 관계 코드를 보자.

@Entity
public class Locker {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
}

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String memberName;

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

일대일 단방향 관계도 다대일 단방향과 비슷하다

Locker를 단방향 관계 주인으로 하고 싶으면 @OneToOne을 Locker에 붙여주면 된다.

단지 일대일 단방향 관계는 어디든 주인이 될 수 있다는 점이 특징이다.

반대로 Locker가 연관관계의 주인이 될 수 있다.

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String memberName;
}

@Entity
public class Locker {
    @Id
    @GeneratedValue
    private Long id;
    private String lockerName;

    @OneToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
}

일대일 양방향 연관관계

일대일 양방향 관계도 다대일 양방향 관계랑 비슷하다. 바로 코드를 보자.

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String memberName;

    @OneToOne(mappedBy = "member")
    private Locker locker;
}

@Entity
public class Locker {
    @Id
    @GeneratedValue
    private Long id;
    private String lockerName;

    @OneToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
}

만약 Member 객체가 연관관계의 주인으로 설정하고 싶다면 mappedBy 속성을 변경해주면 된다.


다대일 단방향 연관관계

RDBMS에서는 다대다 관계를 2개의 테이블을 이용해 표현이 불가능하다.

그래서 중간에 관계 테이블을 두어 일대다, 다대일 관계로 표현한다.

하지만 객체는 테이블과 다르게 객체 2개로 다대다 연관관계를 만들 수 있다.

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String userName;

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

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String productName;
}

@ManyToMany

  • 다대다 매핑을 위한 어노테이션

@JoinTable

  • 연결 테이블을 매핑하기 위한 어노테이션

<@JoinColumn 속성>

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

MEMBER_PRODUCT 테이블은 다대다 관계를 일대다, 다대일 관계로 풀어내기 위해 필요한 연결 테이블일 뿐이기 때문에, 다대다 관계를 사용할 때는 이 연결 테이블을 신경쓰지 않아도 된다.


다대다 양방향 연관관계

위의 매핑들 처럼 반대 객체에 @ManyToMany(mappedBy = "")을 사용해 양방향 연관관계를 시킨다.

@Entity
public class Product {
    @Id
    @GeneratedValue
    private Long id;
    private String productName;
    @ManyToMany(mappedBy = "product")
    private List<Member> members = new ArrayList<>();
}

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String memberName;

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

역시나 mappedBy 속성이 없는 부분이 연관관계의 주인이다.

양방향으로 연관관계를 매핑했으니 객체 그래프 탐색을 이용해 product.getMembers()를 사용해 역방향으로도 조회할 수 있다.(단방향 관계시 member.getProducts()만 사용해 객체 그래프 탐색 가능)


다대다: 한계

위의 예제처럼 다대다 연관관계를 사용하면 좋겠지만, 실무에서는 다대다 연관관계를 사용하기에는 한계가 있다.

만약 회원이 상품을 주문할 때 주문 수량이나, 날짜같은 컬럼이 추가가 된다.

이렇게 컬럼이 추가 되면 더이상 @ManyToMany를 사용할 수 없다.

결국 연결 테이블과의 일대다, 다대일 연관관계를 이용해 매핑해야 한다.

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String memberName;

    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProduct = new ArrayList<>();
}

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String productName;
}

Member 객체와 MemberProduct 객체와는 양방향 관계지만, Product 객체와 MemberProduct는 단방향 관계이다.

상품 엔티티에서 회원상품 엔티티로 객체 그래프 탐색 기능이 필요하지 않다고 판단되었기 때문에 단방향으로 연관관계를 맺었다.

가장 중요한 회원상품 엔티티를 보자.

복합 기본키 사용

회원상품 엔티티

@Entity
@IdClass(MemberProductId.class)
public class MemberProduct {
    @Id
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    @Id
    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;

    private String orderAmount;
}

회원상품 식별자 클래스

public class MemberProductId implements Serializable {
    private Long member;
    private Long product;

    @Override
    public int hashCode() {
        return super.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        return super.equals(obj);
    }
}

회원상품 엔티티를 보면 기본키를 매핑하는 @Id와 외래키를 매핑하는 @JoinColumn을 동시에 사용해 기본 키 + 외래 키를 한번에 매핑했다.

그리고 @IdClass 어노테이션을 사용해 복합 기본 키를 매핑했다.

<복합 기본키>

  • 복합 기본키를 사용하려면 별도의 식별자 클래스가 필요하다.
  • Serializabel을 구현해야 한다.
  • equals와 hashCode 메소드를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public 이어야 한다.
  • @IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있다.

이렇게 회원과 상품의 기본 키를 받아 자신의 기본키로 사용하고 외래 키로도 사용하는 것을 식별 관계라고 한다.

하지만 복합 기본 키를 사용하는 방법은 복잡하기 때문에 새로운 키를 사용해 복합 기본 키를 생성하지 않고 매핑하는 것이다.

마치 연결테이블이 새로운 테이블을 만드는 것처럼 사용하는 것이다.

새로운 기본 키 사용

DB에서 자도으로 생성해주는 대리 키를 Long 값으로 사용헤 기본키를 설정 하는 것이다.

해당 방법은 간편하고 거의 영구히 쓸 수 있으며 비즈니스에 의존하지 않는다.

또한 ORM매핑 시 복합키를 만들지 않아도 되므로 간단히 매핑을 완성 할 수 있다.

새로운 ORDER_ID라는 기본키를 만들어 다시 매핑 해보자.

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

개인적인 나의 의견

가장 처음 JPA 공부를 할 때, 궁금한 점이 있었다.

만약 회원(1)과 주문(N)간의 매핑을 할 때, 회원의 주문 목록을 들여오기 위해서는 양방향 연관관계를 써야 member.getOrders()로 객체 그래프 탐색을 이용해 주문 목록을 받아 올 수 있다고 생각을 했다.

하지만, 실무에서는 다대일 단방향 연관관계를 사용해도 해당 회원의 주문 목록을 가져올 수 있다는 것이다.

나는 회원의 주문 목록을 가져오기 위해서는 테이블에서 뿐만 아니라, 객체에서도 객체 그래프 탐색을 이용하여 받아와야지만 가능한 줄 알았다.

하지만, 객체 그래프 탐색을 이용하지 않고, 테이블의 양방향 연관관계로 회원Id를 이용해 가져올 수 있다고 알게 되었다.

처음에 나는 객체도 양방향으로 연관관계를 매핑하지 않으면 객체지향의 특징을 잃어 버리는 것인 줄 알았다.

구글링 해본 결과, 양방향으로 매핑하는 이유는 따로 있었다.

양방향 매핑이 필요한 이유

<양방향 매핑이 필요한 이유>

  • 양방향 매핑이 필요한 경우는 단방향 매핑된 관계를 역방향으로 조회할 때다.

  • 데이터에 대한 일관성을 유지할 때 사용된다.

  • 개발을 하다보면 단방향으로 매핑된 연관 관계를 역순으로 조회해야 하는 경우가 발생한다.

  • 이처럼 필요한 경우에 한하여, 양방향 매핑을 추가적으로 구현하는 것을 권장한다.

결국 객체에서의 연관관계를 역순으로 조회할 때 양방향 매핑이 필요한 것이었다.

그게 아니면 단방향으로도 사용 가능하다는 것을 알게 되었다.

그래서 처음 설계시 대부분 단방향으로 사용하고 양방향으로 구현하지 않는 것이 좋다.

이렇게 하나 또 알게 되면서 지식이 늘어나는 것이 행복하다.

profile
개발자가 되는 그날까지

0개의 댓글