
자바 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 | 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입정보를 알 수 있다. |
참고