Spring Data JDBC 에서 연관관계 매핑

2022. 4. 25. 02:20·Java

코드스쿼드에서 미션을 진행하면서 Spring Data JDBC 를 사용하게 되었다. 이전까지는 스프링 프레임워크에서 기본적으로 제공하는 JDBCTemplate 만을 사용했지만, 처음으로 자바 애플리케이션 내의 도메인 객체와 관계형 데이터베이스의 테이블을 매핑하는 모듈을 다루게 되었다. 그 과정에서 특히 어려움을 겪었던 Spring Data JDBC 에서의 1:1, 1:N, N;1, N:M 연관관계 매핑에 대해, 스스로 시행착오를 겪으면서 알게된 방법을 정리해보고자 한다.

 

연관관계 매핑을 연습하면서 정리한 GitHub 리포지토리의 링크는 아래와 같다.

 

https://github.com/rxdcxdrnine/relation-spring-data-jdbc

 

GitHub - rxdcxdrnine/relation-spring-data-jdbc: Spring Data JDBC 의 연관관계 매핑 연습

Spring Data JDBC 의 연관관계 매핑 연습. Contribute to rxdcxdrnine/relation-spring-data-jdbc development by creating an account on GitHub.

github.com

 

Spring Data JDBC 와 DDD

연관관계에 대해 이야기하기 전에, 먼저 Spring Data JDBC 의 전반적인 철학과 함께 DDD (Domain Driven Design) 의 구성 요소에 대해 알아보고자 한다. Spring Data JDBC 는 DDD 에서 repository 와 aggregate, aggregate root 라는 개념으로부터 영감을 얻었는데, 사실 이는 관계를 가진 테이블 사이에 기본키 (Primary Key, PK) 와 외래키 (Foreign Key, FK) 를 통해 양방향으로 접근 가능한 관계형 데이터베이스와는 반대되는 개념이다.

 

aggregate

연관 객체의 묶음을 말한다.

e.g. 유저가 상품을 주문하는 애플리케이션에서 Order 와 OrderItem 은 하나의 aggregate 에 속한다.

 

aggregate root

연관 객체의 묶음인 aggregate 에서 root 에 해당하는 엔티티로, aggregate 에 대해서 aggregate root 에 해당하는 엔티티의 Repository 로만 aggregate 를 가져와야 한다. 즉, aggregate root 하나 당 하나의 repository 를 만들어야 한다.

e.g. Order 와 OrderItem 이 속한 aggregate 에서 Order 는 aggregate root 에 해당하며 OrderItem 은 Order 객체에 종속적이다. 그리고 OrderItem 을 데이터베이스로부터 가져올 때 Order 의 repository 를 통해서만 가져올 수 있다.

 

