스프링부트 너 뭐 돼?🤷‍♀️(8) - 연관관계

joyfulwave·2022년 12월 7일
0

피할 수 없다면 즐기자! 스프링부트 너.. 뭐 돼?




📚 연관관계 매핑

  • 객체와 테이블 연관관계의 차이를 이해
  • 객체의 참조와 테이블의 외래키를 매핑
  • 연관관계 주인(Owner)
  • 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾지만 객체는 참조를 사용해서 연관된 객체를 찾아요. 테이블과 객체 사이에는 이런 큰 간격이 있어요.

📌 예제(1)

조건 1) 회원 <-> 팀
조건 2) 회원은 하나의 팀에만 소속될 수 있어요.
조건 3) 회원과 팀은 다대일 관계에요.

  • 객체 참조를 통한 member가 속한 team의 이름을 찾아내기

⚫ Member.java(1)

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter @Setter
public class Member {

	@Id @GeneratedValue
	@Column(name = "MEMBER_ID")
	private Long id;
	
	@Column(name = "USERNAME")
	private String name;
	
	@Column(name = "TEAM_ID")
	private Long teamid;
	
}

⚫ Team.java(1)


import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter @Setter
public class Team {

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

⚫ JpaMain.java (1)


import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import com.koreait.jpaitem.relation.Member;
import com.koreait.jpaitem.relation.Team;

public class JpaMain {

	public static void main(String[] args) {

		EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
		EntityManager em = emf.createEntityManager();
		EntityTransaction tx = em.getTransaction();
		tx.begin();
		
		try {
			
			Team team = new Team();
			team.setName("TeamA");
			// 영속상태가 되면, PK의 값이 세팅이 된 후 
			em.persist(team);
			
			Member member = new Member();
			member.setName("member1");
			member.setTeamid(team.getId());
			em.persist(member);
			
			// select
			// 어느팀 소속인지 알고 싶을 때 jpa or db에게 계속 물어봐야 한다.
			Member findMember = em.find(Member.class, member.getId());
			Long findTeamid = findMember.getTeamid();
			Team findTeam = em.find(Team.class, findTeamid);
			System.out.println("==============================\n\n\n");
			System.out.println("findTeam : " + findTeam.getName());
			System.out.println("\n\n\n==============================");
			tx.commit();
			
		} catch (Exception e) {
			tx.rollback();			
		}finally {
			em.close();
			emf.close();
		}
		
	}

}

📌 예제(2)

  • 예제(1)과 조건은 동일
  • @ManyToOne, @JoinColumn을 사용하여 FK로 연결할 객체를 명시하여 해결하기

⚫ Member.java (2)

 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter @Setter
public class Member {
	
	// 객체 참조로 연관된 객체를 갖는 방법(1)
//		@Id @GeneratedValue
//		@Column(name = "MEMBER_ID")
//		private Long id;
//		@Column(name = "USERNAME")
//		private String name;
//		@Column(name = "TEAM_ID")
//		private Long teamid;
	
	
	// 객체 참조로 연관된 객체를 갖는 방법(2)
	// @ManyToOne : 여기에선 Team이 하나 (많은 것 중에 하나)
	// @JoinColumn(name = "TEAM_ID") :  해당 관계 컬럼을 명시해줘야한다. TEAM_ID와 조인해야 한다.
	@ManyToOne
	@JoinColumn(name = "TEAM_ID")
	private Team team;
	@Id @GeneratedValue
	@Column(name = "MEMBER_ID")
	private Long id;
	@Column(name = "USERNAME")
	private String name;
}

⚫ Team.java (2)

package com.koreait.jpaitem.relation;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter @Setter
public class Team {

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

⚫ JpaMain.java (2)

package com.koreait.jpaitem;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import com.koreait.jpaitem.relation.Member;
import com.koreait.jpaitem.relation.Team;

public class JpaMain {

	public static void main(String[] args) {

		EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
		EntityManager em = emf.createEntityManager();
		EntityTransaction tx = em.getTransaction();
		tx.begin();
		
		try {
						
			Team team = new Team();
			team.setName("TeamA");
			// 영속상태가 되면, PK의 값이 세팅이 된 후 
			em.persist(team);
			
			Member member = new Member();
			member.setName("member1");
			member.setTeam(team);
			em.persist(member);
			
			// 강제 db 쿼리를 보고 싶을 때
			em.flush();
			em.clear();
			
			// select
			// find시에 1차캐시에서 가지고 와서 select문이 없다.
			Member findMember = em.find(Member.class, member.getId());
			Team findTeam = findMember.getTeam();
			
			System.out.println("==============================\n\n\n");
			System.out.println("findTeam : " + findTeam.getName());
			System.out.println("\n\n\n==============================");
			
			
			tx.commit();
			
			
		} catch (Exception e) {
			tx.rollback();			
		}finally {
			em.close();
			emf.close();
		}
	}

}




📚 양방향 매핑

