Eddy의 스프링 디자인 패턴 - Spring Data(1)
스프링 데이터는 데이터베이스 접근을 위한 일관되고, 추상화된 모델을 제공한다. 다양한 데이터베이스의 데이터 연동 개발을 심플하게 구축할 수 있다. 공식 홈페이지에서 소개하는 Features는 아래와 같다.
Powerful repository and custom object-mapping abstractions
Dynamic query derivation from repository method names
Implementation domain base classes providing basic properties
Support for transparent auditing (created, last changed)
Possibility to integrate custom repository code
Easy Spring integration via JavaConfig and custom XML namespaces
Advanced integration with Spring MVC controllers
Experimental support for cross-store persistence
Spring Data Commons - Core Spring concepts underpinning every Spring Data module.
Spring Data JDBC - Spring Data repository support for JDBC.
Spring Data JDBC Ext - Support for database specific extensions to standard JDBC including support for Oracle RAC fast connection failover, AQ JMS support and support for using advanced data types.
Spring Data JPA - Spring Data repository support for JPA.
Spring Data KeyValue - Map based repositories and SPIs to easily build a Spring Data module for key-value stores.
Spring Data LDAP - Spring Data repository support for Spring LDAP.
Spring Data MongoDB - Spring based, object-document support and repositories for MongoDB.
Spring Data Redis - Easy configuration and access to Redis from Spring applications.
Spring Data REST - Exports Spring Data repositories as hypermedia-driven RESTful resources.
Spring Data for Apache Cassandra - Easy configuration and access to Apache Cassandra or large scale, highly available, data oriented Spring applications.
Spring Data for Apace Geode - Easy configuration and access to Apache Geode for highly consistent, low latency, data oriented Spring applications.
Spring Data for Apache Solr - Easy configuration and access to Apache Solr for your search oriented Spring applications.
Spring Data for Pivotal GemFire - Easy configuration and access to Pivotal GemFire for your highly consistent, low latency/high through-put, data oriented Spring applications.
Spring Data Aerospike - Spring Data module for Aerospike.
Spring Data ArangoDB - Spring Data module for ArangoDB.
Spring Data Couchbase - Spring Data module for Couchbase.
Spring Data Azure DocumentDB - Spring Data module for Microsoft Azure DocumentDB.
Spring Data DynamoDB - Spring Data module for DynamoDB.
Spring Data Elasticsearch - Spring Data module for Elasticsearch.
Spring Data Hazelcast - Provides Spring Data repository support for Hazelcast.
Spring Data Jest - Spring Data module for Elasticsearch based on the Jest REST client.
Spring Data Neo4j - Spring based, object-graph support and repositories for Neo4j.
Spring Data Vault - Vault repositories built on top of Spring Data KeyValue.
Spring-Data-Commons 는 스프링 데이터 프로젝트의 핵심이며, 기본적인 추상화를 제공한다.
가장 기본이 되는 인터페이스는 Repository, CrudRepository이다. CrudRepository는 기본적인 Create, Read, Update, Delete 를 정의한다.
Spring-Data-Commons 의 CrudRepository 를 활용한 간단한 샘플 코드를 작성해보자. 참고로 필자는 Spring-Data-Commons 모듈만 별개로 분리해서 실무에서 사용해본 적은 없었다. Spring-Data의 모든 프로젝트는 Spring-Data-Commons 모듈을 사용한다. 대표적으로 많이 사용하는 모듈이 Spring-Data-JPA인데, 당연히 Spring-Data-Commons 모듈을 기본으로 사용한다.
즉, 우리는 Spring-Data-JPA 프로젝트로 개발하면, 기본으로 Spring-Data-Commons 디펜던시를 사용했던 것이다. 하지만, 필자의 샘플 테스트에서는 Spring-Data-Commons 모듈만 별도로 분리해서 사용해보겠다. Spring-Data-Commons 모듈에 대한 공식 스타터를 제공하지 않기 때문에 디펜던시를 직접 추가해야 한다. 스프링부트 버전은 2.0.6.RELEASE 이다.
buildscript {
ext {
springBootVersion = '2.0.6.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
생략...
dependencies {
implementation('org.springframework.data:spring-data-commons')
implementation('org.springframework.boot:spring-boot-starter-web')
compileOnly('org.projectlombok:lombok')
testImplementation('org.springframework.boot:spring-boot-starter-test')
}
아래 화면과 같이 Spring-Data-Commons 모듈이 잘 추가되었다.
CrudRepository 를 상속받는 CustomDataBaseRepository 인터페이스를 정의하자.
public interface CustomDataBaseRepository<T, ID> extends CrudRepository<T, ID> {
List<T> findAll();
T findByName(String name);
}
필자는 2개의 메서드를 신규로 정의하였다. findAll 메서드는 상위 인터페이스인 CrudRepository에 정의가 되어있는데, 필자가 임의로 재정의하였다. findByName 메서드는 상위 인터페이스에 정의되지 않은 메서드로 필자가 신규로 작성한 메서드이다. CustomDataBaseRepository 를 구현하는 구현체는 CrudRepository 인터페이스에 정의된 메서드와 필자가 정의한 메서드를 모두 구현해야 한다. 이제 CustomDataBaseRepository 를 구현하는 SimpleCustomDataBaseProvider 클래스를 작성하자.
/**
* CustomDataBaseRepository 구현체
*
* @author Eddy Kim
*/
@Repository
public class SimpleCustomDataBaseProvider<T, ID> implements CustomDataBaseRepository<T, ID> {
private Set<T> hashSet = new HashSet<>();
public List<T> findAll() {
return new ArrayList<>(hashSet);
}
public Optional<T> findById(ID id) {
return null;
}
public <S extends T> S save(S entity) {
hashSet.add(entity);
return entity;
}
public T findByName(String name) {
return null;
}
//CrudRepository 에 정의된 메서드를 모두 구현해야 한다.
public <S extends T> Iterable<S> saveAll(Iterable<S> entities)
public boolean existsById(ID id)
public Iterable<T> findAllById(Iterable<ID> ids)
생략...
SimpleCustomDataBaseProvider 클래스는 CrudRepository 인터페이스와 CustomDataBaseRepository 인터페이스에 정의된 모든 메서드를 구현해야 한다. 추가로, 필자가 임의로 데이터셋 자료구조는 HashSet 으로 하였다. 지금까지 정의 및 구현한 인터페이스와 클래스를 다이어그램으로 작성하면 아래와 같다.
정리하면, Repository 와 CrudRepository 는 Spring-Data-Commons 의 인터페이스이고, CustomDataBaseRepository 와 SimpleCustomDataBaseProvider 는 필자가 작성한 인터페이스&클래스이다. 이제 테스트를 위해 간단한 Controller 를 작성하였다. 사실... 소스 코드가 중요하지는 않다. Spring-Data-Commons 를 이해하는 과정이라서, 소는 그냥 무시해도 된다.
@RestController
@RequestMapping("/coffees")
public class CoffeeController {
@Autowired
private final CustomDataBaseRepository customDataBaseRepository;
public CoffeeController(CustomDataBaseRepository customDataBaseRepository) {
this.customDataBaseRepository = customDataBaseRepository;
}
@PostMapping
public String init(){
customDataBaseRepository.save(new Coffee(1,"모카",1200));
customDataBaseRepository.save(new Coffee(2,"라떼",1100));
customDataBaseRepository.save(new Coffee(3,"아메리카노",900));
return "OK";
}
@GetMapping
public List<Coffee> list(){
return customDataBaseRepository.findAll();
}
@GetMapping("/{name}")
public Coffee find(@PathVariable(name = "name") String name){
return (Coffee) customDataBaseRepository.findByName(name);
}
}
해당 코드에서 의존성 주입은 CustomDataBaseRepository 타입으로 하는데, 해당 인터페이스를 구현하는 구현체는 위에서 작성한 SimpleCustomDataBaseProvider 클래스 이다.
즉, SimpleCustomDataBaseProvider 라는 클래스에 @Repository 어노테이션이 붙어있고, 해당 클래스는 Bean(빈) 이름을 따로 명시하지 않는다면 simpleCustomDataBaseProvider 라는 빈이 생성될 것이다. 해당 코드에서는 SimpleCustomDataBaseProvider 의 빈이 주입될 것이고 실제 핵심 로직이 해당 클래스의 메서드가 실행될 것이다.
내용이 매우 기초적인 내용이라서 이 글을 읽는 대부분은 너무 쉽게 느껴질 것이다. CustomDataBaseRepository 인터페이스를 구현한 SimpleCustomDataBaseProvider 클래스에서 실제로 구현해야 하는 핵심 로직이 포함되어있다. 또한 해당 클래스는 Spring-Data-Commons 의 CrudRepository 인터페이스의 모든 메서드, 예를 들어서 findAll(), save() 같은 메서드 역시 구현해야 한다. 근데 만약 데이터를 제공하는 데이터베이스가 변경되었다고 가정해보자. 즉, 구현체라고도 부르고, 또는 데이터 프로바이더라고도 부르는 레이어가 변경되면 어떻게 될까?
신규로 NewCustomDataBaseRepository라는 인터페이스를 정의해보자. 그리고 해당 인터페이스를 구현하는 구현체 클래스를 작성하자. HomeController 클래스를 아래와 같이 변경하자.
@Autowired
//private final CustomDataBaseRepository repository;
private final NewCustomDataBaseRepository repository;
이렇게 간단하게 수정하면 끝이다. HomeController 에서 사용하는 save(), findAll() 등의 메서드를 그대로 호출하면 된다. 물론, 구현체마다 실제 구현 코드는 다를 것이다.
위에서 설명한 방법은 CrudRepository 를 상속해서 필자가 커스텀한 인터페이스를 정의하고, 구현을 직접 했었다. 하지만, 실무에서는 사실 Spring Data Commons만 별도로 사용하는 경우가 많지는 않다. Spring-Data 하위 공식 모듈을 많이 사용하는데, 대표적으로는 Spring Data JPA, Spring Data MongoDB, Spring-Data-Redis등이 있다. 하나씩 살펴보자.
실무에서 제일 많이 사용한다. 필자가 임의로 JpaRepository 인터페이스를 상속받는 CityReposity 라는 인터페이스를 아래와 같이 생성하였다.
Spring-Data-Commons : Repository, CrudRepository, PagingAndSortingRepository
Spring-Data-JPA : JpaRepository, SimpleJpaRepository
필자가 만든 : CityRepository
위에서 필자가 구현한 소스와 비교해보면, CustomDataBaseRepository 는 JpaRepository 와 유사하고, SimpleCustomDataBaseProvider 는 SimpleJpaRepository 와 유사하다.
CustomDataBaseRepository 는 JpaRepository
SimpleCustomDataBaseProvider 는 SimpleJpaRepository
단, 필자가 작성한 코드와의 차이점이 있다. 여기서부터 조금 헷갈린다. 필자가 글을 쓰면서도 헷갈린다. 필자는 CustomDataBaseRepository 를 구현하는 SimpleCustomDataBaseProvider 를 작성하였고, 컨트롤러에서는 CustomDataBaseRepository 타입의 빈을 주입해서 사용하였다. 하지만, JPA 코드에서는 한 단계 더 상속받는다. 즉, JpaRepository를 구현하는 SimpleJpaRepository를 작성하는것 까지는 필자의 샘플 코드와 같지만, 컨트롤러에서 JpaRepository타입의 빈을 주입하는 게 아니라, JpaRepository를 다시 상속받는 CityRepository 라는 커스텀한 인터페이스가 작성되었고 CityRepository 를 주입받아서 사용해야 한다. 이 경우 특별한 작업을 하지 않는다면, 컨트롤러에서는 CityRepository 를 주입할 수가 없지만, Spring-Data-JPA 에서 주입받을 수 있도록 자동으로 빈으로 생성해준다. CityRepository 는 cityRepository 라는 Bean 으로 생성될 것이다. 궁금하다면 소스를 디버깅하면 따라가보면 된다. RepositoryConfigurationDelegate 클래스의 registerRepositoriesIn 메서드를 참고하면 된다.
Spring-Data-JPA 에서는, 개발자가 커스텀하게 만든 Repository의 Bean을 자동으로 생성해준다. 필자가 이 부분은 따로 공부 중인데 확실하게 파악이 안 되어서 글로 상세하게 남기지는 않겠다. 혹시라도 잘 아시는 스프링 고수 분들께서는 피드백을 남겨주길 바란다.
Spring-Data-MongoDB 역시 JpaRepository 와 유사한 MongoRepository 인터페이스가 정의되어 있다. 또한 SimpleJpaRepository 와 유사한, SimpleMongoRepository 라는 구현체가 존재한다.
필자가 작성한 BookRepository 에 대한 구현체 및 Bean은 Spring-Data-MongoDB 에서 자동으로 만들어줄 것이다.
앞선 두 모듈(Jpa, MongoDB)와는 다르게, Spring-Data-Redis 에서는 사용자가 작성하는 Repository 는 Spring-Data-Commons 의 CrudRepository 를 바로 상속받는다.
Spring-Data 하위 모든 데이터베이스 모듈을 검토한 것은 아니지만, 대부분 Spring-Data-Commons 를 기반으로 구현된 것임을 확인할 수 있다.
주저리주저리 두시간 정도 글을 쓴 거 같은데, 참 의미 없어 보이기도 하다. 브런치 블로그를 때려치울까 몇 달 때 고민 중인데 아직 못 그만뒀다. 여러 가지로 개발블로그로 쓰기에는 불편해서, 깃 헙 페이지나 미디엄 또는 티스토리로 갈아탈까 고민 중인데 아직도 실행을 못하고 있다. 몇 개 안 되는 글이지만, 글 옮기는 것도 귀찮고... 어쩃든 카카오에서 브런치 서비스를 좀 더 개선해줬으면 좋겠다.
이제 드디어 본격적으로 Spring Data 디자인 패턴에 대해서 정리해보겠다. 참고로 지금부터 쓰는 글은 필자의 주관적인 생각임을 먼저 밝힌다. Pivotal 에서 어떤 철학으로 Spring Data 를 개발하는지 따로 전해 들은 바는 없다. 그래서, 혹시라도 오해가 생길까 봐 조심스럽다. 필자가 나름 심각하게 고민한 내용이지만, 필자는 스프링 초보이다. 필자의 글은 지금부터는 한 귀로 듣고 한 귀로 흘려도 된다. 편하게 읽어 주길 바란다. (여기까지 읽은 개발자가 있을는지 모르겠지만...) 만약 필자의 글에 의견을 남기고 싶다면 언제든지 댓글로 피드백을 남겨주길 바란다.
필자가 작성한 샘플 소스를 수정해보자. 기존 코드는 Controller 레이어에서 Repository 를 바로 호출했었는데, 중간에 UseCase(서비스 레이어)를 아래와 같이 추가하였다.
@Service
public class CoffeeUseCase {
@Autowired
private final CustomDataBaseRepository customDataBaseRepository;
public CoffeeUseCase(CustomDataBaseRepository customDataBaseRepository) {
this.customDataBaseRepository = customDataBaseRepository;
}
public List<Coffee> findAll(){
return customDataBaseRepository.findAll();
}
public Coffee findByName(String name){
return (Coffee) customDataBaseRepository.findByName(name);
}
public void save(Coffee coffee){
customDataBaseRepository.save(coffee);
}
}
Controller 에서는 UseCase 레이어를 호출하도록 변경하였다..
@RestController
@RequestMapping("/coffees")
public class CoffeeController {
@Autowired
private final CoffeeUseCase coffeeUseCase;
생략...
필자의 디펜던시 규칙을 이해하기 위해서 아래와 같이 그림을 그려봤다.
원 안으로 갈수록 추상화 단계는 높아진다.
바깥쪽 원에서는 안쪽 원에서 변경된 사항에 대해서 반드시 알아야 한다.
안쪽 원에서는 바깥쪽 원에서 변경된 사항에 대해서는 알지 못한다. 즉, 바깥쪽 원에서 변경사항이 있어도 안쪽 원에서는 변경이 없어야 한다. 바깥쪽 원이, 안쪽 원에 영향을 끼치면 안 된다.
이 그림은, 로버트밥 마틴의 클린아키텍처 또는 Onion아키텍처와 매우 유사하다. 클린 아키텍처가 궁금하다면 아래 원서를 읽어보자.
https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164
물론 필자의 그림이 해당 아키텍처를 완벽하게 따라한것은 아니다. 참고로, 클린 아키텍처는 해외에서는 조금 논란이 많은 것 같다. 로버트 마틴의 원서인데 일부 개발자들 사이에서 좋지 않은 평이 많다. 하지만, 필자는 해당 원서를 매우 추천하는 편이다. 시간이 된다면 꼭 읽어보자.
아무튼, 그림을 통해서 디펜던시룰 에 대해서 이해가 되었는가? 이해가 되었다면 다음을 읽어도 된다. 혹시 이해가 잘 안 되었다면... 필자의 설명이 부족하기 때문이다. 관련 자료를 찾아보거나, 개발 경험을 더 쌓고 이 글을 읽기를 바란다.(다시 찾아와서 이 글을 읽는 감사한 일은 없을 것 같지만ㅠㅠ)
필자의 샘플 코드는 아래 링크를 참고하면 된다.
https://github.com/sieunkr/spring-data/tree/master/spring-data
데이터베이스가 변경되면 어떻게 될까? 필자의 그지 같은 샘플 소스에서는 SimpleCustomDataBaseProvider 클래스에서 데이터베이스 연동 로직이 들어갈 것이다. 물론, 샘플이라서 아주 심플하게 HashSet 으로 작성하였지만, 실제로 해당 소스에 DB 연동 코드가 작성이 되어야 한다. DB가 변경이 된다면 우리는 SimpleCustomDataBaseProvider 를 수정해야 할 것이다. 즉, 원에서는 Data Providers 라는 영역이 변경되는 것이다.
이경우에 안쪽에 속한 UseCase 영역은 변경이 될까? 변경이 되지 않는다. 디펜던시 룰에 의해서 바깥쪽원의 변경사항은 안쪽 영역에 영향을 끼치면 안 된다. 정리해보면, Spring-Data-Commons를 별도로 사용하면 필자가 생각하는 디펜던시룰에 적합한 프로젝트를 구성할 수 있겠다는 판단이다. 하지만, 필자가 생각하는 이 디펜던시 룰, 즉 디자인 패턴이 실제로 Pivotal 개발자가 생각하는 방향이 맞는가??
Spring-Data-Commons만 따로 생각하지 말고, 해당 기본 모듈을 사용하는 Spring-Data-JPA를 생각해보자. Spring-Data-JPA의 JpaRepository 같은 추상화된 인터페이스는 원 안쪽에 속한다. Spring-Data-JPA 를 사용하면 Mysql 을 사용하던, 오라클을 사용하던 내부 로직은 전혀 변화가 없을 것이다. 디비가 변경되면 내부 원의 영역은 그대로 두고, DB연동하는 부분만 변경되면 된다. Spring-Data-JPA를 사용하면서 DB변경 하는 경우는 필자가 생각하는 디자인 패턴과 매우 유사하다고 판단이 된다. 하지만, 여기서 필자는 한 가지 의문점이 생긴다. 만약 Spring-Data-JPA 에서 Spring-Data-MongoDB로 변경하게 된다면 어떻게 될까? Spring-Data-JPA 는 Entity 정의를 위해 @Entity 라는 어노테이션을 제공한다. Spring-Data-MongoDB 에서는 @Document 라는 어노테이션을 사용한다. 만약 Spring-Data-JPA를 사용하다가 Spring-Data-MongoDB로 바꾸면 어떻게 될까? Spring-Data-JPA, Spring-Data-MongoDB 두 모듈에 맞게 @Entity 정의가 달라질 수 있기 때문에 Entity 를 전부 수정해야 할것 같다. 필자가 생각하는 의존성이 완전히 깨지게 된다.
의문점이 생기면서, 혼란에 빠지기 시작한다.
참고로, 필자의 샘플 코드는 Entity 에 DB 의존적인 코드가 없기 때문에 문제가 없었다.
한참을 주저리주저리 글을 쓰다가, 결국 원점으로 돌아왔다. 하... 주말을 이렇게 날리는구나. 어쨌든 이제 ORM에 대해서 고민해봐야 한다. 필자 생각으로는 ORM이라는 개념 자체가 Entity 에 강한 의존성이 있다는 생각이 들었다. ORM 기술을 사용하기 위해서는, 객체 매핑을 해야 하고, 매핑을 위해서는 불가피하게 Entity 의존성이 필요하다는 생각이다. 또한 만약 JPA의 Many to Many 연동이 필요하다면, Entity 간의 복잡한 설정을 해야 하는데, 이런 경우 비즈니스 요구사항으로 인해서 Entity를 계속 변경해야 하는 이슈도 발생한다. 아무리 생각해도 Spring-Data-JPA , 즉 ORM 은 필자가 생각하는 디펜던시 룰을 너무 많이 깨트려 버린다.
최근에 공식 릴리스한 Spring-Data-JDBC 를 살펴보자. 참고로, Spring-Data-JDBC 가 이미 있었다고 착각하는 개발자가 많은데, 절대 그렇지가 않다. 물론 Spring-Jdbc 모듈은 오래 전부터 존재했었지만, Spring-Data 프로젝트는 최근 2018년9월 말이 공식 1.0.0 릴리스가 맞다. Spring-Data-JDBC는 Entity의 의존성이 매우 낮다. 필자가 생각하는 디펜던시 룰을 따르고 싶다면, Spring-Data-JDBC와 같이 내부 의존성이 낮은 모듈이 조금 더 적합하다는 판단이다.
결국 필자는 Spring-Data의 디자인패턴에 대한 결말을 내지 못하였다. 필자가 Spring 환경에서 데이터베이스 연동 경험이 많지가 않아서 전체적으로 이해도가 부족한 것 같다. 어쨋든 정리해보면 Spring-Data 프로젝트는 추상화된 일관된 기능을 제공하기 때문에 데이터베이스 변경이 쉽고, 확장성 높게 개발이 가능하다. 하지만, ORM 기술은 Entity에 생기는 의존성으로 인해서, 완벽한 레이어간의 분리가 쉽지 않고, 필자가 생각하는 디펜던시 룰을 깨트리는 경우가 존재한다. 필자가 일부 내용에 대해서 잘못 알고 있을 가능성도 있기 때문에 Spring-Data에 대해서 더 심도있게 공부할 예정이다. 또한, 열심히 공부한 이후에 Spring-Data Design Pattern의 두번째 글을 작성할 예정이다.