본문 바로가기

JAVA/Spring Boot

Spring MyBatis사용 및 설정

반응형

Mybatis

Mybatis 의존설정

기존 스프링 기반에서는 Mybatis.jar, Mybatis-Spring.jar 사용.
스프링 부트 기반에서는 mybatis-spring-boot-starter를 사용.

compile ('org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.3')

implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.3'

Mybatis에서는 쿼리 매핑 구문을 실행하기 위해 sqlSession 객체를 사용
sqlSession 객체를 생성하기 위해 SqlSessionFactory를 사용.

스프링과 같이 사용할때는 SqlSessionTemplate를 사용.
why? 스프링 사용시에 sqlSession을 대체할수 있고, 예외처리를 Mybatis가 아니라 Spring에 DataAcessException으로 치환하는 역할을 해주기 때문.

mybatis SqlSessionTemplate 설정

package ee.swan.study.config;

@Slf4j
@Configuration
@ComponentScan(basePackages = {"ee.swan.study.repository"})
public class MyBatisConfig {

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setConfigLocation(
                new PathMatchingResourcePatternResolver()
                    .getResource("classpath:mybatis/mybatis-config.xml"));
        sqlSessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver()
                    .getResources("classpath:mapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }
}
  • SqlSessionFactory에서 Sql 매퍼들의 위치하는 경로
  • Mybatis 설정 경로
  • sqlSessionTemplate 생성자에 Mybatis 매퍼 설정 정보가 있는 sqlSessionFactory 빈을 전달해서 빈을 생성.

데이터베이스 설정

스키마 생성 및 초기 데이터 적재

  • Resources 폴더에 DDL, DML 파일을 두고 실행되게 할수 있다.

application.properties 파일에 지정

spring.datasource.platform=h2
spring.datasource.schema=data/schema-h2.sql
spring.datasource.data=data/data-h2.sql
spring.datasource.separator=;
spring.datasource.sql-script-encoding=UTF-8

MyBatis 쿼리 실행

Mapper XML 작성

resources 폴더 하위에 mapper 폴더에 userMapper.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="user.mapper.userMapper">

    <select id="selectUserInfoAll" resultType="hashMap">
    <![CDATA[
        SELECT id, username, email
        FROM tbl_user
    ]]>
    </select>

</mapper>    

매퍼 네임스페이스는 매퍼가 저장된 패키지명과 파일명으로 지정

Repository 생성

SqlSessionTemplate을 사용해서 sqlMap을 사용하는 UserRepository 클래스 생성.

package ee.swan.study.repository;

@Repository
public class UserRepository {

    private static final String MAPPER_NAME_SPACE = "user.mapper.userMapper.";

    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;

    public List getUserInfoAll() {
        return sqlSessionTemplate.selectList(MAPPER_NAME_SPACE + "selectUserInfoAll");
    }

}

매퍼 네임스페이스는 패키지+Mybatis매퍼명 으로 하고 sqlSessionTemplate을 Autowired를 사용해서 의존성을 주입한다.

Main 클래스 생성

package ee.swan.study;

@SpringBootApplication(exclude = WebMvcAutoConfiguration.class)
public class MybatisSampleApplication implements CommandLineRunner {

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


    @Autowired
    UserRepository userRepository;

    @Override
    public void run(String... args) throws Exception {
        System.out.println(userRepository.getUserInfoAll().toString());
    }
}

@SpringBootApplication(exclude = WebMvcAutoConfiguration.class)
데이터에 대한 처리만하는것으로 웹에 대한 설정은 배제

[           main] o.s.jdbc.datasource.init.ScriptUtils     : Executed SQL script from class path resource [data/schema-h2.sql] in 10 ms.
[           main] o.s.jdbc.datasource.init.ScriptUtils     : Executing SQL script from class path resource [data/data-h2.sql]

insert문 실행