  • 현재 객체는 Member가 Team을 가졌으나, Team은 Member를 가지지 못해요.

  • 객체 참조와 외래키의 가장 큰 차이점
  • 테이블은 FK만 있으면 양쪽에 연관관계를 알 수 있어요.
  • Member -> Team : N -> 1 => @ManyToOne
  • Team -> Member : 1 -> N => @OneToMany

📌 연관관계

⚫ 테이블

  • 관계 1개(양방향)
  • Member 테이블 입장에서 Team 테이블 조인 가능
  • Team 테이블 입장에서 Member 테이블 조인 가능

⚫ 객체

  • 관계 2개(단방향)
  • Member 객체에서 Team 객체로 연관관계 1개(단방향)
  • Team 객체에서 Member 객체로 연관관계 1개(단방향)

📌 관리의 딜레마

  • RDB를 객체화 하는 상황에는 딜레마가 발생해요.

⚫ 딜레마

  • Member에서 Team으로 가는 team 참조 값과, Team에서 Member로 가는 members 참조값이 있어요. Member에서 Team 값이 수정되었을 때 Member table의 TEAM_ID가 수정되야 하는지, Team에 있는 members를 수정했을때 Member table의 TEAM_ID가 수정되야 하는지?에 대한 딜레마가 발생해요.
  • DB 입장에서는 Member table에 있는 TEAM_ID만 update되면 된다. -> 룰(주인)이 생긴다.

📌 연관관계의 주인(Owner) - 양방향 매핑 규칙

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래키를 관리(등록, 수정)
  • 단방향 두개를 양방향으로 칭하고 있지만 등록, 수정등의 일을 담당하는 주인을 JPA에서는 규정해야하고 주인의 반대편은 read only 만 가능해요.
  • 주인이 아닌 쪽은 읽기만 가능
  • 주인은 mappedBy 속성 사용 x
    -> mappedBy : 내가 누군가에 의해서 mapping되었다 라는 뜻이에요.
  • 주인이 아니면 mappedBy 속성으로 주인을 지정해요.
  • DB로 생각했을 때 FK를 거는 자식 테이블이 주인이 된다고 생각하면 돼요.

⚫ 실수 많이 하는 부분

// 양방향 매핑시 가장 많이 하는 실수
// readOnly 만가능하다.
Member member = new Member();
member.setName("member1");
em.persist(member);

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

📌 예제(3)

  • 예제(1)과 조건은 동일
  • 양방향 매핑

⚫ Member.java (3)

package com.koreait.jpaitem.relation;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.SequenceGenerator;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter @Setter
@SequenceGenerator(
		name = "MEMBER_SEQ_GENERATOR",
		sequenceName = "MEMBER_SEQ",
		initialValue = 1, allocationSize =  1)
public class Member {
	
	// 객체 참조로 연관된 객체를 갖는 방법(1)
//		@Id @GeneratedValue
//		@Column(name = "MEMBER_ID")
//		private Long id;
//		@Column(name = "USERNAME")
//		private String name;
//		@Column(name = "TEAM_ID")
//		private Long teamid;
	
	
	// 객체 참조로 연관된 객체를 갖는 방법(2)
	// @ManyToOne : 여기에선 Team이 하나 (많은 것 중에 하나)
	// Member -> Team : N -> 1		=> @ManyToOne
	// @JoinColumn(name = "TEAM_ID") :  해당 관계 컬럼을 명시해줘야한다. TEAM_ID와 조인해야 한다.
	// 외래키가 있는 객체가 주인
	@ManyToOne
	@JoinColumn(name = "TEAM_ID")
	private Team team;
	@Id @GeneratedValue
	@Column(name = "MEMBER_ID")
	private Long id;
	@Column(name = "USERNAME")
	private String name;

	
}

⚫ Team.java (3)

package com.koreait.jpaitem.relation;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter @Setter
@SequenceGenerator(
		name = "TEAM_SEQ_GENERATOR",
		sequenceName = "TEAM_SEQ",
		initialValue = 1, allocationSize =  1)
public class Team {
	
