연관관계 편의 메소드

Su hwan Choi·2022년 11월 9일
0

연관관계 편의 메소드

ORM에서 두 엔티티가 양방향 관계에서 RDB와 데이터가 일치하도록 보장하기 위해 사용된다.

얼마전 인프런에서 김영한님의 hibernate 강의를 복습하는 중, 양방향 관계일경우 연관관계 편의 메소드라는 것을 사용하는것을 보았다.
사용해야 하는 이유는 강의 내용에 있었지만(위의 적어놓은 내용과 크게 다르지 않았다) JPA 레퍼런스, hibernate 에서는 어떻게 설명하고 있는지 궁금해서 좀더 찾아보기로 했다.


왜 사용되는가?

지난번 JoinColumn과 관계의 주인 에 언급되었던 객체와 RDB의 불일치 가 근본적인 이유라고 볼 수 있다.
이때 개발자가 역참조를 통해 접근하게 되면 문제가 발생할 수 있다.(NPE 발생)

문제가 발생할 ‘수’ 있다 라고 적은 이유는, 만약 EntityManager 의 cache 를 지우고 다시 조회를 할 경우에는 문제가 없기 때문이다.

하지만 1차 캐시를 사용하는 목적이 퇴색 되므로 코드에 객체의 연관관계를 설정하는 개발자가 코드를 추가하는데, 이것을 연관관계 편의 메소드라고 부른다.

테스트코드로 확인

1:N 관계인 두 엔티티가 양방향 관계로 설정되어 있다.

@Entity
@Getter
@Table(name = "orders")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order
{
...
    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "member_id"
            , foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
    private Member member;

...
}
@Entity 
@Getter
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"name"}))
@NoArgsConstructor
public class Member
{
...
	  @OneToMany(mappedBy = "member")
	  private List<Order> orders = new ArrayList<>();

    public int orderPriceAmount()
    {
        return orders.stream()
                .filter(order -> order.getStatus() == OrderStatus.ORDER)
                .mapToInt(Order::getTotalPrice)	//가격 * 갯수
                .sum();							//합계
    }
...
}

회원(Member)에 여러개의 주문(Order) 이 있다.
관계의 주인은 Order 이다. Order에는 Member를 설정하고, 이를 Member에서 조회를 하면 위의 문제를 확인할 수 있다.

    @DisplayName("객체-관계 불일치 확인")
    @Test
    void assignOrder()
    {
		  ...
		  //member
        Member member = Member.build("kim", address);
        em.persist(member);
		  
		  ...

        int orderItemPrice = 10000;	//가격
        int orderItemCount = 1;		//갯수
        OrderItem orderItem = OrderItem.create(book, orderItemPrice, orderItemCount);

		  //위의 member를 order에 할당
        Order order = Order.create(member, delivery, orderItem);
        em.persist(order);

        em.flush();

//        em.clear();


        Member expected = em.find(Member.class, member.getId());
		  //여기서 실패한다. 
        Assertions.assertEquals(orderItemPrice * orderItemCount, expected.orderPriceAmount());
    }

테스트 코드의 내용을 요약하면 다음과 같다.

  • 1:N 양방향 관계
  • orderPriceAmount() 는 Member가 참조하는 N개의 Order의 금액 총계를 리턴한다.
    • 10000(가격) * 1(갯수) 로 10000이 나와야한다
    • 이 금액은 order 객체와 이 order를 참조하는 member의 order에서도 동일해야 한다.
    • 하지만 실행하면 0 이 나온다.

이 테스트코드를 통과시키는 방법은 다음과 같다.

테스트코드에 추가한다
member.getOrders().add(order);
그럼 성공하는것을 확인할 수 있다.

방법이 하나 더 있다. 주석처리된 em.clear()를 호출하면 member.getOrders().add(order); 를 넣지 않아도 같은 결과이다.
em.clear()를 호출하면 EntityManager의 1차 캐시를 지우기 때문에 그 이후에 최신 데이터를 RDB에서 가져오게 된다. RDB에는 양방향 관계가 정상적으로 반영되어 있으니 테스트는 통과한다.

하지만 주기적으로 clear() 를 호출하는 방식은 ORM 코드가 프로젝트의 다른계층으로 침투할 가능성이 있기 때문에 좋은 방법은 아니다.

왜 궁금했던건가?

Mybatis 프로젝트의 ORM 의 객체간 관계 표현방법.
임피던스 부정합은 완전히 극복할 수 없고, 개발자가 개발하는 상황에 맞게 선택해서 적용해야 한다.

Mybatis에서도 객체 참조와 같은 기능을 지원 했었기에 Domain 모델을 정의하고 구체적으로 구현한후 ORM을 적용하기 쉽게 객체관계를 정의할 수 있지 않을까? 하는게 내 생각 이었다. (물론 이 생각은 진행하면서 바뀌게 된다.)

그리고 JPA 를 다시 보면서 JPA 스펙에는 이 내용이 어떻게 나와있는지 궁금했다. 이전에 잠시 얘기 했듯이 Mybatis가 적용된 프로젝트에 장기적으로 MSA 구조로 변환하는것을 염두하고 리팩토링을 했을 때, Mybatis를 모두 ORM으로 교체하는것이 아니라. 패키지 구조와 여러 클래스를 조금씩 교체하면서 ORM 은 거의 마지막에 교체하는것을 생각하고 있었다.

하지만 내가 놓지고 있었던 부분이 있었는데 그중 하나가 양방향 관계이다.

3.2.4 Synchronization to the Database
...
Bidirectional relationships between managed entities will be persisted based on references held by the owning side of the relationship. It is the developer’s responsibility to keep the in-memory references held on the owning side and those held on the inverse side consistent with each other when they change. In the case of unidirectional one-to-one and one-to-many relationships, it is the developer’s responsibility to insure that the semantics of the relationships are adhered to.
관리되는 엔터티 간의 양방향 관계는 관계의 소유 측이 보유한 참조를 기반으로 유지됩니다. 소유 측에 보유된 메모리 내 참조와 반대 측에 보유된 참조가 변경될 때 서로 일관성을 유지하는 것은 개발자의 책임입니다. 단방향 일대일 및 일대다 관계의 경우 관계의 의미가 준수되도록 하는 것은 개발자의 책임입니다.
...

위 내용은 Java Persistence API Version2.2 의 스펙문서 내용이다.

JPA에서는 객체-RDB 불일치(임피던스 부정합)를 내 생각보다 심각하게 보고 있었다.(뒤집어 말하면 내가 너무 쉽게 생각했던 거다) 가령 나는 모든 엔티티의 관계는 양방향 관계로 고려했지만, JPA에서는 단방향을 정의하고 이를 조합해서 사용함으로써 양방향관계를 정의했다.

조합할때도 기본적으로는 단방향 관계인데, 이를 보완하기 위한 수단으로 양방향관계를 정의하는 방식이었다.

지금 다시 결론을 내려 보자면 임피던스 부정합은 완전히 극복할 수 없고, 개발자가 개발하는 상황에 맞게 선택해서 적용해야 한다 라고 결론을 내려 보겠다.

참고자료

0개의 댓글