[JPA] 연관관계 매핑 개념
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
이 글은 인프런에서 제공하는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 참고했고
강의 내용을 다시 복습하면서 정리하려는 목적으로 작성합니다.
테이블은 외래 키를 이용하여 자신과 연관관계가 있는 테이블을 탐색 할 수 있습니다.
하지만 객체는 다릅니다.
객체는 레퍼런스를 이용해서 자신과 연관관계가 있는 객체를 탐색할 수 있습니다.
다듬어서 이야기 하면 다음과 같습니다.
테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾습니다.
객체는 참조를 사용해서 연관된 객체를 찾습니다.
이처럼 테이블과 객체사이의 패러다임의 불일치가 존재하게 되는데,
이를 해결한 기술이 바로 ORM
입니다.
패러다임 불일치 해결!!!
ORM을 사용하면, 객체의 참조를 이용해서 테이블의 외래키를 매핑 할 수 있습니다.
연관관계를 매핑을 하기위해서는 다음과 같은 용어가 등장합니다.
- 방향
- 단방향, 양방향
- 다중성
- 다대일(
N:1
), 일대다(1:N
), 일대일(1:1
), 다대다(N:M
)
- 다대일(
- 연관관계 주인
- 객체 양방향 연관관계는 관리 주인이 필요함
이러한 용어는 계속 등장하게 되니 너무 큰 걱정 안하셔도 됩니다.
이제 연관관계를 매핑하는 방법에 대해서 설명드리겠습니다.
연관관계가 필요한 이유
매핑하는 방법을 설명하기에 앞서
연관관계가 필요한 이유에 대해서 고민해봅시다.
불편하기 때문에 연관관계라는 개념을 넣었겠죠?
테이블에 맞추어 객체를 모델링하면 불편함을 피부로 느낄 수 있을것 같습니다.😎
객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다.
조영호(객체지향의 사실과 오해)
라는 말이 있습니다.
협력 공동체 라는 의미를 다시한번 생각하면서 테이블에 맞추어 객체를 모델링 해봅시다.
테이블의 관계는 다음과 같습니다.
- 회원과 팀이 존재
- 회원은 하나의 팀에만 소속될수 있음
- 회원과 팀은 다대일 관계
테이블에 맞추어 객체를 모델링
Member
객체에 Team
객체의 id
에 해당하는 teamId
를 생성했습니다!(외래키를 직접 사용합니다.)
Member
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "username")
private String name;
@Column(name = "team_id")
private Long teamId;
}
Team
@Entity
@Getter @Setter
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
}
Team
과 Member
를 저장합니다.
// 팀 저장
Team team = new Team();
team.setName("SK T1");
em.persist(team);
// 회원 저장
Member member = new Member();
member.setName("Faker");
member.setTeamId(team.getId());
em.persist(member);
이와 같이 외래 키를 직접 사용하면…
다시 식별자로 조회해야 합니다.
객체 지향적인 방법은 아니네요!
// 조회
// Member와 Team은 연관관계가 없음
Member findMember = em.find(Member.class, member.getId());
Team findTeam = em.find(Team.class, team.getId());
객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없습니다.
테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾습니다.
객체는 참조를 사용해서 연관된 객체를 찾습니다.
이것이 "패러다임의 불일치" 입니다.
이러한 불일치를 해결하기 위해서 등장한 ORM
기술 중 하나인 JPA
는 객체 지향 모델링을 할수 있게 해줍니다.
객체 지향 모델링(양방향 매핑)
Member
에 teamId
가 아닌 Team
으로 매핑합니다.
이때, 테이블 연관관계는 전혀 변화가 없는것을 알수 있습니다.
Member
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "username")
private String name;
/**
* 다대일 관계
* 여러명의 회원은 하나의 팀에 소속 될 수 있음
* 연관관계 주인
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
}
Team
@Entity
@Getter @Setter
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
/**
* 일대다 관계
* 하나의 팀에 여러명의 회원이 있음
*/
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
각 객체의 연관관계를 생각하며 저장합니다.
// 팀 저장
Team team = new Team();
team.setName("SK T1");
em.persist(team);
// 회원 저장
Member member = new Member();
member.setName("Faker");
// 연관관계 설정
member.setTeam(team);
team.getMembers().add(member);
em.persist(member);
연관관계를 이용하여 매핑했기 때문에 객체 탐색이 가능합니다.
// 조회
// Team을 Member의 연관관계로 조회 가능
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
// 양방향 관계이기 때문에 Team에서도 Member를 조회할수 있습니다.
Team findTeam2 = em.find(Team.class, team.getId());
int memberSize = findTeam.getMembers().size();
연관관계 주인과 mappedBy
Member
와 Team
을 매핑할때 Team
에서 mappedBy
를 사용한것을 볼수 있습니다.mappedBy
를 설명하기 전에 객체와 테이블이 관계를 맺는 차이에 대해서 알아봅시다.
객체의 양방향 관계
객체의 양방향 관계는 단방향 연관관계 2개가 있다고 생각하면 이해하기 쉽습니다.
객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들면 됩니다…!
- 회원 ➡️ 팀
- 연관관계 1개(단방향)
- 팀 ➡️ 회원
- 연관관계 1개(단방향)
테이블의 양방향 관계
테이블은 외래 키 1개로 2개의 테이블의 연관관계를 관리 할 수 있습니다.
외래 키 1개로 양방향 연관관계를 가질수 있습니다.
- 회원 ↔️ 팀
- 연관관계 1개(양방향)
객체의 양방향 관계
객체 입장에서 양방향 관계로 설정하면 Member
에도 Team
이 있고, Team
에도 Member
가 있습니다.
테이블 입장에서는 외래키는 한 곳에서만 관리되어야 합니다.
객체 입장에서도 1곳에서 외래키를 관리해야 하는데,Member
에서 Team
으로 외래 키를 관리 할지Team
에서 Member
로 외래 키를 관리 할지를 정해야합니다.
이렇게 외래 키를 관리(등록, 수정)할 곳을 정하는 것을 연관관계의 주인을 정한다라고 표현 합니다.
이때 mappedBy
를 사용하여 연관관계 주인인지 아닌지 구분 합니다.
(주인이 아닌경우에 mappedBy
를 사용함)
정리
- 객체의 두 관계중 하나를 연관관계 주인으로 지정
- 연관관계의 주인만이 외래 키를 관리(등록, 수정)
- 연관관계의 주인이 아닌곳은 읽기만 가능
- 주인은
mappedBy
속성 사용하지 않음- 외래키가 있는 곳
- 주인이 아니면
mappedBy
속성으로 주인 지정
외래키가 존재하는 곳을 연관관계 주인으로 정하면 됩니다.
DB
입장에서 외래 키가 있는 곳이 무조건 다(N) 입니다.
외래 키가 없는 곳이 무조건 일(1) 입니다.
따라서 다(N)쪽을 연관관계의 주인으로 정하면 됩니다.
양방향 매핑시 주의 사항
양방향 매핑을 하게되면 항상 연관관계를 고려해야 합니다.
연관 관계 주인에 값을 넣지 않는 경우
// 팀 저장
Team team = new Team();
team.setName("SK T1");
em.persist(team);
// 회원 저장
Member member = new Member();
member.setName("Faker");
// 연관관계 설정
team.getMembers().add(member);
em.persist(member);
Hibernate:
/* insert com.study.purejpa.entity.Team
*/ insert
into
Team
(name, id)
values
(?, ?)
Hibernate:
/* insert com.study.purejpa.entity.Member
*/ insert
into
Member
(username, team_id, id)
values
(?, ?, ?)
insert SQL
은 2개가 실행되지만,DB
를 확인해보면 MEMBER
에 TEAM_ID
가 null
입니다.
연관관계의 주인은 Member
에 있는 Team
이기 때문 입니다.
따라서 반드시 member.setTeam(team);
을 해야 합니다.
연관 주인이 아닌 곳에 값을 넣지 않는 경우
그러면 연관관계 주인이 아닌곳에 값을 넣지 않으면 어떻게 될까요?
// 팀 저장
Team team = new Team();
team.setName("SK T1");
em.persist(team);
// 회원 저장
Member member = new Member();
member.setName("Faker");
// 연관관계 설정
member.setTeam(team);
//team.getMembers().add(member);
em.persist(member);
// 1차 캐시
Team findTeam = em.find(Team.class, team.getId());
for (Member m : findTeam.getMembers()) {
// 값이 없음
System.out.println("m="+m.getName());
}
for
문에 대한 결과가 출력되지 않는것을 확인 할 수 있습니다.
왜 이러한 결과가 발생한 것일까요?
객체지향적으로 생각해보면 당연한 결과 입니다.team.getMembers().add(member);
를 하지 않았기 때문이죠!!! 😊
결론
순수 객체 상태를 고려해서 항상 양쪽에 값을 설정
member.setTeam(team);
team.getMembers().add(member);
연관관계 편의 메소드를 생성
public class Member {
// 연관관계 편의 메소드
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
// 팀 저장
Team team = new Team();
team.setName("SK T1");
em.persist(team);
// 회원 저장
Member member = new Member();
member.setName("Faker");
// 연관관계 편의 메소드 사용
member.changeTeam(team);
em.persist(member);
연관관계 편의 메소드는 일(1)에 넣어도 되고 다(N)에 넣어도 됩니다.
상황에 맞춰 개발하시면 됩니다.^^
양방향 매핑시 무한 루프 주의
(toString()
, lombok
, JSON
생성 라이브러리)
@Entity
@Getter @Setter
public class Team {
@Override
public String toString() {
return "Team{" +
"id=" + id +
", name='" + name + '\'' +
", members=" + members + // member의 toString() 호출
'}';
}
}
@Entity
@Getter @Setter
public class Member {
@Override
public String toString() {
return "Member{" +
"id=" + id +
", name='" + name + '\'' +
", team=" + team + // team의 toString() 호출
'}';
}
}
이렇게 되면 양쪽으로 toString()
을 무한으로 호출하게 됩니다.
Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();
System.out.println("findTeam="+findTeam);
무한으로 호출하게 되면 java.lang.StackOverflowError
를 발생하게 됩니다.
따라서 lombok
으로 toString
을 생성하는 것을 지양하는 것을 추천 드립니다.
그리고 Entity
를 절대로 Controller
에서 반환하는 것을 지양하셔야 합니다.
마지막으로!! 반드시 DTO
를 사용해서 반환하시기 바랍니다.
그렇게 하면 JSON
생성 라이브러리로 무한 호출되는 문제가 발생할 경우는 거의 없습니다.
정리
- 단방향 매핑만으로도 이미 연관관계 매핑은 완료 상태!
- 먼저 단방향 매핑으로 설계를 완료하는 것을 권장 👍
- 양방향 매핑을 할수록 개발자의 고민거리만 늘어간다..! 😭
- 필요한 경우에 양방향 매핑 추가하는 방향으로 개발 진행 👩💻
- 테이블에 영향을 주지 않음
- 왜냐? 양방향은 테이블은 그대로 두고
java
코드만 추가하는 것 이기때문^^
- 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐임을 명심!
- 연관관계의 주인은 외래 키의 위치를 기준으로 선정!
'0+ 스프링 > 0 + 스프링 ORM(JPA)' 카테고리의 다른 글
[JPA] 상속관계 매핑 (0) | 2023.02.20 |
---|---|
[JPA] 연관관계 매핑(@ManyToOne, @OneToMany, @OneToOne, @ManyToMany) (0) | 2023.02.20 |
[JPA] 엔티티 매핑(@Entity, @Table) (0) | 2023.02.16 |
[JPA] JPA 영속성 컨텍스트(+ flush, 준영속 상태) (0) | 2023.02.09 |
[JPA] JPA 구동 방식과 간단 실습(CRUD) (0) | 2023.02.09 |