본문 바로가기
Backend/JPA

[JPA] 연관관계 매핑

by 2245 2023. 12. 18.

출처

https://www.inflearn.com/course/ORM-JPA-Basic/dashboard

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 강의 - 인프런

JPA 를 공부하고 책을 보며 어려웠던 내용을 위주로 먼저 보았습니다. 옆에서 1:1 과외해주는 것 같이 생생하고 이해 잘되는 설명, 예제(코드)가 너무 좋았습니다. 어느 것 하나 애매함없이 모두

www.inflearn.com

 

 

목차

     

     

    연관관계가 필요한 이유

     

    예제 시나리오

    • 회원과 팀이 있다.
    • 회원은 하나의 팀에만 소속될 수 있다.
    • 회원과 팀은 다대일 관계다.

     

    단방향 연관관계

    @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();

     

    수정

    member의 팀을 A에서 B로 변경

    // 새로운 팀B
    Team teamB = new Team();
    teamB.setName("TeamB");
    em.persist(teamB);
    
    // 회원1에 새로운 팀B 설정
    member.setTeam(teamB);

     

     

    양방향 연관관계

    Member 엔티티는 단방향과 동일

    @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;
    	…

     

    Team 엔티티는 컬렉션 추가

    @Entity
    public class Team {
        @Id
        @GeneratedValue
        private Long id;
        private String name;
        @OneToMany(mappedBy = "team")
        List<Member> members = new ArrayList<Member>();
    	…
    }

     

     

    양방향 매핑으로 인해 반대 방향으로 그래프 탐색이 가능

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

     

     

    연관관계의 주인과 mappedBy

    연관관계의 주인과 mappedBy를 이해하기 위해선, 객체와 테이블간의 연관관계를 맺는 차이에 대해 이해해야 합니다.

     

    객체와 테이블의 연관관계 차이

    • 객체의 연관관계는 2개입니다.
      • 회원 → 팀 연관관계 1개 (단방향)
      • 팀 → 회원 연관관계 1개 (단방향)
    • 테이블 연관관계는 1개입니다. 
      • 회원 ↔ 팀의 연관관계 1개 (양방향)

     

     

    객체의 양방향 관계

    • 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단뱡향 관계 2개입니다.
    • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 생성해야 합니다. 
    • A → B (a.getB())
    • B → A (b.getA())
    class A {
        B b;
    }
    class B {
        A a;
    }

     

    테이블의 양방향 연관관계

    • 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리합니다. 
    • 즉, MEMBER 테이블의 TEAM_ID 외래 키 하나로 양방향 연관관계 가집니다. (양쪽으로 조인할 수 있습니다.)
    SELECT *
    FROM MEMBER M
    JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
    SELECT *
    FROM TEAM T
    JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID

     

     

    둘 중 하나로 외래 키를 관리해야 한다. : 연관관계의 주인 (Owner)

    • 만약, Member의 Team이 교체되었다면 Member의 Team을 수정해야 할까요, Team의 members를 수정해야 할까요?
    • DB 입장에서는 한 번의 수정을 거쳐 Member의 Team_ID 값이 변경되면 됩니다.
    • 즉, 둘 중 외래 키를 관리할 연관관계의 주인이 필요합니다.
    • 결론부터 말하자면 연관관계의 주인은 외래키를 포함한 객체입니다.
    • 따라서 현재는 MEMBER가 연관관계의 주인이 됩니다. 
    • Member 객체의 team 을 setTeam()을 통해 수정하면, DB의 MEMBER의 TEAM_ID 값도 변경이 됩니다. 

     

     

    연관관계의 주인(Owner)

    • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정해야 합니다. 
    • 연관관계의 주인만이 외래 키를 관리합니다. (등록, 수정)
    • 주인이 아닌쪽은 읽기 전용입니다.
    • 주인이 아닌 쪽은 mappedBy 속성을 사용합니다. 
    • 주인은 mappedBy 속성을 사용하지 않습니다. 

     

    누구를 주인으로?

    • 외래 키가 있는 있는 곳을 주인으로 정해라. (일대다 중 다(N)이 연관관계 주인)
    • 여기서는 Member.team이 연관관계의 주인입니다.

     

    양방향 매핑 시 가장 많이 하는 실수

    연관관계의 주인에 값을 입력하지 않으면, DB에는 값이 들어가지 않습니다.

    Team team = new Team();
    team.setName("TeamA");
    em.persist(team);
    
    Member member = new Member();
    member.setName("member1");
    
    //역방향(주인이 아닌 방향)만 연관관계 설정
    team.getMembers().add(member);
    
    em.persist(member);

     

     

    양방향 매핑 시 연관관계의 주인에 값을 입력해야 한다.

    Team team = new Team();
    team.setName("TeamA");
    em.persist(team);
    
    Member member = new Member();
    member.setName("member1");
    
    team.getMembers().add(member);
    //연관관계의 주인에 값 설정
    member.setTeam(team); //**
    
    em.persist(member);

    참고 순수한 객체 관계를 고려하면, 양방향 매핑 시 위 코드와 같이 양쪽 다 값을 입력해야 합니다. 

     

     

     

    연관관계 편의 메소드

    • 순수 객체 상태를 고려하면, 항상 양쪽에 값을 설정해야 합니다.
    • 하지만, 두 관계에 모두 값을 설정하는 것을 실수로 누락할 수도 있습니다.
    • 다음과 같은 연관관계 편의 메소드를 통해 해결할 수 있습니다.
    @Entity
    public class Member {
    	…
        public void changeTeam(Team team) {
            this.team = team;
            team.getMembers().add(this);
        }
    참고 chageTeam()
    setTeam()은 getter/setter 관례로 인해 단순히 set 할때만 사용하고, changeTeam 과 같은 이름을 사용하여 다른 로직이 추가된 것을 알 수 있도록 이름명을 짓는 걸 권장합니다. 

     

    💡주의: 양방향 매핑 시에 무한 루프를 조심하자

    예: toString(), lombok, JSON 생성 라이브러리

    @Entity
    public class Member {
    	…
        @Override
        public String toString() {
            return "Member{" +
                    "id=" +  id +
                    ", username=" + username +
                    ", team=" + team +
                    '}';
        }
    @Entity
    public class Team {
    	…
        @Override
        public String toString() {
            return "Team{" +
                    "id=" +  id +
                    ", name=" + name+
                    ", members=" + members +
                    '}';
        }
    • Member의 toString()도 team 을 참조하고, Team의 toString()도 members를 통해 member를 참조하고 있어 무한 루프에 빠집니다.
    • 해답으로는 toString()이나 lombok을 사용하지 않거나, 해당 연관관계 관련된 것은 빼고 사용하여 해결할 수 있습니다. 
    • JSON 생성 라이브러리에서 Controller에서 Entity를 반환하여 생기는 무한루프에서는 절대 Controller가 Entity를 반환하면 안 됩니다. 
    • 만약, 엔티티 컬럼이 추가되거나 변경이 된다면 관련 API 전체가 변경되기 때문입니다. 
    • 따라서, 엔티티는 단순한게 값만 있는 DTO로 변환하여 반환해야 합니다.  

     

     

    양방향 매핑 정리

    • 단방향 매핑만으로도 이미 연관관계 매핑은 완료됩니다. (첫 설계 시 단방향 매핑으로 모든 설계를 끝내야 한다.)
    • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐입니다. 
    • JPQL에서 역방향으로 탐색할 일이 많습니다. 
    • 단방향 매핑으로 설계하고, 양방향은 필요할 때 추가해도 됩니다. (테이블에 영향 주지 않습니다.)

     

     

    실전 예제

    테이블 구조

     

     

    객체 구조

     

    참고 양방향 매핑의 필요성
    현재 Member 객체에 orders를 통해 양방향 매핑을 했으나, 사실 불필요합니다. 
    한 명의 회원이 주문한 목록을 알고 싶다면, Order 테이블이 MEMBER_ID를 가지고 있기 때문에 Order에 Query를 던져 조회하는 것이 낫습니다. Member를 보고 Orders를 가져오는 건 비지니스상 부자연스럽습니다.. 

    Order 객체에 orderItems를 넣는 것은 비지니스적으로 의미있을 확률이 높습니다. 
    주문 내역을 보고 주문한 상품 리스트를 가져오는 것은 자연스럽기 때문입니다. 

    Item 객체에 OrderItem 양방향 매핑도 불필요합니다. 상품 입장에서 주문 내역을 확인하는 것은 비지니스상 부자연스럽기 때문입니다. 

    이처럼, 객체지향적으로 타고 들어가는 것을 주의하고, 양방향 매핑은 되도록 안하는 것이 좋습니다.
    (예제에선 연습을 위해 추가한 것입니다.) 

     

    Member

    @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<>();
    
        //Getter, Setter ...
    }

    Order

    @Entity
    @Table(name = "ORDERS")
    public class Order {
    
        @Id @GeneratedValue
        @Column(name = "ORDER_ID")
        private Long id;
        @ManyToOne
        @JoinColumn(name = "MEMBER_ID")
        private Member member;
        private LocalDateTime orderDate;
        @Enumerated(EnumType.STRING)
        private OrderStatus status;
        @OneToMany(mappedBy = "order")
        private List<OrderItem> orderItems = new ArrayList<>();
    
        public void addOrderItem(OrderItem orderItem) {
            orderItems.add(orderItem);
            orderItem.setOrder(this);
        }
    
        //Getter, Setter ...
    }

    Item

    @Entity
    public class Item {
    
        @Id @GeneratedValue
        @Column(name = "ITEM_ID")
        private Long id;
        private String name;
        private int price;
        private int stockQuentity;
    
        //Getter, Setter ...
    }

    OrderItem

    @Entity
    public class OrderItem {
    
        @Id @GeneratedValue
        @Column(name = "ORDER_ITEM_ID")
        private Long id;
        @ManyToOne
        @JoinColumn(name = "ORDER_ID")
        private Order order;
        @ManyToOne
        @JoinColumn(name = "ITEM_ID")
        private Item item;
        private  int orderPrice;
        private  int count;
    
        //Getter, Setter ...
    }

    'Backend > JPA' 카테고리의 다른 글

    [JPA] 고급 매핑 (상속 관계 매핑, 공통 속성 매핑)  (0) 2023.12.20
    [JPA] 다양한 연관관계 매핑  (0) 2023.12.18
    [JPA] 엔티티 매핑  (0) 2023.12.18
    [JPA] 영속성 컨텍스트  (0) 2023.12.18
    [JPA] Hello JPA  (0) 2023.12.02