[JPA] 연관 관계 매핑 시 주의 사항과 올바른 구현 전략

옹심이·2024년 12월 18일
0
post-thumbnail

시작하며

이전 포스트에서 연관 관계 매핑을 사용하여 데이터 중심 설계의 문제를 해결했다.

오늘은 연관 관계 매핑에 대해 좀 더 자세히 다뤄본다

객체와 테이블의 연관 관계 패러다임 차이

객체 참조와 테이블 외래 키는 서로 다른 패러다임을 가지고 있기 때문에 객체를 설계할 때 중요하게 작용한다.

테이블은 본질적으로 방향성이 존재하지 않는 구조를 가지고 있어, 외래 키를 통한 조인 작업만으로도 연결된 다른 테이블의 모든 데이터를 자유롭게 조회하고 활용할 수 있다.

반면에 객체 지향 프로그래밍에서의 객체 참조는 명확한 방향성을 가지고 있어, 양방향으로 데이터를 참조하기 위해서는 추가적인 설정과 구현이 필요하다.

이러한 객체와 테이블 간의 근본적인 연관 관계 차이를 정확히 이해하고 적절히 다루는 것이 효과적인 데이터 모델링의 핵심이다.

객체의 경우, 연관 관계는 다음과 같이 2개의 독립적인 참조로 구성된다:

회원 → 팀
팀 → 회원

이러한 구조는 각각 독립적인 단방향 연관 관계(참조)로 구현되며, 이 두 개의 단방향 참조를 함께 사용할 때 우리는 이를 양방향 연관 관계라고 정의한다.

이와는 대조적으로, 테이블에서는 단 하나의 연관 관계만으로 충분하다.

데이터베이스에서는 TEAM_ID 외래 키와 TEAM 테이블의 기본 키(PK)를 활용한 조인 연산을 통해 MEMBER와 TEAM 사이의 관계를 양방향으로 즉시 파악하고 접근할 수 있다.

결과적으로, 테이블은 하나의 외래 키 조인만으로도 양방향 연관 관계를 완벽하게 표현할 수 있기 때문에, 객체에서처럼 별도의 방향성 설정이 필요하지 않은 것이다.

양방향 연관 관계의 주인

그렇다면, Member 객체의 team 필드가 변경되었을 때 MEMBER 테이블의 외래 키를 업데이트해야 하는지, 아니면 Team 객체의 members 컬렉션이 변경되었을 때 업데이트해야 하는지에 대한 고민이 생긴다. 이는 양방향 연관 관계에서 매우 중요한 설계 결정이다.

예를 들어, Member의 team 필드에는 특정 Team이 설정되어 있지만, 해당 Team의 members 컬렉션에는 이 Member가 포함되어 있지 않은 상황이 발생할 수 있다. 이러한 불일치는 데이터의 정합성 문제를 야기할 수 있으므로, Member 객체의 team으로 외래 키를 관리할지, Team에 있는 members로 관리할지 명확하게 결정해야 한다.

이러한 문제를 해결하기 위해 JPA에서는 양방향 연관 관계에서 외래 키를 관리하는 주체를 명확히 지정할 수 있도록 '연관 관계의 주인'이라는 개념을 도입했다. 이는 두 객체 중 하나를 선택하여 외래 키를 전적으로 관리하도록 하는 방식이다.

이때 mappedBy 속성이 핵심적인 역할을 한다. mappedBy가 지정된 쪽은 연관 관계의 주인이 아니며, 따라서 데이터베이스의 외래 키에 대한 직접적인 영향력이 없고 단순히 조회 기능만을 수행할 수 있다.

반대로, mappedBy 속성값으로 지정된 필드를 가진 엔티티가 연관 관계의 주인이 되어 외래 키를 관리할 수 있는 권한을 갖게 된다. 이 엔티티만이 실제로 데이터베이스의 외래 키를 등록하고 수정할 수 있다.

이러한 설계로 인해, Team 객체의 members 컬렉션(mappedBy가 설정된 쪽)에 새로운 Member를 추가하더라도 실제 데이터베이스의 MEMBER 테이블에는 아무런 변화가 발생하지 않는다는 점을 반드시 이해해야 한다.

권장되는 주인 결정 방식과 이유

