연관관계 매핑 기초
객체와 테이블 연관관계의 차이를 이해한다. 객체의 참조와 테이블의 외래 키를 매핑하는 방법을 이해한다.
용어 이해
- 방향: 단방향, 양방향
- 다중성: 다대일, 일대다, 일대일, 다대다
- 연관관계의 주인: 객체 양방향 연관관계는 관리 주인이 필요
연관관계가 필요한 이유
예제 시나리오
- 회원과 팀이 있다.
- 회원은 하나의 팀에만 소속될 수 있다.
- 회원과 팀은 다대일 관계다.
객체를 테이블에 맞추어 모델링

@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());객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다. 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는 반면, 객체는 참조를 사용해서 연관된 객체를 찾는다. 테이블과 객체 사이에는 이런 큰 간격이 존재한다.
단방향 연관관계
객체 지향 모델링

@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;
…
}
//팀 저장
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);양방향 연관관계와 연관관계의 주인 1 - 기본
양방향 매핑

객체의 양방향 관계
객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개다. 객체를 양방향으로 참조하려면 단방향 연관관계 2개를 만들어야 한다.
- 회원 → 팀
member.getTeam() - 팀 → 회원
team.getMembers()
테이블의 양방향 관계
테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다. MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계를 가진다.
- 회원 ← → 팀
SELECT * FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_IDSELECT * FROM TEAM T JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
둘 중 하나로 외래 키를 관리해야 함
Member 의 Team 이나 Team 의 members 중에 어떤 값이 바뀔 때 MEMBER.TEAM_ID 를 업데이트 해야 하는가?
둘 중에 하나가 외래 키를 관리하도록 정해주는 것이 연관관계의 주인이다.
연관관계의 주인
객체의 두 관계 중 하나를 연관관계의 주인으로 지정한다. 연관관계의 주인만이 외래 키를 관리할 수 있으며, 주인이 아닌쪽은 읽기만 가능하다.
- 주인은 mappedBy 속성을 사용하지 않는다.
- 주인이 아니면 mappedBy 속성으로 주인을 지정해야 한다.
그러면 누구를 주인으로 정해야해?
외래 키가 있는 곳을 주인으로 정해야한다.
DB 에서 ManyToOne 관계일 경우, 항상 FK 를 갖는 쪽은 Many 가 되기 때문에, Many 쪽이 연관관계의 주인이 되는 것이 좋다.
위 예시에서는 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);위 코드 실행 시 연관관계의 주인에 값을 입력하지 않았기 때문에 member 의 team 은 null 이 된다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team); // 연관관계의 주인에만 값 설정
em.persist(member);
team.getMembers(); // null연관관계의 주인에만 값을 넣어준다면, JPA 에 의해 mappedBy 쪽에서도 값을 가져올 수 있지만, 2가지 문제점이 있을 수 있다.
em.flush()이후em.clear()를 한 뒤 DB 에서 값을 가져온다면 문제가 없지만, 영속성 컨텍스트에 Team 과 Member 를 저장한 뒤 사용할 경우,team.getMembers()를 호출한다면null값이 반환된다.- 테스트 케이스는 대부분 JPA 없이 순수 자바 코드로 이루어지는데, 이럴 경우 null 이 나올 수 있다.
양방향 연관관계 주의
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team); // 연관관계의 주인에 값 설정
em.persist(member);
team.getMembers().add(member); // 역방향에도 값 설정- 위에서 언급한 2가지 문제점과 더불어, 사용되는 코드가 객체지향적이지 못하기 때문에 가급적이면 위 코드 처럼 양쪽에 값을 다 넣어주는 것이 좋다.
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}- 또한, 연관관계 설정 시 양쪽에 모두 값을 설정할 수 있도록 연관관계 편의 메서드를 생성하자.
- 양방향 매핑 시
toString()와 JSON 생성 라이브러리 사용 시member의team을 참조하고team의member를 참조하면서 무한 루프가 발생할 수 있기 때문에 가급적이면toString()을 사용하지 말고Controller에는 엔티티를 절대 반환하지 않도록 하자.Controller가 DTO 를 반환하는 이유가 여기에 있다.
양방향 매핑 정리
- 단방향 매핑만으로도 이미 연관관계 매핑은 완료된 것이다.
- 양방향 매핑은 반대 방향으로 조회(객체 탐색 그래프) 기능이 추가된 것 뿐이다.
- JPQL 에서 역방향으로 탐색할 일이 많다.
- 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 된다. 테이블에 영향을 주지 않는다.
연관관계의 주인을 정하는 기준
- 연관관계의 주인은 외래 키의 위치를 기준으로 정해야한다.
실전 예제 2 - 연관관계 매핑 시작
테이블 구조
테이블 구조는 이전과 같다.
객체 구조
참조를 사용하도록 변경한다.
연관관계 매핑 시 고려사항 3가지
다중성
@ManyToOne다대일@OneToMany일대다@OneToOne일대일@ManyToMany다대다
단방향, 양방향
테이블
- 외래 키 하나로 양쪽 조인 가능
- 사실 방향이라는 개념이 없음 객체
- 참조용 필드가 있는 쪽으로만 참조 가능
- 한쪽만 참조하면 단방향
- 양쪽이 서로 참조하면 양방향
연관관계 주인
- 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺음
- 객체 양방향 관계는 참조가 2군데 있음
- 둘 중 테이블의 외래 키를 관리할 곳을 지정해야 함
- 즉, 연관관계의 주인은 외래키를 관리하는 참조이고, 주인의 반대편은 단순 조회만 가능
다대일 [N:1]
다대일 단방향

