엔티티의 연관관계 매핑 시에는 아래 3가지를 고려하게된다.
두 엔티티가 일대일 관계인지 일대단 관계인지 다중성을 고려하여야한다. 단방향일 때는 괜찮지만, 양방향일 때는 연관관계의 주인을 정해야한다.
@ManyToOne)@OneToMany)@OneToOne)@ManyToMany)테이블은 외래 키 하나로 조인을 사용해 양방향으로 쿼리가 가능하기에 방향이라는 개념이 없다. 다만, 객체는 참조 필드를 가진 객체만 연관 객체를 조회할 수 있다.
데이터베이스에서는 외래 키 하나로 두 테이블이 연관관계를 맺는다. 이때 외래 키는 하나이다. 데이터베이스에 맞춰 객체를 양방향으로 매핑하는 경우에는 다만, 이 외래 키를 관리하는 곳이 2곳이 되어버린다.
이 관리를 위해 연관관계의 주인을 설정해야한다. 대부분은 외래 키를 가지고 있는 테이블의 엔티티가 관계의 주인이된다.
다대일의 관계의 반대는 항상 일대다이다. 데이터베이스에서 테이블의 일(1), 다(N) 관계에서 외래 키는 항상 다(N)쪽에 있기에 다쪽이 연관관계의 주인이다.
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
}
회원은 Member.team으로 팀 엔티티를 참조할 수 있지만, 그 반대로 팀에서는 회원을 참조하는 필드가 없는 위의 상황을 다대일 단방향 관계라고 한다.
@ManyToOne 어노테이션과 함께 @JoinColumn (name = "TEAM_ID")를 사용해 Member.team 필드를 TEAM_ID 외래 키와 매핑한다. 그렇기에 회원의 필드로 회원 테이블의 외래키를 관리하게된다.

다대일 양방향의 객체 연관관계에서 실선이 연관관계의 주인이고, 점선은 연관관계의 주인이 아니다.
양방향은 외래 키가 있는 쪽이 연관관계의 주인
일대다와 다대일 연관관계에서는 항상 다(N)에 외래키가 존재. JPA는 외래 키를 관리할 때 연관관계의 주인만 사용하며, 주인이 아닌 엔티티의 필드는 조회를 위한 JPQL 또는 객체 그래프 탐색을 위해서 사용.
양방향 연관관계는 항상 서로를 참조
양방향 연관관계는 항상 서로를 참조해야하는데, 한 쪽만 참조하는 경우에는 이 연관관계가 성립하지 않는다. 항상 서로를 참조하게 하기 위해서는 연관관계 편의 메소드를 작성하는 것이 좋다. setTeam() 또는 addMembers() 같은 메소드가 이런 편의 메소드이다. 편의 메소드는 양쪽에 다 작성하는 경우 무한루프에 빠질 수 있기에 주의해야한다.
일대다 관계는 다대일 관계의 반대 방향이다. 다만, 일대다 관계는 엔티티를 하나 이상 참조할 수 있기에 자바 컬렉션 중 하나를 사용해야한다.
하나의 팀이 여러 회원을 참조할 수 있는 이런 관계를 일대다 관계라한다. 만약 팀은 회원들은 참조하지만, 회원이 팀을 참조하지 않으면 이를 두고 일대다 단방향이라한다.

