[JPA] JPA 기초 3
27 Nov 2022JPA 기초 3
단방향 연관관계 매핑

- 객체 연관관계
- 회원 객체(Member)는 Member.team 필드(멤버 변수)로 팀 객체와 연관관계를 맺는다.
- 회원은 Member.team 필드를 통해 팀을 알 수 있지만, 팀 객체로 소속된 회원들을 알 수 없기 때문에 단방향 관계이다. (member → team 의 조회는 member.getTeam()으로 가능하지만, team → member 를 접근하는 필드는 없다.)
- 객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가하여 참조를 보관하면 되지만, 엄밀히 말하면 양방향이 아닌 서로 다른 단방향 관계 2개가 된다.
- 테이블 연관관계
- 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다.
- 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고, 반대로 팀과 회원도 조인할 수 있기 때문에 양방향 관계이다. (MEMBER 테이블의 TEAM_ID 외래 키 하나로 MEMBER JOIN TEAM 과 TEAM JOIN MEMBER 둘 다 가능)
-
연관관계 사용
@Entity public class Member { @Id @GeneratedValue private Long id; @Column(name = "USERNAME") private String name; @ManyToOne @JoinColumn(name = "TEAM_ID") private Team team; // Getter, Setter ... } @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("conas"); member.setTeam(team); //단방향 연관관계 설정, 참조 저장 em.persist(member);- member에 teamId를 넣지 않고, team을 넣어준다.
- 참조로 연관관계 조회 - 객체 그래프 탐색
//조회 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);
-
@ManyToOne
- 다대일(N:1) 관계라는 매핑 정보 (위에서 봤던 회원과 팀은 다대일 관계)
- 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.
- @ManyToOne 속성

@JoinColumn
- 외래 키를 매핑할 때 사용
- 생략 가능
- 생략 시, 외래 키를 찾을 때 기본 전략을 사용
- 기본 전략 : 필드명_참조하는 테이블의 컬럼명
- @JoinColumn 속성

양방향 연관관계 매핑

@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
// 연관관계 설정
public void setTeam(Team team) {
this.team = team;
}
// Getter, Setter ...
}
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
// 연관관계 설정
public void setTeam(Team team) {
this.team = team;
}
// Getter, Setter ...
}
- Team과 Member는 일대다 관계이므로 Team 엔티티에 List
members를 추가 - 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용
- @OneToMany 어노테이션의 mappedBy 속성은 양방향 매핑일 때 반대쪽 매핑의 필드 이름을 값으로 설정하여 사용
-
일대다 컬렉션 조회
// member1: (id=1, username="회원1", team="team1") // member2: (id=2, username="회원2", team="team1") Team team = em.find(Team.class, "team1"); List<Member> members = team.getMembers(); // (팀 -> 회원) 객체 그래프 탐색 for (Member member : members) { System.out.println("member.username = " + member.getUsername()); } // === 결과 === // member.username = 회원1 // member.username = 회원2
연관관계의 주인
- 테이블은 외래 키 하나만으로 양방향 연관관계를 맺는다.
- 그에 반해 객체는 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 하는 것이 한계이다.
- 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나이다. 따라서 둘 사이에 차이가 발생
- JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라 한다.
- 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있으며, 주인이 아닌 쪽은 읽기만 할 수 있다.
- 주인은 mappedBy 속성을 사용하지 않으며, 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
-
Member.team, Team.members 둘 중 연관관계의 주인은?

