본문 바로가기
Spring 이해하기

Spring Data JDBC 가볍게 살펴보기 - 1

by simplify-len 2021. 5. 9.

Spring Data JPA 마찬가지로, Spring Data 라는 Prefix 가 붙습니다. 여기서 Spring Data 란 무엇일까? 간단하게 Docs 에서 말하는 컨셉을 이해해보자.

 

Spring Data의 핵심은 Repository 이다. 이는 ID가 되는 Type과 Domain Class가 되는 Type을 Argument로 갖는다. 이 인터페이스는 주로 작업할 Type을 캡처하고 인터페이스를 확장하는 인터페이스를 발견하는 데 도움이 되는 마커 인터페이스 역할을 합니다.
- docs.spring.io/spring-data/jdbc/docs/current/reference/html/#repositories.core-concepts

 아마도 Spring Data 라고 하는 것들은 Repository 가 핵심인가 봅니다.

왜 Spring Data JDBC 를 사용하는 걸까요?

 

Spring Data는 위에서 말한 것과 같이 Repository 라는 인터페이스로 사용됩니다. 그리고 우리에게는 주변에서 흔하게 JPA가 사용됩니다. JPA를 사용하는 이유에 대해서 간단하게 생각해볼까요? Object에 대해서 변화를 추적할 수 있고, 캐시 역할을 해주는 1차 엔티티라 불리는 엔티티매니저, Lazy loding 을 활용해 쉽게 RDB에 데이터를 CRUD 할 수 있습니다. JPA에 대해서 튜트리얼을 한번 해보고 나서 바로 개발에 적용할 수 있을 정도로 사용하기 편리합니다. 

 

 그러나 이렇게 편리하게 사용할 수 있는 만큼, 가끔씩 혼란을 일으킬 때가 있습니다. 예를 들어 Deteched 된 상태의 Object에 접근한다거나, 또는 관계 맵핑에서 Eager Loading시 예상하지 못했던 엔티티의 정보까지 가져오는 등이 있을 것입니다. 이런 혼란이 일어난 이유를 잘 생각해보면, 이전에 말했던 우연한 복잡성이 낳은 결과가 아닐까 싶습니다. 관련 포스트는 아래 첨부합니다.

 

Hibernate, JPA, Querydsl는 과연 좋은 도구일까?

 

Hibernate, JPA, Querydsl는 과연 좋은 도구일까?

Photo by Annie Spratt on Unsplash 오늘도 여전히 사이드프로젝트를 진행하며 함께 하는 형님과의 대화에서 또다른 깨달음을 얻었습니다. 들어가기  최근 회사에서 시간이 남아, Querydsl 을 사내서비스에

happy-coding-day.tistory.com

 Spring Data JDBC는 앞서 말한 우연한 복잡성을 줄이기 위해서 나온 라이브러리라고 판단됩니다. 

 

Spring Data JDBC는 JPA에 비해서 더욱 심플한 컨셉을 가지고 있습니다.

 

1. Lazy Loading 또는 캐시와 같은 것이 없습니다.

 

2. 만약 엔티티를 저장하면, 무조건 저장이 됩니다. JPA와 같이 1차 엔티티에 가지고 있어서 저장을 안하는 등의 일은 일어나지 않습니다. 

 

 

Spring Data JDBC 는 Domain Driven Design 의 철학을 지녔습니다.

Spring Data에서 사용되는 용어로, Repository, Aggregate 그리고 Aggregate root 는 도메인 주도 설계에서 영향을 받았다고 합니다. 도메인 주도 설계는 기존에 데이터베이스에 스키마를 먼저 설계하고 하는 방식의 트랜잭션 스크립트 방식과 다르게, 앞서 언급한 도메인주도설게에서 도메인을 중심으로 설계합니다. 그렇기 때문에 Repository, Aggregate와 같은 것이 얼마나 중요한지 알 수 있습니다.

 

 

Aggregate는 복수의 Entitiy 집합으로 구성되어, 트랜잭션에 의해서 엔티티간에 변경이 일어날 때 문제가 없다는 것을 보장할 수 있도록 합니다. 가장 간단한 예시로 Order와 OrderItems 있습니다. OrderItems의 변경은 오직 Order가 변경이 일어날 때 변경되어야 할 것입니다. OrdeItem이 스스로 변경된다면 Order 와 OrderItem의 일관성이 깨지게 되겠죠?

 

 

또한,복수의 Aggregate 가 서로 참조할 경우, 동시에 일관성을 보장할 수는 없습니다만, 결국에는 일관성을 보장하게 될 것입니다.

 

 

각각의 Aggregate 는 정확히 하나의 Aggregate Root를 가집니다.  Aggregate의 조작은 오직 Aggreagte Root의 Methods에 의해서만 가능합니다. 그리고 일반적으로, Spring Data에서는 하나의 Repository가 Aggregate root를 가집니다. 이는 Aggregate Root로부터 모든 엔티티를 접근할 수 있습니다. Spring Data JDBC에서 Aggreagte는 오직 Aggreagte Root가 아닌 엔티티의 foreign Key 를 가지고, 그 외에 다른 루트로 접근할 수 없습니다.

 

Entity 를 저장할 때 Spring Data JDBC는

 JPA와는 달리 무조건 Save 메소드를 호출시에 저장됩니다. 이때 JPA와 다른 점이 있습니다. 만약 aggregate가 새롭게 만든 객체라면, 저장이 수행되고, 그렇지 않다라면, 모든 관련된 Referenced 는 삭제하고 다시 삽입합니다. 

 

