[자바 ORM 표준 JPA 프로그래밍 - 기본편] 5. 연관관계 매핑 기초

jada·2024년 3월 25일
0

Spring 스터디

목록 보기
24/35

📂 연관관계 매핑 기초

- 객체와 테이블 연관관계의 차이를 이해
- 객체의 참조와 테이블의 외래키를 매핑
- 용어 이해
- 📝 방향: 단방향, 양방향
- 📝 다중성: 다대일, 일대다, 일대일, 다대다 이해
- 📝 연관관계의 주인: 객체 양방향 연관관계는 관리 주인이 필요

❗ 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, -> 협력 관계를 만들 수 없다 !!
- 테이블은 외래키로 join을 사용해서 연관된 테이블을 찾는다.
- 객체는 참조를 사용해서 연관된 객체를 찾는다.
- 테이블과 객체 사이에는 이런 큰 간격이 있다. (패러다임이 완전히 다르다.)

단방향 연관관계


가장 중요하고 기본이 되는 연관관계이다.

객체 지향 모델링 ) Team의 Id가 아니라 Team의 참조값을 그대로 가져왔다.


Member 입장에서는 Many, Team 입장에서는 One이므로 Member 엔티티의 Team 필드에 애노테이션은 @ManyToOne이 된다. 여기서의 필드 Team은 DB의 Member 테이블의 TEAM_ID(FK)와 매핑이 되므로, @JoinColumn 애노테이션을 이용해 어떤 column과 join해야하는지 명시해준다.

객체 지향 모델링을 해주었으므로, 연관관계 저장 및 조회가 다음과 같이 객체지향스럽게 reference로 가지고 오는 것을 통해 진행된다.

//팀 저장
 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()

양방향 연관관계와 연관관계의 주인


양방향 매핑

  • ✅ 테이블의 연관관계는 사실상 외래키 하나로 양방향이 다 있는 것이다.
    (FK로 join하면 member가 속한 team을 알 수 있고, 반대로 team의 member들도 찾을 수 있음.)

    • 사실상 테이블의 연관관계에는 방향이라는 개념 자체가 없다.
  • 문제는 객체다. 이전 예제에서 봤듯이, team에서는 member를 참조할 수 있는 방법이 없었다.

  • 그래서 위 그림과 같이 team에 members라는 List를 넣어줘야 양쪽에서 서로를 참조할 수 있다.

//조회
 Team findTeam = em.find(Team.class, team.getId()); 
 int memberSize = findTeam.getMembers().size(); //역방향 조회

연관관계의 주인과 mappedBy

  • 객체와 테이블이 관계를 맺는 차이

    • 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다.
    • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.

    • 테이블의 양방향 연관관계 ) 테이블은 외래키 하나로 두 테이블의 연관관계를 관리
  • ❗ 둘 중 하나로 외래키를 관리해야 한다 !

    • member에 있는 team으로 외래키를 관리할지, team에 있는 members로 외래키를 관리해야 할지..? -> 딜레마,, 정답은 ❓

연관관계의 주인(Owner)

양방향 매핑 규칙

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정

  • 연관관계의 주인만이 외래키를 관리(등록, 수정)

  • 주인이 아닌쪽은 읽기만 가능

    • 주인이 아닌 쪽의 값을 변경해도 DB 상에는 아무런 변화가 일어나지 않는다.
  • 주인은 mappedBy 속성 사용 X

  • 주인이 아니면 mappedBy 속성으로 주인 지정


누구를 주인으로 ?
  • 외래키가 있는 곳을 주인으로 정해라 ! (DB 테이블에서의 N측!)
  • 여기서는 Member.team이 연관관계의 주인

  • Why? ) Team의 members가 연관관계의 주인이 되면, members를 변경했을 때 업데이트 쿼리가 Team엔티티와 매핑된 TEAM테이블이 아니라, 다른 테이블(MEMBER 테이블)로 나가게 된다. 이것 자체가 헷갈리고 설계가 깔끔하게 되지 않는다. (+ 성능상 이슈도 있다.)
  • 연관관계 주인은 비즈니스 상에서 중요한 것이 키가 아니다.

양방향 매핑시 가장 많이 하는 실수
(연관관계 주인에 값을 입력하지 않음)

양방향 연관관계 주의 - 실습

  • ✅ 양방향 매핑시, 순수한 객체 관계를 고려하면 항상 양쪽 다 값을 입력해야 한다.
    • flush, clear를 하기 전 순수하게 1차 캐시에 넣은 상태에서는 JPA가 member의 team을 변경한 값으로 members를 못 읽어들이기 때문이다.
  • ✅ 이러한 실수를 방지하기 위해 연관관계 편의 메서드를 생성하자.
    @Entity
    class Member {
    ...
    public void changeTeam(Team team) { 
    	this.team = team;
    	team.getMembers().add(this);
    }
    }
  • ✅ 양방향 매핑시에 무한루프를 조심하자
  • 예: toString(), lombok, JSON 생성 라이브러리 (컨트롤러에서 엔티티를 반환하지 말자!)

양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료
  • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
  • JPQL에서 역방향으로 탐색할 일이 많음
  • 단방향 매핑을 잘하고 양방향은 필요할 때 추가해도 됨
    (테이블에 영향을 주지 않음)

연관관계 주인을 정하는 기준

  • 비즈니스 로직을 기준 X, 외래키의 위치를 기준으로 정해야 함


실전예제 2 - 연관관계 매핑 시작


Member 클래스

package hellojpa;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    private String name;
    private String city;
    private String street;
    private String zipcode;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public String getZipcode() {
        return zipcode;
    }

    public void setZipcode(String zipcode) {
        this.zipcode = zipcode;
    }
}

Order 클래스

package hellojpa;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
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
    @JoinColumn(name = "ORDER_ITEM_ID")
    private List<OrderItem> orderItems = new ArrayList<>();

    private LocalDateTime orderDate;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Member getMember() {
        return member;
    }

    public void setMember(Member member) {
        this.member = member;
    }

    public LocalDateTime getOrderDate() {
        return orderDate;
    }

    public void setOrderDate(LocalDateTime orderDate) {
        this.orderDate = orderDate;
    }

    public OrderStatus getStatus() {
        return status;
    }

    public void setStatus(OrderStatus status) {
        this.status = status;
    }

    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }
}

OrderItem 클래스

package hellojpa;

import javax.persistence.*;

@Entity
public class OrderItem {
    @Id @GeneratedValue
    @Column(name = "ORDER_ITEM_ID")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "ORDER_ID")
    private Order order;

    public Item getItem() {
        return item;
    }

    public void setItem(Item item) {
        this.item = item;
    }

    @ManyToOne
    @JoinColumn(name = "ITEM_ID")
    private Item item;

    private int orderPrice;

    private int count;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Order getOrder() {
        return order;
    }

    public void setOrder(Order order) {
        this.order = order;
    }

    public int getOrderPrice() {
        return orderPrice;
    }

    public void setOrderPrice(int orderPrice) {
        this.orderPrice = orderPrice;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }
}

JpaMain 클래스

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin(); // 데이터베이스 트랜잭션 시작함

        try {
            // 실제 애플리케이션 동작 code
            Order order = new Order();
            order.addOrderItem(new OrderItem());

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }
}
profile
꾸준히 발전하는 개발자가 되자 !

0개의 댓글