(참고 : https://docs.spring.io/spring-data/jdbc/docs/current/reference/html/#jdbc.why)

 

 

하나 특이한 점은 Spring Data JPA 에서 연관관계의 주인이 테이블에 외래키가 있는 객체인 반면, Spring Data JDBC 에서는 외래키가 없는 객체라는 것이다. 외래키가 있는 테이블은 참조하는 테이블에 종속적이고, 테이블을 객체에 대응했을 때 외래키가 있는 객체는 참조하는 객체에 종속적이다. 따라서 aggregate root 는 참조하는 객체, 외래키가 없는 객체이며 해당 객체의 repository 를 통해서만 aggregate 의 데이터베이스 CRUD가 가능하므로, 연관관계의 주인은 외래키가 없는 객체이어야 한다.

 

위의 예에서 Order 과 OrderItem 객체 간에 연관관계의 주인은 Order 에 해당하며, OrderItem 은 Order 객체의 items 필드에 추가된 뒤 OrderRepository 를 통해서만 데이터베이스 CRUD가 가능하다. 즉, 아래와 같이 연관관계가 매핑되어야 한다.

 

Order.java
OrderItem.java

(개인적인 의견이므로 틀린 내용일 수 있습니다. 피드백 주시면 감사하겠습니다.)

 

 

 

1:1 연관관계 매핑

먼저 movie 테이블과 rental 테이블이 아래와 같이 1:1 관계를 가지고 있다고 가정하자.

schema.sql

 

위의 테이블 내 필드와 연관관계에 따라 아래와 같이 도메인 객체를 추가한다. 여기서 연관관계의 주인은 외래키가 없는 테이블에 해당하는 객체이자 aggregate root 인 Movie 에 해당하고, 해당 객체에 non-root aggregate 에 해당하는 객체의 필드에 Rental 을 추가해 연관관계를 매핑한다.

 

Movie.java
Rental.java

 

그리고 아래와 같이 Movie 객체의 속성으로 Rental 객체를 추가하고 MovieRepository 에 저장했을 때 Movie 객체와 함께 Rental 객체가 연관관계 매핑이 되고 데이터베이스에 저장되는 것을 확인할 수 있다.

 

 

해당 소스코드는 아래의 커밋에서 확인할 수 있다.

 

one-to-one 을 @Column 을 이용해 연관관계 매핑 · rxdcxdrnine/relation-spring-data-jdbc@4026714

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

github.com

 

혹은 2개의 테이블에 대응하는 객체를 매핑하기보다, @Embedded 를 이용해 한 객체에서 다른 값 객체를 포함해 하나의 테이블에 대응할 수 있다. 자세한 내용은 아래를 참고하자.

 

https://docs.spring.io/spring-data/jdbc/docs/current/reference/html/#jdbc.entity-persistence.embedded-entities

 

 

1:N 관계 매핑

먼저 Order 테이블과 OrderItem 테이블이 아래와 같이 1:N 관계를 가지고 있다고 가정하자.

 

 

위의 테이블 내 필드와 연관관계에 따라 아래와 같이 도메인 객체를 추가한다. 여기서 연관관계의 주인은 외래키가 없는 테이블에 해당하는 객체이자 aggregate root 인 Order 에 해당하고, 해당 객체에 non-root aggregate 에 해당하는 객체의 필드에 OrderItem 을 추가해 연관관계를 매핑한다.

 

연관관계 매핑 시에는 One 에 해당하는 테이블의 컬럼을 @MappedCollection 의 idColumn 에 추가한다. 그리고 Many 에 해당하는 객체의 컬렉션을 Set 타입으로 추가한다. Many 에 해당하는 객체 컬렉션을 추가할 때 Set 외에도 Many 와 List 를 사용할 수 있는데, 이 때 @MappedCollection 의 keyColumn 에 별도의 컬럼을 추가해야한다. 자세한 내용은 아래 링크를 참고하자.

 

https://docs.spring.io/spring-data/jdbc/docs/current/reference/html/#jdbc.entity-persistence.types

 

Order.java
OrderItem.java

그리고 아래와 같이 Order 객체의 속성으로 OrderItem 객체를 추가하고 OrderRepository 에 저장했을 때 Order 객체와 함께 OrderItem 객체가 연관관계 매핑이 되고 데이터베이스에 저장되는 것을 확인할 수 있다.

 

 

해당 소스코드는 아래의 커밋에서 확인할 수 있다. (테이블의 경우 이후 커밋에서 기본 키를 다시 설정했다.)

 

 

one-to-many 를 @MappedCollection 을 이용해 연관관계 매핑 · rxdcxdrnine/relation-spring-data-jdbc@2d2d3df

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

github.com

 

 

Spring Data JDBC 는 1:1 과 1:N 관계 매핑을 지원하지만, N:1 과 N:M 관계 매핑을 지원하지 않는다. 따라서 N:1 관계 매핑은 별도의 RowMapper 를 정의해 조회만 가능하고, N:M 관계 매핑은 결합 테이블을 생성한 뒤 1:N 관계 2개를 사용해 매핑해야한다.

 

 

N:1 관계 매핑

1:N 관계에서 Order 와 OrderItem 에 대해, Order 에서 OrderItem 을 조회하는 대신 OrderItem 에서 Order 를 조회하는 경우를 생각해보자. 1:1 관계와 비슷하게 OrderItem 에서 Column("order_id") 으로 Order 필드를 추가한다.

 

OrderItem.java

 

그리고 아래의 테스트를 진행하면, 다음과 같이 동일한 쿼리가 반복해서 호출되며 스택 오버플로가 발생한다.

 

 

정확한 이유는 찾기 어렵지만, 객체의 순환 참조로 인해 해당 문제가 발생한 것으로 보인다. OrderItem 에서 참조하는 Order 를 조회하고, Order 객체에서는 다시 참조하는 OrderItem 컬렉션을 조회하면서 순환 참조가 발생하고 따라서 위와 같이 서로 참조가 일어난 것으로 추측된다. 하지만 순환 참조를 제거하기 위해 이미 연관관계의 주인인 Order 에서 OrderItem 을 참조하고 있으므로 해당 속성을 제거하기는 어렵다.

 

순환 참조 문제를 해결하는 대신, 별도의 조회 쿼리를 작성하고 RowMapper 를 이용해 조회 결과를 객체로 매핑하는 방법을 선택하자.

 

OrderItemRepository.java
OrderItemRowMapper.java

 

이 때 OrderItem 에는 Order 객체를 속성으로 추가하지만 객체를 테이블로 매핑하지 않도록 @Transient 을 추가한다. 즉, Spring Data JDBC 에서 객체를 테이블로 매핑할 때 Order 객체는 포함되지 않고, JDBCTemplate 을 이용해 RowMapper 로 조회 결과를 객체로 매핑할 때 수동으로 Order 객체를 포함시킨다.

 

OrderItem.java

 

이제 해당 방법으로 조회했을 때 순환 참조가 발생하지 않고, 테이블 간의 조인을 사용했으므로 1번의 쿼리 실행 만으로 연관관계가 매핑된 객체를 조회할 수 있다.

 

 

해당 소스코드는 아래의 커밋에서 확인할 수 있다.

 

many-to-one 에 대해 @transient 와 RowMapper 를 이용해 연관관계 매핑 · rxdcxdrnine/relation-spring-data-jdbc@5

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

github.com

 

 

M:N 관계 매핑

M:N 관계는 관계형 데이터베이스에서 결합 테이블이라는 별도의 테이블을 만들고, 1:N 관계 2개로 나눠 생각해야한다. 예를 들어 book 과 author 테이블이 M:N 관계를 가지고 있다고 가정하자. book 과 author 테이블 사이에 결합 테이블 book_author 를 만들면, book 과 book_author 는 1:N 의 관계, author 와 book_author 는 1:N 관계를 가진다.

 

 

여기서 book 과 author 테이블에 대응하는 Book 과 Author 객체는 각각 다른 aggregate 에 속한다. 서로 다른 aggregate 에 속하는 엔티티가 있을 때 엔티티 간에 직접적으로 참조하기보다, 다른 엔티티의 Id 를 가지고 접근하는 방식을 사용해야 한다. 따라서 book 과 author 는 서로 직접적으로 참조할 수 없고, 서로의 Id 를 가지고만 접근해야한다.

 

먼저 Book 에서 Author 를 접근한다고 가정해보자. Book 과 BookAuthor 객체 사이에 1:N 관계 매핑을 하면 되는데, 여기서 Book 이 Author 객체를 참조하지만 직접 참조하지 못하고 Author 의 Id 를 가진 객체만 참조한다. 따라서 BookAuthor 대신에 AuthorRef 를 참조하는 것으로 생각하는 쪽이 더 편하다. (여기서 Book 과 Author 는 같은 패키지에 있으면 더 좋을 것이다.) 즉 Book 과 AuthorRef 는 같은 aggregate 에 속하고, 1:N 관계에 있으므로 Book 에서 AuthorRef 의 컬렉션을 속성으로 가진다.

 

AuthorRef.java

 

그리고 Author 는 AuthorRef 와 독립적으로 author 테이블의 컬럼을 속성으로 가지는 객체이다. AuthorRef 는 여기에서 Id 필드만 가지는 객체이며, author 테이블이 아닌 book_author 테이블에 매핑되는 점이 다르다.

 

 

그리고 Book 조회 시 AuthorRef 와 함께 조회된 author 테이블의 Id 를 가지고 별도로 Author 를 조회해야한다. 다시 말해서 N:M 관계 매핑은 한 객체를 조회 시에 관계에 있는 다른 객체의 컬렉션을 조회할 수 없고, 별도의 쿼리로 데이터베이스를 조회한 뒤, 자바 애플리케이션 코드에서 직접 관계를 매핑해야 한다.

 

 

하지만 위의 코드로는 단순히 관계에 있는 Author 를 조회했을 뿐, Book 과 관계에 있는 Author 를 매핑하지 않았다. 관계에 있는 객체를 매핑하기 위해서는 좀 더 복잡한 처리가 필요한데, 자바 8의 스트림을 이용한 코드는 아래 커밋을 참고하자.

 

 

자바 애플리케이션에서 many-to-many 연관관계 매핑 · rxdcxdrnine/relation-spring-data-jdbc@179791f

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

github.com

 

이상으로 Spring Data JDBC 에서 1:1, 1:N, N:1, N:M 관계 매핑을 알아보았다. 아직 부실한 내용이 많아, 공부하면서 내용을 더 보충해야겠다.

 

 

 

 

 

 

 

 

 

 

'Java' 카테고리의 다른 글

코드로 스프링 트랜잭션 API 구조 보기  (0) 2022.05.12
열거형을 이용해 if-else 블록 제거하기  (0) 2022.02.19
주소 변환 (address translation) 의 원리  (0) 2022.01.20
JVM 과 스택 프레임 (Stack Frame)  (0) 2022.01.19
스트림 (Stream) API  (0) 2022.01.13
'Java' 카테고리의 다른 글
  • 코드로 스프링 트랜잭션 API 구조 보기
  • 열거형을 이용해 if-else 블록 제거하기
  • 주소 변환 (address translation) 의 원리
  • JVM 과 스택 프레임 (Stack Frame)
밀러 (miller)
밀러 (miller)
  • 밀러 (miller)
    밀러의 데브로그
    밀러 (miller)
  • 글쓰기 관리
  • 전체
    오늘
    어제
    • 분류 전체보기
      • Java
      • Node.js
      • Production
      • Books
      • Computer Science
      • TIL
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
    • LinkedIn
  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.3
밀러 (miller)
Spring Data JDBC 에서 연관관계 매핑
상단으로

티스토리툴바