1. 연관관계 매핑 소개
- 연관관계 매핑이란 객체의 참조와 테이블의 외래 키를 매핑하는 작업입니다.
- 객체와 테이블은 연관관계를 맺는 방식에 근본적인 차이가 있습니다
- 객체는 참조(주소)로 연관관계를 맺으며, 이는 항상 단방향입니다.
- 테이블은 외래 키로 연관관계를 맺으며, 이는 항상 양방향입니다.
- 이러한 차이를 이해하고 효과적으로 매핑하는 방법을 알아보겠습니다.
2. 연관관계 매핑 시 고려사항
- 연관관계를 매핑할 때는 다음 세 가지를 고려해야 합니다
- 다중성
- 방향
- 연관관계의 주인
2.1 다중성
- 다중성은 엔티티 간의 관계가 몇 대 몇인지를 나타냅니다.
- 연관관계 매핑 시 다중성을 나타내는 어노테이션은 필수적으로 사용해야 합니다.
- 다중성의 종류:
- 다대일(
@ManyToOne
) - 일대다(
@OneToMany
) - 일대일(
@OneToOne
) - 다대다(
@ManyToMany
)
- 다대일(
2.1.1 @ManyToOne
- 다대일 관계를 나타내는 매핑 정보입니다.
- 가장 많이 사용하는 연관관계입니다.
속성 | 설명 | 기본값 |
---|---|---|
optional | false로 설정하면 연관된 엔티티가 항상 있어야 합니다. | TRUE |
fetch | 글로벌 페치 전략을 설정합니다. | FetchType.EAGER |
cascade | 영속성 전이 기능을 사용합니다. | |
targetEntity | 연관된 엔티티의 타입 정보를 설정합니다. 이 기능은 거의 사용하지 않습니다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있습니다. |
2.1.2 @OneToMany
- 일대다 관계를 나타내는 매핑 정보입니다.
- 양방향 매핑에서 주로 사용합니다.
경고
일대다 단방향 매핑 사용 시 주의사항
- 일이 연관관계의 주인인 경우, 엔티티가 관리하는 외래 키가 다른 테이블에 있게 됩니다.
- 연관관계 관리를 위해 추가로 UPDATE SQL이 실행됩니다.
- 일대다 단방향보다는 다대일 양방향 매핑을 사용하는 것이 좋습니다.
- @JoinColumn을 꼭 사용해야 합니다. 그렇지 않으면 조인 테이블 방식을 사용하게 됩니다.
속성 | 설명 | 기본값 |
---|---|---|
mappedBy | 연관관계의 주인 필드의 이름을 값으로 지정해야 합니다. | |
fetch | 글로벌 페치 전략을 설정합니다. | FetchType.LAZY |
cascade | 영속성 전이 기능을 사용합니다. | |
targetEntity | 연관된 엔티티의 타입 정보를 설정합니다. 이 기능은 거의 사용하지 않습니다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있습니다. |
2.1.3 @OneToOne
- 일대일 관계를 나타내는 매핑 정보입니다.
- 주 테이블이나 대상 테이블 중 하나에 외래 키를 설정합니다.
- 다대일 양방향 매핑처럼 외래 키가 있는 곳이 연관관계의 주인입니다.
- 주인이 아닌 곳은 mappedBy 속성을 적용합니다.
주 테이블에 외래 키를 설정하는 경우
- 주 객체가 대상 객체의 참조를 가지는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾습니다.
- 객체지향 개발자가 선호하는 방식입니다.
- JPA 매핑이 편리합니다.
- 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인이 가능합니다.
- 값이 없으면 외래 키에 null을 허용해야 합니다.
대상 테이블에 외래 키를 설정하는 경우
- 대상 테이블에 외래 키가 존재합니다.
- 전통적인 데이터베이스 개발자가 선호하는 방식입니다.
- 주 테이블에 외래 키를 설정하는 것과 달리 null을 허용하지 않아도 됩니다.
- 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조를 그대로 유지할 수 있습니다.
정보
지연 로딩의 한계 대상 테이블에 외래 키가 있는 경우, 연관관계의 주인이 아닌 엔티티를 조회할 때 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됩니다.
예를 들어:
- 멤버와 락커 테이블이 존재하고, 대상 테이블인 락커에 외래키가 있다면(락커가 연관관계의 주인)
- 멤버를 조회할 때(연관관계의 주인이 아닌 엔티티)
- 멤버 테이블만 조회해서는 락커를 같이 조회할 수 없어 락커 테이블을 추가로 조회하는 쿼리가 필요합니다.
- 따라서 지연 로딩으로 설정해도 즉시 로딩처럼 쿼리가 발생하게 됩니다.
경고
단방향 관계 제한 대상 테이블에 외래 키가 있는 단방향 관계는 JPA가 지원하지 않습니다. 따라서 일대일 매핑에서 대상 테이블에 외래 키를 두고 싶으면 양방향으로 매핑해야 합니다.
2.1.4 @ManyToMany
- 다대다 관계를 나타내는 매핑 정보입니다.
- 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없습니다.
- 연결 테이블을 사용해서 일대다, 다대일 관계로 풀어내야 합니다.
@JoinTable
로 연결 테이블을 지정합니다.
위험
실무에서는 @ManyToMany를 사용하지 않습니다.
- 연결 테이블을 엔티티로 승격시켜 다대다 관계를 일대다, 다대일 관계로 풀어내는 것이 좋습니다.
- 이렇게 하면 추가 정보를 넣을 수 있고 더 유연한 설계가 가능합니다.
다대다 연관관계 구현 방법
다대다 관계를 일대다, 다대일 관계로 풀어내기 위해 연결 테이블을 만들 때 식별자를 구성하는 방법:
- 식별 관계: 받아온 식별자를 기본키 + 외래 키로 사용
- 비식별 관계: 받아온 식별자를 외래 키로만 사용하고 새로운 식별자를 추가
팁
비식별 관계를 사용하면 복합 키를 위한 식별자 클래스를 만들지 않아도 되므로 편리하게 ORM 매핑을 할 수 있습니다. 따라서 비식별 관계를 추천합니다.
2.2 방향
연관관계는 방향에 따라 단방향과 양방향으로 나눌 수 있습니다.
2.2.1 단방향 연관관계
@JoinColumn
- 외래 키를 매핑할 때 사용합니다.
속성 | 설명 |
---|---|
name | 매핑할 외래 키의 이름 |
2.2.2 양방향 연관관계
- 단방향 매핑만으로도 이미 연관관계 매핑은 완료됩니다.
- 따라서 양방향 연관관계가 필수적이지 않다면 단방향 매핑으로 끝내는 것이 좋습니다.
- JPQL에서 역방향으로 탐색할 일이 발생한다면 양방향 연관관계를 고려해볼 수 있습니다.
- 테이블에 영향을 주지 않기 때문에 양방향 매핑은 필요할 때 추가하면 됩니다.
- 양방향의 장점은 반대 방향으로 객체 그래프 탐색 기능이 추가된다는 것뿐입니다.
객체의 양방향 관계
- 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개입니다.
- 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 합니다:
Member.team
Team.members
테이블의 양방향 연관관계
- 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리합니다:
TEAM_ID
양방향 매핑 예시
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
//연관관계의 주인
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<Member>();
}
- 양쪽 방향으로 객체 그래프 탐색이 가능합니다:
//Member -> Team 조회
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
//Team -> Member 조회
Team findTeam = em.find(Team.class, team.getId());
int memberSize = findTeam.getMembers().size();
2.3 연관관계의 주인
- 엄밀히 말하면 객체에서는 양방향 연관관계라는 것은 없습니다.
- 서로 다른 단방향 연관관계 2개를 논리적으로 묶어서 양방향인 것처럼 보이게 하는 것입니다.
- 반면에 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리합니다.
- 엔티티를 단방향으로 매핑하면 참조를 하나만 사용하게 되고 이 참조로 외래키를 관리하면 됩니다.
- 반면에 엔티티를 양방향으로 매핑하면 참조는 두 개가 되지만 외래 키는 하나입니다.
- 따라서 두 개의 참조 중 하나를 선택해 외래 키를 관리하도록 해야 합니다.
- 이렇게 외래 키를 관리하는 쪽을 연관관계의 주인이라고 합니다.
연관관계의 주인(Owner)
- 객체의 두 관계 중 하나를 연관관계의 주인으로 지정해야 합니다.
- 외래 키가 있는 있는 곳을 주인으로 정합니다.
- 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있습니다.
- 주인이 아닌 쪽은 읽기만 가능합니다.
- 주인이 아니면 mappedBy 속성으로 주인을 지정합니다.
주의사항
- 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안 됩니다.
- 양방향 매핑 시 연관관계의 주인에 값을 입력해야 합니다.
- 데이터베이스에 외래 키 값이 정상적으로 저장되지 않으면 이 부분을 확인해보세요.
- 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하는 것이 좋습니다.
- 연관관계 편의 메서드를 생성하는 것이 좋습니다.
- 양방향 매핑 시에 무한 루프를 조심해야 합니다.
- 예: toString(), lombok, JSON 생성 라이브러리
3. 상속관계 매핑
- 관계형 데이터베이스는 상속 관계가 없으며, 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사합니다.
- 상속관계 매핑이란 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것입니다.
- 조인 전략, 단일 테이블 전략, 구현 클래스마다 테이블 전략이라는 세 가지 방법이 있습니다.
3.1 조인 전략
- 엔티티 각각을 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략입니다.
- 조회할 때 조인을 사용합니다.
- 객체는 타입으로 구분할 수 있지만 테이블은 타입 개념이 없어 구분 컬럼(DTYPE)을 추가해야 합니다.
장점
- 테이블 정규화
- 외래 키 참조 무결성 제약조건 활용 가능
- 저장공간 효율적 사용
단점
- 조회 시 조인을 많이 사용해 성능이 저하될 수 있음
- 조회 쿼리가 복잡함
- 데이터 저장 시 INSERT SQL이 2번 호출됨
예시
@DiscriminatorColumn(name = "DTYPE")
@Inheritance(strategy = InheritanceType.JOINED)
@Entity
public abstract class Item {
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
}
@DiscriminatorValue("A")
@Entity
public class Album extends Item {
private String artist;
}
@DiscriminatorValue("M")
@Entity
public class Movie extends Item {
private String director;
private String actor;
}