- 가장 많이 사용하는 연관관계
- 다대일의 반대는 일대다
다대일 양방향

- 외래 키가 있는 쪽이 연관관계의 주인
- 양쪽을 서로 참조하도록 개발
일대다 [1:N]
일대다 단방향
결론부터 말하자면 이런 구조는 거의 가져가지 않는다.
일대다 단뱡향은 일대다에서 일이 연관관계의 주인이 된다.
하지만, 테이블 일대다 관계에서는 항상 다쪽에 외래 키가 존재한다.
이 경우, 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조가 된다.
Member member = new Member();
member.setUserName("member1");
em.persist(member);
Team team = new Team();
team.setName("teamA");
team.getMembers().add(member);
em.persist(team);해당 코드를 실행했을 경우, 테이블 입장에서는 MEMBER 가 TEAM_ID 를 가지고 있기 때문에, TEAM 을 추가하기 위해 MEMBER 테이블에 UPDATE 쿼리가 추가로 나가게 된다.
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
}또한, 위처럼 일대다 관계를 갖는 컬럼에 @JoinColumn 을 꼭 사용해야 한다.
그렇지 않으면 TEAM_ID 와 MEMBER_ID 를 가지고 있는 중간 테이블을 하나 추가하여 조인 테이블 방식을 사용하여 외래 키를 관리하게 된다.
일대다 단방향 정리
일대다 단방향 매핑의 단점
- 엔티티가 관리하는 외래 키가 다른 테이블에 있음
- 연관관계 관리를 위해 추가로 UPDATE SQL 실행 때문에, 객체지향적으로 손해를 보더라도 일대다 단방향 매핑보다는 다대일 양방향 구조를 사용하는 관계로 만들어주는 것이 더 좋은 설계일 수 있다.
일대다 양방향
이런 매핑은 공식적으로 존재하지 않지만 만들 수는 있다.
이런 경우, @JoinColumn(insertable=false, updatable=false) 로 읽기 전용 필드를 사용해서 양방향 처럼 사용할 수 있다.
이런 구조를 사용하기 보다는 다대일 양방향 구조를 사용하는 것이 좋다.
일대일 [1:1]
일대일 관계
일대일 관계는 그 반대도 일대일이기 때문에 대칭 관계라고 생각해볼 수 있다. 때문에, 주 테이블이나 대상 테이블 중에 한 곳에서 외래 키를 선택할 수 있다. 테이블 관점에서 일대일 관계를 구현하기 위해선 외래 키에 UNI 제약조건이 추가되어야 한다.
주 테이블에 외래 키 단방향
@ManyToOne 단방향 매핑과 유사하다는 것을 알 수 있다.
주 테이블에 외래 키 양방향
다대일 양방향 매핑 처럼 외래 키가 있는 곳이 연관관계의 주인이 되며, 반대편은 mappedBy 를 적용하면 된다.
대상 테이블에 외래 키 단방향
단방향 관계는 JPA 에서 지원하지 않지만, 양방향 관계는 지원한다.
MEMBER 테이블을 많이 SELECT 한다는 가정하에, MEMBER 가 LOCKER 를 가지고 있는 것이 더 효율적이다.
대상 테이블에 외래 키 양방향
사실 일대일 주 테이블에 외래 키 양방향과 매핑 방법은 같다.
일대일 정리
주 테이블에 외래 키를 위치시키면, 주 객체가 대상 객체의 참조를 가지는 것 처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾을 수 있다.
이는 객체지향 개발자들이 선호하는 방법이며, JPA 매핑이 편리하다는 점이 있다.
주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인이 가능하다는 장점이 있지만, 값이 없으면 외래 키에 null 을 허용한다는 단점 역시 존재한다.
대상 테이블에 외래 키를 위치시키면, 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 다쪽이 되는 테이블에 이미 외래 키가 존재하기 때문에, 테이블 구조를 유지하고 변경할 수 있다는 장점이 존재한다.
하지만, 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩되는 단점 역시 존재한다.
다대다 [N:M]
관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
이를 구현하기 위해 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 한다.
객체는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하다.
@ManyToMany 와 @JoinTable 로 연결 테이블을 지정해서 사용해야 한다.
다대다 매핑의 한계
편리해 보이지만 실무에서 사용하지 않는다. 연결 테이블이 단순히 연결만 하고 끝나지 않고, 주문시간, 수량 같은 데이터가 들어올 수 있다.
다대다 한계 극복
연결 테이블을 엔티티로 승격시켜 연결 테이블용 엔티티를 추가한다.
@ManyToMany → @OneToMany @ManyToOne
실전 예제 3 - 다양한 연관간계 매핑

