💡 본 게시글은 김영한님의 인프런(Inflearn) 강의 자바 ORM 표준 JPA 프로그래밍 - 기본편에 대해 공부하고, 정리한 내용입니다.
1. 객체와 테이블 연관관계의 이해
JPA에서 객체와 테이블의 연관관계를 어떻게 매핑하고 관리할 것인가는 매우 중요합니다. 이 글에서는 연관관계의 기본 개념과 매핑 방법을 설명하고, 실무에서 사용하는 올바른 패턴을 구체적으로 다룹니다.
1) 목표
- 객체와 테이블 연관관계의 차이를 이해합니다.
- 객체의 참조와 테이블의 외래 키를 매핑하는 방법을 익힙니다.
- 연관관계의 주요 용어를 이해합니다:
- 방향(Direction): 단방향, 양방향
- 다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)
- 연관관계의 주인(Owner): 객체 양방향 연관관계에서는 관리 주인이 필요합니다.
2) 연관관계가 필요한 이유
- 객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것입니다.
- 조영호, '객체지향의 사실과 오해'에서 제시된 바와 같이, 객체들은 서로 협력해야 하며, 이는 객체 간의 관계 설정을 통해 이루어집니다.
3) 예제 시나리오
- 회원(Member)과 팀(Team)이 있으며, 회원은 하나의 팀에만 소속될 수 있습니다.
- 회원과 팀은 다대일(N:1) 관계입니다.
(1) 객체를 테이블에 맞추어 모델링 (연관관계가 없는 객체)
기본적으로 객체와 테이블 사이에는 관계를 맺기 위한 외래 키 또는 참조 필드가 필요합니다.
객체의 데이터 중심 설계
- Member 엔티티 예시:
@Entity
public class Member {
@Id
@GeneratedValue
private Long id; // 기본 키
@Column(name = "USERNAME")
private String name; // 사용자 이름
@Column(name = "TEAM_ID")
private Long teamId; // 외래 키 대신 사용되는 필드
// Getter, Setter ...
}
- Team 엔티티 예시:
@Entity
public class Team {
@Id
@GeneratedValue
private Long id; // 기본 키
private String name; // 팀 이름
// Getter, Setter ...
}
데이터 중심 설계의 문제점
// 팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
// 회원 저장
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId()); // 외래 키 설정
em.persist(member);
// 조회
Member findMember = em.find(Member.class, member.getId());
// 연관관계가 없으므로, Team도 직접 조회해야 함
Team findTeam = em.find(Team.class, team.getId());
- 문제점:
Member에서Team을 조회하려면teamId를 통해 다시find()를 호출해야 합니다. 이는 객체지향스럽지 않고 협력 관계를 만들기 어렵습니다.
(2) 테이블과 객체의 차이
- 테이블은 외래 키로 조인을 사용해 연관된 테이블을 찾습니다.
- 객체는 참조를 사용해 연관된 객체를 찾습니다.
- 이러한 차이 때문에 객체와 테이블 사이에는 큰 간격이 존재합니다.
4) 단방향 연관관계
(1) 객체 연관관계 사용
- 객체의 참조와 테이블의 외래 키를 매핑하여 단방향 연관관계를 설정합니다.
- Member 엔티티의 단방향 매핑 예시:
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne // N:1 관계 설정
@JoinColumn(name = "TEAM_ID") // 외래 키 매핑
private Team team; // Team 객체 참조
// Getter, Setter ...
}
(2) 연관관계 저장
- 팀과 회원을 저장하는 예시:
// 팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
// 회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team); // 단방향 연관관계 설정, 참조 저장
em.persist(member);
(3) 참조로 연관관계 조회 - 객체 그래프 탐색
// 조회
Member findMember = em.find(Member.class, member.getId());
// 참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();
- 객체지향적인 방법으로 참조를 통해 연관된 객체를 직접 탐색할 수 있습니다.
(4) 연관관계 수정
- 연관된 팀을 변경하는 예시:
// 새로운 팀B 생성
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);
// 회원1에 새로운 팀B 설정
member.setTeam(teamB);
5) 양방향 매핑
- 테이블의 연관관계는 외래 키 하나로 양방향을 모두 지원합니다.
(실제로는 방향이라는 개념 자체가 없습니다.)
(1) 양방향 매핑 설정
- Member 엔티티:
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne // N:1 관계 설정
@JoinColumn(name = "TEAM_ID") // 외래 키 매핑
private Team team; // Team 객체 참조
// Getter, Setter ...
}
- Team 엔티티:
@OneToMany컬렉션을 추가합니다.
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team") // 역방향 매핑
private List<Member> members = new ArrayList<>(); // Member 객체 참조 리스트
// Getter, Setter ...
}
(2) 반대 방향으로 객체 그래프 탐색
// 조회
Team findTeam = em.find(Team.class, team.getId());
int memberSize = findTeam.getMembers().size(); // 역방향 조회
- Note: 객체는 양쪽으로 가려면 양쪽에 레퍼런스를 가질 수 있는 필드를 추가해야 합니다.
(3) 양방향 매핑의 이해
- 객체와 테이블의 관계를 이해해야 합니다.
- 객체 연관관계: 실제로는 단방향 2개입니다.
- 회원 -> 팀 연관관계 1개 (단방향)
- 팀 -> 회원 연관관계 1개 (단방향)
- 테이블 연관관계: 외래 키 하나로 양방향 모두 지원합니다.
- 회원 <-> 팀의 연관관계 1개 (양방향)
- 객체 연관관계: 실제로는 단방향 2개입니다.
6) 연관관계의 주인과 mappedBy
(1) mappedBy의 이해
mappedBy속성은 연관관계의 주인이 아닌 쪽에 사용됩니다.- 연관관계의 주인만이 외래 키를 관리합니다(등록, 수정).
- 주인이 아닌 쪽은 읽기만 가능합니다.
(2) 연관관계의 주인 설정하기
- 양방향 매핑 규칙:
- 객체의 두 관계 중 하나를 연관관계의 주인으로 지정합니다.
- ManyToOne의 Many(다) 쪽을 연관관계의 주인으로 설정합니다.
- 외래 키가 있는 곳을 주인으로 정하는 것이 좋습니다.
- Member 엔티티는 주인, Team 엔티티는
mappedBy사용:
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team") // 역방향 매핑
private List<Member> members = new ArrayList<>(); // Member 객체 참조 리스트
// Getter, Setter ...
}
(3) 연관관계 편의 메서드 생성
- **
연관관계 편의 메서드**를 생성하여 실수를 줄입니다.
@Entity
public class Member {
// ...
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public Team getTeam() {
return team;
}
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
- 객체 상태를 고려하여 항상 양쪽에 값을 설정해야 합니다.
- Note: 연관관계 편의 메서드는
setter를 사용하지 않습니다.
7) 양방향 매핑시 주의사항
- 양방향 매핑 시 가장 많이 하는 실수는 연관관계의 주인에 값을 입력하지 않는 것입니다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
// 역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);
em.persist(member);
mappedBy속성이 지정된 가짜 매핑은 단순한 읽기 전용이므로 실제 DB에 반영되지 않습니다.- 연관관계의 주인(Member)에 설정을 해야 DB에 반영됩니다.
(1) 양방향 매핑시 무한 루프 주의
- 무한 루프가 발생할 수 있는 상황:
toString(),lombok,JSON생성 라이브러리- 컨트롤러에서 엔티티를 직접 반환하지 말고, DTO로 변환해서 반환해야 합니다.
8) 양방향 매핑 정리
- 단방향 매핑만으로도 이미 연관관계 매핑은 완료되었습니다.
- 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 필요할 때만 추가합니다.
- 양방향 매핑은 테이블에 영향을 주지 않고, 엔티티에 코드 몇 줄만 추가하면 됩니다.
9) 정리
- 단방향 매핑으로 설계를 시작합니다.
- 양방향 매핑은 필요할 때 추가합니다.
- 연관관계의 주인은 외래 키가 있는 곳으로 설정합니다.
- 객체 상태를 고려하여 항상 양쪽에 값을 설정하고, 연관관계 편의 메서드를 사용하여 실수를 방지합니다.
- 무한 루프와 같은 문제를 피하기 위해 엔티티를 DTO로 변환해서 반환해야 합니다.
'Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
| 06. 다양한 연관관계 매핑 (1) | 2024.09.08 |
|---|---|
| 04. 엔티티 매핑 (1) | 2024.09.08 |
| 03. 영속성 관리 - 내부 동작 방식 (0) | 2024.08.11 |
| 02. JPA 시작하기 (0) | 2024.08.11 |
| 01. JPA 소개 (0) | 2024.07.24 |