Spring Boot + MyBatis 설정 방법(HikariCP, H2)

2021. 4. 22. 03:30 Spring Framework/Spring boot #3

 

Spring Boot + MyBatis 설정 방법(HikariCP, H2)

📝 순서

1. 스프링 부트 프로젝트 생성
2. 초기화 스크립트 설정(schema.sql, data.sql)
3. DBCP/DataSource 설정(HikariCP)
4. MyBatis 설정(@MapperScan, XML 위치, CamelCase, Alias, 로그레벨)
5. Model, Mapper 생성
6. 테스트

 

1. 스프링 부트 프로젝트 생성

MyBatis를 이용한 DB 연동을 위한 새 스프링 부트 프로젝트를 생성한다.

스프링 부트 프로젝트는 IDE를 이용하던지, spring initializr(start.spring.io)를 이용해 생성할 수 있다.

 

자바는 8로, 의존성은 Spring Web, Spring Data JDBC, Spring Boot DevTools, MyBatis, H2, Lombok을 넣어주었다.

 

📝 Spring Boot DevTools
스프링 부트 애플리케이션을 개발할 때 여러 편의 기능을 제공하는 라이브러리.
여기서는 H2 콘솔을 사용하기 위해 추가하였다.
H2 콘솔을 사용하기 위한 또 다른 방법은 application.properties에 다음 프로퍼티를 추가하는 것이다.
spring.h2.console.enabled=true (어플리케이션 재시작 필요)

📝 H2 Database

위와 같이 스프링 부트 프로젝트를 만들 때 H2 Database 의존성을 추가하면 간단히 사용할 수 있다.
스프링 부트가 지원하는 인메모리 데이터베이스는 H2, HSQL, Derby가 있다.
이 중 H2가 자주 사용되고 추천되는 이유는 콘솔이 제공되기 때문이다.

📝 Spring Data JDBC
Spring JDBC와 인메모리 데이터베이스가 클래스패스에 있으면(= 의존성 설정이 되어있으면) 스프링 부트가 
DataSource, JdbcTemplate 빈을 자동으로 설정해준다.

 

2. 초기화 스크립트 설정

/src/main/resources에 schema.sql, data.sql을 생성하여 본 예제에서 사용할 테이블을 자동으로 생성하고 데이터를 insert하도록 한다.

 

schema.sql

DROP TABLE IF EXISTS Products;

CREATE TABLE Products
(
    prod_id     IDENTITY        PRIMARY KEY,
    prod_name   VARCHAR(255)    NOT NULL,
    prod_price  INT             NOT NULL
);

 

data.sql

INSERT INTO Products (prod_name, prod_price) values ('베베숲 물티슈', 2700);
INSERT INTO Products (prod_name, prod_price) values ('여름 토퍼', 35180);
INSERT INTO Products (prod_name, prod_price) values ('페이크 삭스', 860);
INSERT INTO Products (prod_name, prod_price) values ('우산', 2900);

 

📝 스프링 부트는 DDL 스크립트와 DML 스크립트를 설정하여 스키마를 자동으로 생성하고 초기화할 수 있다(DML 스크립트). 기본적으로 DDL 스크립트는 schema.sql, DML 스크립트는 data.sql을 로드한다.

참고: docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-initialize-a-database-using-spring-jdbc

 

참고 - DataSource와 JdbcTemplate 빈 자동 설정 확인

스프링 부트 프로젝트의 의존성에 Spring JDBC와 H2와 같은 인메모리 데이터베이스가 설정되어 있으면 AutoConfiguration에 의해 데이터베이스 연동에 필요한 DataSource, JdbcTemplate 빈이 자동으로 주입되므로 특정 datasource를 설정하지 않아도 해당 데이터베이스와 즉시 연동할수 있다.

 

이를 직접 확인해보기 위해 ApplicationRunner를 생성하여 @Autowired로 DataSource 빈을 주입받아 Connection의 메타 정보를 찍어보자. 이 부분은 참고용이므로 스킵해도 무방하다.

 

