Spring Data JDBC Getting Started
추가내용 : 2020.04/28
이 글은 Spring Data JDBC 가 처음 나왔을 때 작성했던 글로서, 필자가 잘 모르는 상황에서 작성한 글입니다. Spring Data JDBC는 초기 버전 이후, 많은 내용이 변경되었습니다. 이 글을 읽는 것보다는, 공식 레퍼런스를 참고하시길 바랍니다. 나중에 시간이 되면 다시 정리해서 글을 새로 발행하겠습니다.
https://docs.spring.io/spring-data/jdbc/docs/1.1.6.RELEASE/reference/html/#reference
이 글은 최근 공식 발표된 Spring Data JDBC 프로젝트에 대한 글이다. 나온 지 얼마 안 된 신규 프로젝트라서 객관적인 검증이 되지는 않아서, 실무에 바로 사용하기에는 무리가 있다. 하지만, 필자의 호기심으로 가볍게 훑어본 후 글로 정리해본다. 단, 이 글의 주제인 Spring Data JDBC를 얘기하기 전에, 간단하게 스프링에서의 DB 연동에 대해서 아주 간략하게 정리하고 시작하겠다.
JDBC는 데이터베이스에 접근하기 위한 자바 표준 API이다. JDBC가 하는 역할은 아래와 같다.
JDBC 드라이버를 로딩한다.
DBMS에 연결한다.
SQL 문을 데이터베이스에 전송하고 결과 값을 받는다.
참고로, JDBC Driver는 자바 프로그램의 요청을 DBMS가 이해할 수 있는 프로토콜로 변환해주는 클라이언트 사이드 어댑터이다. 일반적으로 데이터베이스 연동 작업은 커넥션 연결, Sql 쿼리 전송 등의 작업을 수행하는데 핵심 키워드는 아래와 같다.
Connection : 데이터베이스와 연결(세션)
Statement : SQL문을 실행하거나 SQL 문의 결과를 반환하는 데 사용
자바로 대충 짜면 아래와 같은 코드들을 작성해야 한다. JDBC Driver를 로딩하고, 데이터베이스 커넥션을 연결하고, SQL문을 실행하고 등등등
````java
Connection connection;
Class.forName("driverClassName");
Connection connection = DriverManager.getConnection(url, user, password);
Statement statement = connection.createStatement();
String sql = "select * from coffees";
ResultSet result = statement.executeQuery(sql);
connection.close();
````
하지만, 필자는 실무에서 이런 코드를 사용하지는 않는다.
스프링에서는 JDBC를 쉽게 사용하게 해주는 JdbcTemplate 클래스를 제공한다. JDBCTemplate는 아래 작업을 개발자 대신 자동으로 수행한다.
Connection 열기와 닫기
Statement 준비와 닫기
Statement 실행
Exception 처리와 반환
Transaction 처리.
JDBC Template는 JDBC를 쉽게 사용할 수 있게 도와준다.
샘플 소스는 아래와 같다.
````java
@Autowired
JdbcTemplate jdbcTemplate;
생략...
jdbcTemplate.execute("DROP TABLE cafe IF EXISTS");
jdbcTemplate.execute("CREATE TABLE coffees(" +
"id SERIAL, name VARCHAR(255))");
List<String> coffees = Arrays.asList("mocha", "latte", "americano");
//jdbcTemplate.batchUpdate("INSERT INTO... 생략
//jdbcTemplate.query("SELECT id...생략
````
간단한 데이터베이스 연동 작업이라면 JDBC Template를 사용해도 괜찮을 것 같다. 즉, SQL을 이용한 직관적인 개발을 선호한다면 JDBC Template 도 괜찮을 듯 하다. 하지만, 이런 JDBC 코드는 관리하기가 번거롭다. 그래서 다음 두 프레임워크가 스프링 프로젝트 환경에서 많이 사용되어 왔다.
Mybatis
Hibernate
Mybatis는 자바에서 데이터베이스 연동 프로그래밍을 좀 더 쉽게 해주는 프레임워크이다. 위에서 설명한 JDBCTemplate로 연동하는 경우에는 세부적인 작업을 직접 해야 하는 경우에, 작업 별로 각각의 메서드를 호출해야 한다. 즉, 다수의 메서드를 호출하고 객체를 해제해야 하는 단점이 있었다. 그래서 JDBC를 그대로 쓰기보다는, Mybatis를 사용하여 좀 더 편하게 개발을 한다. 생산성도 뛰어나고, SQL문을 소스 코드에서 분리할 수 있는 장점이 있다.
````xml
<select id="selectUsers" resultType="map">
select
<include refid="userColumns"><property name="alias" value="t1"/></include>,
<include refid="userColumns"><property name="alias" value="t2"/></include>
from some_table t1
cross join some_table t2
</select>
````
http://tech.javacafe.io/2018/07/31/mybatis-with-spring/
ORM(Object-Relational Mapping)이란 이름 그대로 객체와 관계형 DB를 매핑해주는 것이다. OOP 언어와 RDBMS의 상이한 시스템을 맵핑하여, 개발자가 데이터 접근보다 로직 자체에 집중할 수 있도록 해준다. ORM 이 없을 때는 데이터베이스 작업을 하기 위해서 SQL 문을 직접 만들었다. 하지만, ORM을 사용하면 SQL 문에서 자유로워지고 유지보수가 편해질 수 있다. 물론, ORM 도 단점이 있다. 근데, ORM 은 추상적인 기술에 대한 개념에 불과하다. ORM 기술에 대한 표준 명세가 바로 JPA이고, JPA 표준을 구현한 프레임워크가 바로 Hibernate(하이버네이트)이다. 대표적은 ORM 프로젝트는 아래와 같다.
JPA는 자바 ORM 기술에 대한 표준 명세를 정의한 것이다. 표준 명세라는 단어는 스펙이라고 표현할 수도 있겠다. JPA는 표준 명세 일 뿐이라서, JPA 만으로 실제로 무언가를 할 수는 없다. JPA 표준 명세를 실제로 구현한 구현체가 필요하다. 대표적인 구현체가 바로 Hibernate이고, JPA 구현체 또는 JPA 프로바이더라고 부른다. 아무튼 JPA를 사용하면 뭐가 좋은가?
직접 SQL문을 작성하지 않고, JPA API를 통해서 만들어진 SQL을 DBMS에 전달한다.
JPA 표준으로 인해서, 다른 구현 기술로 변경이 용이하다.
동일 트랜잭션 내 데이터 중복 조회 시 한 번만 DB에 전달한다.
유지보수가 쉬워진다.
필자가 나열하지 않은 더 많은 장점이 있다. 물론 단점도 있지만, 어쨌든 스프링 프로젝트에서 JPA는 빠질 수 없는 필수 기술이 된 상황이다.
스프링 부트에서 Spring Data JPA 디펜던시를 추가하면, 기본으로 Hibernate가 연동된다.
참고로, JPA 역시 결국에는 JDBC Driver를 사용한다. JDBC와 Java Application 사이에 JPA+Hibernate 레이어가 추가된다.
혹시, 이 부분에 대해서 잘못된 점이 있다면 피드백 부탁드립니다.
레퍼런스는 아래 링크를 참고하자.
https://terasolunaorg.github.io/guideline/5.1.0.RELEASE/en/ArchitectureInDetail/DataAccessJpa.html
Spring Data 관련해서는 아래 필자의 글을 참고하면 된다.
https://brunch.co.kr/@springboot/107
Spring Data JPA는 JPA위에 추가된 추상화 계층으로, JPA기반 데이터 액세스에 대한 심플하고 향상된 기능을 제공하는 모듈이다. 스프링 프로젝트를 사용한다면 애플리케이션을 좀 더 쉽게 구축할 수 있도록 도와준다. 고급 CRUD 기능을 갖춘 DAO를 제공하여 개발자의 작업을 간소화시켜준다. Spring Data JPA 적용하면, 훨씬 간결하게 개발이 가능하다.
이 글의 주제는, Spring Data JDBC 이므로, Spring-Data-JPA 관련한 자세한 내용은 생략하겠다. 2018년10월13일 기준으로 최신 버전은 2.1.0.RELEASE 이다.
https://docs.spring.io/spring-data/jpa/docs/2.1.0.RELEASE/reference/html/
지난달에 Spring Data JDBC라는 신규 프로젝트가 정식 릴리스 되었다. 근데, Spring-Data-JPA 기술이 잘 사용되고 있는데 갑자기 등장한 이유는 무엇일까? 2018년 SpringOne Platform 세미나에서 발표된 영상을 참고해서 정리해보겠다.
https://www.youtube.com/watch?v=AnIouYdwxo0
Spring Data JDBC 가 등장한 배경, 즉 Spring-Data-JPA의 단점은 아래와 같다.
JPAs Complexity
Lazy Loading Exception
Dirty Checking
Session / 1st Level Cache
Proxies for Entities
Map almost anything to anything
필자는 영어를 잘 못해서, 잘못된 정보를 전달하면 안 되기 때문에 영문 그대로 작성하였다. 어쨌든 JPA의 복잡성 및 여러 가지 단점으로 인해서 Spring Data JDBC라는 신규 프로젝트가 등장하였다. Spring Data JDBC의 설계 디자인은 아래와 같다.
No Lazy Loading
No Caching
No Proxies
No deferred flushing
Very simple & limited & opinionated ORM
Simplicity is King - 2018SpringOnePlatform
Spring Data JDBC 는 심플함을 강조한다. 하지만, Spring-Data-JPA 프로젝트를 당장 대체할 수 있는 기술이라고 판단하기에는 아직 이르다. JPA는 매우 훌륭하고, 실무에서도 유용하게 사용 중이기 때문이다. Spring Data JDBC는 하나의 옵션 정도로 생각하면 될 것 같다. 아직 1.0.0 버전이라서 검증도 되지 않은 상황이다. 아마도 Spring Data JPA, Spring Data JDBC는 서로 공존하면서 발전해 나갈 것으로 추측된다.
Spring Data JDBC는 아직 미완성의 프로젝트이고 검증도 되지 않았다. 섣부르게 신규 기술을 도입하지는 말자. 이 글은 단지 호기심으로, Spring Data JDBC를 간단하게 소개하는 글이다.
현재 2018년10월13일 기준으로 spring-boot-starter-data-jdbc, 스프링 데이터 JDBC 스타터가 아직 공식적으로 릴리스 되지 않았다. 공식 버전은 아니지만, 마일스톤 버전의 스타터를 사용할수 있고 스프링부트 2.1.0.M4 버전을 적용하면 된다.
buildscript {
ext {
springBootVersion = '2.1.0.M4'
}
repositories {
mavenCentral()
maven { url "https://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/milestone" }
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
maven { url "https://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/milestone" }
}
dependencies {
implementation('org.springframework.boot:spring-boot-starter-data-jdbc')
implementation('org.springframework.boot:spring-boot-starter-web')
runtimeOnly('org.hsqldb:hsqldb')
testImplementation('org.springframework.boot:spring-boot-starter-test')
}
정상적으로 추가가 되었다면 아래와 같이 spring-data-jdbc:1.0.0.RELEASE 모듈이 디펜던시 추가된 것을 확인할 수 있다.
추가로, spring-data-jdbc 에 필요한 디펜던시는 아래와 같다. 스타터를 통해서 모든 모듈이 정상적으로 디펜던시가 적용되었을 것이다.
마일스톤 버전을 실무에서 사용하는 것은 추천하지 않는다. 물론 조만간 정식 스타터 버전이 릴리스가 될 것으로 생각되지만... 정식 버전이 나왔다고 바로 실서비스에 적용하지는 말자. 좀 더 지켜보는 게 좋을 듯하다.
스타터를 사용하지 않고 디펜던시를 적용해보겠따.
buildscript {
ext {
springBootVersion = '2.0.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
implementation('org.springframework.data:spring-data-jdbc:1.0.0.RELEASE')
implementation('org.springframework.boot:spring-boot-starter-web')
testImplementation('org.springframework.boot:spring-boot-starter-test')
}
근데 이상한 점은, Maven Repository 사이트에서는 spring-data-jdbc:1.0.0.RELEASE 에 필요한 디펜던시로 spring-data-commons 모듈이 2.1.0.RELEASE 버전으로 표기되어 있지만, 필자가 테스트한 결과 spring-data-commons 2.0.10.RELEASE 버전이 디펜던시로 추가되었다. 스타터를 통해서 추가했을 때는 spring-data-commons.2.1.0.RELEASE 버전이 정상적으로 추가되었는데... 왜이러지?
두 개의 Entity 를 생성하겠다. Cafe 와 Coffee 이다.
public class Cafe {
@Id
public long id;
public String title;
private Map<String, Coffee> coffeeMap = new HashMap<>();
public void addCoffee(String name, Integer price) {
Coffee coffee = new Coffee();
coffee.coffeename = name;
coffee.price = price;
coffeeMap.put(name, coffee);
}
}
public class Coffee {
@Id
public long id;
public String coffeename;
public Integer price;
}
그냥 평범한 클래스이다. Spring-Data-JPA 를 사용하면 @Entity 와 같은 어노테이션을 붙여야 하고, MongoDB 를 사용한다면 @Document 와 같은 어노테이션을 사용한다. 하지만, 지금 작성한 클래스는 DB 모듈에 상관없이 작성하는 의존성이 낮은 Entity 클래스이다. 단, 특이한 점은 Cafe 클래스에 addCoffee 메서드가 있다. 해당 메서드를 통해서 Cafe 에 Coffee 를 추가할 수 있다.
repository 정의는 Spring-Data-Commons 의 CrudRepository 를 상속받아서 정의하면 된다. 그리고 @Query 어노테이션을 사용해보겠다.
//CafeRepository.java
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
public interface CafeRepository extends CrudRepository<Cafe, Long> {
@Query("select * from Cafe c where c.title = :title")
Cafe findByName(@Param("title") String title);
}
//CoffeeRepository.java
@Transactional(readOnly = true)
public interface CoffeeRepository extends CrudRepository<Coffee, Long> {
@Query("select * from Coffee c where c.coffeename = :coffeename")
Coffee findByName(@Param("coffeename") String coffeename);
}
HSQL을 사용하기 위해서 간단하게 설정을 추가하였고, 애플리케이션이 초기화할 때 생성하는 쿼리도 작성하였다.
//ApplicationConfig.java
@Configuration
@EnableJdbcRepositories
class ApplicationConfig {
@Bean
public DataSource dataSource() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
return builder.setType(EmbeddedDatabaseType.HSQL).build();
}
}
//resources/schema.sql
CREATE TABLE IF NOT EXISTS Coffee (id IDENTITY, coffeename VARCHAR(100), price INTEGER, cafe INTEGER, cafe_key VARCHAR(100));
CREATE TABLE IF NOT EXISTS Cafe (id IDENTITY, title VARCHAR(100));
테스트 코드는 @DataJdbcTest 어노테이션을 사용하면 된다.
@RunWith(SpringRunner.class)
@DataJdbcTest
public class CoffeeRepositoryTests {
@Autowired
CoffeeRepository coffeeRepository;
@Autowired
CafeRepository cafeRepository;
@Test
public void coffee_test(){
Coffee coffee = new Coffee();
coffee.coffeename = "mocha";
coffee.price = 1200;
coffeeRepository.save(coffee);
Coffee mocha = coffeeRepository.findByName("mocha");
assertTrue(mocha.coffeename.equals("mocha"));
assertFalse(mocha.coffeename.equals("latte"));
}
@Test
public void cafe_test(){
Cafe cafe = new Cafe();
cafe.title = "cafe23";
cafe.addCoffee("mocha", 1200);
cafeRepository.save(cafe);
Cafe save = cafeRepository.findByName("cafe23");
}
}
테스트 코드를 보자. CrudRepository에서 제공하는 save 메서드를 사용할 수 있다. 또한, 필자가 작성한 addCOffee 메서드를 통해서 Cafe 객체에 Coffee 를 추가할 수 있다. 이렇게 생성된 객체와 함께 save 메서드를 실행하면 데이터베이스에 정상적으로 저장된다. 저장된 이후, 필자가 정의한 findByName 메서드도 테스트를 해보자. 잘된다. !! 정말 너무 심플하다.
Very simple & limited & opinionated ORM
이번 글에서는 Spring Data JDBC에 대해서 간략하게 정리하였다. SpringOne 2018 세미나 발표를 보면 발표 중간부터 DDD 얘기를 하기 시작한다. Spring-Data-JDBC 모듈이 도메인 주도 개발 방법론에 적합한 프로젝트인 것 같다. 그래서, 다음 글에서는 Spring Data JDBC 에 적용된 DDD 개발 방법론에 대해서 논의할 예정이다. 하지만, 필자가 DDD 관련 책을 1~2권 정도 정독한 이후에 글을 작성할 예정이라서, 다음 글은 아마 한참 후에 발행될 것이다. 혹시 DDD 관련 괜찮은 책이 있다면 추천해주길 바란다. 초보가 잃기 좋은 책으로!!
https://www.youtube.com/watch?v=AnIouYdwxo0