6장 다양한 연관관계 매핑

자바 ORM 표준 JPA 프로그래밍 6장을 요약한 내용 입니다.

엔티티의 연관관계를 매핑할 때는 다음 3가지를 고려해야 한다.

  • 다중성

    • 다대일(@ManyToOne)

    • 일대다(@OneToMany)

    • 일대일(@OneToOne)

    • 다대다(@ManyToMany)

  • 단방향, 양방향

    • 테이블은 외래 키 하나로 조인을 사용해서 양방향으로 쿼리가 가능하므로 사실상 방향이라는 개념이 없다.

    • 반연에 객체참조용 필드를 가지고 있는 객체만 연관된 객체를 조회할 수 있다.

  • 연관관계의 주인

    • JPA는 두 객체 연관관계 중 하나를 정해서 데이터 베이스 외래 키를 관리하는데 이것을 연간관계의 주인이라 한다. 주인이 아닌 방향은 외래 키를 변경할 수 없고 읽기만 가능하다.

    • 연관관계의 주인이 아니면 mappedBy 속성을 사용하고 연관관계의 주인 필드 이름을 값으로 입력해야 한다.

다대일

객체 양방향 관계에서 연관관계의 주인은 항상 다 쪽이다. 예를 들어 회원(N)과 팀(1)이 있으면 회원 쪽이 연관관계의 주인이다.

다대일 단방향 [N:1]

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

  • 양방향은 외래 키가 있는 쪽이 연관관계의 주인이다.

    일대다다대일 연관 관계는 항상 다(N)에 외래 키가 있다. 여기서는 다 쪽인 MEMBER 테이블이 외래 키를 가지고 있으므로 Member.team이 연관관계의 주인이다. JPA는 외래 키를 관리할 때 연관관계의 주인만 사용한다.

  • 양방향 연관관계는 항상 서로를 참조해야 한다.

    항상 서로 참조하게 하려면 연관관계 편의 메소드를 작성하는 것이 좋은데 회원의 setTeam(), 팀의 addMember() 메소드가 이런 편의 메소드들이다.

    @Entity
    public class Member {
    	@Id @GeneratedValue
    	@Column(name = "MEMBER_ID")
    	private Long id;
    
    	...
    
    	public void setTeam(Team team) {
    		this.team = team;
    		
    		//무한루프에 빠지지 않도록 체크
    		if(!team.getMembers().contains(this)) {
    			team.getMembers().add(this);
    		}
    	}
    }
    @Entity
    public class Team {
    	...
    
    	public void addMember(Member member) {
    		this.members.add(member);
    		
    		//무한루프에 빠지지 않도록 체크
    		if (member.getTeam() != this) {
    			member.setTeam(this);
    		}
    	}
    }

일대다

일대다 관계는 엔티티를 하나 이상 참조할 수 있으므로 자바 컬렉션인 Collection, List, Set, Map 중에 하나를 사용해야 한다.

Q1. 다양한 컬렉션 구현체중에 어느것을 사용하는게 적절한가?

  • List는 순서를 가지고 있으며 중복이 허용된다. 또한 @Orderby 어노테이션을 이용하여 정렬이 가능하다. List에 add할 경우 기존 테이블을 날리고 insert하는 쿼리가 발생한다.

  • Set은 순서가 없으며 중복을 허용하지 않는다. @Orderby 어노테이션을 이용하여 정렬을 하여도 해쉬값에 의해 정렬되므로 원하는대로 정렬이 되지 않을 수 있다.

  • List를 사용할 경우 순서가 보장되지 않기 때문에 조인 테이블 전략을 사용할 경우 데이터를 삭제하고 다시 삽입하는 쿼리가 발생할 수 있다.

    • 전제조건

      • @OneToMany에서 @JoinTable 전략을 설정한다.

      • @OneToMany의 컬렉션을 List로 설정한다.

보통 자신이 매핑한 테이블의 외래 키를 관리하는데, 이 매핑은 반대쪽 테이블에 있는 외래 키를 관리한다.

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

일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 한다. 그렇지 않으면 JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블(JoinTable) 전략을 기본으로 사용해서 매핑한다.