ApplicationRunner 생성 위치

 

@Slf4j
@Component
public class TestRunner implements ApplicationRunner {

    @Autowired
    DataSource dataSource;

    @Override
    public void run(ApplicationArguments args) throws Exception {

        Connection connection = dataSource.getConnection();
        log.info("Url: " + connection.getMetaData().getURL());
        log.info("UserName: " + connection.getMetaData().getUserName());

    }
}

ApplicationRunner에는 @Component를 붙여줘야 함에 유의하자.

 

🖥 실행 결과

...
INFO --- [           main] TestRunner  : Url: jdbc:h2:mem:testdb
INFO --- [           main] TestRunner  : UserName: SA
...

 

📝 스프링 부트 인메모리 DB 이름 설정

스프링 부트 2.3부터 인메모리 DB가 매번 새로운 이름으로 만들어 지도록 변경되었다. 이전 버전에서와 같이 testdb로 고정하려면 다음 설정을 application.properties에 추가하면 된다.

spring.datasource.generate-unique-name=false

 

H2 콘솔을 통해 DB 초기화 스크립트가 제대로 적용되었는지 확인해보자.

 

브라우저에서 http://localhost:8080/h2-console에 접속한다.

 

위와 같이 JDBC URL 입력 후 Connect 클릭

 

 

추가로 JdbcTemplate도 주입받아 사용해봄으로써 빈이 자동 설정 되었는지 확인해보자.

 

TestRunner.java

@Slf4j
@Component
public class TestRunner implements ApplicationRunner {

    @Autowired
    DataSource dataSource;

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Override
    public void run(ApplicationArguments args) throws Exception {

        // DataSource
        Connection connection = dataSource.getConnection();
        log.info("Url: " + connection.getMetaData().getURL());
        log.info("UserName: " + connection.getMetaData().getUserName());

        // JdbcTemplate
        jdbcTemplate.execute("INSERT INTO Products (prod_name, prod_price) values ('버킷햇', 6900)");
    }
}

 

 

3. DBCP/DataSource 설정

application.properties - datasource 설정

# DataSource
spring.datasource.url=jdbc:h2:mem:mybatis-test
spring.datasource.username=sa
spring.datasource.password=

 

참고로 spring.datasource.driver-class-name은 스프링 부트가 url을 보고 추측할 수 있으므로 명시하지 않아도 된다.

 

스프링 부트에서는 HikariCP, Tomcat CP, Commons DBCP2를 지원하며 스프링 부트 2점대에서는 HikariCP를 기본적으로 사용한다.

따라서 여기서도 변경하지 않고 HikariCP를 사용하도록 하겠다.

현재 프로젝트에는 HikariCP에 별도로 설정할 것은 없으므로 설정 방법만 간단히 다룬다.

 

HikariCP는 application.properties에 spring.datasource.hikari.* 프로퍼티로 설정할 수 있다.

 

예: HikariCP Maximum Pool Size를 4로 설정

spring.datasource.hikari.maximum-pool-size=4

 

이제 datasource가 정상적으로 설정됐는지 확인해보자.

TestRunner.java

@Slf4j
@Component
public class TestRunner implements ApplicationRunner {

    @Autowired
    DataSource dataSource;

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Override
    public void run(ApplicationArguments args) throws Exception {

        // DataSource
        Connection connection = dataSource.getConnection();
        log.info("DBCP: " + dataSource.getClass()); // 사용하는 DBCP 타입 확인
        log.info("Url: " + connection.getMetaData().getURL());
        log.info("UserName: " + connection.getMetaData().getUserName());

        // JdbcTemplate
        jdbcTemplate.execute("INSERT INTO Products (prod_name, prod_price) values ('버킷햇', 6900)");
    }
}

log.info("DBCP: " + dataSource.getClass()); 코드를 추가하였다.

 

🖥 실행 결과