일대다 단방향의 경우에는 팀 엔티티의 Team.members를 통해 회원 테이블의 TEAM_ID 외래 키를 관리하게 된다. 보통은 자신이 매핑한 테이블의 외래 키를 관리하는데 여기서는 반대쪽 테이블의 외래 키를 관리하게 된다. 이는 특이한 양상이다.
@Entity
public clas Team{
@Id @GeneratedValue
@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<>();
}
일대다 단방향 관계의 매핑을 위해서는 @JoinColumn을 명시해야한다. 그렇지 않으면 JPA는 연결 테이블을 중간에 두고, 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용해 매핑한다.
매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점
자신이 관리하는 테이블에 외래 키가 있으면, 엔티티의 저장과 연관관계 처리를 INSERT SQL 한 번으로 처리할 수 있는 것과 다르게 다른 테이블에 있다면, UPDATE SQL을 추가로 실행해야함.
예를 들면 Member 엔티티는 Team 엔티티를 모르고, 연관관계 정보는 Team 에서 관리하기에 Member 를 저장할 때에는 TEAM_ID에 아무 값도 저장되지 않고 Team 엔티티를 저장할 때, 참조값을 확인해 회원 테이블의 TEAM_ID 외래 키를 업데이트한다.
위와 같은 단점이 있기에 일대일 단방향 매핑보다는 다대일 양방향 매핑을 사용하는 것이 좋다.
일대다 양방향 매핑은 존재하지 않기에 다대일 양방향 매핑을 사용해야한다. 정확하게는 @OneToMany는 양방향 매핑에서 연관관계의 주인일 수 없다.
@ManyToOne - @OneToMany 둘 중에 연관관계의 주인은 항상 다 쪽인 @ManyToOne을 사용하는 곳이다. 따라서 mappedBy 속성은 @ManyToOne에만 존재한다.
일대다 양방향 매핑이 완전히 불가능한 것은 아니고, 일대다 단방향 매핑 반대편에 같은 외래 키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 추가하면 된다.

@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn (name = "TEAM_ID")
private List<Member> members = new ArrayList<Member>();
}
@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;
}
일대다 단방향 매핑의 반대편에 다대일 단방향 매핑을 (멤버쪽에) 추가한다. 이때 일대다 단방향 매핑과 같이 외래 키 컬럼을 매핑하는 대신 둘 다 같은 키를 관리하는 문제를 방지하고자 다대일 쪽에는 insertable=false와 updatable = false로 설정에 읽기만 가능하게 설정한다.
하지만 이 경우 일대다 단방향 매핑이 가지는 단점을 그대로 가지기에 다대일 양방향 매핑을 사용하는게 좋다.
일대일 관계는 양쪽이 서로 하나의 관계만 가짐.
객체지향 개발자들이 주로 해당 방식을 선호하며, JPA에서 역시 주 테이블에 외래 키가 있으면 좀 더 편리하게 매핑할 수 있음.
회원과 사물함을 예로 뒀을 때.

@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: 일대일 관계이기에 객체 매핑에 해당 어노테이션 사용. LOCKER_ID: 외래 키에 유니크 제약 조건(UNI) 추가
단방향일 때와 나머지는 동일하고, 매핑이 없던 Locker 엔티티 내에 매핑을 해주고, 양방향 관계일 때는 관계의 주인을 설정해줘야하기에 Locker 엔티티에 mappedBy를 설정해줘, 주인이 아님을 명시한다.
@Entity
public class Locker {
...
@OneToOne (mappedBy = "locker")
private Member member;
}
JPA에서는 아래처럼 일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 지원하지 않는다. 단방향 관계를 Locker -> Member 방향으로 수정하거나, 양방향 관계로 만들어 Locker를 연관관계의 주인으로 설정해야한다.
JPA 2.0부터는 일대다 단방향 관계에서 대상 테이블에 외래 키가 있는 매핑을 허용했지만, 일대일 단방향은 허용되지 않는다.


