HyunJun 기술 블로그

스프링 프레임워크 MyBatis 본문

Spring Framework

스프링 프레임워크 MyBatis

공부 좋아 2023. 5. 28. 21:18
728x90
반응형

1. MyBatis란?


2001년 클린턴 비긴이 만든 iBATIS 프로젝트의 3.0에서 포크된 프로젝트로 아파치 라이센스 2.0에 따라 배포되는 무료 소프트웨어입니다. SQL 실행 결과를 자바 빈즈 또는 Map 객체에 매핑해주는 Persistence 솔루션으로 관리합니다.

 

  • 기존 ORM 프레임워크와 달리 Java 객체를 데이터베이스 테이블에 매핑하지 않고, Java 메서드를 SQL에 매핑합니다.
  • SQL문을 소스코드가 아닌 XML 파일로 관리합니다.
  • MyBatis를 사용하는 가장 큰 이유는, SQL문이 독립되어 유지 보수가 편리해지고, 개발자 수준별로 코드를 분리할 수 있기 때문입니다.
  • JPA는 ORM 기술이고, MyBatis는 SQL Builder 또는 SQL Mapper의 한 종류입니다.
  • JPA는 컴파일 타임에 오류를 확인할 수 있지만, MyBatis는 런타임 시에 오류를 확인할 수 있습니다.
  • JDBC와 다른 점은 MyBatis는 프로그래밍 코드와 SQL문을 분리하여 구현합니다.
  • JDBC를 사용하면서 반복적인 Connection, Stmt, Pstmt, 소스코드에 섞인 쿼리, close 등을 해결해 줄 수 있습니다.

2. MyBatis 원리

  • record에 원시 타입과, Map 인터페이스, 그리고 자바 POJO를 설정해서 매핑하기 위해 xml 혹은 Annotation을 사용할 수 있습니다.

출처: https://pangtrue.tistory.com/141

  1. MyBatis Config 파일에 설정된 SqlSessionFactoryBuilder 객체를 생성합니다.
    MyBatis Config 파일에는 DataSource(DB 설정 정보), Mapper 파일 등록, typeAlias 설정 등이 있습니다.
  2. SqlSessionFactoryBuilder 객체를 이용해 SqlSessionFactory 객체를 생성합니다.
    단순히 SqlSessionFactory 객체를 생성해 주기 위한 용도입니다.
    MyBatis Spring 연동 모듈에서는 sqlSessionFactoryBean이 사용됩니다.
  3. 앱 실행 중(런타임)에 CRUD 처리가 들어오면 SqlSessionFactory로 SqlSession 객체를 생성합니다.
  4. SqlSession 객체를 이용해 DB 요청을 한 후, 결과값을 받아옵니다.

 

3. SqlSession

  • 데이터베이스에 SQL 명령을 실행하기 위한 메서드들을 가지고 있는 Interface입니다. SqlSession Interface Docs
  • MyBatis에서 SqlSession을 생성하기 위해 SqlSessionFactory을 사용합니다.
  • 세션을 한번 생성하면 매핑 구문을 실행하거나, 커밋 또는 롤백을 하기 위해 세션을 사용할 수 있습니다.
  • 더 이상 필요하지 않은 상태가 되면 세션이 닫힙니다.
SqlSessionTemplate은 sqlSession 인터페이스를 구현한 구현체입니다. 스레드에 안전하고 여러 개의 DAO나 매퍼에서 공유할 수 있습니다.

 

 

 

3-1. Spring Boot AutoConfiguration

SpringBoot로 구현하고 있다면 AutoConfiguration이라는 편리한 기능 덕분에 많은 부분의 설정은 직접 해주지 않아도 되지만, 하지만 분명 기초부터 아는 것이 좋기 때문에, 설정이 필요한 부분은 설정을 다 해보고, 스프링 부트로써 얼마나 편리한지도 같이 구현해 보려고 합니다.

 

 