...
INFO [  restartedMain] TestRunner  : DBCP: class com.zaxxer.hikari.HikariDataSource
INFO [  restartedMain] TestRunner  : Url: jdbc:h2:mem:mybatis-test
INFO [  restartedMain] TestRunner  : UserName: SA
...

 

로그를 통해 설정한 대로 h2 DB 이름이 mybatis-test로 바뀌었고, HikariCP를 사용함을 알 수 있다.

 

4. MyBatis 설정

스프링 부트 메인 어플리케이션에 @MapperScan을 이용해 스프링 부트가 @Mapper가 붙은 MyBatis 매퍼를 스캔하여 빈으로 등록할 수 있도록 한다.

 

import org.mybatis.spring.annotation.MapperScan;

@MapperScan(basePackageClasses = MybatisSampleApplication.class)
@SpringBootApplication
public class MybatisSampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(MybatisSampleApplication.class, args);
    }

}

설정 방법은 @ComponentScan과 유사하다.

 

application.properties에 MyBatis에 관한 4가지 설정을 추가한다.

# MyBatis
# mapper.xml 위치 지정
mybatis.mapper-locations: mybatis-mapper/**/*.xml

# model 프로퍼티 camel case 설정
mybatis.configuration.map-underscore-to-camel-case=true

# 패키지 명을 생략할 수 있도록 alias 설정
mybatis.type-aliases-package=com.atoz_develop.mybatissample.model

# mapper 로그레벨 설정
logging.level.com.atoz_develop.mybatissample.repository=TRACE

 

5. Model, Mapper(Interface, XML) 생성

(1) Model

model 패키지를 만들고 위에서 만든 Proudcts 테이블과 매핑되는 Model을 생성한다.

 

 

Product.java

import lombok.*;
import org.apache.ibatis.type.Alias;

@NoArgsConstructor @RequiredArgsConstructor @Getter @Setter @ToString
public class Product {

    private Long prodId;
    @NonNull private String prodName;
    @NonNull private int prodPrice;
}

application.properties에 설정한 MyBatis 설정 중

mybatis.configuration.map-underscore-to-camel-case=true

에 의해 프로퍼티에 카멜 케이스 네이밍 컨벤션을 사용할 수 있다.

 

@NonNull은 @RequiredArgsConstructor로 해당 필드(prodName, prodPrice)를 받는 생성자를 만들기 위해 붙여주었으나, @NonNull을 사용하지 않고 final로 선언해줘도 가능하다.

private final String prodName;
private final int prodPrice;

 

(2) Mapper Interface

repository 패키지를 만들고 매퍼 인터페이스를 생성하여 Prouducts 테이블과 연동할 메소드를 정의한다.

 

ProuductMapper.java

@Mapper
public interface ProductMapper {

    Product selectProductById(Long id);
    List<Product> selectAllProducts();
    void insertProduct(Product product);
}

@MapperScan에 의해 Mapper로 스캔될 수 있도록 @Mapper 애노테이션을 붙여준다.

 

(3) XML

위에서 application.properties에 MyBatis XML 위치를 아래와 같이 설정했었다.

 

# mapper.xml 위치 지정
mybatis.mapper-locations: mybatis-mapper/**/*.xml

 

해당 위치에 XML 파일을 생성하여 ProductMapper 메소드 호출 시 실제로 동작할 SQL을 설정한다.

 

 