    public void addUserInfo(UserVO userVO) {
        sqlSessionTemplate.insert(MAPPER_NAME_SPACE + "addUserInfo", userVO);
    }
    <insert id="addUserInfo"  parameterType="ee.swan.study.model.UserVO">
    <![CDATA[
        INSERT INTO TBL_USER(ID ,USERNAME, email)
        values(#{id},#{userName}, #{email})
     ]]>
    </insert>
package ee.swan.study.model;

@NoArgsConstructor
@AllArgsConstructor
@Data
@ToString
public class UserVO {
    private String id;

    private String userName;

    private String email;

}
        UserVO userEntity = new UserVO("addUser1", "user1", "user1@test.com");
        userRepository.addUserInfo(userEntity);

명시적인 DataSource 지정

SpringBoot를 사용시 application.properties 파일에 필요한 정보만 입력하면 기능이 동작한다.
소스를 보면 DataSource에 대한 아무런 설정도 하지 않았는데 sqlSessionFactoryBean을 생성하는 부분에 파라미터로 전달받는다.

[           main] org.mybatis.spring.SqlSessionUtils       : SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@12a2585b] was not registered for synchronization because synchronization is not active
  • dataSource 부분이 인스턴스 생성하거나 의존성을 주입하는 부분이 없다고 경고.

SqlSessionFactory 설정

dataSource 반환하는 메서드 만들고 메서드 안에서 스크립트 파일들을 로드하도록 지정하고 스프링이 관리할수 있는 빈으로 등록한다.
sqlSessionFactory 메서드에서 파라미터 입력 받는 부분을 삭제하고 dataSource 인스턴스를 주입한다.

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource());
        sqlSessionFactoryBean.setConfigLocation(
                new PathMatchingResourcePatternResolver()
                .getResource("classpath:mybatis/mybatis-config.xml"));
        sqlSessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }

dataSource에 Autowired를 할수 없다는 경고 문구가 사라지고 dataSource() 메서드가 없다는 오류가 발생한다.
dataSource 메서드를 생성하고 내장형 데이터베이스로 설정한다.

dataSource 빈 설정

    @Bean(destroyMethod = "shutdown")
    public DataSource dataSource(){
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        EmbeddedDatabase h2 = builder
                .setType(EmbeddedDatabaseType.H2)
                .addScript("data/schema-h2.sql")
                .addScript("data/data-h2.sql")
                .build();

        return h2;
    }

dataSource 메서드의 역할은 application.properties 파일에 정의된
spring.datasource.platform, spring.datasource.schema, spring.datasource.data 과 같다.
스프링 부트의 프로퍼틔 정의하는것만으로 대부분 처리할수 있지만 javaConfig 방식으로 설정하면 추후에 dataConnectionPool 설정하거나 세밀한 설정이 필요할때 도움이 된다.

데이터베이스 서버와 연동

HikariCP

HikariCP는 데이터베이스 커넥션 풀 라이브러리 중의 하나로 성능이 좋다.

## 외부 mysql
spring.datasource.url=jdbc:mysql://localhost:3306/jpub?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.classname=org.mysql.jdbc.MySQLDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=jpub
spring.datasource.password=jpub
spring.datasource.platform=mysql
# boot 2.0 add option
spring.datasource.initialization-mode=always
spring.datasource.schema=data/schema-mysql.sql
spring.datasource.data=data/data-mysql.sql
spring.datasource.separator=;
spring.datasource.sql-script-encoding=UTF-8
package ee.swan.study.config;


@Configuration
@PropertySource("application.properties")
public class MySqlDBConfig implements TransactionManagementConfigurer {

    @Value("${spring.datasource.url}")
    private String dbUrl;
    @Value("${spring.datasource.username}")
    private String dbUsername;
    @Value("${spring.datasource.password}")
    private String dbPassword;
    @Value("${spring.datasource.classname}")
    private String dbClassname;

    @Lazy
    @Bean(destroyMethod = "close")
    public DataSource dataSource() {
        final HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setUsername(dbUsername);
        hikariConfig.setPassword(dbPassword);
        hikariConfig.setJdbcUrl(dbUrl);
        hikariConfig.setDriverClassName(dbClassName);        

        //hikariConfig.addDataSourceProperty("url", dbUrl);
        //hikariConfig.setDataSourceClassName(dbClassName);
        hikariConfig.setLeakDetectionThreshold(2000);
        hikariConfig.setPoolName("jpubDBpool");

        final HikariDataSource dataSource = new HikariDataSource(hikariConfig);
        return dataSource;
    }

    @Bean
    public DataSourceTransactionManager transactionManager(){
        return new DataSourceTransactionManager(dataSource());
    }

    @Override
    public PlatformTransactionManager annotationDrivenTransactionManager() {
        return transactionManager();
    }    
}

@Configuration 클래스 상단에 정의
@PropertySource를 사용해 application.properties 파일 참조
@Value 자바 클래스 필드에 매핑

DataSource 인스턴스를 만들때 메서드 안에서 HikariDataSource 인스턴스를 생성한 후에 DataSource를 반환.
HikariDataSource가 DataSource와 Closeable 인터페이스를 구현한 클래스이므로 hikariCpDataSource 객체를 생성한 후에 DataSource 타입을 반환해야 하는데 메서드에서 hikariCpDataSource 반환해도 문제가 생기지 않는다.
데이터베이스 접속 종료시에 커넥션 인스턴스도 함께 초기화되도록 @Bean(destroyMethod="close") 사용한다.

