5 ~ 7 : 연관관계 매핑 기초, 다양한 연관관계 매핑, 고급 매핑

5장 : 연관관계 매핑 기초

4장에서 객체와 테이블 매핑, 기본 키 매핑, 필드와 컬럼 매핑에 대해 알아보았고 앞으오 5,6,7장에서는 연관관계 매핑에 대해 알아본다.

엔티티들은 대부분 다른 엔티티와 연관관계가 있다. 객체의 참조와 테이블이 외래 키를 매핑하는 것이 이 장의 목표다.

핵심 키워드

  • 방향(Direction) : 방향은 객체관계에만 존재하고 테이블 관계는 항상 양방향이다.

  • 다중성(Multiplicity) : [다대일, 일대다, 일대일, 다대다] 다중성이 있다.

  • 연관관계의 주인(owner) : 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.

단방향 연관관계

✔️ 객체 연관관계 vs 테이블 연관관계 정리

객체

  • 객체는 참조로 연관관계를 맺는다.

  • 참조를 사용하는 객체의 연관관계는 단방향이다. 연관된 데이터를 조회할 때 참조(a.getB().getC())를 사용한다. 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.

    • A → B (a, b)

테이블

  • 테이블은 외래 키로 연관관계를 맺는다.

  • 외해 키를 사용하는 테이블의 연관관계는 양방향이다. 연관된 데이터를 조회할 때 조인(JOIN)을 사용한다.

    • A JOIN B가 가능하면 B JOIN A도 가능하다.

순수한 객체 연관관계

public class Memeber {

	private String id;
	private String username;

	private Team team;

	public void setTeam(Team team) {
		this.team = team;
	}	

	// Getter, Setter ...
}

public class Team {

	private String id;
	private String name;

	// Getter, Setter
}
public static void main(String[] args) {

	Member member1 = new Member("member1", "회원");
	Member member2 = new Member("member2", "회원2");

	member1.setTeam(team1);
	member2.setTeam(team1);

	Team findTeam = member1.getTeam();
}

객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라 한다.

테이블 연관관계

CREATE TABLE MEMBER (
	MEMBER_ID VARCHAR(255) NOT NULL, 
	TEAM_ID VARCHAR(255),
	USERNAME VARCHAR(255),
	PRIMARY KEY (MEMBER_ID)
)

CREATE TABLE TEAM (
	TEAM_ID VARCHAR(255) NOT NULL,
	NAME VARCHAR(255),
	PRIMARY KEY (TEAM_ID)
)

ALTER TABLE MEMBER ADD CONSTRAINT FK_MEMBER_TEAM
	FOREIGN KEY (TEAM_ID)
	REFERENCES TEAM
INSERT INTO TEAM(TEAM_ID, NAME) VALUES('team1', '팀1');
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME)
VALUES('member1', 'team1', '회원1');
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME)
VALUES('member2', 'team1', '회원2');
SELECT T.*
FROM MEMBER M
	JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
WHERE M.MEMBER_ID = 'member1'

데이터베이스는 외래 키를 사용해서 연관관계를 탐색할 수 있는데 이것을 조인이라 한다.

객체 관계 매핑

@Entity
public class Member {

	@Id
	@Column(name = "MEMBER_ID")
	private String id;

	private String username;

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

	public void setTeam(Team team) {
		this.team = team;
	}

	// Getter, Setter ...
}
@Entity
public class Team {

	@Id
	@Column(name = "TEAM_ID")
	private String id;

	private String name;

	// Getter, Setter ...
}

@JoinColumn

외래 키를 매핑할 때 사용한다.

@JoinColumn 생략

@JoinColumn을 생략하면 외래 키를 찾을 때 기본 전략을 사용한다.

@ManyToOne

private Team team;

  • 기본 전략 : 필드명 + _ + 참조하는 테이블의 컬럼명

  • ex. 필드명(team) + _(밑줄) + 참조하는 테이블의 컬럼명(TEAM_ID)= team_TEAM_ID 외래 키를 사용한다.

@ManyToOne

다대일 관계에서 사용한다.

@ManyToOne 속성

  • optional : false로 설정하면 연관된 엔티티가 항상 있어야 한다.

    • 기본값 : true

  • fetch : 글로벌 페치 전략을 설정한다.

    • @ManyToOne=FetchType.EAGER

    • @OneToMany=FetchType.LAZY

  • cascade : 영속성 전이 기능을 사용한다.

  • targetEntity : 연관된 엔티티의 타입 정보를 설정한다. (거의 사용 x)

code
@OneToMany
private List<Member> members;

