본문 바로가기
Spring Boot

[Spring Boot] REST API 제작기 - 2.DB연결(Mybatis)

by 오이가지아빠 2022. 3. 28.

이전글 - [Spring Boot] - [Spring Boot] REST API 제작기 - 1.프로젝트 생성

#1. 데이터 소스 연동(Oracle, MS-SQL, H2)

멀티데이터소스 연동을 위해 application.properties 에 아래와 같이 각 db별로 접속정보를 작성한다.

# H2 DB 접속정보
spring.datasource.hikari.jdbc-url=jdbc:h2:~/test
spring.datasource.hikari.driver-class-name=org.h2.Driver
spring.datasource.username=sa

# Oracle DB 접속정보(itsvc)
spring.itsvc.datasource.hikari.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.itsvc.datasource.hikari.jdbc-url=jdbc:log4jdbc:oracle:thin:@xx.xx.xx.xx:xxxx:BMSD
spring.itsvc.datasource.hikari.pool-name=bmsd-cp
spring.itsvc.datasource.hikari.username=itsvcuser
spring.itsvc.datasource.hikari.password=pw

# Oracle DB 접속정보(core)
spring.core.datasource.hikari.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.core.datasource.hikari.jdbc-url=jdbc:log4jdbc:oracle:thin:@xx.xx.xx.xx:xxxx:CMMD
spring.core.datasource.hikari.pool-name=cmmd-cp
spring.core.datasource.hikari.username=cmmduser
spring.core.datasource.hikari.password=pw

# SqlServer DB 접속정보(asset)
spring.asset.datasource.hikari.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.asset.datasource.hikari.jdbc-url=jdbc:log4jdbc:sqlserver://xx.xx.xx.xx:xxxx;encrypt=true;trustServerCertificate=true
spring.asset.datasource.hikari.pool-name=asset-cp
spring.asset.datasource.hikari.username=assetuser
spring.asset.datasource.hikari.password=pw

다음은 데이터 소스 설정 파일(xxxDataSourceConfig.java) 을 생성한 후 아래와 같이 작성한다.

hikaricp 설정(connectionTimeout, poolSize 등등) 은 변경할 수도 있지만, 지금은 일단 기본값으로 설정 한다.

package com.mbti.intj.infra.datasource;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration
@MapperScan(value = "com.mbti.intj.sample.ep", annotationClass = Mapper.class, sqlSessionFactoryRef = "coreSf")
@EnableTransactionManagement
public class CoreDataSourceConfig {

    @Primary
    @Bean
    @ConfigurationProperties(prefix="spring.core.datasource.hikari")
    public HikariConfig hikariConfig() {
        return new HikariConfig();
    }

    /**
     * JDBC를 사용한 DataSource
     * @return
     */
    @Primary
    @Bean(name="coreDs")
    public DataSource epDataSource() {
        return new HikariDataSource(hikariConfig());
    }

    /**
     * SqlFactoryBean에 dataSource와 mapper 경로 등록
     * @param dataSource
     * @return
     * @throws Exception
     */
    @Primary
    @Bean(name="coreSf")
    public SqlSessionFactory sqlSessionFactory(@Autowired @Qualifier("coreDs") DataSource dataSource) throws Exception{
        //RefreshableSqlSessionFactoryBean sqlFactoryBean = new RefreshableSqlSessionFactoryBean();
        SqlSessionFactoryBean sqlFactoryBean = new SqlSessionFactoryBean();
        sqlFactoryBean.setDataSource(dataSource);
        // Mapper file location
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlFactoryBean.setMapperLocations(resolver.getResources("/mapper/ep/**/**.xml"));
        // ResultType vo package location
        sqlFactoryBean.setTypeAliasesPackage("com.mbti.intj.**.vo");

        SqlSessionFactory sqlSessionFactory = sqlFactoryBean.getObject();
        sqlSessionFactory.getConfiguration().setMapUnderscoreToCamelCase(true);
        sqlSessionFactory.getConfiguration().setLazyLoadingEnabled(true);
        sqlSessionFactory.getConfiguration().setJdbcTypeForNull(JdbcType.NULL);
        return sqlFactoryBean.getObject();
    }

    @Primary
    @Bean(name="coreSt")
    public SqlSessionTemplate sqlSessionTemplate(@Autowired @Qualifier("coreSf") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

}

 

package com.mbti.intj.infra.datasource;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

/**
 * packageName : com.mbti.intj.infra.datasource
 * className   : AssetDataSourceConfig
 * author      : garve32
 * date        : 2022-03-25
 * description : ASSET DB(SQL SERVER) 에 대한 datasouece 설정
 *               MapperScan value(interface)와 Mapper Location(xml) 설정에 따른다
 * ========================================================================
 * DATE          AUTHOR      NOTE
 * ------------------------------------------------------------------------
 * 2022-03-25    garve32     최초 생성
**/
@Configuration
@MapperScan(value = "com.mbti.intj.sample.asset", annotationClass = Mapper.class, sqlSessionFactoryRef = "assetSf")
@EnableTransactionManagement
public class AssetDataSourceConfig {