N:M 관계는 1:N, N:1 로 변경
테이블의 N:M 관계는 중간 테이블을 이용해서 1:N, N:1 로 변경하는 것이 좋다. 실전에서는 중간 테이블이 단순하지 않기 때문에 N:M 은 사용하지 않는다.
@JoinColumn
외래 키를 매핑할 때 사용한다.
name매핑할 외래 키 이름referencedColumnName외래 키가 참조하는 대상 테이블의 컬럼명foreignKey(DDL)외래 키 제약조건을 직접 지정할 수 있음uniquenullableinsertableupdatablecolumnDefinitiontable@Column 의 속성과 같음
@ManyToOne
다대일 관계 매핑 시 사용한다.
optionalfalse 로 설정 시 연관된 엔티티가 항상 있어야 함fetch글로벌 페치 전략을 설정cascade영속성 전이 기능을 사용targetEntity연관된 엔티티 타입 정보를 설정. 제네릭으로 타입 정보를 알 수 있기 때문에 거의 사용하지 않음
@OneToMany
다대일 관계 매핑 시 사용한다.
mappedBy연관관계의 주인 필드를 선택fetch글로벌 페치 전략을 설정cascade영속성 전이 기능을 사용targetEntity연관된 엔티티 타입 정보를 설정. 제네릭으로 타입 정보를 알 수 있기 때문에 거의 사용하지 않음
상속관계 매핑
객체는 상속관계가 있지만 RDB 에는 상속관계가 없다.
RDB 의 슈퍼타입 서브타입 관계라는 모델링 기법이 객체의 상속과 유사하다.
즉, 상속관계 매핑이란, 객체의 상속구조와 RDB 의 슈퍼타입 서브타입 관계를 매핑하는 것을 의미한다.
슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 법은 총 3가지가 존재한다.
- 조인 전략
- 단일 테이블 전략
- 구현 클래스마다 테이블 전략
조인 전략
@Inheritance*(*strategy = InheritanceType.JOINED*)*
@DiscriminatorColumn(name = "DTYPE") 기본값은 “DTYPE” 이며, 자식 테이블을 구분하기 위해 DTYPE 이 추가되는 것이 좋다.
@DiscriminatorValue("XXX") 기본값은 “클래스명” 이며, 자식 테이블의 테이블명을 명시해주고 싶을 때 사용할 수 있다.
조인 전략의 장점으로는, 테이블이 정규화 되어있고, 외래 키 참조 무결성 제약조건을 활용할 수 있으며, 효율적으로 저장공간을 사용할 수 있다는 점이 있다.
반면, 단점으로는, 조회 시 조인을 많이 사용되어 성능이 저하되고, 데이터 저장 시 INSERT 쿼리가 2번 호출되는 점이 있다.
객체의 상속과 가장 유사하기 때문에 기본적으론, 조인 전략이 정석이라고 생각하는 것이 좋다.
단일 테이블 전략
@Inheritance*(*strategy = InheritanceType.SINGLE_TABLE*)*
단일 테이블 전략은 모든 자식 테이블 요소를 한 테이블에서 관리하기 때문에 자식 테이블을 구분하기 위하여DTYPE 를 자동으로 추가해준다.
단일 테이블 전략의 장점으로는, 조인이 필요 없기에 일반적으로 조회가 빠르다는 점이 있다.
반면, 단점으로는, 자식 엔티티가 매핑한 컬럼은 모두 null 을 허용해야하며, 단일 테이블에 모든 것을 저장하기 때문에 상황에 따라 조회 성능이 오히려 느려질 수 있다는 점이 있다.
구현 클래스마다 테이블 전략
@Inheritance*(*strategy = InheritanceType.TABLE_PER_CLASS*)*
부모 테이블이 없기 때문에 Item 타입으로 조회 시 모든 자식 테이블을 뒤져서 결과를 찾는다.
이 전략은 사용하지 않는 것이 좋다.
@MappedSuperclass - 매핑 정보 상속
@MappedSuperclass
public abstract class BaseEntity {
private String createdBy;
private LocalDateTime createdDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
}공통 매핑 정보가 필요할 때 사용한다. 즉, 속성만 상속받아서 쓰고 싶을 때 사용한다. 테이블과 관계 없이 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할이다. 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용한다. 직접 생성해서 사용할 일이 거의 없기 때문에 추상 클래스를 권장한다.
실전 예제 4 - 상속관계 매핑
도메인 모델
