ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JPA] 단방향 양방향 연관관계와 다중성 #다대일 #일대다 #일대일 #다대다
    JPA 2023. 6. 25. 01:16

     

    [JPA] 단방향 양방향 연관관계와 다중성 #다대일 #일대다 #일대일 #다대다


    안녕하세요? 장장스입니다.

    JPA는 자바 진영의 표준 ORM(Object Relational Mapping)입니다. ORM에서 가장 중요한 것이 객체와 모델 사이의 관계를 정의하는 것입니다. 각 모델과의 연관 관계를 자바 코드로서 객체로 옮기는 것에 백퍼센트 정답은 없습니다. 구현하고자 하는 서비스의 도메인과 요구사항에 따라 적절하게 연관 관계를 정의해야 합니다. 

     

     

    단방향 관계, 양방향 관계


     데이터베이스에서 테이블과 조인할때, 외래키(FK)로 하나로 양쪽 테이블을 조인이 가능합니다. 때문에 데이터베이스에서 테이블과 다른 테이블에는 방향이 없습니다.

     

     반면에 JPA에서는 객체(엔티티)를 통해 또다른 객체(엔티티)를 참조합니다. Orders 객체가 Customer 객체를 참조하기 위해 Customer 클래스를 필드로 가집니다.

     위의 그림처럼 객체사이에서 한 객체만이 다른 객체를 참조하고 있으면 단방 관계라고 합니다.

     

    객체 사이에서 양쪽 객체가 서로를 참조하고 있으면 양방향 관계라고 합니다. 양방향 관계를 정확히말하면 객체가 서로를 단방향 관계로 참조하고 있는 것입니다.

    • Orders -> Customer : Orders.getCustomer()
    • Customer -> Orders : Customer.getOrders()

    여기까지 내용을 정리하며 문득 드는 의문이 있었습니다.

     

    데이터베이스처럼 양쪽 객체를 서로 참조하도록 하면 되지 않나?

     

    실제로 양방향 관계로 객체끼리 서로 참조하게 해보면 문제가 매우 복잡해 지게 됩니다.

     예를 들어, 일반적인 비즈니스에서 회원 엔티티는 많은 엔티티와 연관 관계를 형성할 수 밖에 없습니다. 이러한 경우에 모든 객체(엔티티)를 양방향 관계로 연결하게 되면 수많은 참조 필드가 생기게 된다는 것을 예상할 수 있습니다.

     다른 문제로,객체가 서로를 참조하다보면 순환참조 문제가 발생할 수 있습니다. 때문에 toString(), lombok, JSON 생성 라이브러리를 사용할 때 주의를 기울여야 합니다. 컴파일 단계에서는 확인되지 않기 때문에 자칫하면 운영중에 큰 사고로 이어질 수 있습니다.

     

    연관 관계의 주인


    이제 우리는 객체의 두 관계중 하나에 연관 관계의주인을 정해야 한다는 사실을 알았습니다. 어떤 객체에 연관 관계의 주인을 설정해야 할까요?

    대부분은 외래키가 있는 곳을 연관 관계의 주인으로 정합니다. (어떤 예외 상황이 있는지는 알지 못하여 나중에 그런 상황이 발생하면 추가로 정리하겠습니다.) 연관 관계의 주인인 객체만이 외래키를  등록하고 수정할 수 있습니다.

    연관 관계의 주인이 아닌 쪽은 읽기만 가능하며, mappedby 속성으로 주인을 지정할 수 있습니다.

     

    @Entity
    public class Customer {
        @Id
        @GeneratedValue
        @Column(name = "customer_id")
        private Long id;
    
        private String name;
    
        @OneToMany(mappedBy = "customer")
        private List<Orders> ordersList = new ArrayList<>(); 
    }
    @Entity
    public class Orders {
    
        @Id
        @GeneratedValue
        @Column(name = "order_id")
        private Long id;
    
        @ManyToOne
        @JoinColumn(name = "customer_id")
        private Customer customer;
    
        private LocalDateTime order_date;
    }

     

    연관 관계 메서드


     

    JPA는 객체 참조이기 때문에 연관 관계 주인과 주인이 아닌 객체에 모두 값을 입력해야 합니다.

    Customer customer = new Customer();
    customer.setName("장장스");
    em.persist(customer); //INSERT
    
    Orders order1 = new Orders();
    order1.setCustomer(customer);
    order1.setOrder_date(LocalDateTime.now());
    em.persist(order1); //INSERT

    위 코드를 실행시키고 나서 아래 코드를 출력하면 어떻게 될까요?

    customer.getOrdersList().isEmpty();

      ture 를 반환한다.

     

    왜? ordersList가 비어있을까요? 데이터베이스와 달리 객체입니다. 연관관계의 주인인 customer객체에 ordersList에 별도로 order를 추가하도록 코드를 아래와 같이 수정해야 합니다.

    Customer customer = new Customer();
    customer.setName("장장스");
    em.persist(customer); //INSERT
    
    Orders order1 = new Orders();
    order1.setCustomer(customer);
    order1.setOrder_date(LocalDateTime.now());
    
    customer.getOrdersList().add(order1); //연관 관계 주인에도 값을 추가한다.
    
    em.persist(order1); //INSERT

     

    위 코드처럼  order와 customer에 각각 값을 추가해야 하는데, 솔직히 불편하고 번거롭습니다. 이를 편리하게 하기 위한 방법으로 연관 관계 메서드를  정의하여 사용할 수 있습니다.

    public void setCustomer(Customer customer) {
            this.customer = customer;
            customer.getOrdersList().add(this);
        }

     

    양방향 연관 관계에서는 자바의 객체 상태를 고려해서 항상 양쪽에 값을 설정할 수 있도록 해야 합니다.

     

     

    다중성


    JPA는 테이블의 관계를 정할 때는 데이터베이스 테이블 관계를 기준으로 정합니다. JPA에서 제공하는 다중성 관계는 총 4가지 입니다.

     

     다대일 → 일대다

     일대일 → 일대일 (반대 동일)

    다대다 → 다대다 (반대 동일)

     

      다대일(N:1)

    가장 많이 사용하는 다중성 관계이다. 데이터베이스는 다대일에서 무조건 다 쪽에 외래키(FK)가 있다.

     

      다대일(N:1)의 단방향 관계

    JPA로 넘어와서 먼저 다대일(N:1) 관계에서 다(N)인 객체에서 단순하게 일(1)인 객체로 단방향 참조를 살펴보겠습니다.

    단방향 참조는, 테이블에 외래키가 있는 다(N)인 객체에서 일(1)인 객체로 연관관계 매핑 하면 됩니다.

     

    다(N) : Member 일(1) : Team

    1. @ManyToOne을 사용해서 다대일 관계를 매핑합니다..

    2. @JoinColumn 외래 키를 매핑할 사용한다. 매핑할 외래 이름은 name에 작성합니다.

    @Entity
    public class Member {
        //...
        @ManyToOne
        @JoinColumn(name = "TEAM_ID")
        private Team team;
        //...
    }

     

     

      다대일(N:1)의 양방향 관계

    단방향 관계에서 추가적으로 양방향 관계로 매핑할 때, 일(1)인 객체에서 단방향 매핑을 해주게 됩니다.

    일(1)인 객체에서 단방향 매핑을 하는 것은 데이터베스상의 테이블에 영향을 전혀 주지 않습니다. 이미 다대일(N:1) 관계로 다(N)인 객체에서 연관관계의 주인으로 외래키를 관리하고 있습니다.

     

    다(N) : Member  일(1) : Team

    1. @OneToMany 어노테이션을 사용한다.

    2. 연관관계의 주인이 아니므로, 어디에 매핑이 되었는지에 명시해주어야만 합니다. 아래 코드의 mappedBy = "team" 은 Team 엔티티의 members 필드가 Member 엔티티의 team 필드에 의해 매핑 되었음을 의미합니다.

    @Entity
    public class Member {
        //...
        @ManyToOne
        @JoinColumn(name = "TEAM_ID")   //외래 키가 있는 곳이 진짜 주인으로 정해라, Member.team
        private Team team;
        //...
    }
    
    
    @Entity
    public class Team {
        //...
        @OneToMany(mappedBy = "team") //읽기용 외래키로 mappedBy
        private List<Member> members = new ArrayList<>();
        //...
    }

     

     

      일대다(1:N)

    일대다(1:N)는 일(1) 연관관계의 주인을 갖는 관계입니다. 일(1) 객체에서 외래키를 관리하는 경우입니다. 보통 실무에서 사용을 권장하지는 않는다고 합니다. 예외적으로 일대다(1:N) 관계를 사용하는 경우가 있다고 하는데, 나중에 알게되면 추가적으로 포스팅하도록 하겠습니다.

     

      일대다(1:N)의 단방향 관계

    일(1)인 객체가 다(N)인 객체를를 가지는데, 다(N) 인 객체에서는 일(1)인 객체를 참조하지 않아도 된다라는 설계가 나올 수 있다. (객체지향적인 설계 관점에서 보면 충분이 나올 수 있는 경우라고 생각됩니다.)

    그러나 데이터베이스 테이블 관점에서 보면, 무조건 다(N)쪽에 외래키가 들어가게 됩니다. 일(1)인 객체에서 다(N)인 객체가 변경되면, 데이터베이스에서 다(N) 테이블에 업데이트 쿼리가 실행되어 버립니다. 실무에서는 여러 테이블이 복잡하게 얽히기 때문에 이러한 상황이 발생하면 유지보수가 어렵게 됩니다.

    테이블이 수십, 수백개 존재하는 실무에서는 매핑이나 설계는 명확해야 누구나 사용이 편리합니다. 가능하면 다대일 단방향 관계로 매핑하고, 필요할 경우 양방향 매핑을 통해서 해결하도록 합니다.

     

    일(1) : Team  다(N) : Member

     @OneToMany와 @JoinColumn을 이용해서 일대다(1:N) 단방향 매핑을 한다. @JoinColumn을 꼭 사용해야만 하며, 사용하지 않으면 조인 테이블 방식을 사용합니다.

    @Entity
    public class Team {
        //...
        @OneToMany
        @JoinColumn(name = "TEAM_ID")
        private List<Member> members = new ArrayList<>();
        //...
    }

     

      일대다(1:N)의 양방향 관계

    일대다(1:N) 양방향 관계는 공식적으로는 존재하지 않습니다.

    @ManyToOne과 @JoinColumn을 사용해서 연관관계를 매핑하면, 다대일(N:1) 단방향 매핑이 되어버린다. 그런데 반대쪽 일(1)인 객체에서 이미 일대다(1:N) 단방향 매핑이 설정되어있다. 이런 상황에서는 두 엔티티에서 모두 테이블의 외래키를 관리 하게 되는 상황이 벌어진다.

     

    이러한 상황을 막기 위해 insertable, updatable 속성값을 FALSE로 설정하고 읽기 전용 필드로 사용해서 양방향 매핑처럼 사용하는 방법이다. @JoinColumn(name = "team_id", insertable = false, updatable = false) .

    insertable, updatable  속성값을 false로 만들어 읽기 전용으로 사용하도록 할 수 있다.

     

      일대일(1:1)

    일대일(1:1) 관계는 반대도 일대일(1:1)로 동일합니다.

     

      일대일(1:1)의 단방향 관계

    일대일 관계는 주 테이블, 대상 테이블 중에 외래키를 넣을 테이블을 선택 할 수 있습니다. 또한 외래 키에 유니크 제약조건이 추가되어야 일대일 관계가 됩니다. 

     

     

      일대일(1:1)의 양방향 관계

    다대일(N:1)처럼 외래키가 있는 곳이 연관관계의 주인이다.

    @OneToOne 어노테이션으로 일대일(1:1) 단방향 관계를 매핑합니다. @JoinColumn을 사용합니다.

    @Entity
    public class Member {
       //...       
       @OneToOne
       @JoinColumn(name = "locker_id")
       private Locker locker;
       //...
    }
    
    @Entity
    public class Locker {
       //...       
       @OneToOne(mappedBy = "locker")
       private Member member;
       //...
    }

     

      다대다(N:M)

    일반적인 관계형 데이터베이스에서는 정규화된 테이블 2개로 다대다(N:M)을 표현할 수 가 없어 중간에 연결 테이블을 추가해서 다대일, 일대다 관계로 풀어서 설계 합니다.

    JPA에서 객체는 컬렉션을 사용하여 다대다 관계를 풀어낼 수 있습니다. 그러나 개발을 하다보면 서비스가 커지고 요건이 추가되며, 중간 연결 테이블에 칼럼이 추가되며 데이터가 들어가야 하는 일이 생기기도 합니다. 이렇게 처리 하게 되면, 중간에 숨겨진 테이블로 인해, 예상치 못한 쿼리가 나가게 됩니다. 따라서 다대다(N:M) 관계는 실무에서는 절대 지양해야 합니다. 다대일, 일대다 관계로 풀어서 사용하도록 합니다.

     

     

     

     

    Post


    •  

    References


     

     


    잘못된 코드나 내용이 있다면 댓글을 남겨주세요. 즉시 수정하도록 하겠습니다! :)

     

     

    댓글