@Lazy은 빈 생성 시점을 늦춘다.
웹 애플리케이션의 경우 WAS가 계속 실행되고 있는 상태에서 데이터베이스 서버가 꺼지너가 종료되면 처음 시작할때 데이터베이스 커넥션을 얻어와야 한다.
그런데 해당 부분에 대한 처리가 되지 않아서 500 에러가 발생하면 사이트 전체가 표시되지 않는다.
웹 서비스는 데이터베이승 연결말고도 오류를 보여주는 것보다 일시적인 데이터베이스 서버 접속 오류로 오류 범위를 축소시키고 데이터베이스와 상관없는 화면들을 보여주고 오류를 처리하는것이 좋다.
Lazy을 사용하면 다른 빈들의 최기화가 먼저 진행된 후에 Lazy에 붙은 메서드가 실행한다.

DataSource와 같이 초기화 단계에 한번만 프로퍼티들이 설정되는 값들이 추후에 변경을 막기 위해 final로 선언하는 것이 좋다.

MyBatis와 Mysql 연동

@Slf4j
@Configuration
@Import(MySqlDBConfig.class)
@ComponentScan(basePackages = {"ee.swan.study.repository"})

public class MyBatisConfig {

    @Autowired
    private MySqlDBConfig mySqlDBConfig;

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(mySqlDBConfig.dataSource());
        sqlSessionFactoryBean.setConfigLocation(
                new PathMatchingResourcePatternResolver().getResource("classpath:mybatis/mybatis-config.xml"));
        sqlSessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }

}

@Import을 사용해서 MySqlDBConfig 클래스와 Autowired을 이용해서 의존성을 주입한다.
MySqlDBConfig 클래스는 설정 정보를 나타내는 클래스이다.
데이터 소스를 위해서 실제로 클래스 자체가 아니라 MySqlDBConfig 클래스가 스프링이 관리하는 루트 컨텍스트에 로드된 이후에 dataSource 메서드를 사용하는것이다.
다수의 설정 파일들을 유기적으로 연결해서 사용할 경우에는 스프링이 인식할수 있도록 @Import 애너테이션을 사용한다.
설정 클래스가 서비스클래스들을 componentscan하는 것처럼 설정 클래스 안에서 또 다른 설정 클래스의 의존성 주입이 가능하도록 해준다.

HikariCP 속성

HikariCP는 JDBC 4 type을 지원하는 커넥션 풀 라이브러리로 socket 옵션 및 데이터베이스 서버에 대한 다양한 옵션을 제공한다.

autoCommit= TRUE
readyOnly=FALSE
connectionTimeout=30000(30초)
ideTimeout=600000(10분)
maxLifeTime=1800000
poolName=none

autoCommit 속성은 커넥션을 종료하거나 pool에 반환할때 트랜잭션 자동화 여부를 결정으로 TRUE이므로 트랜잭션이 자동으로 처리한다.

readyOnly는 autoCommit과 반대로 데이터베이스 변경에 대해서 transaction 처리를 하지 않고 읽기만 하도록 하는 옵션이다.

해당 옵션을 적용하기 전에 readyOnly가 지원하는지 확인이 필요하다.
connectionTimeout는 커넥션을 얻는데 소요되는 최대 시간을 명시하는 옵션이다. 30초 이상 지연되면 오류가 발생한다.

MyBatis 고급 기능활용

selectKey

selectKey는 mybatis에서 지원하는 쿼리로 insert나 update 전후에 select 구문을 실행해서 키가 되는 값을 매핑할수 있게 해주는 기능.
게시판 순번(시퀀스)역할을 하는 칼럼의 값을 데이터베이스의 시퀀스 값으로 사용할수 있다.
시간과 코드를 조합한 유니크한 값으로 사용할수도 있다.

drop table if exists free_board;
create table free_board
(
    board_id  varchar(20) not null,
    uname     varchar(20) not null,
    title     varchar(20) not null,
    category  varchar(6),
    content   text,
    viewcount int default 0,
    wdate     datetime,
    primary key (board_id)
);   

데이터를 입력하는 insert 맵퍼를 생성한다.
insert문은 기존에 userMapper.xml 파일의 addUserInfo와 다르지 않다.
다만 free_board 테이블에 새로 추가된 seq 값을 입력하는 방법을 알아야 한다.

<?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="user.mapper.freeBoard">
    <insert id="insertBoard" parameterType="ee.swan.study.model.FreeBoardVO">
        <selectKey keyProperty="boardId" resultType="String" order="BEFORE">
            SELECT CONCAT('FB', DATE_FORMAT(NOW(), '%y%m%d%h%i%s')) AS BOARD_ID
        </selectKey>
            INSERT INTO FREE_BOARD (
                BOARD_ID, UNAME, TITLE, CATEGORY, CONTENT, WDATE
            ) VALUES (
                #{boardId}, #{userName}, #{title}, #{category}, #{content}, now()
            )
    </insert>