Q2. 조인 테이블전략이란? @JoinColumn 어노테이션이 없을 경우에 A table 과 B table이 연결된 매핑 테이블이 생성된다. 업데이트 로직 삭제 로직에 매핑 테이블에 갯수 만큼 반영된다. @OneToMany 어노테이션에만 적용된다. @ManyToOne에서는 @JoinColumn을 선언하지 않아도 조인 테이블이 생성되지 않는다.

  • 일대다 단방향 매핑의 단점

    일대다 단방향 매핑의 단점은 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점이다. 본인 테이블에 외래 키가 있으면 엔티티의 저장연관 관계 처리INSERT SQL 한 번으로 끝낼 수 있지만, 다른 테이블에 외래 키가 있으면 연관 관계 처리를 위한 UPDATE SQL을 추가로 실행해야 한다.

    public void testSave() {
    	Member member1 = new Member("member1");
    	Member member2 = new Member("member2");
    
    	Team teaml = new Team("teaml");
    	teaml.getMembers().add(memberl);
    	teaml.getMembers().add(member2);
    
    	em.persist(memberl); // INSERT-memberl
    	em.persist(member2); // INSERT-member2
    	em.persist(teaml);   // INSERT-teaml, UPDATE-memberl.fk,
    										   // UPDATE-meinber2. fk
    	transaction.commit();
    }

Q3. @JoinColumn 과 mappedBy 속성을 같이 사용하면 에러가 발생한다. 왜 그럴까? "Associations marked as mappedBy must not define database mappings like @JoinTable or @JoinColumn" 와 같은 에러가 발생한다. 그 이유는 mappedBy 옵션과 @JoinTable 이 같은 전략을 사용하기 때문에 충돌이 발생하기 때문이다. mappedBy를 사용해도 @JoinColumn 전략을 사용한다.

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

다대일 관계는 항상 다 쪽에 외래 키가 있다. 이런 이유로 @ManyToOne에는 mappedBy 속성이 없다. 일대다 양방향 매핑이 완전히 불가능한 것은 아니다. 일대다 단방향 매핑 반대편에 같은 외래 키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 하나 추가하면 된다.

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

	private String username;

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

	//Getter, Setter ...
}

둘 다 같은 키를 관리하므로 문제가 발생할 수 있기 때문에 반대편인 다대일 쪽은 insetable,updatable 설정으로 읽기만 가능하게 했다. 이 방법은 일대다 양방향 매핑 이라기보다는 일대다 단방향 매핑 반대편을 읽기 전용으로 추가해서 일대다 앙방향처럼 보이도록 하는 방법이다.

일대일 [1:1]

일대일 관계는 주 테이블이나 대상 테이블 중에 누가 외래 키를 가질지 선택해야 한다.

  • 주 테이블에 외래 키

    주 객체가 대상 객체를 참조하는 것처럼 주 테이블외래 키를 두고 대상 테이블을 참조한다.

    이 방법의 장점은 주 테이블이 외래 키를 가지고 있으므로 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다.

  • 대상 테이블에 외래 키

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

주 테이블에 외래 키

단방향

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

  private String username;

  @0neTo0ne
  @JoinColunm (name = "LOCKER_ID")
  private Locker locker;

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

  private String name;
  
  ...
}

양방향

@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 테이블이 외래 키를 가지고 있으므로 Member 엔티티에 있는 Member.locker가 연관관계의 주인이다. 따라서 반대 매핑인 사물함의 Locker.membermappedby를 선언해서 연관관계의 주인이 아니라고 설정했다.

대상 테이블에 외래 키

단방향

일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다. 이때는 단방향 관계를 Locker에서 Member 방향으로 수정하거나 양방향 관계로 만들고 Locker를 연관관계의 주인으로 설정해야 한다.

양방향

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

관계형 데이터베이스는 정규화된 테이블 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<Product>();
  ...

}

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

  private String name;
  
  ...
}
  • @JoinTable.name : 연결 테이블을 지정한다.

  • @JoinTable.joinColumns : 현재 방향인 회원과 매핑할 조인 컬럼 정보를 지정한다.

  • @JoinTable.inverseJoinColums : 반대 방향인 상품과 매핑할 조인 컬럼 정보를 지정한다.

public void save() {
  Product productA = new Product();
  productA.setld("productA");
  productA.setName("상품A");
  em.persist(productA) ;

  Member member1 = new Member();
  member1.setld("member1");
  member1.setUsername("회원1");
  member1.getProducts().add(productA) //연관관계 설정
  em.persist(member1);
}