4. 구현하기

4-1. 데이터베이스, 테이블 생성

Database는 AWS RDS에 MySQL을 사용하였고, DBeaver로 접근하여 spring-mybatis라는 테이블을 생성해 주었습니다.

 

테스트에 사용할 person 테이블 생성

drop table if exists person CASCADE;
create table person
(
id bigint NOT NULL AUTO_INCREMENT,
name varchar(255),
age int(255),
PRIMARY KEY (id)
);

 

 

테스트 용도의 데이터 2개 입력

INSERT into person(name, age) VALUES('brown', 23),('jack', 32);

4-2. Codes

build.gradle

dependencies {
    /*web*/
    implementation 'org.springframework.boot:spring-boot-starter-web'
    /*jdbc*/
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    /*mybatis*/
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.3.1'
    /*mysql*/
    runtimeOnly 'com.mysql:mysql-connector-j'
    /*lombok*/
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    /*test*/
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

application.yml

db:
  driver-class-name: com.mysql.cj.jdbc.Driver
  url: jdbc:mysql://serverurl.ap-northeast-2.rds.amazonaws.com:3306/spring-mybatis
  username: 계정
  password: 비밀번호
  
# Hikari Connection Pool Logging
logging:
  level:
    com.zaxxer.hikari: TRACE
    com.zaxxer.hikari.HikariConfig: DEBUG

 

 

MyBatis-Config

package com.example.springmybatis.config;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import javax.sql.DataSource;
import java.sql.SQLException;

@Configuration
public class MyBatisConfig {
    
    @Value("${db.driver-class-name}")
    private String driver;

    @Value("${db.url}")
    private String url;

    @Value("${db.username}")
    private String username;

    @Value("${db.password}")
    private String password;

    /*DriverManagerDataSource를 사용할 경우 항상 새로운 Connection을 획득한다.*/
//    @Bean
//    public DriverManagerDataSource dataSource() throws SQLException {
//        DriverManagerDataSource dataSource = new DriverManagerDataSource();
//        dataSource.setDriverClassName(driver);
//        dataSource.setUrl(url);
//        dataSource.setUsername(username);
//        dataSource.setPassword(password);
//
//        return dataSource;
//    }

    /*HikariDataSource*/
    @Bean
    public HikariDataSource dataSource(){
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setPoolName("myPool");
        hikariConfig.setDriverClassName(driver);
        hikariConfig.setJdbcUrl(url);
        hikariConfig.setUsername(username);
        hikariConfig.setPassword(password);
        return new HikariDataSource(hikariConfig);

    }

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

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        /*SqlSessionFactoryBean 객체*/
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        /*DataSource 설정*/
        sqlSessionFactoryBean.setDataSource(dataSource);
        /*config 파일을 설정*/
        sqlSessionFactoryBean.setConfigLocation(new PathMatchingResourcePatternResolver()
                .getResource("classpath:mybatis/config/mybatis-config.xml"));
        /*mapper 파일을 설정*/
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mybatis/mapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }
}
  • DataSource, SqlSessionFactory(Mapper, Config, DataSource) 등을 설정합니다.
  • 마이바티스만 사용하면, SqlSessionFactory는 SqlSessionFactoryBuilder를 사용해서 생성해야 합니다. 하지만,
    마이바티스 스프링 연동 모듈에서는, SqlSessionFactoryBean이 사용됩니다. (.getObject 사용 시 내부적으로 SqlSessionFactory가 리턴됩니다.)

 

 

MyBatisConfig 파일의 설정은, Spring Boot Auto Configuration 기능으로, 아래와 같이 application.yml로 간단하게 설정 가능합니다. (둘 중 하나만 해도 동작, 하지만 이번 예제는 yml 파일 말고 MyBatisConfig 파일로 진행하겠습니다.)
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://serverurl.ap-northeast-2.rds.amazonaws.com:3306/spring-mybatis
    username: 계정
    password: 비밀번호