여기서 Spring Data JDBC가 JPA와 다르다고 말할 수 있는 가장 큰 차이점이 나타납니다. 관련 엔티티에 대해서 특정 필드의 데이터를 업데이트 할 경우, 모든 관련 Entity가 삭제되고 다시 삽입되는 쿼리가 날라가는 것이 JDBC의 명백한 단점이라고 말합니다.

 아주 사소한 참조 객체에 대한 변경이 일어났을 뿐인데- 관련 엔티티가 모두 삭제되고 다시 삽입되는 건 낭비라고 표현합니다. 이렇게 하는 이유는, Aggregate의 이전 상태를 기억할 수 없기 때문에 발생합니다. [참고]

 

간단히 코드로 표현하면 아래와 같습니다.

 

먼저, 유저가 취미를 가진 일대다 관계의 코드입니다.

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.springframework.data.annotation.Id;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor(access = AccessLevel.PACKAGE)
public class User {

	@Id
	private Long id;
	private String name;

	private Set<Hobby> hobbies;

	public static User of(String name) {
		return new User(null, name, Collections.emptySet());
	}

	public void add(Hobby hobby) {
		Set<Hobby> hobbies = getHobbies();
		hobbies.add(hobby);
		Set<Hobby> temp = new HashSet<>(hobbies);
		setHobbies(temp);
	}

	public void setHobbies(Set<Hobby> hobbies) {
		this.hobbies = hobbies;
	}
}
import lombok.Value;

@Value(staticConstructor = "of")
public class Hobby {

	String title;

	public Hobby(String title) {
		this.title = title;
	}

	public Hobby updateTitle(String title) {
		return new Hobby(title);
	}
}

 

이 때 Hobby 의 데이터를 변경하는 테스트코드를 아래와같이 작성했습니다.

	@Test
	void 관련엔티티_add_테스트() {
		User user = User.of("Foo");
		HashSet<Hobby> hobbies = new HashSet<>();
		hobbies.add(Hobby.of("독서"));
		hobbies.add(Hobby.of("코딩"));
		user.setHobbies(hobbies);
		User savedUser = userRepository.save(user);
		assertThat(savedUser.getName()).isEqualTo("Foo");
		assertThat(savedUser.getHobbies().size()).isEqualTo(2);

		User found = userRepository.findById(savedUser.getId()).get();
		assertThat(found.getName()).isEqualTo("Foo");
		assertThat(found.getHobbies().size()).isEqualTo(2);

		User saved = userRepository.save(found);
		assertThat(saved.getId()).isEqualTo(found.getId());
		assertThat(saved.getHobbies().size()).isEqualTo(2);

		User saved2 = userRepository.save(saved);
		assertThat(saved2.getId()).isEqualTo(found.getId());
		assertThat(saved2.getHobbies().size()).isEqualTo(2);

		/**
		 *  SQL Result
		 *  [INSERT INTO "USER" ("NAME") VALUES (?)]
		 *  [INSERT INTO "HOBBY" ("TITLE", "USER") VALUES (?, ?)]
		 *  [INSERT INTO "HOBBY" ("TITLE", "USER") VALUES (?, ?)]
		 *  [SELECT "USER"."ID" AS "ID", "USER"."NAME" AS "NAME" FROM "USER" WHERE "USER"."ID" = ?]
		 *  [SELECT "HOBBY"."TITLE" AS "TITLE" FROM "HOBBY" WHERE "HOBBY"."USER" = ?]
		 *  [UPDATE "USER" SET "NAME" = ? WHERE "USER"."ID" = ?]
		 *  [DELETE FROM "HOBBY" WHERE "HOBBY"."USER" = ?]
		 *  [INSERT INTO "HOBBY" ("TITLE", "USER") VALUES (?, ?)]
		 *  [INSERT INTO "HOBBY" ("TITLE", "USER") VALUES (?, ?)]
		 *  ...
		 */
	}

 

결과적으로, Hobby에 대한 데이터를 삭제하고 다시 삽입합니다.

 

마지막으로, Spring Data JDBC문서에서 말하는 추천하는 개발방식에 대해서 언급하고 마무리하려고 합니다.

 

1. immutable 한 객체를 만들려고 시도하라.

 

이러한 시도는, Setter 메서드로 도메인 객체을 깨지게 하는 것을 보호할 수 있습니다. 또한 Side-effect도 방지할 수 있을 것입니다.

 

 

2.  all-args constructor 를 제공하세요.

 

그렇지 않을 경우, Object에 속성을 건너뛰고 만드는 경우가 발생할 수 있습니다.

 

 

3. factory method 를 사용해, 객체를 생성하세요. 

 

이는 id가 null인 객체를 통해서 새로운 엔티티를 만들어야 할 때, 억지로 ID가 null이라는 것을 노출시킬 필요가 없어지기 때문에 활용하면 좋을 듯합니다.

 

 

4. 엔티티를 생성한다면, all-arguement 생성자에 final field 를 사용하세요. 또는 With... 메소드를 사용하세요.

 

 

5. boilerplate code를 피하기 위해서는 Lombok 를 사용하세요.

 

 

여기까지 Spring Data에 대해서 간략히 알아봤습니다. 다음에는 조금 더 자세한 내용에 대해서 살펴보겠습니다.

댓글