@OneToMany(targetEntity=Member.class)
private List members;
다대일 vs 일대일

다대일과 비슷한 일대일 관계도 있다. 단방향 관계를 매핑할 때 둘 중 어떤 것을 사용해야 할지는 반대편 관계에 달려 있다. 반대편이 일대다 관계면 다대일을 사용하고 반대편이 일대일 관계면 일대일을 사용하면 된다.

연관관계 사용

저장

public void testSave() {

	Team team1 = new Team("team1", "팀1");
	em.persis(team1);

	Member member1 = new Member("member1", "회원1");
	member1.setTeam(team1);
	em.persist(member1);

	Member member2 = new Member("member2", "회원2");
	member2.setTeam(team1);
	em.persist(member2);
}

조회

연관관계가 있는 엔티티를 조회하는 2가지 방법

1 . 객체 그래프 탐색 (객체 연관관계를 사용한 조회)

객체를 통해 연관된 엔티티를 조회하는 것을 객체 그래프 탐색이라 한다.

Member member = em.find(Member.class, “member1”);

Team team = member.getTeam();

System.out.println(”팀 이름 =+ team.getName());

2 . 객체지향 쿼리 사용 JPQL

private static void queryLogicJoin(EntityManager em) {
	
	String jpql = "select m from Member m join m.tea t where " + "t.name=:teamName";

	List<Member> resultList = em.createQuery(jpql, Member.class)
		.setParameter("teamName", "팀1");
		.getResultList();

	for (Member member : resultList) {
		System.out.println("[query] member.username=" + member.getUsername());
	}
}
//[query] member.username = 회원1
//[query] member.username = 회원2
  • :로 시작하는 것은 파라미터를 바인딩받는 문법이다.

  • 실행된 SQL과 JPQL을 비교하면 JPQL은 객체(엔티티)를 대상으로 하고 SQL보다 간결하다.

수정

private static void updateRelation(EntityManager em) {

	Team team2 = new Team("team2", "팀2");
	em.persist(team2);

	Member member = em.find(Member.class, "member1");
	member.setTeam(team2);
}

연관관계 제거

private static void deleteRelation(EntityManager em) {

	Member member1 = em.find(Member.class, "member1");
	member1.setTeam(null);
}

연관된 엔티티 삭제

연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다.

member1.setTeam(null);
member2.setTeam(null);
em.remove(team);

양방향 연관관계

지금까지 회원에서 팀으로만 접근하는 다대일 단방향 매핑을 알아보았다. 이번에는 회원에서 팀으로 접근하고 반대 방향인 팀에서도 회원으로 접근할 수 있도록 양방향 연관관계로 매핑해보자.

일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다.

JPA는 List를 포함해서 Collection, Set, Map 같은 다양한 컬렉션을 지원한다.

양방향 연관관계 매핑

@Entity
public class Member {

	@Id
	@Column(name = "MEMBER_ID")
	private String id;

	private String username;

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

	public void setTeam(Team team) {
		this.team = team;
	}

	// Getter, Setter ...
}
@Entity 
public class Team {

	@Id
	@Coulmn(name = "TEAM_ID")
	private String id;

	private String name;

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

	// Getter, Setter ...
}

일대다 컬렉션 조회

public void biDirection() {
	
	Team team = em.find(Team.class, "team1");
	List<Member> members = team.getMembers();

	for (Member member : members) {
		System.out.println("member.username = " + member.getUsername());
	}
}

//member.username = 회원1
//member.username = 회원2

연관관계의 주인

mappedBy는 왜 필요할까 ?

엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나라는 부분에서 둘의 차이가 발생한다. 이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 하는데 이것을 연관관계의 주인이라 한다.

양방향 매핑의 규칙 : 연관관계의 주인

양방향 연관관계 매핑 시 두 연관관계 중 하나를 반드시 연관관계의 주인으로 정해야 하며, 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 주인이 아닌 쪽은 읽기만 할 수 있다.

  • 주인은 mappedBy 속성을 사용하지 않는다.

  • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.

연관관계의 주인은 외래 키가 있는 곳

  • 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.

  • 연관관계의 주인만 데이터베이스 연관관계와 매핑되고 외래 키를 관리 할 수 있다. 주인이 아닌 반대편은 읽기만 가능하고 외래 키를 변경하지 못한다.

양방향 연관관계의 주의점

  • 양방향 연관관계를 설정하고 나면 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 실수를 조심해야 한다.

순수한 객체까지 고려한 양방향 연관관계

  • 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다. 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.

  • 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주자

연관관계 편의 메소드

아래처럼 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드라 한다.

public class Member {

	private Team team;

