김영한님의 인프런 강의 '자바 ORM 표준 JPA 프로그래밍'을 참고했습니다.
객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 객체마다 협력 관계를 맺을 수 없을 뿐만 아니라 객체지향적 사고에서 벗어나게 된다.
객체에서 외래 키(teamId)를 직접 다룬다. 객체지향스럽지 못하다.
따라서 객체를 테이블에 맞추어 데이터 중심으로 모델링하면 안된다. 이는 테이블과 객체의 패러다임이 서로 다르기 때문이다.
테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
객체는 참조를 사용해서 연관된 객체를 찾는다.
어떻게 하면 객체지향스럽게 모델링 할 수 있을까?
위 질문에 대한 해답으로 객체의 참조와 테이블의 왜래 키를 매핑하면 된다. JPA는 어노테이션을 통해 매핑 해준다.
Team 엔티티
@Entity
public class Team {
@Id @GeneratedValue;
private Long id;
private String name;
...
}
Member 엔티티
@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;
...
@ManyToOne
: 다대일 관계라는 매핑 정보다. 회원과 팀은 다대일 관계다. 연관 관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.@JoinColumn(name = "TEAM_ID")
: 조인 컬럼은 외래 키를 매핑할 때 사용한다. name 속성에는 매핑할 외래 키 이름을 지정한다. 회원과 팀 테이블은 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);
지금까지는 회원에서 팀으로만 접근하는 다대일 단방향 매핑을 알아봤다. 이번에는 반대 방향인 팀에서 회원으로 접근하는 관계를 추가해 양방향 연관관계를 맺어보자.
먼저 객체 연관관계를 살펴보자. 팀에서 회원을 조회하기 위해서 회원 객체들을 담을 수 있는 참조가 필요하다. 따라서 컬렉션(List)을 추가했다. 반면 데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할 수 있다. 처음부터 양방향 관계이므로 데이터베이스에 추가할 내용은 없다.
따라서 객체 연관관계는 다음과 같다.
테이블 연관관계는 다음과 같다.
엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외부 키를 관리하면 된다. 하지만 엔티티를 양방향으로 설정하면 객체의 참조는 둘인데 외래 키는 하나다. 따라서 둘 사이에 차이가 발생한다. 따라서 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라 한다. 연관관계의 주인만이 외래 키를 관리(등록, 수정)할 수 있으며 주인이 아닌쪽은 읽기만 가능하다.
🤔 그렇다면 누구를 주인으로 정할까?
여기서는 Member.team이 연관관계의 주인이다. 주인임을 설정하려면 주인이 아닌 쪽에서, 즉 Team.members에서 mappedBy 속성으로 주인을 지정해주면 된다.
Team 엔티티
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<Member>();
...
}
mappedBy = "team" 을 통해 Member.team을 연관관계의 주인으로 지정했다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);
em.persist(member);
...
연관관계의 주인에 값을 입력하지 않았다. 실행 결과를 보면 객체와 테이블이 매핑이 되지 않았음을 볼 수 있다.
그렇다면 연관관계의 주인에만 값을 입력해야 할까?
NO! 해보면 결과가 이상하다.(여기선 생략)
결국 순수한 객체 관계를 고려해서 항상 양쪽 다 값을 입력해야 한다.
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);
...
양쪽 모두에 값을 입력했다. 실행 결과를 보면 정상적으로 객체와 테이블이 매핑되었음을 볼 수 있다.
연관관계 편의 메서드를 사용해 위의 코드를 리팩토링 해보자. 아래와 같은 방식도 좋지만, 양쪽 다 신경을 써야 하고 실수로 둘 중 하나만 호출해 양방향이 깨질 수 있다.
//양쪽 모두 값 입력
team.getMembers().add(member);
member.setTeam(team);
따라서 다음과 같이 리팩토링하면 실수도 줄어들고 좀 더 그럴듯하게 양방향 연관관계를 설정할 수 있다.
@Entity
public class Member {
...
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public void changeTeam(Team team) { //setTeam -> changeTeam
this.team = team;
team.getMembers().add(this); // 코드 추가
}
...
자바의 getter, setter는 단순히 값을 넣고 빼는 용도로만 사용하는 것이 좋다. 로직이 들어갔으므로 메서드명을 changeTeam으로 변경했다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.changeTeam(team); //리팩토링
em.persist(member);
...
양방향 매핑시에 무한 루프를 조심하자.
실무에서 굉장히 많이 하는 실수다. 명심하자 꼭!!
📌 단방향 매핑으로 설계를 먼저 끝내자.
📌 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 된다.
📌 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것일 뿐이다(JPQL에서 역방향으로 탐색할 일이 많음).