본문 바로가기
Spring/JPA

#5 연관관계 매핑 기본

by 히포파타마스 2021. 8. 27.

연관관계 매핑 기본 

 

엔티티들은 대부분 다른 엔티티와 연관관계가 있다.

그런데 객체는 참조(주소)를 사용해서 관계를 맺고 테이블은 외래 키를 사용해서 관계를 맺는다.

이 둘은 완전히 다른 특징을 갖는다.

 

JPA에서는 객체의 참조와 테이블의 외래 키를 매핑하기 위해서 여러 다양한 방법을 지원한다.

 

 

 

 

1. 단방향 연관관계

Member와 Team이 있다고 하자.

Member가 하나의 Team에만 소속될 수 있다고 한다면 Member와 Team은 다대일 관계라고 볼 수 있다(Member가 '다').

 

[Member와 Team의 관계 - DB]

DB에서 테이블상 관계는 위의 예시처럼 구성될 것이다.

 

DB에서 테이블은 연관관계 방향이 없다.

Member는 TEAM_ID(FK)로, Team도 PK로 서로를 자유롭게 조회할 수 있기 때문이다.

 

그러나 객체는 참조(주소)로 각 객체를 조회한다.

 

그렇다면 어플리케이션에서 Member와 Team은 어떤 식으로 구성되어야 할까?

 

우선 단방향 연관관계로 객체의 연관관계를 구성할 수 있다.

 

[Member와 Team의 관계 - 단방향]

DB의 Member 테이블은 Team의 외래 키를 갖고 있기 때문에 Team을 조회할 수 있었다.

 

이를 매핑하기 위해서 Member 객체가 Team 객체를 필드로 갖는다.

 

테이블에 맞춰, Member가 team.id를 갖는 것보다는 Team의 객체를 갖는 것이 객체의 특성을 해치지 않고 Member에서 Team으로 보다 자연스럽게 조회가 가능하다.

 

[객체의 참조와 테이블의 외래 키를 매핑]

@Entity
public class Member { 
	@Id @GeneratedValue 
	private Long id;

	@Column(name = "USERNAME") 
	private String name;
	private int age;
    
	@ManyToOne
	@JoinColumn(name = "TEAM_ID")
	private Team team;
}

Member 테이블에서 Team을 조회할 수 있는 것처럼 Member 객체도 Team 객체를 필드로 갖기 때문에 Member만으로 Team을 조회할 수 있다.

그리고 Member 객체의 Team이 Member 테이블의 Team의 외래 키와 매핑돼서 외래 키 역할을 한다.

 

● @JoinColumn

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

◎ name 속성에는 매핑할 외래 키 이름을 지정한다.

◎ 생략 가능하다.

 

● @ManyToOne

◎ 다대일 관계라는 매핑 정보이다.

◎ 연관관계를 매핑할 때 다중성을 나타내는 어노테이션으로, 필수로 사용해주어야 한다.

 

위의 예제처럼 매핑하면 다음과 같이 Member를 통해서 Team을 조회할 수 있다.

 

[Member.team 사용]

//팀 저장
Team team = new Team(); 
team.setName("TeamA"); 
em.persist(team);

//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team); //단방향 연관관계 설정, 참조 저장
em.persist(member);

//조회
Member findMember = em.find(Member.class, member.getId());

//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();

find() 메서드로 DB의 member를 조회하면 member를 통해서 해당 member와 연관된 DB에 저장되어있는 team도 조회할 수 있다.

 

단, Team에는 Member와 관련된 필드가 없기 때문에 Team을 통해서 Member를 조회하려면 JPQL 등을 사용해 SQL을 적용해야 한다.

 

[Team에서 Member 조회]

EntityManager.createQuery("select m from Member m join m.team t " +
		"where t.name=:teamName")
		.setParameter("teamName", "팀1")
		.getResultList();

팀 이름이 "팀1"인 팀에 속한 모든 Member를 조회하려면 위 예시와 같이 JPQL을 사용해서 조회할 수 있다.

 

 

 

 

2. 양방향 매핑

앞의 단방향 매핑에서는 Member 객체에서 Team은 쉽게 조회할 수 있었다.

그러나 Team에서 Member 객체를 직접 조회할 수는 없고 JPQL 등을 사용하였다.

 

양방향 매핑은 Team에도 Member와 관련된 필드를 생성해서 Team에서 Member를 쉽게 조회할 수 있게 한다.

 

[양방향 객체 연관관계]

Team은 Member와 일대다 관계이기 때문에 여러 Member를 가질 수 있다.

때문에 List로 member에 관련된 필드를 추가하였다.

 

양방향 관계에 의해 새로 추가된 members는 다음과 같이 매핑한다.

 

[양방향 관계 매핑]

@Entity
public class Team {

