Backend/JPA

[JPA/Java Persistence API] 엔티티 연관관계

냠냠:) 2021. 5. 23. 23:45

[우아한형제들 김영환님의 인프런 강의 자바 ORM 표준 JPA 프로그래밍 - 기본편을 수강하고 정리한 내용입니다]


학습목표

  • 객체와 테이블 연관관계의 차이 이해
  • 객체의 참조와 테이블의 외래 키 매핑
  • 방향(Direction) : 단방향, 양방향
  • 다중성(Multiplicity) : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다
  • 연관관계의 주인(Owner) : 객체 양뱡향 연관관계에서, 두 객체의 외래 키를 관리하는 관점 이해

 

엔티티 연관관계

관계형 DB에서는 관계를 외래 키로 표현. 외래 키만 가지고 있으면 두 테이블 중 어느 쪽의 데이터든 간에 한 번에 조회 가능. 그러나 객체는 레퍼런스를 활용하여 연관되어있는 객체를 참조함. DB와 객체에서 오는 패러다임 차이를 극복하기 위해 사용하는 것이 JPA의 연관관계

 

 

단방향 연관관계


두 객체 사이에서 한쪽에서만 반대편 객체를 바라보는 것.

 

[예시]

N:1

위와 같은 멤버와 팀의 테이블 모델에서 멤버는 팀의 기본키(PK)외래 키로 가지고 있음. 이때, 멤버 입장에서 단방향 관계를 표현하면 아래와 같다.

@Entity
public class Member {

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

	@Column(name = "username")
	private String name;

	@ManyToOne
	@JoinColumn(name = "TEAM_ID")
	private Team team;
}
@Entity
public class Team {

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

	private String name;
}

ManyToOne으로 멤버는 팀에 대해 N:1 관계를 표시함과 동시에 JoinColumn으로 단방향 연관관계를 표현함.

 

**편의상 Setter/Getter은 생략

 

 

양방향 연관관계

 

두 객체 모두 서로를 바라보는 것.

 

[예시]

@Entity
public class Member {

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

	@Column(name = "username")
	private String name;

	@ManyToOne
	@JoinColumn(name = "TEAM_ID")
	private Team team;
}
@Entity
public class Team {

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

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

OneToMany로 1:N 관계를 표현하고 mappedBy로 Member가 가지고 있는 자기 자신(Team)을 식별할 수 있는 변수(private Team team)를 지정해줌. 

이로써 양방향 연관관계를 표현할 수 있음.


 

연관관계 주인

JPA에서 연관관계는 외래 키를 관리할 수 있는 주인이 필요함. 

양방향 연관관계 [예시]에서 봤을 때 외래키를 Member 쪽에서 관리하는 것을 볼 수 있다. 그러나 저 값을 언제, 어떻게 DB에 Insert 해야 하는지에 대한 고민이 생김

 

  • Member에서 Team을 Set 해주는 방법
  • Team에서 List에 Member을 Add 해주는 방법

둘 중 한 가지 방법으로 할 수 있는데, 답은 Member에서 Team을 Set 해주는 방법이다.

public class JPAMain {

	public static void main(String[] args) {
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-application");
		EntityManager em = emf.createEntityManager();

		EntityTransaction tx = em.getTransaction();
		tx.begin();
		try {
			Team team = new Team();
			team.setName("teamA");

			em.persist(team);

			Member member = new Member();
			member.setName("test");
			member.setTeam(team);

			em.persist(member);

			tx.commit();
		}catch (Exception e) {
			tx.rollback();
			e.printStackTrace();
			System.out.println("ROLLBACK!!");
		}

		em.close();
		emf.close();
	}
}

다른 부분은 JPA설정과 트랜잭션, 영속성컨텍스트 관리니 무시해도 좋다. 여기서 봐야 하는 곳은 team을 member.setTeam으로 member 쪽에 Set 해준다는 것이다.

콘솔에 찍힌 Insert 문
Member의 TEAM_ID가 1로 삽입됨

이제 반대로 Team에 Member을 Add 하는 코드를 보자

 

일단 Team Class에 아래 메서드를 추가한다.

public void addMember(Member member) {
	members.add(member);
}
public class JPAMain {

	public static void main(String[] args) {
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-application");
		EntityManager em = emf.createEntityManager();

		EntityTransaction tx = em.getTransaction();
		tx.begin();
		try {
			Member member = new Member();
			member.setName("test");

			em.persist(member);

			Team team = new Team();
			team.setName("teamA");
			team.addMember(member);

			em.persist(team);

			tx.commit();
		}catch (Exception e) {
			tx.rollback();
			e.printStackTrace();
			System.out.println("ROLLBACK!!");
		}

		em.close();
		emf.close();
	}
}

똑같이 TEAM_ID를 Insert문에 넣긴 한다.
실제 값을 들어오지 않음

예시처럼 양방향 연관관계에서 외래 키를 관리하는 쪽은 Member다. 근데 이게 어떻게 가능할까

 

 

연관관계의 주인(Owner)

 

JPA에서 연관관계에는 규칙이 존재한다. 즉, 외래키를 관리하는 주인이 정해져 있다. 

주인이 아니면 외래 키를 등록, 수정이 가능하지 않고 읽기만 가능하다.

 

위의 예시에서 연관관계의 주인은 Member다. Team은 조회만 가능하다.

 

주인은 mappedBy 속성을 사용하지 않고 JoinColumn으로 반대편 객체의 기본키를 설정한다.

 

반면에 주인이 아니면 mappedBy 속성을 사용한다.

 

기준은?

외래 키가 있는 곳!

위의 캡처처럼 Member가 Team의 기본키(PK)를 외래 키로 가지고 있다. 이렇게 외래 키를 관리하는 쪽을 연관관계 주인으로 설정하는 것이 좋다.

  • 실제 DB에서도 Member 테이블에서 TEAM_ID(FK)를 가지고 있기 때문에 객체에서도 같이 관리해주는 것이 직관적이고 개발함에 있어서 헷갈리지 않는다.
  • 만약 TEAM이 연관관계 주인이라면, Team의 members의 값을 수정해줘도 Team의 쿼리가 아닌 Member에 대한 쿼리가 나가기 때문에 보기 어렵다.(Member를 바꿔서 Member에 대한 쿼리가 나가는 것이 보기 편함)

 

연관관계 편의 메서드

위에 예시에서 Member에 Team을 Set 한 다음, commit 혹은 flush 전, Team의 members를 참조하면 IndexOutOfBoundsException이 뜰 것이다. 그럴 때는 아래와 같이 Set 대신 양쪽 객체에 값을 넣어줌으로써 해결할 수 있다.

public void addMember(Member member) {
	member.setTeam(this);
	members.add(member);
}

이렇게 Team에만 값을 넣어줘도 Member가 가지고 있는 Team이 세팅되게 하거나, Member 쪽에서 Team을 설정해주면 flush 전에 Team객체의 members를 활용할 수 있다.


 

정리

  • 단방향 매핑만으로 연관관계는 완료
  • 양방향 매핑은 단방향 매핑 후 필요할 때 추가 줘도 된다
  • 양뱡향 매핑은 객체 상태를 고려해서 양쪽에 값을 설정해주는 것이 좋다(연관관계 편의 메서드)
  • 양방향 매핑 시 무한루프 조심 (toString 시 member와 team이 서로의 toString을 호출)
  • 연관관계 주인은 외래 키가 있는 곳으로 하자

 

반응형