    @Autowired
    private ApplicationContext ctx;

    @Bean
    @ConfigurationProperties(prefix="spring.asset.datasource.hikari")
    public HikariConfig assetHikariConfig() {
        return new HikariConfig();
    }

    /**
     * JDBC를 사용한 DataSource
     * @return
     */
    @Bean(name="assetDs")
    public DataSource assetDataSource() {
        return new HikariDataSource(assetHikariConfig());
    }

    /**
     * SqlFactoryBean에 dataSource와 mapper 경로 등록
     * @param dataSource
     * @return
     * @throws Exception
     */
    @Bean(name="assetSf")
    public SqlSessionFactory sqlSessionFactory(@Autowired @Qualifier("assetDs") DataSource dataSource) throws Exception{
        SqlSessionFactoryBean sqlFactoryBean = new SqlSessionFactoryBean();
        sqlFactoryBean.setDataSource(dataSource);
        // Mapper file location
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlFactoryBean.setMapperLocations(resolver.getResources("/mapper/asset/**/**.xml"));
        // ResultType vo package location
        sqlFactoryBean.setTypeAliasesPackage("com.mbti.intj.**.vo");

        SqlSessionFactory sqlSessionFactory = sqlFactoryBean.getObject();
        sqlSessionFactory.getConfiguration().setMapUnderscoreToCamelCase(true);
        sqlSessionFactory.getConfiguration().setLazyLoadingEnabled(true);
        sqlSessionFactory.getConfiguration().setJdbcTypeForNull(JdbcType.NULL);
        return sqlFactoryBean.getObject();
    }

    @Bean(name="assetSt")
    public SqlSessionTemplate sqlSessionTemplate(@Autowired @Qualifier("assetSf") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

}

 

여기서 눈여겨 보아야 할 구문은 다음과 같다.

 

@MapperScan의 value값은 Mybatis Mapper(Interface) 파일을 찾을 수 있는 최상단 경로로 지정한다.

@ConfigurationPropertiesprefixapplication.properties에 작성했던 db property 정보의 prefix값이다.

sqlFactoryBean.setMapperLocations에는 Mybatis 쿼리파일이 담긴 xml 경로를 지정해준다(/resources 하위)

sqlFactofyBean.setTypeAliasesPackage에는 Mybatis 리턴데이터가 담길 dto혹은 vo파일들의 package경로를 설정해 주어서 추후 xml 파일 작성시 일일히 전체패키지 경로를 작성하는 수고를 줄여준다.

 

또한, 반드시 메인으로 사용할 데이터소스 작성시 @Bean에 @Primary 어노테이션을 걸어준다.

 

datasourceconfig 파일들과 mapper 파일의 경로는 아래와 같다.(MapperScan 설정값 참고)

 

 

 

#2. Mapper 인터페이스와 mybatis xml 파일 생성

이제 Mapper 인터페이스와 쿼리구문이 담길 xml 파일을 작성해보자.

 

Mapper 인터페이스의 메소드 명이 반드시 쿼리파일의 id와 동일해야 정상적으로 호출할 수 있다.

단건 조회를 위한 메소드, 페이징 처리를 위한 카운트 쿼리와 리스트쿼리를 작성해주자.

 

Mapper 인터페이스에는 @Mapper 어노테이션을 붙이는 것을 잊지 말자.

package com.mbti.intj.sample.asset.mapper;

import com.mbti.intj.sample.asset.vo.AssetUserVo;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface AssetSampleMapper {

    AssetUserVo getUser(Object req) throws Exception;

    List<AssetUserVo> getUserList(Object req) throws Exception;

    int getUserListCnt(Object req) throws Exception;
}

 

Mybatis 문법의 쿼리는 아래와 같이 작성한다.

Mapper Interface 파일의 풀 경로namespace에 작성하고, id값을 Mapper 인터페이스의 메소드 명과 일치시킨다.

resultType에는 원래 com.mbti.intj.sample.asset.vo.AssetUserVo 로 작성해야 하지만

데이터소스 설정파일에서 sqlFactofyBean.setTypeAliasesPackage 에 **.vo 까지 미리 alias를 설정해주었기 때문에 AssetUserVo 처럼 줄여서 사용할 수 있다.

<?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 interface의 패키지 풀네임 -->
<mapper namespace="com.mbti.intj.sample.asset.mapper.AssetSampleMapper">
    <select id="getUser" resultType="AssetUserVo">
        /* AssetSampleMapper.getUser */
        SELECT EMPNO
             , NAME
             , ENAME1
             , EPID
        FROM EMPMAST
       WHERE EPID = #{epid}
    </select>

