JPA를 사용하는 데 가장 중요한 일은 엔티티와 테이블을 정확히 매핑하는 것이다.
그러려면 JPA의 매핑 어노테이션을 잘 사용해야한다.
JPA가 지원하는 어노테이션은 크게 4가지로 분류할 수 있다.
- 🔗 객체와 테이블 매핑 : @Entity @Table
- 🔗 기본키 매핑 : @Id
- 🔗 필드와 컬럼 매핑 : @Column
- 연관관계 매핑 : @ManyToOne @JoinCloumn @OneToMay
아래와 같은 객체와 테이블이 있다고 가정해보고,
테이블이 갖는 연관관계를 객체에 적용해보자.
//Member
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "member_id)
private Long id;
@Column(name = "username")
private String name;
@Column(name = "team_id")
private Long temaId; // 테이블의 외래키를 그대로 사용
}
// Team
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "team_id")
private Long id;
private String name;
}
위처럼 객체를 테이블에 맞추어 모델링 했을 경우, 저장과 조회는 다음과 같은 방식으로 하게된다.
// Team 저장
Team team = new Team();
team.setName = ("TeamA")
em.persist(team);
// Member 저장
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId()); // 외래키 식별자를 직접 다룸
em.persist(member);
//Member의 Team을 조회하고 싶은 경우
Member findMember = em.find(Member.class, member.getId());
Long temaId = findMember.getTeamId();
Team findTeam = em.find(Team.class, teamId);
위 코드처럼 데이터 중심으로 모델링 하게 되면, 연관관계 조회시 식별자로 재조회를 해야하는 번거로움이 발생한다. 관계형 데이터베이스는 연관된 객체를 찾을 때 외래키를 사용해서 찾으면 되지만, 객체에는 join
이라는 기능이 없다. 객체는 연관된 객체를 찾을 때 참조
를 사용해야한다.
그렇다면 JPA로 객체 지향 모델링을 어떻게 구현할 수 있는지 알아보자.
위의 데이터 중심의 모델링을 객체 지향적으로 모델링을 하면 다음과 같은 모양을 갖는다.
//Member
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "member_id)
private Long id;
@Column(name = "username")
private String name;
@ManyToOne // N:1 연관관계
@JoinColumn(name = "team_id")
private Team team;
}
// Team
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "team_id")
private Long id;
private String name;
}
위 방식으로 모델링을 하면 Team findTeam = member.getTeam();
의 객체 그래프 탐색이 가능하다.
저장하는 코드도 아래와 같은 형식을 취하게 된다.
// Team 저장
Team team = new Team();
team.setName = ("TeamA")
em.persist(team);
// Member 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member);
member.setTeam(team);
에서 JPA가 알아서 team
의 식별자 값을 member
의 외래키 값으로 사용한다.
객체지향방식으로 모델링한 Member
클래스에서 Team
이라는 객체를 참조해놓고 @ManyToOne
어노테이션을 달아주었다. @ManyToOne
은 해당 Entity가 참조하는 Entity와 N:1
관계임을 의미한다.
@ManyToOne
의 속성은 아래와 같다.
속성 | 기능 | Default |
---|---|---|
optional | false로 설정하면 연관된 Entity가 항상 있어야 한다. | true |
fetch | 글로벌 fetch 전략을 설정한다. | @ManyToOne=FethchType.EAGER @OneToMany=FetchType.LAZY |
cascade | 영속성 전이 기능을 사용한다. | |
targetEntity | 연관된 Entity의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. |
@ManyToOne
를 설정한 필드의 외래키를 매핑한다.
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
Member
에 매핑된 테이블이 team_id
를 외래키로 갖고 있다고 해석할 수 있다.
이 매핑 정보로 JPA는 team_id
로 Member
와 Team
을 join 하는 쿼리를 생성한다.
1:N
의 관계를 지닌 필드에 @OneToMany
어노테이션을 설정한다.
Team
과 Member
는 1:N
의 관계이다. 때문에 Team
에서 Member
를 조회할 수 있도록 List<Member>members
를 참조했다. 이것을 JPA에게 알려주기 위해, @OneToMany
매핑 정보를 사용한다.
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany
private List<Member> members = new ArrayList<Member>();
}
mappedBy
속성은 쉽게 말해, 해당 필드가 어디에 매핑되었는지를 알려주는 정보다. mappedBy
는 해당 필드를 @JoinColumn
으로 매핑한 Entity의 필드를 찾는다.
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany (mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
}
엄밀히 말하면 객체에는 양방향 연관관계라는 것이 없다.
객체는 단방향 연관관계를 2개 만들어 양방향 연관관계 처럼 보이게 한다.
이렇게 되면 고민거리가 하나 생길 수 있다.
Entity를 조작할 때, 어느 쪽의 Entity를 기준으로 조작할 것인가 하는 문제이다.
Member
에 Team
을 추가할 지, Team
의 members
에 Memeber
를 추가할지, 마찬가지로 수정을 할 때도 같은 고민이 생기게 된다.
따라서 이 중에 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라고 한다.
연관관계의 주인만이 데이터베이스 연관관계와 매핑되고, 외래키를 관리(등록, 수정, 삭제) 할 수 있다. 반면 주인이 아닌 쪽은 읽기만 할 수 있다.
연관관계의 주인은 mappedBy
를 갖지 않은 반대편의 Entity가 해당 연관관계의 주인이 된다.
그럼 mappedBy
를 이용해 주인을 정할 때 어떤 것을 주인으로 정해야할까.
연관관계의 주인을 정한다는 것은 외래키 관리자를 선택하는 것이라고 보면 된다.
때문에 외래키를 가진 Entity가 주인이 된다.
양방향 연관관계 설정 후 가장 많이 하는 실수는 연관관계의 주인에 값을 입력하지 않는 것이다.
데이터베이스에 외래키 값이 정상적으로 저장되지 않을 경우 이 문제를 가장 먼저 의심해보면 된다.
Member member1 = new Member("kitty");
em.persist(member1);
Member member2 = new Member("doggy");
em.persist(member2);
Team team1 = new Team("team1");
team1.getMembers().add(member1);
team1.getMembers().add(member2);
em.persist(team1);
member1
과 member2
를 저장하고, team1
의 컬렉션에 이들을 담은 후 team1
을 저장했다.
이 상태로 member
를 조회하면 아래와 같은 결과가 나온다.
member_id | username | team_id |
---|---|---|
1 | kitty | null |
2 | doggy | null |
이유는 연관관계의 주인인 Member
가 아닌 Team.members
에만 값을 저장했기 때문이다.
✅ 중요한 것은 연관관계의 주인만이 외래키의 값을 변경할 수 있다 는 것이다.
그렇다면, 연관관계의 주인에만 값을 저장하면 상관 없느냐.
그렇지않다. 객체 관점에서 보면 양쪽 방향 모두에 값을 입력해주는 것이 안전하다.
왜냐하면 한 쪽에만 값을 입력하면 JPA를 사용하지 않는 순수한 객체 상태에서는 연관관계 주인에만 값을 저장하고, 반대 방향에서 연관관계를 조회할 시 (현재 예제로 따지면 team1.getMembers()
를 할 경우) 우리가 기대하는 결과가 나오지 않기 때문이다.
따라서 양방향은 양쪽 다 관계를 설정해야 한다.
Team team1 = new Tema("team1");
em.persist(team1);
// 양방향 연관관계 설정
Member member1 = new Memebr("kitty");
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
team1.getMembers.add(member1); // 연관관계 설정 team1 -> member1
em.persist(member1);
Member member2 = new Memebr("doggy");
member2.setTeam(team1); // 연관관계 설정 member2 -> team1
team1.getMembers.add(member2); // 연관관계 설정 team1 -> member2
em.persist(member2);
양방향 연관관계는 결국 양쪽 모두를 신경써야한다.
하지만, 사람이 하는 일이다 보니 member.setTeam(team);
과 team.getMembers().add(member1);
을 각각 호출하다 보면 둘 중 하나를 누락하는 실수가 날 수 있다.
이 두 코드를 하나인 것 처럼 사용하는 메소드를 만들어두면 실수를 줄일 수 있다.
public class Member {
private Team team;
public void setTeam (Team team) {
this.team = team;
team.getMembers().add(this);
}
}
setTeam()
메소드 하나로 양방향 관계를 모두 설정하도록 변경했다.
이제 위의 양방향 관계를 설정한 코드는 아래처럼 간단하게 수정이 가능하다.
Team team1 = new Tema("team1");
em.persist(team1);
Member member1 = new Memebr("kitty");
member1.setTeam(team1); // 양방향 연관관계 설정
em.persist(member1);
Member member2 = new Memebr("doggy");
member2.setTeam(team1); // 양방향 연관관계 설정
em.persist(member2);
사실 setTeam()
에는 버그가 있다.
member1의 team을 team2로 변경한다고 가정해보자.
Team team2 = new Team("team2");
em.persist(team2);
member1.setTeam(team2);
이 경우, member1의 team1과의 관계는 제거되지 않은 상태이다. 따라서 연관관계를 변경할 때는 기존의 연관관계인 team1과 member1의 관계도 삭제하는 코드를 추가해야한다.
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(this);
}
}
이처럼 단방향 연관관계를 2개 설정하여 양방향 연관관계 처럼 동작시키기 위해선 고민과 수고가 필요하다.
즉, 객체에서 양방향 연관관계를 사용하려면 로직을 견고하게 작성해야 한다.
- 객체의 연관관계 매핑은 단방향 연관관계 매핑만으로 이미 완료된 것이다.
- 객체를 양방향 연관관계를 맺는다는 것은 객체 그래프 탐색 기능을 추가한 것이다.
- 양방향 연관관계는 객체 양쪽 방향을 모두 관리해야 한다.
- 연관관계의 주인은 외래키가 있는 쪽이여야 한다.
강의_자바 ORM 표준 JPA 프로그래밍 - 기본편
교재_자바 ORM 표준 JPA 프로그래밍(김영한)다정한 피드백 환영해요 🤗
정리가 잘 되어있어서 이해하기 좋아요. 혹시 어떤 교재 혹은 강의로 공부하셨는지 여쭤봐도 될까요?