# config, 별칭 패키지, 매퍼 위치
mybatis:
  config-location: classpath:mybatis/config/mybatis-config.xml
  type-aliases-package: com.example.springmybatis.entity
  mapper-locations: classpath:mybatis/mapper/**/**.xml

위처럼 AutoConfiguration으로 사용하는 경우 DataSoruce의 구현체는 자동으로 HikariDataSource가 사용됩니다.

 

 

여기서 잠깐! DriverManagerDataSource와 HikariDataSource의 차이는 뭘까요?

DriverManagerDataSource의 경우 실제로 커넥션 풀을 가지고 있지 않기 때문에 실제 서비스(서버)에는 사용하면 안 됩니다. 실제 서비스에 사용한다면 db connection이 계속 증가하게 되어 서비스 장애가 발생할 수 있습니다. MyBatis-Config 코드 상에는, DriverManagerDataSource와 HikariDataSource 두 가지를 작성해 놓고 HikariDataSource를 사용합니다.(주석 처리)

 

커넥션은 비싸다.

 

아래와 같이 DataSource라는 인터페이스가 있기 때문에, 인터페이스를 구현한 구현체만 바꿔껴주면 쉽게 변경 가능합니다.

 

 

 

또한, typeAliases 설정의 경우 주석한 부분의 코드를 아래처럼 사용 가능하게 해줍니다.

<!--    <select id="listPerson" resultType="com.example.springmybatis.entity.Person">-->
    <select id="listPerson" resultType="Person">

 

 

 

resources/mybatis/config/mybatis-config.xml

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <settings>
        <!--PERSON_ID -> personId로 리턴해준다.-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <typeAliases>
        <package name="com.example.springmybatis.entity"/>
    </typeAliases>
</configuration>

 

resources/mybatis/mapper/Person.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybats.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.springmybatis.dao.PersonDAO">
    <!--Select All Person-->
