이 포스트는 김영한님의 "자바 ORM 표준 JPA 프로그래밍"을 참조하였음을 알립니다.
Spring Boot를 어느정도 만져보다 포스팅하는 거라 모든 내용을 정리하긴 좀 그렇고..
다시 정리할 만한 내용들만 중점으로 다룰 예정
📕 목차
1. 양방향 연관관계 주의점
• 순수한 객체까지 고려한 양방향 연관관계
• 연관관계 편의 메서드
• 연관관계 편의 메서드 주의사항
2. 다대다 관계 분리 p. 226
• 복합 키 사용
• 새로운 기본 키 사용
1. 양방향 연관관계 주의점
Team과 Member가 일대다 관계라 가정했을 때, 일반적으로 저장 방식은 이렇다.
public void testSave() {
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); // member1 -> team1
em.persist(member1);
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // member2 -> team1
em.persist(member2);
}
- 단방향 연관관계 저장 방식과 동일하다.
- 주인이 아닌 방향은 값을 설정하지 않아도 데이터베이스에 정상 입력된다.
- Member 테이블의 team_id 외래키에 Team의 기본 키 값이 저장되어 있다.
- 외래키 주인인 Member가 외래키를 관리하므로,Team.members는 고려하지 않아도 된다.
- team2.getMembers().add(member1); // 연관관계 주인이 아니므로 무시된다.
- 만약 주인이 아닌 방향에만 연관관계를 매핑을 하면 데이터베이스에 반영되지 않는다.
📌 순수한 객체까지 고려한 양방향 연관관계
💡 객체 관점에서 양쪽 방향 모두 값을 입력해주는 것이 가장 안전하다.
Team과 Member가 일대다 관계라고 가정
public void testDomain() {
Team team1 = new Team("team1", "팀1");
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
member1.setTeam(team1); // member1 -> team1
team1.getMembers().add(member1); // team1 -> member1
member2.setTeam(team2); // member2 -> team1
team1.getMembers().add(member2); // team1 -> member2
}
- 양방향이라면 양쪽 다 관계를 설정해주어야 한다.
- 외래키 주인만 매핑해줘도 데이터베이스 상에서는 문제 없지만, 순수한 객체 상태에서 문제가 발생할 수 있다.
- Member.team : 연관관계의 주인, 해당 값으로 외래 키 관리
- Team.members : 연관관계의 주인이 아니므로 저장 시에 사용되지는 않는다.
📌 연관관계 편의 메서드
public class Member {
private Team team;
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(member);
}
}
- 양방향 매핑으로 인한 중복 코드로 인해 코드가 지저분해지고, 오류가 발생할 확률이 높아진다.
- 연관관계의 주인인 Member 클래스에서 도우미 메서드를 호출하는 쪽이 좋다.
public void testDomain() {
Team team1 = new Team("team1", "팀1");
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
member1.setTeam(team1);
member2.setTeam(team2);
}
📌 연관관계 편의 메서드 주의사항
위 방식을 그대로 사용하면 버그가 생긴다.
member1.setTeam(teamA);
member1.setTeam(teamB);
Member findMember = teamA.getMember();
- 기존에 매핑되어 있던 teamA와 매핑을 끊어주지 않았다.
- 따라서, getMember()를 호출하면 teamA가 같이 조회되는 문제가 발생한다.
public class Member {
private Team team;
public void setTeam(Team team) {
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(member);
}
}
- 기존에 매핑된 팀이 있으면 연관관계를 삭제하는 코드를 추가해주어야 한다.
✒️ 양방향 연관관계 삭제 시
이 문제는 제법 오랫동안 날 괴롭혀 왔던 이슈였다. 뭐가 더 나은 해결책인지 모르겠어서..
기본적으로 단방향 관계에서 연관관계를 삭제하기 위해서는 외래키 주인이 setFK(null)을 해주면 그만이었다.
그런데 양방향 매핑에서 도우미 메서드를 활용하다보니 null을 입력하면 문제가 발생한다.
public void setTeam(Team team) {
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(member);
}
member.setTeam(null); // (null).getMembers -> 에러
이렇게 되면 setTeam에서 team이 null인지를 체크해주는 로직도 처리해주어야 하는데, '이게 과연 맞을까?' 라는 의문이 머리에서 떠나질 않았다.
대체 왜 책에는 delete에 대한 내용이 정의되지 않은 걸까를 고민한 끝에 아래 댓글이 의문점을 해소해주었다.
기업 입장에서 사용자가 남긴 로그는 하나하나가 데이터고 자산이다.
그러다보니 delete라는 행위가 거의 없을 것이라는 생각을 하질 못했던 것 같다.
교재에서도 관련한 로직을 굳이 고려하지 않은 이유는 애초에 이런 비지니스 로직을 처리할 일이 거의 없기 때문일 것이라는 생각이 든다.
✒️ 양방향 매핑 시 무한루프를 조심하라. (@ToString)
lombok의 @ToString을 남용하다가 낭패를 본 적이 있었다.
User와 Group이 서소를 참조하고 있는 상태에서 무턱대고 @ToString을 양쪽에 걸어버리면 순환 참조에 빠진다.
User user = new User();
Group group = new Group();
user.setGroup(group);
user.toString();
/////// toString 예시
#####user#####
id=1
userId=gillog
group=
#####group####
id=1
name=java
users={####user####
....
2. 다대다 관계 분리
일반적으로 다대다 관계는 미완성 관계로 취급한다.
따라서 이를 분리해줄 필요가 있는데, 두 가지 방법이 있다.
📌 복합 키 사용
@Entity
public class Member {
@Id @Column(name = "MEMBER_ID")
private String id;
@OneToMany(mappedBy = "member")
private List<MebmerProduct> memberProducts = new ArrayList<>();
...
}
@Entity
public class Product {
@Id @Column(name = "PRODUCT_ID")
private String id;
private String name;
}
@Endity
@IdClass(MemberProductId.class)
public class MemberProduct {
@Id @ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@Id @ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
private int orderAmount;
...
}
public class MemberProductId implements Serializalbe {
private String member;
private String product;
@Override public boolean equals(Object obj) {...}
@Override public int hashCode() { ... }
}
- 복합 기본키
- JPA에서 복합키를 사용하려면 별도의 식별자 클래스(@IdClass)를 만들어야 한다.
- 식별 관계(Identifying Relationship) : 부모 테이블의 기본키를 받아서 자신의 기본키 + 외래키로 사용하는 것
- 식별자 클래스
- 복합 키는 별도의 식별자 클래스로 만들어야 한다.
- Serializable을 구현해야 한다.
- equals와 hashCode 메서드를 구현해야 한다. (기본 설계 원칙)
- 기본 생성자가 있어야 한다.
- 식별자 클래스는 public이어야 한다.
- @EmbeddedId를 사용하는 방법도 있다.
📌 새로운 기본 키 사용
@Endity
public class MemberProduct {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
private int orderAmount;
...
}
- 식별자 클래스를 사용하지 않고, Order 클래스 별도의 pk 필드를 지정해주는 것도 좋은 방법이다.
- 이를 비식별 관계라고 한다. (외래키는 외래키로만 사용)