1. 테이블 중심 매핑
연관 관계 매핑 시나리오 하나를 살펴 보겠습니다.
- 회원과 팀 entity 존재
- 회원은 하나의 팀에만 소속
- 회원과 팀은 다대일 관계
다음은 객체의 테일블에 맞추어 모델링한 예시입니다.
@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;
…
}
//팀 저장
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 findTeam = em.find(Team.class, team.getId());
위와 같이 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력관계를 만들수 없습니다. 테이블은 외래키로 조인을 사용해서 연관된 테이블을 찾습니다. 객체는 참조를사용해서 연관된 객체를 찾습니다. 테이블과 객체 사이에는 이런 큰 차이점이 있습니다.
2. 단방향 연관관계
Member가 Team을 갖는게 객체지향스럽게 모델링하는 것 입니다.
@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;
…
}
누가 m이고 누가 1인지가 중요합니다. Member 입장에서는 ManyToOne입니다. 이 Team에 있는 TEAM_ID로 조인을합니다.
//팀 저장
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 findTeam = findMember.getTeam();
연관관계 수정은 다음과 같이 하면 됩니다.
// 새로운 팀B
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);
// 회원1에 새로운 팀B 설정
member.setTeam(teamB);
3. 양방향 연관관계와 연관관계의 주인
테이블과 객체에서 연관관계의 차이를 이해하는 것이 중요합니다. 테이블의 세계에서는 외래키를 반대편에 하나 추가해주면 양쪽다 참조 가능합니다. 회원에서 TEAM에 어떤 데이터가 있는지, TEAM에서는 TEAM에 속한 회원이 누가 있는지 조회가 가능합니다.
그런데 Team에서는 양쪽다 레퍼런스가 있어야합니다. 회원 -> 팀을 조회하기 위해서는 TEAM 참조값을 넣어야하고, 팀 -> 회원을 조회하기 위해서도 회원 참조값을 넣어놔야합니다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
…
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<Member>();
…
}
관례로 private List<Member> members = new ArrayListM<>(); 로 선언합니다. Team에 @OneToMany(mappedBy = "team") → team으로 매핑이 되있다는 뜻입니다. 즉, 연관관계의 주인은 Member이고, mappedBy는 Team에 붙여줍니다. 양방향보다는 단방향으로 설계를 하고 필요할 경우 추후에 추가를 하는 것을 추천합니다.
//조회
Team findTeam = em.find(Team.class, team.getId());
int memberSize = findTeam.getMembers().size(); //역방향 조회
객체와 테이블이 관계를 맺는 차이점은 다음과 같습니다.
잘보면 객체는 단방향 연관 관계가 2개가 있는 것 입니다. 이것을 그냥 양방향 연관관계라고 합니다. 그런데 테이블의 연관 관계는 1개입니다(양방향). 이 2개는 차이점이 있다는걸 먼저 이해해야합니다.
class A{
B b;
}
class B{
A a;
}
테이블은 외래키 하나로 두 테이블의 연관관계를 정리합니다. 그리고 둘중 하나로 외래키를 관리해야합니다.
mappedBy를 어디에 달아야할지 결정하는것은 헷갈릴 수 있습니다. 객체와 테이블간에 연관관계를 맺는 차이를 이해해야합니다. 연관관계의 주인을 어디에 달아야하는지 결론을 먼저 말하겠습니다.
외래키가 있는 곳을 주인으로 정해라
Member 객체에서 team값을 바꿨을때 외래키(TEAM_ID)가 업데이트 되야할지 , TEAM에 있는 members를 업데이트했을때 MEMBER 테이블의 외래키(TEAM_ID)를 업데이트 해줘야할지 결정해야합니다. 그래서 연관관계의 주인은 양방향 매핑에서 나옵니다.
연관관계의 주인만이 외래키를관리(등록,수정)할 수 있습니다. 주인이 아닌쪽은 읽기만 가능합니다. 주인은 mappedBy 속성 사용하면 안됩니다. mappedBy를 사용한다는 것은 무엇인가에 의해서 매핑되고있다! 내가 주인이 아니다라는 것 입니다. 주인이 아니면 mappedBy로 주인을 지정해줘야합니다.
• 객체의 두 관계중 하나를 연관관계의 주인으로 지정
• 연관관계의 주인만이 외래 키를 관리(등록, 수정)
• 주인이 아닌쪽은 읽기만 가능
• 주인은 mappedBy 속성 사용X
• 주인이 아니면 mappedBy 속성으로 주인 지정
다음은 양방향 매핑시 가장 많이 하는 실수입니다.(연관 관계의 주인에 값을 입력하지 않음)
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);
em.persist(member);
양방향 매핑시 연관관계의 주인에 값을 입력해야합니다. 하지만 순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야합니다. member에서 team을 가지고오는데 이때 flush를 안해주면 1차 캐시에서 가지고옵니다. 이 Team에서 member를 가져오면 1차 캐시에서 가지고 오기 때문에 select 쿼리가 안나가고, member에는 null이 들어있을겁니다.
이런 관점에서 양쪽에 세팅을 해주어야합니다. 또한 테스트 케이스 작성할때도 이런 경우가 있을 것 입니다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
team.getMembers().add(member);
//연관관계의 주인에 값 설정
member.setTeam(team);
em.persist(member);
또한 양방향 매핑시에 무한 루프를 조심해야합니다. (toString(), lombok,JSON 생성 라이브러리)
Team에서 toString 호출 시 member의 toString 호출하고 이런식으로 무한루프를 생성하게됩니다. toString 뿐만 아니라 lombok, JSON생성 라이브러리에서도 그렇습니다. JSON생성 라이브러리는 컨트롤러에서 entity를 response로 바로 반환하면 entity를 JSON으로 바꿀 때 장애가 납니다. 일단 lombok에서 toString 만드는건 왠만하면 사용하지말고, 사용하게 되면 빼고 써야합니다.
컨트롤러에서는 entity를 절대 그냥 반환하면 안됩니다. 왜냐하면 무한루프가 생길 수 있고, entity가 변경 될 수 있는데 entity를 변경하는순간 api스펙이 바뀌어 버립니다. entity는 dto로 변환을 해서 반환하는걸 추천합니다. 그러면 JSON 생성 라이브러리에서 문제가 생길일이 거의 없습니다.
'JPA' 카테고리의 다른 글
[JPA] 상속 관계 매핑(고급매핑) (0) | 2020.04.19 |
---|---|
[JPA] 연관 관계 매핑의 종류 (0) | 2020.04.19 |
[JPA] 기본 키 매핑 (0) | 2020.04.10 |
[JPA] 객체와 테이블 매핑 (0) | 2020.04.10 |
[JPA] 영속성 콘텍스트 (0) | 2020.04.10 |