자바 ORM 표준 JPA 프로그래밍 - 기본편 수업을 듣고 정리한 내용입니다.
✔️ 연관관계 매핑시 고려사항 3가지
(1) 다중성
(2) 단방향, 양방향
(3) 연관관계의 주인
1. 다중성
- 다대일 :
@ManyToOne
- 일대다 :
@OneToMany
- 일대일 :
@OneToOne
- 다대다 :
@ManyToMany
2. 단방향, 양방향
테이블
- 외래 키 하나로 양쪽 조인 가능하기 때문에 방향이라는 개념이 없다.
객체
- 참조용 필드가 있는 쪽으로만 참조가 가능하다.
- 한쪽만 참조 : 단방향
- 양쪽이 서로 참조 : 양방향 (단방향이 두 개인 것이다.)
3. 연관관계의 주인
- 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺는다.
- 객체 양방향 관계는
A → B
,B → A
처럼 참조가 두 곳이다.- 객체 양방향 관계는 참조가 두 곳이 있으므로 둘중 테이블의 외래 키를 관리할 곳을 지정해야한다.
- 연관관계의 주인 : 외래 키를 관리하는 참조
- 주인의 반대편 : 외래 키에 영향을 주지 않음, 단순 조회만 가능
다
가 연관관계 주인이다.
(1) 회원 엔티티
@Entity
public class Member
{
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne @JoinColumn(name = "TEAM_ID")
private Team team;
...
}
(2) 팀 엔티티
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
...
}
Member.team
으로 참조가 가능하지만, 팀에선 회원을 참조할 필드가 없어 단방향이다.Member
에만 존재한다.
jpamain
try {
Team team = new Team();
team.setName("team1");
em.persist(team);
Member member = new Member();
member.setName("chang");
member.setTeam(team);
em.persist(member);
Member member2 = new Member();
member2.setName("kChang");
member2.setTeam(team);
em.persist(member2);
tx.commit();
// HelloAb
}
H2
(1) 회원 엔티티
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne @JoinColumn(name = "TEAM_ID")
private Team team;
public void setTeam(Team team) {
this.team = team; // 무한루프 방지
if(!team.getMembers().contains(this))
{
team.getMembers().add(this);
}
}
}
(2) 팀 엔티티
@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<>();
public void addMember(Member member) {
this.members.add(member); // 무한루프 방지
if (member.getTeam() != this) {
member.setTeam(this);
}
}
}
List
가 추가되었고, 주인 필드인 Member.team
을 가르키고 있다.Team.members
는 주인이 아니므로 조회가 필요할 때 사용한다.setTeam
, addMember
는 서로를 참조할 때 무한루프에 빠지지 않도록 처리되어 있다.
JpaMain
Team team = new Team();
team.setName("team1");
em.persist(team);
Member member = new Member();
member.setName("chang");
member.setTeam(team);
em.persist(member);
Member member2 = new Member();
member2.setName("kChang");
member2.setTeam(team);
em.persist(member2);
em.flush();
em.clear();
Member foundMember = em.find(Member.class, member.getId());
Team team2 = foundMember.getTeam();
List<Member> members = team2.getMembers();
for (Member member1 : members) {
System.out.println("member1.getName() = " + member1.getName());
}
실행 결과
일
이 연관관계의 주인이다.
Team
을 중심으로 외래 키를 관리하고, Member
입장에서는 Team
에 대한 참조가 없다.N
) 쪽에 외래 키가 있다.➡️ 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조이다!
(1) 팀 엔티티
@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<>();
...
}
(2) 회원 엔티티
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
...
}
@JoinColumn
을 보면 일대다
인 경우에도 N
(다)인 쪽에 외래키가 존재함을 알 수 있다.
JpaMain
try {
Member member = new Member();
member.setUsername("chang");
em.persist(member);
Team team = new Team();
team.setName("team1");
team.getMembers().add(member);
em.persist(team);
Team team2 = new Team();
team2.setName("team2");
team2.getMembers().add(member);
em.persist(team2);
tx.commit();
}
실행 결과
✔️ 일대다 단방향을 권장하지 않는 이유
N
) 쪽에 외래 키가 있기 때문에 패러다임 충돌이 발생한다.@JoinColumn
을 꼭 사용해야 한다. 사용하지 않는다면 조인 테이블 방식을 사용해야 한다.Team
의 List members
값을 변경하면 다른 테이블(Member
) 속 외래 키 TEAM_ID
를 업데이트 해줘야 한다.
✏️ 일대다 단방향 매핑의 단점
- 엔티티가 관리하는 외래 키가 다른 테이블에 있다.
- 연관관계 관리를 위해 추가로
UPDATE SQL
을 실행해야 한다.➡️ 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자!
(1) 팀 엔티티
@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<>();
...
}
(2) 회원 엔티티
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
...
}
@JoinColumn(insertable=false, updatable=false)
소스를 보면 양방향으로 연관관계 주인을(JoinColumn
) 선언하였다. 이는 어긋난 것이다. (외래 키는 하나만 있어야 하고, 주인인 곳에만 있어야 한다.)
➡️ 일대다를 사용하지말고, 다대일 양방향을 사용하자!
일대일
에서 일대다
로 변경할 때 테이블 구조가 그대로 유지된다. UNI
) 제약조건 추가
@ManyToOne
) 단방향 매핑과 유사하다.
(1) 회원 엔티티
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
...
}
(2) 라커룸 엔티티
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
...
}
주 테이블인 회원에 Locker
필드(외래키)가 포함되어 있다.
실행 결과
mappedBy
적용
(1) 회원 엔티티
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
...
}
(2) 라커룸 엔티티
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "member")
private Member member;
...
}
주인 필드인 회원 엔티티가 외래키를 가지므로 주인 필드를 나타내는 mappedBy
속성과 @OneToOne
어노테이션을 추가하였다.
📌 일대일 정리
- 주 테이블에 외래 키
- 주 객체가 대상 객체의 참조를 가지는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾는다.
- 객체지향 개발자 선호
- JPA 매핑 편리
- 장점 : 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능하다.
- 단점 : 값이 없으면 외래 키에
null
허용- 대상 테이블에 외래 키
- 대상 테이블에 외래 키가 존재
- 전통적인 데이터베이스 개발자 선호
- 장점 : 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
- 단점 : 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨(프록시는 뒤에서 추가 공부한다.)
실무에서 사용하지 않는다!
다대다
관계를 표현할 수 없으며 두 테이블을 연결하는 별도의 테이블이 필요하다.일대다
, 다대일
관계로 풀어야한다.1. 회원 : 회원_상품 = 1:N
2. 회원_상품 : 상품 = M:1
@ManToMany
사용한다. (회원 엔티티에)@JoinTable
로 연결 테이블을 지정한다.
다대다 단방향
(1) 회원 엔티티
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT", joinColumns = @JoinColumn(name = "MEMBER_ID"), inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
private List<Product> products = new ArrayList<>();
...
}
(2) 상품 엔티티
@Entity
public class Product {
@Id @Column(name = "PRODUCT_ID")
private String id;
private String name;
...
}
다대다 양방향
(1) 회원 엔티티
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT", joinColumns = @JoinColumn(name = "MEMBER_ID"), inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
private List<Product> products = new ArrayList<>();
...
}
(2) 상품 엔티티
@Entity
public class Product {
@Id @Column(name = "PRODUCT_ID")
private String id;
private String name;
// 상품 엔티티에 역방향 참조 추가
@ManyToMany(mappedBy = "products")
private List<Member> members;
...
}
@ManyToMany
를 지정하고, mappedBy
로 주인 필드를 지정한다.
✔️ 다대다 매핑의 한계
✔️ 다대다 한계 극복
@ManyToMany
→ @OneToMany
, @ManyToOne
✔️ 엔티티
@OneToOne
)@ManyToMany
)
✔️ ERD
✔️ 테이블이 추가된 ERD 분석
주문과 배송 : 주문(ORDERS
)와 배송(DELIVERY
)은 일대일 관계다. 객체 관계를 고려할 때 주문에서 배송으로 자주 접근할 예정이므로 외래 키를 주문 테이블에 두었다. 참고로 일대일 관계이므로 ORDERS
테이블에 있는 DELIVERY_ID
외래 키에는 유니크 제약조건을 주는 것이 좋다.
상품과 카테고리 : 한 상품은 여러 카테고리(CATEGORY
)에 속할 수 있고, 한 카테고리도 여러 상품을 가질 수 있으므로 둘은 다대다
관계다. 테이블로 이런 다대다
관계를 표현하기는 어려우므로 CATEGORY_ITEM
연결 테이블을 추가해서 다대다
관계를 일대다
, 다대일
관계로 풀어냈다.
추가된 요구사항을 객체에 반영해서 아래 그림의 상세한 엔티티를 완성했다!
✔️ 엔티티 상세
Order
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member; //주문 회원
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems = new ArrayList<OrderItem>();
@OneToOne
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery; //배송정보
private Date orderDate; //주문시간
@Enumerated(EnumType.STRING)
private OrderStatus status;//주문상태
//==연관관계 메서드==//
public void setMember(Member member) {
//기존 관계 제거
if (this.member != null) {
this.member.getOrders().remove(this);
}
this.member = member;
member.getOrders().add(this);
}
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
public void setDelivery(Delivery delivery) {
this.delivery = delivery;
delivery.setOrder(this);
}
//Getter, Setter
...
}
Delivery
import javax.persistence.*;
@Entity
public class Delivery {
@Id @GeneratedValue
@Column(name = "DELIVERY_ID")
private Long id;
@OneToOne(mappedBy = "delivery")
private Order order;
private String city;
private String street;
private String zipcode;
@Enumerated(EnumType.STRING)
private DeliveryStatus status; //ENUM [READY(준비), COMP(배송)]
//Getter, Setter
...
}
DeliveryStatus
public enum DeliveryStatus {
READY, //준비
COMP //배송
}
Order
와 Delivery
는 일대일 관계고 그 반대도 일대일 관계다.Order
가 매핑된 ORDERS
를 주 테이블로 보고 주 테이블에 외래 키를 두었다.Order.delivery
가 연관관계의 주인이다.Delivery.order
필드에는 mappedBy
속성을 사용해서 주인이 아님을 표시했다.
Category
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Category {
@Id @GeneratedValue
@Column(name = "CATEGORY_ID")
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "CATEGORY_ITEM",
joinColumns = @JoinColumn(name = "CATEGORY_ID"),
inverseJoinColumns = @JoinColumn(name = "ITEM_ID"))
private List<Item> items = new ArrayList<Item>();
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Category parent;
@OneToMany(mappedBy = "parent")
private List<Category> child = new ArrayList<Category>();
//==연관관계 메서드==//
public void addChildCategory(Category child) {
this.child.add(child);
child.setParent(this);
}
public void addItem(Item item) {
items.add(item);
}
//Getter, Setter
...
}
Item
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Item {
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name; //이름
private int price; //가격
private int stockQuantity; //재고수량
@ManyToMany(mappedBy = "items")
private List<Category> categories = new ArrayList<Category>();
//Getter, Setter
...
}
Category
와 Item
은 다대다 관계고 그 반대도 다대다 관계다. Category.items
필드를 보면 @ManyToMany
와 @JoinTable
을 사용해서 CATEGORY_ITEM
연결 테이블을 바로 매핑했다. Category
를 연관관계의 주인으로 정했다. Item.categories
필드에는 mappedBy
속성을 사용해서 주인이 아님을 표시했다.
다대다 관계는 연결 테이블을 JPA가 알아서 처리해주므로 편리하지만 연결 테이블에 필드가 추가되면 더는 사용할 수 없으므로 실무에서 활용하기에는 무리가 있다. 따라서 CategoryItem
이라는 연결 엔티티를 만들어 일대다
, 다대일
관계로 매핑하는 것을 권장한다!
✔️ @JoinColumn
외래 키를 매핑할 때 사용
속성 | 설명 | 기본값 |
---|---|---|
name | 매핑할 외래 키 이름 | 필드명 + _ + 참조하는 테 이블의 기본 키 컬럼명 |
referencedColumnName | 외래 키가 참조하는 대상 테이블의 컬럼명 | 참조하는 테이블의 기본 키 컬럼명 |
foreignKey(DDL) | 외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다. | |
unique nullable insertable updatable columnDefinition table | @Column의 속성과 같다. |
✔️ @ManyToOne
다대일 관계 매핑
속성 | 설명 | 기본값 |
---|---|---|
optional | false로 설정하면 연관된 엔티티가 항상 있어야 한다. | TRUE |
fetch | 글로벌 페치 전략을 설정한다. | - @ManyToOne=FetchType.EAGER - @OneToMany=FetchType.LAZY |
cascade | 영속성 전이 기능을 사용한다. | |
targetEntity | 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입정보를 알 수 있다. |
✔️ @OneToMany
다대일 관계 매핑
속성 | 설명 | 기본값 |
---|---|---|
mappedBy | 연관관계의 주인 필드를 선택한다. | |
fetch | 글로벌 페치 전략을 설정한다. | - @ManyToOne=FetchType.EAGER - @OneToMany=FetchType.LAZY |
cascade | 영속성 전이 기능을 사용한다. | |
targetEntity | 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입정보를 알 수 있다. |
참고