	/*
	 * team에 의해서 관리가 된다.
	 * mappedBy가 적힌 곳은 읽기만 가능하다.
	 * 값을 넣어봐야 아무일도 벌어지지 않는다.
	 * 대신 조희는 가능하다.
	 */
	@OneToMany(mappedBy = "team")
	private List<Member> member = new ArrayList<Member>(); 

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

⚫ JpaMain.java (3)

package com.koreait.jpaitem;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import com.koreait.jpaitem.relation.Member;
import com.koreait.jpaitem.relation.Team;

public class JpaMain2 {

	public static void main(String[] args) {

		EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
		EntityManager em = emf.createEntityManager();
		EntityTransaction tx = em.getTransaction();
		tx.begin();
		
		try {
			
			/*
				// 양방향 매핑시 가장 많이 하는 실수
				// readOnly 만가능하다.
				Member member = new Member();
				member.setName("member1");
				em.persist(member);
				
				Team team = new Team();
				team.setName("TeamA");
				team.getMember().add(member);
				em.persist(team);	 
			 */					
			Team team = new Team();
			team.setName("TeamA");
			em.persist(team);			
			
			Member member = new Member();
			member.setName("member1");
			member.setTeam(team);
			em.persist(member);
			
			// DB 트렌젝션
//			em.flush();
//			em.clear();
			
			// 객체 지향적인 입장에서 양쪽에 모두 값을 넣어주어야한다.
			// 양방향 매핑시에는 양쪽의 값을 모두 입력해 주어야 한다.
			// DB를 다시 다녀오지 않고 객체 상태로만 사용할 수 있다.
			team.getMember().add(member);
			
			System.out.println("==============================");
			
			Team findTeam = em.find(Team.class, team.getId());
			List<Member> members = findTeam.getMember();
			
			for( Member m : members ) {
				System.out.println("m = " + m.getName());
			}
			
			System.out.println("==============================");
			
			
			tx.commit();			
			
		} catch (Exception e) {
			tx.rollback();			
		}finally {
			em.close();
			emf.close();
		}
		
	}

}

📌 양방향 연관관계 주의(1)

  • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하기 위해서 연관관계 편의 메소드를 생성하자

JpaMain2.java 에서

team.getMember().add(member); 

이 코드와 Member.java 의

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

와 같은 역할을 해요.

일반적으로 setter의 형태가 아니면 메서드 이름을 바꿔주는데,

public void changeTeam(Team team) {
	this.team = team;
	// this : 나 자신의 인스터를 넣어준다.
	team.getMember().add(this);
}

// Team.java 에서의 선언을 위해 추가 생성
public void setTeam(Team team) {
	this.team = team;
}

이러한 형태는 추후 소스코드를 봤을 때 단순 setter의 작업이 아닌 중요한 작업을 진행하는지를 파악 할 수 있게 도와줍니다.

⚫ Member.java

package com.koreait.jpaitem.relation;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter @Setter
public class Member {
	
	// 객체 참조로 연관된 객체를 갖는 방법(1)
//		@Id @GeneratedValue
//		@Column(name = "MEMBER_ID")
//		private Long id;
//		@Column(name = "USERNAME")
//		private String name;
//		@Column(name = "TEAM_ID")
//		private Long teamid;
	
	@Id @GeneratedValue
	@Column(name = "MEMBER_ID")
	private Long id;
	@Column(name = "USERNAME")
	private String name;
	
	// 객체 참조로 연관된 객체를 갖는 방법(2)
	// @ManyToOne : 여기에선 Team이 하나 (많은 것 중에 하나)
	// Member -> Team : N -> 1		=> @ManyToOne
	// @JoinColumn(name = "TEAM_ID") :  해당 관계 컬럼을 명시해줘야한다. TEAM_ID와 조인해야 한다.
	// 외래키가 있는 객체가 주인
	@ManyToOne
	@JoinColumn(name = "TEAM_ID")
    // @Setter 클래스 정의도 가능하지만, 전역 변수 정의도 가능하다.
	@Setter(value = AccessLevel.NONE) // lombok 옵션으로 lombok 에서 자동 setter 생성을 막아준다.
	private Team team;
	
	// 일반적으로 setter의 형태가 아니면 메서드 이름을 바꿔준다.
	// 추후 소스코드를 봤을 때 단순 setter의 작업이 아닌 중요한 작업을 진행하는지를 파악 할 수 있다.
	public void changeTeam(Team team) {
		this.team = team;
		// this : 나 자신의 인스턴스를 넣어준다.
		team.getMember().add(this);
	}