ProudctMapper.xml

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.atoz_develop.mybatissample.repository.ProductMapper">

    <select id="selectProductById" resultType="Product">
        SELECT prod_id
              ,prod_name
              ,prod_price
        FROM products
        WHERE prod_id = #{prodId}
    </select>

    <select id="selectAllProducts" resultType="Product">
        SELECT prod_id
              ,prod_name
              ,prod_price
        FROM products
    </select>

    <insert id="insertProduct" parameterType="Product">
      INSERT INTO products (prod_name, prod_price)
      VALUES (#{prodName}, #{prodPrice})
    </insert>

</mapper>

application.properties에 설정한 MyBatis 설정 중

mybatis.type-aliases-package=com.atoz_develop.mybatissample.model

에 의해 type 매핑 시 패키지 명을 생략할 수 있다.

 

6. 테스트

Service 빈과 테스트 코드를 작성하여 실제로 테스트해보자.

 

service 패키지를 만들고 ProudctService를 생성한다.

 

ProductService.java

@Service
public class ProductService {

    @Autowired
    private ProductMapper productMapper;

    public Product getProductById(Long id) {

        return productMapper.selectProductById(id);
    }

    public List<Product> getAllProducts() {

        return productMapper.selectAllProducts();
    }

    @Transactional
    public void addProduct(Product product) {

        productMapper.insertProduct(product);
    }
}

 

ProductMapper를 주입받아 각 서비스 메소드에서 호출한다.

 

다음으로 ProductService에 대한 테스트 클래스를 생성한다.

 

ProuductServiceTest.java

@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class ProductServiceTest {

    @Autowired
    private ProductService productService;

    @Test
    public void getProductById() {
        Product product = productService.getProductById(1L);
        log.info("product : {}", product);
    }

    @Test
    public void getAllProducts() {
        List<Product> products = productService.getAllProducts();
        log.info("products : {}", products);
    }

    @Transactional
    @Test
    public void addProduct() {
        productService.addProduct(new Product("쿤달 샴푸", 7900));
        productService.addProduct(new Product("마스크팩", 1000));
        productService.addProduct(new Product("티셔츠", 5900));
    }
}

 

이제 MyBatis 설정과 테스트 코드 작성이 완료되었다.

테스트 코드를 실행하면 다음과 같은 결과를 볼 수 있다.

 

🖥 실행 결과

getProductById

DEBUG 956 --- [           main] c.a.m.r.ProductMapper.selectProductById  : ==>  Preparing: SELECT prod_id ,prod_name ,prod_price FROM products WHERE prod_id = ?
DEBUG 956 --- [           main] c.a.m.r.ProductMapper.selectProductById  : ==> Parameters: 1(Long)
TRACE 956 --- [           main] c.a.m.r.ProductMapper.selectProductById  : <==    Columns: PROD_ID, PROD_NAME, PROD_PRICE
TRACE 956 --- [           main] c.a.m.r.ProductMapper.selectProductById  : <==        Row: 1, 베베숲 물티슈, 2700
DEBUG 956 --- [           main] c.a.m.r.ProductMapper.selectProductById  : <==      Total: 1
 INFO 956 --- [           main] c.a.m.service.ProductServiceTest         : product : Product(prodId=1, prodName=베베숲 물티슈, prodPrice=2700)

 

getAllProducts

DEBUG 956 --- [           main] c.a.m.r.ProductMapper.selectAllProducts  : ==>  Preparing: SELECT prod_id ,prod_name ,prod_price FROM products
DEBUG 956 --- [           main] c.a.m.r.ProductMapper.selectAllProducts  : ==> Parameters: 
TRACE 956 --- [           main] c.a.m.r.ProductMapper.selectAllProducts  : <==    Columns: PROD_ID, PROD_NAME, PROD_PRICE
TRACE 956 --- [           main] c.a.m.r.ProductMapper.selectAllProducts  : <==        Row: 1, 베베숲 물티슈, 2700
TRACE 956 --- [           main] c.a.m.r.ProductMapper.selectAllProducts  : <==        Row: 2, 여름 토퍼, 35180
TRACE 956 --- [           main] c.a.m.r.ProductMapper.selectAllProducts  : <==        Row: 3, 페이크 삭스, 860
TRACE 956 --- [           main] c.a.m.r.ProductMapper.selectAllProducts  : <==        Row: 4, 우산, 2900
TRACE 956 --- [           main] c.a.m.r.ProductMapper.selectAllProducts  : <==        Row: 5, 버킷햇, 6900
DEBUG 956 --- [           main] c.a.m.r.ProductMapper.selectAllProducts  : <==      Total: 5
 INFO 956 --- [           main] c.a.m.service.ProductServiceTest         : products : [Product(prodId=1, prodName=베베숲 물티슈, prodPrice=2700), Product(prodId=2, prodName=여름 토퍼, prodPrice=35180), Product(prodId=3, prodName=페이크 삭스, prodPrice=860), Product(prodId=4, prodName=우산, prodPrice=2900), Product(prodId=5, prodName=버킷햇, prodPrice=6900)]

 

addProudct

 INFO 956 --- [           main] o.s.t.c.transaction.TransactionContext   : Began transaction (1) for test context [DefaultTestContext@327af41b testClass = ProductServiceTest, testInstance = com.atoz_develop.mybatissample.service.ProductServiceTest@7740b0ab, testMethod = addProduct@ProductServiceTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@6cb6decd testClass = ProductServiceTest, locations = '{}', classes = '{class com.atoz_develop.mybatissample.MybatisSampleApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@40005471, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@49438269, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@7770f470, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@71e9ddb4, org.springframework.boot.test.context.SpringBootTestArgs@1], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]; transaction manager [org.springframework.jdbc.datasource.DataSourceTransactionManager@3eb9c575]; rollback [true]
DEBUG 956 --- [           main] c.a.m.r.ProductMapper.insertProduct      : ==>  Preparing: INSERT INTO products (prod_name, prod_price) VALUES (?, ?)
DEBUG 956 --- [           main] c.a.m.r.ProductMapper.insertProduct      : ==> Parameters: 쿤달 샴푸(String), 7900(Integer)
DEBUG 956 --- [           main] c.a.m.r.ProductMapper.insertProduct      : <==    Updates: 1
DEBUG 956 --- [           main] c.a.m.r.ProductMapper.insertProduct      : ==>  Preparing: INSERT INTO products (prod_name, prod_price) VALUES (?, ?)
DEBUG 956 --- [           main] c.a.m.r.ProductMapper.insertProduct      : ==> Parameters: 마스크팩(String), 1000(Integer)
DEBUG 956 --- [           main] c.a.m.r.ProductMapper.insertProduct      : <==    Updates: 1
DEBUG 956 --- [           main] c.a.m.r.ProductMapper.insertProduct      : ==>  Preparing: INSERT INTO products (prod_name, prod_price) VALUES (?, ?)
DEBUG 956 --- [           main] c.a.m.r.ProductMapper.insertProduct      : ==> Parameters: 티셔츠(String), 5900(Integer)
DEBUG 956 --- [           main] c.a.m.r.ProductMapper.insertProduct      : <==    Updates: 1
 INFO 956 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@327af41b testClass = ProductServiceTest, testInstance = com.atoz_develop.mybatissample.service.ProductServiceTest@7740b0ab, testMethod = addProduct@ProductServiceTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@6cb6decd testClass = ProductServiceTest, locations = '{}', classes = '{class com.atoz_develop.mybatissample.MybatisSampleApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@40005471, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@49438269, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@7770f470, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@71e9ddb4, org.springframework.boot.test.context.SpringBootTestArgs@1], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]

 

References

mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/#

mybatis.org/mybatis-3/ko/getting-started.html

tech.javacafe.io/2018/07/31/mybatis-with-spring/

taetaetae.github.io/2019/04/21/spring-boot-mybatis-mysql-xml/

goddaehee.tistory.com/205

linked2ev.github.io/gitlog/2019/08/21/springboot-mvc-4-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-MyBatis-+-HikariCP-+-MariaDB-%EC%84%A4%EC%A0%95/

lts0606.tistory.com/249

 

 

출처 : atoz-develop.tistory.com/entry/Spring-Boot-MyBatis-%EC%84%A4%EC%A0%95-%EB%B0%A9%EB%B2%95?category=869242