class Member { @ManyToOne @JoinColumn(name = "TEAM_ID") private Team team; // ... }- 팀 -> 회원(Team.members) 방향 ~~~ java class Team { @OneToMany private List<Member> members = new ArrayList<>(); // ... }- 연관관계의 주인을 정한다는 것은 외래 키 관리자를 선택하는 것
- Member.team을 주인으로 선택하는 경우 (테이블에 외래 키가 있는 곳)
- 자기 테이블에 있는 외래 키를 관리하면 된다.
- Team.members를 주인으로 선택하는 경우
- 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 한다. (Team.members가 있는 Team 엔티티는 TEAM 테이블에 매핑되어 있는데 관리할 외래 키는 MEMBER 테이블에 있기 때문)
- Member.team을 주인으로 선택하고 Team.members에는 mappedBy=”team” 속성을 사용하여 주인이 아님을 설정해야 한다.
- mappedBy의 값으로 사용된 team은 연관관계의 주인인 Member 엔티티의 team 필드를 말한다.
- 연관관계의 주인만 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다. 주인이 아닌 반대편은 읽기만 가능하고 외래 키를 변경하지 못한다.
- 데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가지며, 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 따라서 @ManyToOne에는 mappedBy 속성이 없다.

- Member.team을 주인으로 선택하는 경우 (테이블에 외래 키가 있는 곳)
- 연관관계의 주인을 정한다는 것은 외래 키 관리자를 선택하는 것
양방향 연관관계 저장
public void save() {
// 팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
// 회원1 저장
Member member1 = new Member("member1", "회원1");
member.setTeam(team1); // 연관관계 설정 team1 -> team1
em.persist(member1);
// 회원2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // 연관관계 설정 member2 -> team1
em.persist(member2);
}

- 연관관계의 주인인 Member.team 필드를 통해서 회원과 팀의 연관관계를 설정하고 저장
- team1.getMembers().add(member1); // 무시됨 (연관관계의 주인이 아님)
- 위와 같은 코드가 추가로 있어야 할 것 같지만 Team.members는 연관관계의 주인이 아니므로 주인이 아닌 곳에 입력된 값은 외래 키에 영향을 주지 않는다.
양방향 연관관계의 주의점
Member member1 = new Member("member1", "회원1");
em.persist(member1);
Member member2 = new Member("member2", "회원2");
em.persist(member2);
Team team1 = new Team("team1", "팀1");
team1.getMembers().add(member1);
team1.getMembers().add(member2);
em.persist(team1);

- 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문에 위와 같은 현상 발생
객체 관점의 양방향 연관관계
- 사실은 객체 관점에서는 연관관계의 주인이 아닌 곳을 포함하여 양쪽 방향에 모두 값을 입력해주는 것이 안전하다.
- JPA를 사용하지 않는 순수한 객체 상태에서 문제가 발생한다.
public class Member{
private Team team;
public void setTeam(Team team){
if(this.team != null) {
// this.team이 null이 아니면 이 member객체는 team이 있음을 의미
this.team.getMembers().remove(this); // 해당 팀의 멤버에서 삭제
}
this.team = team;
team.getMembers().add(this);
}
// ...
}
public void test() {
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1);
em.persist(member1);
Member member2 = new Member("member2", "회원1");
member2.setTeam(team1);
em.persist(member2);
}
- 위의 setTeam()과 같이 수정하여 양방향 관계를 모두 설정하도록 한다. (연관관계를 변경할 때는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 한다.)
- 한 번에 양방향 관계를 설정하는 메서드를 ‘연관관계 편의 메서드’라 한다.
연관관계 매핑 총 정리
- (참고 : https://velog.io/@conatuseus/JPA-다양한-연관관계-매핑)
임베디드 타입
- JPA에서 새로운 값 타입을 직접 정의해서 사용하는 것
- 하이버네이트는 임베디드 타입을 컴포넌트(Components)라 한다.
// 임베디드 타입 사용
@Entity
public class Member {
@Id @GeneratedVAlue
private Long id;
private String name;
@Embedded
private Period workPeriod; // 근무 기간
@Embedded
private Address homeAddress; // 집 주소
}
// 기간 임베디드 타입
@Embeddable
public class Peroid {
@Temporal(TemporalType.DATE)
Date startDate;
@Temporal(TemporalType/Date)
Date endDate;
// ...
public boolean isWork (Date date) {
// .. 값 타입을 위한 메서드를 정의할 수 있다
}
}
@Embeddable
public class Address {
@Column(name="city") // 매핑할 컬럼 정의 가능
private String city;
private String street;
private String zipcode;
// ...
}

- 장점
- 재사용 및 높은 데이터 응집도
- 위의 Period 객체에서 isWork() 메서드처럼 해당 값 타입만 사용하는 의미있는 메서드 생성 가능
- 사용법
- 기본 생성자 필수
- @Embeddable : 값 타입을 정의하는 곳에 표시
- @Embeded : 값 타입을 사용하는 곳에 표시
- 임베디드 타입을 포함한 모든 값 타입은 엔티티의 생명주기에 의존한다.
- 임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이다.
임베디드 타입과 테이블 매핑

- 임베디드 타입은 엔티티의 값일 뿐이므로 값이 속한 엔티티의 테이블에 매핑한다.
- 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
- 잘 설계한(객체와 테이블을 세밀하게 매핑한) ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.
-
@AttributeOverride: 속성 재정의
// 같은 임베디드 타입을 가지고 있는 회원 @Entity public class Member { @Id @GeneratedValue private Long id; private String name; @Embedded Address homeAddress; @Embedded Address companyAddress; }- 위와 같은 경우에 테이블에 매핑하고자 하는 컬럼명이 중복됨
- 아래와 같이 @AttributeOverride를 사용하여 매핑 정보 재정의
@Entity public class Member { @Id @GeneratedValue private Long id; private String name; @Embedded Address homeAddress; @Embedded @AttributeOverrides({ @AttributeOverride(name="city", column=@Column(name="COMPANY_CITY")), @AttributeOverride(name="street", column=@Column(name="COMPANY_STREET")), @AttributeOverride(name="zipcode", column=@Column(name="COMPANY_ZIPCODE")) }) Address companyAddress; }- 생성된 테이블은 아래와 같다.
CREATE TABLE MEMBER ( COMPANY_CITY varchar(255), COMPANY_STREET varchar(255), COMPANY_ZIPCODE varchar(255), city varchar(255), street varchar(255), zipcode varchar(255), ... )- 특성
- @AttributeOverride를 사용하면 엔티티 코드가 복잡해진다. (자주 사용하지 않음)
- @AttributeOverride는 엔티티에 설정해야 한다. 임베디드 타입이 임베디드 타입을 가지고 있어도 엔티티에 설정해야 한다.
“[JPA] JPA 기초” 시리즈는 인프런 김영한 강사님의 강좌를 기반으로 쓰여졌습니다.
(참고 : https://www.inflearn.com/users/@yh)