	@Id @GeneratedValue
	private Long id;
	private String name;

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

@OneToMany는 @ManyToOne과 같이 연관관계를 나타내는 어노테이션이다.

mappedBy는 연관관계의 주인을 명시한다.

Team은 Member와 연관관계이고 예제에서는 Member를 연관관계의 주인으로 보기 때문에 team(Member의 team)을 적어준다.

 

 

 

■ 연관관계 주인

엔티티를 양방향으로 매핑하면 Member -> Team, Team -> Member 두 곳에서 서로를 참조한다(단 방향 두 개).

양방향에서는 이처럼 객체의 연관관계를 관리하는 포인트가 2곳으로 늘어난다.

그런데 테이블에서는 외래 키 하나로 양 테이블이 연결된다.

두 참조 포인트에서 외래 키를 동시에 관리할 수는 없다.

따라서 양방향 매핑 시 반드시 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 한다.

이때, 외래 키를 관리하는 객체를 연관관계의 주인이라 한다.

 

● 양방향 매핑 시 주인이 아니면 mappedBy 속성으로 주인을 지정해주어야 한다.

● 연관관계의 주인이 아닌 쪽은 읽기만 가능하다.

● 외래 키가 있는 곳을 연관관계의 주인으로 지정하는 것이 권장된다.

만약 외래 키를 갖고 있지 않는 테이블과 매핑된 객체에서 외래 키를 관리하게 되면 물리적으로 다른 테이블의 값을 관리하게 된다.

 

예를 들어 위의 예제에서 Team.members가 외래 키를 관리한다고 하자.

Team.members는 Team에 속해있지만 Member 테이블에 외래 키가 있기 때문에 Team 객체는 Member 테이블의 외래 키를 관리하게 된다.

 

 

양방향 매핑을 하면 mappedBy로 지정된 필드 멤버로 연관된 객체를 조회할 수 있다.

 

[maappedBy로 조회]

Team team = new Team(); 
team.setName("TeamA"); 
em.persist(team);

Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member)

em.flush();
em.clear();

Team findTeam = em.find(Team.class, team.getId());
System.out.println("member 조회 " + findTeam.getMembers().isEmpty());
// "member 조회 = false"

member에 team을 넣고 DB에서 team을 찾아서 members를 조회하면 team에 소속된 member를 조회할 수 있다.

중요한 건 따로 team.getMembers().add(member) 같이 member를 추가하지 않아도 JPA에 의해서 연관된 객체를 조회할 수 있다는 것이다.

 

 

 

■ 양방향 매핑 시 주의점

□ 역방향에만 값 세팅

● 주인이 아닌 연관관계에 값을 세팅해도 DB에 어떤 영향도 줄 수 없다.

● mappedBy로 지정된 필드 멤버는 단순 조회 기능만 가능하다.

 

 

□ 양방향 값 세팅

양방향 연관관계에서 주인에 값을 설정할 때 반대 관계에도 값을 설정해주는 것이 좋다.

 

만약 한 트랜잭션 내에서 연관관계에 있는 엔티티를 생성하고 persist()로 저장한 뒤, 해당 엔티티 중 주인이 아닌 쪽을 find()로 조회하면 어떻게 될까?

persist()로 저장되면 1차 캐시에 저장된 객체 상태를 반환해주기 때문에 아무것도 조회되지 않는다.

예를 들어 Member와 Team을 생성하고 Member에 Team을 저장한 뒤 persist()로 저장하고, find()로 Team을 찾아서 Team.members를 조회해도 아무것도 조회되지 않는다.

 

또한 JPA 없이 테스트할 경우에도 위와 같은 경우에는 해당 필드에 값을 세팅하지 않았기 때문에 어떠한 값도 조회되지 않는다.

 

이 때문에 양방향 매핑 시 다음과 같이 연관관계 편의 메소드를 만들어서 한쪽 값만 세팅해도 양쪽 모두 값이 세팅되도록 하는 것이 좋다.

 

[연관관계 편의 메서드]

@Entity
public class Member { 
	@Id @GeneratedValue 
	private Long id;

	@Column(name = "USERNAME") 
	private String name;
	private int age;
    
	@ManyToOne
	@JoinColumn(name = "TEAM_ID")
	private Team team;
    
    // 연관관계 편의 메서드 추가
	public void changeTeam(Team team){
		this.team = team;
		team.getMembers().add(member);
    }
}

changeTeam 이라는 연관관계 편의 메서드를 추가해서 연관관계에 있는 값을 세팅할 때 반대 연관관계에도 동시에 값을 세팅해준다.

 

 

□ 무한 루프

양방향 연관관계에 있는 엔티티를 사용할 때 toString()과 같은 메서드를 사용하면 무한루프가 발생할 수 있다.

ex) member.toString() -> member내의 team에 toString() 호출 -> team 내의 members에 toString() 호출 -> 무한 루프

 

lombok, JSON 생성 라이브러리 등에서도 해당 문제가 발생하기 쉽다.

 

 

 

■ 양방향 매핑 정리

● 단방향 매핑만으로도 이미 연관관계 매핑은 완료

◎ 양방향 매핑은 반대 방향으로 조회 기능이 추가된 것뿐이다.

 

● 초기 설계는 단방향 매핑만으로 끝내고 추후에 필요할 경우 양방향을 추가하는 방식이 권장된다.

◎ JPQL을 사용해서 역방향으로 탐색할 일이 많을 경우 상황을 고려해서 양방향을 추가할 수 있다.

◎ 단방향에서 양방향 매핑으로 확장했을 때 테이블에 아무 변화도 일어나지 않는다.

 

'Spring > JPA' 카테고리의 다른 글

#7 고급 매핑  (0) 2021.08.28
#6 다양한 연관관계 매핑  (0) 2021.08.28
#4 엔티티 매핑  (0) 2021.08.27
#3 영속성 관리  (0) 2021.08.25
#2 JPA 시작  (0) 2021.08.25

댓글