단방향 연관관계, 양방향 연관관계 뭔지는 알겠는데, 막상 스키마를 구성하려면 개념이 안 잡히는
알 것 같으면서, 또 모르는 그런 개념이어서 이번 기회에 정리를 해보려고 합니다.
1. 데이터 베이스 형태의 설계
테이블 연관관계에 나타나 있는 형태 그대로 엔티티를 구성해 보도록 하겠습니다.
아래와 같이 두개의 엔티티를 생성할 수 있습니다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@Column(name = "TEAM_ID")
private Long teamId;
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
}
위와 같이 엔티티를 작성하면 데이터는 성공적으로 저장할 수 있습니다.
이제 memberId를 이용하여 해당 member의 팀을 확인해 보는 작업을 해보겠습니다.
Member member = memberRepository.findById(memberId);
Long teamId = member.getTeamId();
Team team = teamRepository.findById(teamId);
멤버를 찾고 -> 멤버에서 팀 아이디를 가지고 와서 -> 팀을 찾는 과정을 거쳐야 합니다.
멤버에서 팀아이디를 가지고 와서 다시 팀을 찾아야 하는 거추장 스러운 과정을 거치는 것이죠.
이것은 객체 지향적으로 코드를 작성하는 방법이 아닙니다.
테이블은 외래키로 조인을 사용해서 연관된 테이블을 찾는 반면
객체는 참조를 통해서 연관된 객체를 찾는 방법을 지향하는 차이가 있습니다.
애초에 member에 team 정보를 가지고 있었으면 member가 어떤 team에 소속되어 있는지 바로 알 수 있는 것이죠.
이러한 문제를 해결하기 위해 우리는 객체 지향적으로 코드를 작성하고 JPA에게 정보를 전달해줍니다.
"우리는 객체 지향적으로 코드를 작성했고 두개의 테이블 간의 관계는 이러한 것이니 니가 알아서 잘 처리해줘"
JPA에게 전달해줘야 하는 정보는 연관관계이며,
이 연관관계(@ManyToOne, @OneToMany) 정보는 JPA가 데이터 베이스 형태에 맞추기 위해서 알아야 하는 것들입니다.
객체지향 모델링 (단방향 연관관계)
자 그럼 이제 우리 코드를 객체 지향적으로 짜 보고, 나머지 일은 JPA에게 맡겨 둡시다.
우선 데이터베이스 개념에서 테이블은 외래키 하나로 양쪽 조인이 가능하기 떄문에 방향이라는 개념이 없습니다.
다시 말해서 단방향 연관관계, 양방향 연관관계가 존재하지 않는다는 것입니다.
연관관계 모델링은 데이터 베이스적인 설계를 객체지향적인 설계로 바꾸면서 생기는 개념인 것이죠
단순하게 설명하자면
한쪽에서만 다른쪽의 객체를 꺼낼 수 있으면 단방향 연관관계,
양쪽에서 서로의 객체를 꺼낼 수 있으면 양방향 연관관계 라고 볼 수 있을 것 같습니다.
그 중에서 단방향 연관관계에 대해서 먼저 알아보도록 하겠습니다.
예시로 알려드리는 단방향 연관관계는 멤버와 팀에 대한 연관관계이며,
멤버가 팀을 참조하는 형태로 모델링을 완료하였습니다.
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
// @Column(name = "TEAM_ID")
// private Long teamId;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
여기서 JoinColumn(name = "TEAM_ID")라는 어노테이션이 보이나요?
이것이 바로 JPA에게 알려줘야 하는 부분중 하나입니다.
team이라는 객체와 TEAM_ID라는 외래키를 매칭시켜주는 부분인 것이죠.
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private String name;
}
자 이제 member와 team을 저장하고
member를 repository에서 가져온 후, member가 속한 팀을 확인해 보겠습니다.
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class TestJpa {
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");
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 team1 = findMember.getTeam();
System.out.println(team1.getName());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
멤버를 저장하고, 팀을 저장하는 2개의 쿼리가 나가는 것을 확인할 수 있습니다.
멤버와 팀은 이제 영속성 컨텍스트에 저장이 되어있으며, 조회를 할 때는 영속성 컨텍스트에서 다시 가지고 오기 때문에
조회하는 쿼리는 따로 안 나가는 것을 확인할 수 있습니다.
Hibernate:
/* insert hellojpa.Team
*/ insert
into
Team
(name, id)
values
(?, ?)
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(name, team_id, member_id)
values
(?, ?, ?)
조회하는 쿼리를 다시 확인하기 위해서는 영속성 컨텍스트를 비워줘야 겠네요??
em.flush(), em.clear()를 이용하여 영속성 컨텍스트를 비우고 다시 조회해 봅시다.
public class TestJpa {
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");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
Team team1 = findMember.getTeam();
System.out.println(team1.getName());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
쿼리는 아래와 같이 나가는 것을 확인할 수 있습니다.
Hibernate:
/* insert hellojpa.Team
*/ insert
into
Team
(name, id)
values
(?, ?)
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(name, team_id, member_id)
values
(?, ?, ?)
Hibernate:
select
member0_.member_id as member_i1_9_0_,
member0_.name as name2_9_0_,
member0_.team_id as team_id3_9_0_,
team1_.id as id1_19_1_,
team1_.name as name2_19_1_
from
Member member0_
left outer join
Team team1_
on member0_.team_id=team1_.id
where
member0_.member_id=?
member와 team을 저장하는 2개의 insert query와 member와 team을 같이 조회하는 select 쿼리가 발생합니다.
현재는 team의 조회 전략이 eager이기 때문에 (기본)
Member findMember = em.find(Member.class, member.getId());
member를 불러올 때 team도 같이 불러오는 전략으로 쿼리를 날리고 있습니다.
LAZY 모드로 변경하게 되면 team이 사용되는 시점에 조회하는 쿼리가 발생하는 전략으로 바뀌게 됩니다.
아래에서 확인해 보시죠.
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
}
아래의 커맨드를 실행하게 되면 쿼리가 어떻게 나가는지 확인해 보도록 하겠습니다.
public class TestJpa {
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");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
System.out.println("여기서 조회 쿼리가 나가네요");
Member findMember = em.find(Member.class, member.getId());
System.out.println("여기서 조회 쿼리가 나가네요");
Team team1 = findMember.getTeam();
System.out.println("---------아래에서 조회 쿼리가 발생할 예정--------------");
System.out.println(team1.getName());
System.out.println("---------위에서 조회 쿼리가 발생할 예정--------------");
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
Hibernate:
/* insert hellojpa.Team
*/ insert
into
Team
(name, id)
values
(?, ?)
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(name, team_id, member_id)
values
(?, ?, ?)
여기서 조회 쿼리가 나가네요
Hibernate:
select
member0_.member_id as member_i1_9_0_,
member0_.name as name2_9_0_,
member0_.team_id as team_id3_9_0_
from
Member member0_
where
member0_.member_id=?
여기서 조회 쿼리가 나가네요
---------아래에서 조회 쿼리가 발생할 예정--------------
Hibernate:
select
team0_.id as id1_19_0_,
team0_.name as name2_19_0_
from
Team team0_
where
team0_.id=?
teamA
---------위에서 조회 쿼리가 발생할 예정--------------
Team이 실제로 사용되는 시점에서 조회 쿼리가 나가는 것을 확인할 수 있습니다.
양방향 연관관계
이제 양방향 연관관계에 대해서 알아보도록 하겠습니다.
앞서 말했던 것과 동일하게, 단방향 연관관계, 양방향 연관관계는 객체 모델링을 하면서 나타난 개념입니다.
데이터 베이스 모델링에서는 단방향 연관관계와 양방향 연관관계라는 용어가 존재하지 않는 것이죠
기본적으로 데이터 베이스는 양방향으로 설계되어 있습니다.
foreign key는 primary key와 연결이 되어있기 때문에 데이터 베이스 상에서는 Member와 Team 양쪽에서 서로를 확인할 수 있는 방법이 있는 것이죠
양방향 연관관계란 결국 양쪽에서 모두 객체를 이용하여 서로를 참조하기 위한 방법입니다.
Team에서도 List<Member>를 통해서 확인할 수 있도록 만들고,
Member에서도 Team이라는 객체를 이용하여 확인할 수 있도록 만드는 방법인 것입니다.
양방향 매핑시 가장 많이 하는 실수
연관 관계의 주인이 아닌 값에 입력하는 것.
- mappedBy 에 필드 값을 수정하는 것.
- 이 필드는 읽기 전용이 되는 것을 꼭 명심해야 함.
연관관계의 주인은 멤버인 상태에서
팀에 멤버를 넣어준 상태입니다.
public class TestJpa {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setName("member1");
em.persist(member);
Team team = new Team();
team.setName("teamA");
team.getMembers().add(member);
em.persist(team);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
트랜잭션이 끝난 상태에서 db를 확인해 보면 member에 team이 존재하지 않는 것을 확인할 수 있습니다.
반대로 연관관계 주인에게 데이터를 저장하면, db에 저장되는 것을 확인할 수 있습니다.
public class TestJpa {
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");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member);
System.out.println("팀에는 몇명의 인원이 존재하나요?");
System.out.println(team.getMembers().size());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
db에는 존재하지만 팀이라는 객체에는 멤버가 존재하지 않는 상태가 되었습니다.
team에도 member를 따로 넣어주는 메서들를 따로 만들어야 겠네요
이 경우 연관관계 주인 (Member)에서 Team에 member를 넣어주는 메서드를 만드면 되겠죠?
(물론 team을 db에서 다시 받아온다면 db에서 받아온 team에는 member가 존재할 것입니다)
- 양방향 매핑시에 무한 루프 조심, toString, lombok, JSON 생성 라이브러리
- 스프링 부트는 컨트롤러 레이어에서 유저로 데이터를 반환할 때 @ResponseBody 로 반환하는 경우 Json 파싱을 하게 되는데 이 때 Jackson 라이브러리를 사용합니다. 만약 엔티티를 컨트롤러에서 사용하게 되면 무한 참조가 발생하게 됩니다. 처리하는 방법은 @JsonIgnore 나 엔티티를 사용하지 않는 방식이나 일반적으로 엔티티를 사용하지 않고 DTO 로 변환해서 사용하는 것이 매우 매우 매우 권장됩니다.
연관관계의 주인을 정하는 기준
비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안됨
연관관계의 주인은 외래키의 위치를 기준으로 정해야 함.
일대다 단방향 관계
https://cjw-awdsd.tistory.com/47
일대다 단방향 관계 (미친 정리력 너무 잘하셨음)
https://hyeon9mak.github.io/omit-join-column-when-using-many-to-one/
https://www.inflearn.com/questions/266753
양방향 연관관계는 언제 필요할까?
https://www.inflearn.com/questions/268269
'강의 > 자바 ORM 표준 JPA' 카테고리의 다른 글
필드와 컬럼 매핑 (0) | 2022.04.09 |
---|---|
데이터 스키마 자동 생성 (0) | 2022.04.09 |
JPA, 영속성 컨텍스트의 이점 (변경감지) (0) | 2022.04.09 |
JPA, 영속성 컨텍스트의 이점 (트랜잭션을 지원하는 쓰기 지연) (0) | 2022.04.09 |
JPA, 영속성 컨텍스트의 이점 (영속 엔티티의 동일성 보장) (0) | 2022.04.09 |