@Entity
public class Member {
@Id @GeneratedValue
@Column (name = "MEMEBER_ID")
private Long id;
private String username;
@OneToOne (mappedBy = "member")
private Locker locker;
}
public class Locker {
@Id @GeneratedValue
@Column (name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne
@JoinColumn (name = "MEMBER_ID")
private Member member;
}
일대일 매핑에서 대상 테이블에 외래 키를 두고 싶을 때는 이렇게 양방향으로 매핑해야한다. 주 엔티티인 Member 엔티티 대신, 대상 엔티티인 Locker 를 연관관계의 주인으로 만들어 LOCKER 테이블의 외래 키를 관리하도록 한다.
관계형 데이터베이스에서 정규화된 테이블 2개로 다대다 관계를 표현할 수는 없다. 따라서 중간에 연결 테이블을 추가해줘야한다.
- 회원과 상품 사이의 관계
- 둘은 한 회원이 여러 상품과 관계가 있을 수 있음
- 한 상품이 여러 회원과 관계가 있을 수 있음

하지만 또 객체는 테이블과 다르게 객체 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 @Colun (name = "PRODUCT_ID")
private String id;
private String name;
}
@ManyToMany: 회원 엔티티와 상품 엔티티를 매핑.
@JoinTable: @ManyToMany와 함께 사용해서 연결 테이블을 바로 매핑하여, 회원과 상품을 연결하는 회원_상품 엔티티 없이 매핑.
@JoinTable의 속성
@JoinTable.name: 연결 테이블을 지정.@JoinTable.joinColumns: 현재 방향인 회원과 매핑할 조인 컬럼 정보 지정. 여기서는 MEMBER_IDJoinTable.inverseJoinColumns: 반대 방향인 상품과 매핑할 조인 컬럼 정보 지정. 여기서는 PRODUCT_ID.
public void save() {
Product productA = new Product();
productA.setId("productA");
productA.setName("상품A");
em.persist(productA); //영속 상태 등록
Member member1 = new Member();
member1.setId("member1");
member1.setUsername("회원1");
member1.getProducts().add(productA); //연관관계 설정
em.persist(member1); //영속 상태 등록
}
회원과 상품의 연관관계를 설정했기에 회원1을 저장하면 연결 테이블에도 값이 저장됨.
public void find() {
Member member = em.find(Member.class, "member1");
List<Product> products = member.getProducts(); //객체 그래프 탐색
for (Product product : products) {
System.out.println("product.name =" + product.getName());
}
}
SQL에서는 알아서 조인을 사용해서 연관된 상품들을 조회를 해준다. @ManyToMany를 사용해 매핑해주면 JPA에서 알아서 SQL을 적합하게 만들어낸다.
다대다 매핑이기에 역방향도 @ManyToMany를 사용하고, 연관관계의 주인을 지정하기 위해 mappedBy를 사용해주면된다.
@Entity
public class Product {
@Id
private String id;
@ManyToMany (mappedBy = "products") //역방향 추가
private List<Member> members;
}
다대다 양방향 연관관계는 멤버 -> 상품, 상품 -> 멤버 양쪽에 추가를 해줘야하는데 이를 위해서 연관관계 편의 메소드를 추가해서 관리하는 것이 편하다.
products.add(product);
product.getMembers().add(this);
양방향 연관관계에서 연관관계 편의 메소드를 추가했듯 다대다 양방향에서도 위처럼 한 군데에서 양쪽 객체(엔티티) 모두에 관계를 설정/업데이트 해주는 것이 좋다.
@ManyToMany 사용 시 연결 테이블을 자동으로 처리해주기에 도메인 모델이 단순해지고 여러모로 편리하지만, 실무에서 사용하기에는 한계가 있다고한다.
예로는 회원이 상품을 주문하면 연결 테이블에 주문 회원, 상품 아이디 외에 주문 수량이나 날짜 등의 컬럼이 필요하다.