연관 관계의 주인을 결정할 때는 외래 키를 직접 관리하는 엔티티를 선택하는 것이 가장 합리적인 접근 방식이다. 이는 데이터베이스의 구조와 객체 모델링을 가장 자연스럽게 일치시킬 수 있는 방법이다.

이러한 원칙을 따르지 않고 Team 객체의 members 컬렉션을 통해 관계를 관리하려고 할 경우, 직관적이지 않은 동작이 발생할 수 있다. 예를 들어, Team 객체를 수정했음에도 실제로는 MEMBER 테이블에 대한 업데이트 쿼리가 발생하게 되며, 이는 성능 최적화 측면에서도 불리할 수 있다.

데이터베이스의 관점에서 보면, 외래 키가 존재하는 테이블이 N(다수)의 역할을, 외래 키가 없는 테이블이 1(단수)의 역할을 하게 된다. 이는 관계형 데이터베이스의 기본적인 설계 원칙과도 일치한다.

따라서, 데이터베이스 구조상 N의 위치에 있는 엔티티가 연관 관계의 주인이 되는 것이 가장 자연스럽고 효율적인 방식이며, 이는 JPA에서도 강력히 권장되는 설계 패턴이다.

양방향 연관 관계 사용 시 자주 하는 실수

실수 1 - 역방향에서만 연관 관계를 설정

연관 관계의 주인이 아닌 역방향의 가짜 매핑된 곳에서만 값을 설정하면, 실제 데이터베이스의 외래 키가 변경되지 않는다.

이는 mappedBy가 설정된 쪽이 단순히 조회용으로만 사용되며 실제 데이터베이스의 상태를 변경할 수 있는 권한이 없기 때문이다. 따라서 이러한 방식으로 코드를 작성하면 예상했던 데이터베이스 업데이트가 이루어지지 않아 심각한 버그의 원인이 될 수 있다.

하지만 객체 지향적 설계 관점에서는 양방향 매핑 시 양쪽 모두에 값을 설정하는 것이 바람직하다

Team 객체의 members에 member를 추가하는 코드를 제거했다.

이렇게 하면 영속성 컨텍스트에는 순수한 Team 객체만 들어가고 연관 관계는 설정되지 않는다. 따라서 양쪽 모두에 값을 설정해주어야 한다.

또한 테스트 케이스 작성 시 데이터 일관성 문제가 발생할 수 있다. 예를 들어 Member에는 team이 설정되어 있지만, Team의 members 컬렉션에는 해당 member가 누락될 수 있다.

다시 한번 알고 가자, JPA는 데이터베이스 접근 시 객체 지향 패러다임에 따라 컬렉션처럼 자연스럽게 다룰 수 있도록 도와주는 기술이다.

이 실수를 방지하기 위한 방법은 없을까? 연관 관계 편의 메서드를 만들어 사용하면 된다.

Member 객체에서 team을 등록할 때 역방향에서도 변화가 일어날 수 있도록 하는 책임을 추가해주자.

(단순히 setter를 사용하는 것보다 객체가 책임을 가질 수 있도록 하는 것이 권장된다)

실수 2 - toString으로 인한 무한 루프

toString 등 롬복 라이브러리를 사용할 때 발생할 수 있다.

Team 객체와 Member 객체에 toString 메서드를 만들어주었다.

toString은 객체를 출력할 때 객체의 정보를 보여주는 메서드이다.

예를 들어, member를 출력할 때 toString 메서드가 호출되면 team 객체의 toString 메서드도 호출되어 무한 루프가 발생한다.

따라서 롬복의 toString 메서드 생성 기능은 신중하게 사용해야 한다.

컨트롤러에서 엔티티를 직접 반환할 때도 같은 문제가 발생할 수 있으므로 DTO를 사용하는 것이 바람직하다.

단방향 매핑으로 모든 설계를 끝내자

양방향 매핑은 가급적 사용을 피하고 단방향 매핑으로 설계를 완료하는 것이 좋다.

초기에는 반드시 단방향 매핑으로 설계를 마무리하고, 개발 과정에서 객체 그래프의 역방향 탐색이 필요할 때 양방향 매핑으로 전환하는 것이 바람직하다.

연관 관계 설정은 테이블에 영향을 주지 않으며, 단방향 매핑이 올바르게 구현되어 있다면 양방향 매핑으로의 전환은 어렵지 않다.

0개의 댓글