      // Team.java 에서의 선언을 위해 추가 생성
      public void setTeam(Team team) {
          this.team = team;
      }

	
}

⚫ Team.java

package com.koreait.jpaitem.relation;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter @Setter
public class Team {
	
	@Id @GeneratedValue
	@Column(name = "TEAM_ID")
	private Long id;
	private String name;
	
	/*
	 * team에 의해서 관리가 된다.
	 * mappedBy가 적힌 곳은 읽기만 가능하다.
	 * 값을 넣어봐야 아무일도 벌어지지 않는다.
	 * 대신 조희는 가능하다.
	 */
	@OneToMany(mappedBy = "team")
	private List<Member> member = new ArrayList<Member>(); 

	public void addMember(Member member) {
    	// 추가 생성한 Membet setter
		member.setTeam(this);
		this.member.add(member);
	}
	
}

⚫ JpaMain2.java

package com.koreait.jpaitem;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import com.koreait.jpaitem.relation.Member;
import com.koreait.jpaitem.relation.Team;

public class JpaMain2 {

	public static void main(String[] args) {

		EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
		EntityManager em = emf.createEntityManager();
		EntityTransaction tx = em.getTransaction();
		tx.begin();
		
		try {
			
			/*
				// 양방향 매핑시 가장 많이 하는 실수
				// readOnly 만가능하다.
				Member member = new Member();
				member.setName("member1");
				em.persist(member);
				
				Team team = new Team();
				team.setName("TeamA");
				team.getMember().add(member);
				em.persist(team);	 
			 */					
			Team team = new Team();
			team.setName("TeamA");
			em.persist(team);			
			
			Member member = new Member();
			member.setName("member1");
//			member.setTeam(team);
			member.changeTeam(team);
			em.persist(member);
			
			// DB 트렌젝션
//			em.flush();
//			em.clear();
			
			// 객체 지향적인 입장에서 양쪽에 모두 값을 넣어주어야한다.
			// 양방향 매핑시에는 양쪽의 값을 모두 입력해 주어야 한다.
			// DB를 다시 다녀오지 않고 객체 상태로만 사용할 수 있다.
//			team.getMember().add(member);
			
			System.out.println("==============================");
			
			Team findTeam = em.find(Team.class, team.getId());
			List<Member> members = findTeam.getMember();
			
			for( Member m : members ) {
				System.out.println("m = " + m.getName());
			}
			
			System.out.println("==============================");
			
			
			tx.commit();			
			
		} catch (Exception e) {
			tx.rollback();			
		}finally {
			em.close();
			emf.close();
		}
		
	}

}

📌 양방향 연관관계 주의(2)

  • 양방향 매핑시에 무한 루프 조심하자
    -> toString(), lombok lib 조심할 것

⚫ Member.java

package com.koreait.jpaitem.relation;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter @Setter
public class Member {
	
    ...
    
    // lombok에서 지원하는 어노테이션 @toString으로 만들 수 있다. 
	// 그러나 Member.java와 Team.java에서 서로를 호출하고 있기 때문에 무한 루프가 발생한다.
	// 그래서 @toString 선언은 주의해야하며 필요시에는 한 곳에서만 호출하는 것이 좋다. (추가적으로 주의할 어노테이션 @Data)
	@Override
	public String toString() {
		return "Member [id=" + id + ", name=" + name + ", team=" + team + "]";
	}

	
}

⚫ Team.java

package com.koreait.jpaitem.relation;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter @Setter
public class Team {
	
    ...
    
	@Override
	public String toString() {
		return "Team [id=" + id + ", name=" + name + ", member=" + member + "]";
	}
	
}




📚 연관관계 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료가 되었어요.
  • 양방향 매핑은 반대 방향으로 조회 기능이 추가 된 것 뿐이에요.

⚫ 양방향 사용 이유

  • JPQL에서 역방향으로 탐색할 일이 많기 때문이에요. 그렇지만 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 돼요. (테이블에 영향을 주지 않아요.)

⚫ 결론 : 객체 입장에서 양방향 매핑은 필수는 아니에요. 필요 시에 그때 생성해도 늦지 않아요.




📚 연관관계의 주인을 정하는 기준

  • 비지니스 로직을 기준으로 연관관계의 주인을 선택하면 안돼요.
  • 연관관계의 주인은 외래키의 위치를 기준으로 정해야해요.



무사히 적응할 그 날을 기대 ✔️




출처
https://media.giphy.com/media/kyUIknbbDNvID5XzU4/giphy.gif
https://media.giphy.com/media/A6aHBCFqlE0Rq/giphy.gif

0개의 댓글