만약 이렇게 된다면 더 이상 @ManyToMany를 사용할 수 없다. 주문 엔티티나 상품 엔티티에 추가한 컬럼을 매핑할 수 없기 (= 필드의 값으로 가져올 수 있는 방법이 없다)
따라서 연결 테이블에 매핑하는 연결 엔티티를 만들고 추가 컬럼을 해당 엔티티에 매핑해야한다.
@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;
private String name;
}
위의 코드에서는 상품 엔티티 -> 회원상품 엔티티로 탐색 기능이 필요하지 않다고 생각해 연관관계를 두지 않았다.
@Entity
@IdClass (MemberProductId.class)
public class MemberProduct {
@Id
@ManyToOne
@JoinColumn (name = "MEMEBER_ID")
private Member member;
@Id
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
private int order;
}
@Id와 외래 키를 매핑하는 @JoinColumn을 동시에 사용해 기본 키+외래 키를 한 번에 매핑한다.@IdClass를 사용해 복합 기본 키를 매핑복합 기본 키:
회원 상품 엔티티의 기본키는 MEMBER_ID와 PRODUCT_ID 둘로 이루어진 복합 기본 키이다. JPA에서 복합 키를 사용하기 위해서는 별도의 식별자 클래스를 만들고, @IdClass를 사용해 식별자 클래스를 지정해야한다.
Serializable을 구현해야함equals와 hasCode 메소드를 구현해야함@IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법 역시 있다.public class MemberProductId implements Serializable {
private String member; //MemberProductId.member와 연결
private String product; //MemberProduct.product와 연결
@Override
public boolean equals (Object o) {...}
@Override
public int hashCode() {...}
}
회원상품에서 회원과 상품의 기본 키를 받아 자신의 기본 키로 사용하는 경우처럼, 부모 테이블의 기본 키를 받아서 자신의 기본 키 + 외래 키로 사용하는 것을 데이터베이스 용어로 식별 관계라고 한다.
Member member 1 = new Member();
member1.setId("member1");
member1.setUserName("회원1");
em.persist(member1);
Product productA = new Product();
productA.setId("productA");
productA.setName("상품1");
em.persist(productA);
MemberProduct memberProdcut = new MemberProdcut();
memberProduct.setMember(member1); //주문회원 연관관계 설정
memberProduct.setProduct(productA);// 주문상품 연관관계 설정
memberProduct.setOrderAmount(2);
em.persist(memberProduct);
위에서는 회원 상품 엔티티를 만들면서 연관된 회원 엔티티와 상품 엔티티를 설정한다. 회원상품 엔티티는 데이터베이스에 저장될 때 연관된 회원의 식별자와 상품의 식별자를 가져와 자신의 기본 키 값으로 사용.
MemberProduct memberProduct = em.find(MemberProduct.class, memberProductId);
Member member = memberProduct.getMember();
Product product = memberProduct.getProduct();
복합 키는 항상 식별자 클래스를 만들어야한다. 이렇게 생성한 식별자 클래스를 사용해 엔티티를 조회한다.
복합 키를 사용하기 위해 식별자 클래스를 사용하는 것은 복잡하기에 이를 사용하지 않는 전략 역시 존재한다. 데이터베이스에서 자동으로 생성해주는 대리 키를 사용하는 것이다.
장점은 간편하고 영구히 쓸 수 있고, 비즈니스에 의존하지 않는다는 점이다. 또한, ORM 시에 복합 키를 만들지 않아도 되기에 위에 있는 문제를 해결할 수 있다.

위를 보면 ORDER_ID라는 새로운 기본 키를 하나 만들고 MEMBER_ID와 PRODUCT_ID는 외래 키로만 사용한다.
@Entity
public class Order {
@Id @GeneratedValue
@Column (name = "ORDER_ID")
private Long Id;
@ManyToOne
@JoinColumn (name = "PRODUCT_ID")
private Product product;
private int orderAmount;
}
대리키를 사용하기에 이전에서 본 식별 관계와 복합 키를 사요하는 것보다 매핑이 단순하고 쉽다.
//회원과 상품은 이전과 동일
Member member 1 = new Member();
member1.setId("member1");
member1.setUserName("회원1");
em.persist(member1);
Product productA = new Product();
productA.setId("productA");
productA.setName("상품1");
em.persist(productA);
//주문 저장
Order order = new Order();
order.setMember(member1); //주문회원 연관관계 설정
order.setProduct(productA);//주문상품 연관관계 설정
order.setOrderAmount(2); //주문 수량
em.persist(order);
Long orderId = 1L;
Order order = em.find(Order.class, orderId);
Member member = order.getMember();
Product product = order.getProduct();
식별자 클래스를 사용하지 않으면 훨씬 더 단순한 코드를 통해 저장 및 조회가 가능하다.
다대다 관계를 일대다&다대일 관계로 풀기 위해서는 연결 테이블을 만들 때 식별자를 어떻게 구성할지 선택해야한다.
식별 관계: 받아온 식별자를 기본 키 + 외래 키로 사용
비식별 관계: 받아온 식별자는 외래 키로만 사용하고, 새로운 식별자를 추가.