<!--    <select id="listPerson" resultType="com.example.springmybatis.entity.Person">-->
    <select id="listPerson" resultType="Person">
        select id, name, age from person
    </select>

    <!--Select One Person-->
    <select id="onePerson" resultType="Person">
        select id, name, age from person where id = #{id}
    </select>

    <!--Insert Person / Person-->
    <insert id="insertPerson" parameterType="Person" useGeneratedKeys="true" keyProperty="id">
        insert into person (name, age) VALUES (#{name}, #{age})
    </insert>

    <!--Insert Person / Primitive Type(Map)-->
    <insert id="primitiveInsertPerson" parameterType="map" useGeneratedKeys="true">
        insert into person (name, age) VALUES (#{name}, #{age})
        <selectKey resultType="Long" keyProperty="param.id" order="AFTER">
            SELECT LAST_INSERT_ID()
        </selectKey>
    </insert>

    <!--Insert Person / Map-->
    <insert id="mapInsertPerson" parameterType="map" useGeneratedKeys="true" keyProperty="id">
        insert into person (name, age) VALUES (#{name}, #{age})
    </insert>

    <!--Delete Person-->
    <delete id="deletePerson">
        DELETE FROM person WHERE id = #{id}
    </delete>

    <!--Update Person-->
    <update id="updatePerson" parameterType="Person">
        UPDATE person SET age = #{age}, name = #{name} WHERE id = #{id}
    </update>
</mapper>

 

Person

package com.example.springmybatis.entity;

import lombok.Getter;

@Getter
public class Person {
    private Long id;
    private String name;
    private Long age;

    public Person(Long id, String name, Long age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
}

 

 

PersonDAO

package com.example.springmybatis.dao;

import com.example.springmybatis.entity.Person;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

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

@Mapper
public interface PersonDAO {
    List<Person> listPerson();

    Person onePerson(Long id);
    /*객체*/
    void insertPerson(Person person);
    /*원시타입*/
    int primitiveInsertPerson(Map<String, Long> param, int age, String name);
    /*map*/
    int mapInsertPerson(Map<String, Object> param);
    int deletePerson(Long id);
    int updatePerson(Person person);

    @Select("select * from person")
    List<Person> selectAll();
}

파라미터로는 객체, 원시 타입, map 등으로 받을 수 있습니다.

 

 

 

MyBatisTest

package com.example.springmybatis;

import com.example.springmybatis.dao.PersonDAO;
import com.example.springmybatis.entity.Person;
import org.junit.jupiter.api.Test;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;


@SpringBootTest
@Transactional
public class MyBatisTest {

    private final PersonDAO personDAO;
    private final SqlSessionTemplate sqlSessionTemplate;

    @Autowired
    public MyBatisTest(PersonDAO personDAO, SqlSessionTemplate sqlSessionTemplate) {
        this.personDAO = personDAO;
        this.sqlSessionTemplate = sqlSessionTemplate;
    }

    @Test
    public void 회원가입() throws Exception {
        //Given

        /*1.Primitive Type*/
        String name = "primitive";
        int age = 99;

        Map param = new HashMap<>();
        param.put("id", null);

        /*2.Map Type*/
        Map person2 = new HashMap();
        person2.put("age", 30);
        person2.put("name", "map");

        /*3.Person Type*/
        Person person3 = new Person("Person", 29);

        /*4.SqlSession(SqlSessionTemplate), Map Type*/
        String namespace = "com.example.springmybatis.dao.PersonDAO.mapInsertPerson";
        Map<String, Object> person4 = new HashMap<>();
        person4.put("age", 33);
        person4.put("name", "sqlSessionTemplate");
        
        //When
        personDAO.primitiveInsertPerson(param, age, name);
        personDAO.mapInsertPerson(person2);
        personDAO.insertPerson(person3);
        sqlSessionTemplate.insert(namespace, person4);

        //Then
        Person findMember1 = personDAO.onePerson((Long) param.get("id"));
        Person findMember2 = personDAO.onePerson(Long.valueOf(String.valueOf(person2.get("id"))));
        Person findMember3 = personDAO.onePerson(person3.getId());
        Person findMember4 = personDAO.onePerson(Long.valueOf(String.valueOf(person4.get("id"))));

        assertEquals(name, findMember1.getName());
        assertEquals(person2.get("name"), findMember2.getName());
        assertEquals(person3.getName(), findMember3.getName());
        assertEquals(person4.get("name"), findMember4.getName());
    }

    @Test
    public void 모든_회원_조회() throws Exception{

        // Given
        List<Person> personList = new ArrayList<>();
        Person person1 = new Person("test1",11);
        Person person2 = new Person("test2",22);
        Person person3 = new Person("test3",33);

        personList.add(person1);
        personList.add(person2);
        personList.add(person3);

        // When
        personDAO.insertPerson(person1);
        personDAO.insertPerson(person2);
        personDAO.insertPerson(person3);

        // Then
        List<Person> findPersonList = personDAO.listPerson();

        assertThat(findPersonList.size() - (findPersonList.size() - personList.size()))
                .isEqualTo(personList.size());
    }

    @Test
    public void 회원_수정() throws Exception {
        //Given
        Person person = new Person("bob", 33);
        personDAO.insertPerson(person);

        //When
        person.update("son", 30);
        personDAO.updatePerson(person);

        //Then
        Person findPerson = personDAO.onePerson(person.getId());
        assertEquals(findPerson.getName(), "son");
        assertEquals(findPerson.getAge(), 30);
    }

    @Test
    public void 회원_탈퇴() throws Exception {
        //Given
        Person person = new Person("bob", 33);
        personDAO.insertPerson(person);
        personDAO.onePerson(person.getId());

        //When
        personDAO.deletePerson(person.getId());

        //Then
        assertNull(personDAO.onePerson(person.getId()));


    }
}

 

Github

728x90
반응형
Comments