	public void setTeam(Team team){
		this.team = team;
		team.getMembers().add(this);
	}
}

연관관계 편의 메소드 작성 시 주의사항

  • 연관관계를 변경할 때 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 한다.

정리

단방향과 비교해서 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐이다.

양방향 매핑 시에는 무한 루프에 빠지지 않게 조심해야 한다.

일대다를 연관관계의 주인으로 선택하는 것도 가능하지만, 성능과 관리 측면에서 권장되지 않는다.

대다를 연관관계의 주인으로 선택하는 것도 가능하지만, 성능과 관리 측면에서 권장되지 않는다.

6장 : 다양한 연관관계 매핑

다대일

다대일 단방향 [N:1]

@Entity
public class Member {

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

	private String username;

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

	// Getter, Setter ...

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

	private String name;

	// Getter, Setter
}

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

@Entity
public class Memeber {
	
	@Id @GeneratedValue
	@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;

		if(!team.getMembers().contains(this)) {
			team.getMembers().add(this);
		}
	}
}
@Entity
public class Team {
	
	@Id @GeneratedValue
	@Column(name = "TEAM_ID")
	private Long id;

	private String name;

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

	public void addMember(Member member) {
		this.members.add(member);
		if(member.getTeam() != this) {
			member.setTeam(this);
		}
	}
}

일대다

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

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

  • 일대다 단방향 매핑, 일대다 양방향 매핑보다는 다대일 양방향 매핑을 사용하자

일대일

주 테이블에 외래 키

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

✔️ 단방향

다대일 단방향과 거의 비슷하다.

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

✔️ 양방향

반대 매핑인 사물함의 Locker.member는 mappedBy를 선언해서 연관관계의 주인이 아니라고 설정했다.

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

대상 테이블에 외래 키

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

✔️ 단방향

  • 지원하지 않음 (그림)

✔️ 양방향

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

프록시를 사용할 때 외래 키를 직접 관리하지 않는 일대일 관계는 지연 로딩으로 설정해도 즉시 로딩된다.

다대다

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 보통 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.

다대다: 단방향

@Entity
public class Member {

	@Id @Column(name = "MEMBER_ID")
	private Long 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 Long id;

	private String name;

	...
}

탐색

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());
	}
}

다대다: 양방향

@Entity
public class Product {
	
	@Id
	private String id;

	@ManyToMany(mappedBy = "products")
	private List<Member> members;
	...
}
public void findInvers() {

	Product product = em.find(Product.class, "productA");
	List<Member> members = product.getMembers();
	for (Member member : members) {
		System.out.println("member = " + member.getUsername());
	}
}

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

@Entity
public class Member {

	@Id @Column(name = "MEMBER_ID")
	private Long id;

	@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts;

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

	private String name;
	...
}
  • 상품 엔티티에서 회원상품 엔티티로 객체 그래프 탐색 기능이 필요하지 않다고 판단해서 연관관계를 만들지 않았다.

회원상품 엔티티 코드

@Entity
@IdClass(MemberProducteId.class)
public class MemberProduct {

	@Id
	@ManyToOne
	@JoinColumn(name = "MEMBER_ID")
	private Member member;

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

	private int orderAmount;
	...
}

회원상품 식별자 클래스

public class MemberProductId implements Serializable {

	private String member;
	private String product;

	// hashCode and equals

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

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

복합 기본 키

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

  • Serializable을 구현해야 한다.

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

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

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

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

식별 관계

  • 부모 테이블의 기본 키를 받아서 자신의 기본 키 + 외래 키로 사용하는 것을 데이터베이스 용어로 식별 관계라 한다.

다대다: 새로운 기본 키 사용

저자는 기본 키 생성 전략으로 데이터베이스에서 자동으로 생성해주는 대리 키를 Long 값으로 사용할 것을 추천한다. 이 방법은 간편하고 거의 영구적으로 쓸 수 있으며 비즈니스에 의존하지 않는다.

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

}

다대다 연관관계 정리

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

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

7장 : 고급 매핑

상속 관계 매핑

조인 전략

단일 테이블 전략

구현 클래스마다 테이블 전략

@MappedSuperclass

복합 키와 식별 관계 매핑

식별 관계 vs 비식별 관계

복합 키 : 비식별 관계 매핑

복합 키 : 식별 관계 매핑

비식별 관계로 구현

일대일 식별 관계

식별, 비식별 관계의 장단점

조인 테이블

일대일 조인 테이블

일대다 조인 테이블

다대일 조인 테이블

다대다 조인 테이블

엔티티 하나에 여러 테이블 매핑

Last updated