    <select id="getUserListCnt" resultType="int">
        /* AssetSampleMapper.getUserListCnt */
        SELECT COUNT(*)
          FROM EMPMAST
    </select>

    <select id="getUserList" resultType="AssetUserVo">
        /* AssetSampleMapper.getUserList */
        SELECT EMPNO
             , NAME
             , ENAME1
             , EPID
        FROM EMPMAST
        ORDER BY EMPNO
        OFFSET #{offset} ROWS
            FETCH NEXT #{pageSize} ROWS ONLY
    </select>
</mapper>

 

#3. 컨트롤러에서 호출테스트(API 호출)

해당 데이터소스를 사용한 쿼리를 호출하는 API를 작성해보자.

 

AssetUserDto - Api 입력으로 받을 dto(frontend에서 넘어오는 데이터)

package com.mbti.intj.sample.asset.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import javax.validation.constraints.NotNull;

@Getter
@Setter
@ToString
@NoArgsConstructor
public class AssetUserDto {

    @NotNull
    private String epid;
}

AssetUserVo - Service 레이어에서 리턴할 vo(frontend로 바로 넘겨줄 예정)

package com.mbti.intj.sample.asset.vo;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class AssetUserVo {

    private final String empno;
    private final String name;
    private final String ename1;
    private final String epid;
}

AssetSampleController - RestController로 작성하고 GetMapping으로 api url을 매핑한다.

Paging 부분은 나중에 다루도록 하겠다.

package com.mbti.intj.sample.asset.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.mbti.intj.sample.asset.dto.AssetUserDto;
import com.mbti.intj.sample.asset.dto.AssetUserListReqDto;
import com.mbti.intj.sample.asset.service.AssetSampleService;
import com.mbti.intj.sample.asset.vo.AssetUserVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * packageName : com.mbti.intj.sample.asset.controller
 * className   : AssetSampleController
 * author      : garve32
 * date        : 2022-03-25
 * description : 자산 정보 가져오는 컨트롤러
 * ========================================================================
 * DATE          AUTHOR      NOTE
 * ------------------------------------------------------------------------
 * 2022-03-25    garve32     최초 생성
**/
@Slf4j
@RestController
@RequiredArgsConstructor
@Transactional
@RequestMapping("/asset")
public class AssetSampleController {

    private final AssetSampleService assetSampleService;

    @GetMapping("/user")
    public AssetUserVo getAssetUser(@Validated AssetUserDto dto) throws Exception {
        return assetSampleService.getUser(dto);
    }

    @GetMapping("/users")
    public Page<AssetUserVo> getUsers(AssetUserListReqDto dto, @PageableDefault(value = 20) Pageable page) throws Exception {
        // Input DTO의 가공이 필요한 경우 신규 Object를 만들고 가공한 후 Mapper Interface의 argument로 적용
        ObjectMapper om = new ObjectMapper();
        Map<String, Object> map = om.convertValue(dto, Map.class);
        Page<AssetUserVo> result = assetSampleService.getUserList(map, page);
        return result;
    }

}

AssetSampleService - AssetSampleMapper 를 생성자 주입받고 쿼리를 호출한다.

마찬가지로 페이징 부분은 나중에 자세히 다루도록 하겠다.

package com.mbti.intj.sample.asset.service;

import com.mbti.intj.sample.asset.mapper.AssetSampleMapper;
import com.mbti.intj.sample.asset.vo.AssetUserVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Slf4j
@Service
@RequiredArgsConstructor
public class AssetSampleService {

    private final AssetSampleMapper assetSampleMapper;

    public Page<AssetUserVo> getUserList(Map<String, Object> req, Pageable page) throws Exception {

        req.put("offset", page.getOffset());
        req.put("pageSize", page.getPageSize());
        int cnt = assetSampleMapper.getUserListCnt(req);
        List<AssetUserVo> result = assetSampleMapper.getUserList(req);
        return new PageImpl<>(result, page, cnt);
        //return assetSampleMapper.getUserList(req);
    }
    
    public AssetUserVo getUser(Object req) throws Exception {
        return assetSampleMapper.getUser(req);
    }

}

이제 내장 Tomcat을 기동시키고 /asset/user api를 호출해보면 원하는 데이터 소스의 쿼리가 잘 실행됐음을 확인할 수 있다.

 

Spring boot - Mybatis - Database 설정은 이것으로 모두 완료됐다.

반응형

댓글