다대다: 양방향

다대다 매핑이므로 역방향@ManyToMany를 사용한다. 그리고 양쪽 중 원하는 곳에 mappedBy로 연관관계 주인을 지정한다.

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

  private String name;
  
  @ManyToMany(mappedBy = "products") // 역방향 추가
	private List<Member> members;
	...
}
public void findinverse () {
  Product product = em.find(Product.class, "productA");
  List<Member> members = product.getMembers();
  for (Member member : members) {
	  System.out.printin("member = " + member.getUsername ());
	}
}

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

@ManytoMany를 사용하면 연결 테이블을 자동으로 처리해 주므로 도메인 모델이 단순 해지고 여러 가지로 편리하다. 하지만 이 매핑을 실무에서 사용하기에는 한계가 있다.

연결 테이블에 주문 수량주문 날짜 컬럼을 추가했다. 이렇게 컬럼을 추가하면 더는 @ManyToMany를 사용할 수 없다. 왜냐하면 주문 엔티티나 상품 엔티티에는 추가한 컬럼 들을 매핑할 수 없기 때문이다.

결국 연결 테이블을 매핑하는 연결 엔티티를 만들고 이곳에 추가한 컬럼들을 매핑해야 한다.

@Entity
public class Member {
  @Id @Column(name = "MEMBER_ID")
  private String 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와 연결

  private int orderAmount;
}

public class MemberProductId implements Serializable {
  private String member; //MemerProduct.member 연결
  private String product; //MemberProduct.product 연결

  //hashCode and equals
  @Override
  public boolean equals(Object o) {...}

  @Override
  public int hashCode() {...}
  
}

회원상품(MemberProduct) 엔티티를 보면 기본 키를 매핑하는 @Id와 외래 키를 매핑하는 @JoinColumn을 동시에 사용해서 기본 키 + 외래 키를 한번에 매핑했다. 그리고 @IdClass를 사용해서 복합 기본 키를 매핑했다.

복합 기본 키란? JPA에서 복합 키를 사용하려면 별도의 식별자 클래스를 만들어야 한다. 그리고 엔티티에 @IdClass를 사용해서 식별자 클래스를 지정하면 된다.

복합 키를 위한 식별자 클래스 특징

  • 복합 키는 별도의 식별자 클래스로 만들어야 한다.

  • Serializable을 구현해야 한다.

  • equals와 hashCode 메소드를 구현해야 한다.

  • 기본 생성자가 있어야 한다.

  • 식별자 클래스는 public이어야 한다.

  • @IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있다.

저장 코드

public void save() {
  //회원 저장
  Member member1 = new Member();
  member1.setld("member1");
  member1.setUsername("회원 1");
  em.persist(memberl);

  //상품 저장
  Product productA = new Product();
  productA.setld("productA");
  productA.setName(”상품 1");
  em.persist(productA);

  //회원상품 저장
  MemberProduct memberProduct = new MemberProduct();
  memberProduct.setMember(memberl); //주문 회원 - 연관관계 설정
  memberProduct.setProduct(productA); //주문 상품 - 연관관계 설정
  memberProduct.setOrderAmount(2); //주문 수량
  em.persist(memberProduct);
}

조회 코드

public void find () {
  //기본 키 값 생성
  MemberProductld memberProductId = new MemberProductId();
  memberProductld.setMember("member1");
  memberProductld.setProduct("productA");

  MemberProduct memberProduct = em.find(MemberProduct.class,memberProductld);
  Member member = memberProduct.getMember();
  Product product = memberProduct.getProduct();

  System.out.println("member = " + member.getUsername());
  System.out.println("product = " + product.getName());
  System.out.printIn("orderAmount = " + memberProduct.getOrderAmount());
}

다대다 연관관계 정리

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

  • 식별 관계 : 받아온 식별자를 기본 키 + 외래 키로 사용한다.

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

객체 입장에서 보면 비식별 관계를 사용하는 것이 복합 키를 위한 식별자 클래스를 만들지 않아도 되므로 단순하고 편리하게 ORM 매핑을 할 수 있다.

참고

JPA OneToMany : List vs Set

JPA list or set 차이

JPA 컬렉션 - 머루의개발블로그

일대다 단방향 매핑 잘못하면 생기는

Last updated