</mapper>
package ee.swan.study.model;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class FreeBoardVO {
    private String boardId;
    private String userName;
    private String title;
    private String category;
    private String content;
    private int viewCount;
    private Date wdate;
}
package ee.swan.study.repository;

@Repository
public class FreeBoardRepository {
    private static final String MAPPER_NAME_SPACE = "user.mapper.freeBoard.";

    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;

    public void registBoard(FreeBoardVO freeBoardVO) {
        sqlSessionTemplate.insert(MAPPER_NAME_SPACE + "insertBoard", freeBoardVO);
    }

}
package ee.swan.study;

@SpringBootApplication(exclude = WebMvcAutoConfiguration.class)
public class FreeBoardApp implements CommandLineRunner{

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

    @Autowired
    FreeBoardRepository freeBoardRepository;

    @Override
    public void run(String... args) throws Exception {
        System.out.println("freeBoard run...");
        FreeBoardVO freeBoardVO = new FreeBoardVO();
        freeBoardVO.setUserName("홍길동");
        freeBoardVO.setCategory("101");
        freeBoardVO.setContent("게시판 1번글");
        freeBoardVO.setTitle("안녕하세요!");
        freeBoardRepository.registBoard(freeBoardVO);

    }
}
[           main] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@1369885738 wrapping com.mysql.cj.jdbc.ConnectionImpl@4455f57d] will not be managed by Spring
[           main] u.m.freeBoard.insertBoard!selectKey      : ==>  Preparing: SELECT CONCAT('FB', DATE_FORMAT(NOW(), '%y%m%d%h%i%s')) AS BOARD_ID
[           main] u.m.freeBoard.insertBoard!selectKey      : ==> Parameters: 
[           main] u.m.freeBoard.insertBoard!selectKey      : <==      Total: 1
[           main] user.mapper.freeBoard.insertBoard        : ==>  Preparing: INSERT INTO FREE_BOARD ( BOARD_ID, UNAME, TITLE, CATEGORY, CONTENT, WDATE ) VALUES ( ?, ?, ?, ?, ?, now() )
[           main] user.mapper.freeBoard.insertBoard        : ==> Parameters: FB201013125126(String), 홍길동(String), 안녕하세요!(String), 101(String), 게시판 1번글(String)
[           main] user.mapper.freeBoard.insertBoard        : <==    Updates: 1

동적 쿼리 태그

MyBatis는 동적 SQL을 제공.

if 태그 활용

<select id="findByUserId">
select * from user
where del_yn = 'N'
<if test="userId != null">
    and user_id= #{userId}
</if>
</select>

동적 SQL문을 사용하게 되면 쿼리의 내용을 이해하기 어렵고 복잡해져서 수정이 힘들다.
그래서 되도록 MyBatis의 쿼리보다는 Dao에서 메서드를 호출하기전에 서비스 클래스에서 자바의 조건문을 이용해서 처리하도록 하는것이 보다 더 직관적이다.

foreach 활용
배열일 지정해서 쿼리를 반복적으로 실행할수 있도록 해준다.

<foreach item="item" index="index" collectioin="list" open="(" separator="," close=")">
#{item}
</foreach>
<select id="foreachUserList" parameterType="java.util.HashMap" resultType="java.util.HashMap">
        SELECT
            ID,
            USERNAME,
            email
        FROM TBL_USER
        WHERE ID IN
            <foreach collection="user_list" item="userId" index="index" open="(" separator=", " close=")" >
                #{userId}
            </foreach>
    </select>    

    public List<UserVO> findByForeach(Map<String, Object> paramMap) {
        return sqlSessionTemplate.selectList(MAPPER_NAME_SPACE + "foreachUserList", paramMap);
    }
        List<String> userList = new ArrayList<>();
        userList.add("test1");
        userList.add("test3");
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("user_list", userList);

        System.out.println(userRepository.findByForeach(paramMap));

 

출저 - 스프링 부트로 배우는 자바 웹 개발
저자 - 윤석진

반응형

'JAVA > Spring Boot' 카테고리의 다른 글

스프링 부트 기본-#2  (0) 2020.10.15
스프링 부트 기본-#1  (0) 2020.10.15
메이븐 멀티 프로젝트 구성  (0) 2020.09.04
Spring Data JPA 사용 및 설정  (0) 2020.08.22
REST API  (0) 2020.08.21