Spring + Mybatis 활용 원리

반응형
728x90
반응형

마이바티스(Mybatis)

SQL과 자바 객체를 매핑하는 사상에서 개발된 데이터베이스 접근용 프레임워크다. 마이바티스는 SQL 기반으로 데이터베이스 접근을 수행하는 기존 방법을 받아들이고, 규모가 큰 애플리케이션 개발에서 발생하는 과제를 해결하는 구조를 제공한다. Mybatis는 자체 Connection Pool을 가지며, 환경에 따라 사용할 DB를 설정하여 사용한다.

 

마이바티스 등장

애플리케이션의 규모가 커지면서 SQL이 수백개가 넘는 경우가 많아졌다. SQL 자체의 체계적인 관리 방법이나 SQL의 입출력 데이터와 자바 객체의 효율적인 변환 방법 등 스프링의 기능만으로는 해결할 수 없는 과제가 발생했다.

 

마이바티스 이점
  • SQL의 체계적인 관리, 선언적 정의(설정 파일, 어노테이션)
  • 자바 객체와 SQL 입출력 값의 투명한 바인딩
  • 동적 SQL 조합

 

마이바티스는 SQL과 객체를 매핑하기 위한 'SQL Mapper'라고 부르는 것이 정확한 표현이다. 마이바티스는 'MyBatis SQL Mapper Framework for Java'라는 이름을 가진다.

 

 

마이바티스 특징

마이바티스는 SQL을 설정 파일이나 어노테이션에 선언적으로 정의해서 자바로 작성된 비즈니스 로직에서 SQL 자체를 감출 수 있다. 

 

Mapper 인터페이스

Mapper 인터페이스(POJO 인터페이스)가 SQL을 감추는 역할을 담당하고 있으며 Mapper 인터페이스의 메서드와 SQL의 양쪽을 연결한다. 그래서 자바로 작성된 비즈니스 로직에서는 Mapper 인터페이스를 호출하는 것만으로 연결된 SQL을 실행할 수 있다.

 

 

 

SQL을 정의하는 방법

마이바티스에서는 Mapper 인터페이스에 연결돼있는 SQL을 정의하는 방법으로 '매핑 파일'과 '애노테이션'의 두 종류가 지원된다. 각각의 특징과 그림은 다음과 같다.

SQL 지정 방법 설명
매핑 파일 ibatis 시절부터 지원된 전통적인 지정 방법으로, 마이바티스 기능을 완벽하게 이용할 수 있다.
어노테이션 Mybatis3부터 지원되는 방법으로, 개발의 용이성을 우선시할때 효과적이다.
SQL 지정은 간단하지만 어노테이션의 표현력과 유연성의 제약 탓에 복잡한 SQL이나 매핑을 지정할때는 적합하지 않다.
표준 기능은 지원하지만 매핑 파일에서 표현할 수 있는 모든 것이 지원되는 것은 아니다.

 

1) SQL을 매핑 파일에 지정

Mapper 인터페이스
Room find(String id)
void create(Room room)

 

매핑 파일
<select id = "find">
<insert id = "create">

 

2) SQL을 어노테이션에 지정

Mapper 인터페이스
@Select("SELECT ...")
Room find(String id)

@Insert("INSERT ...")
void create(Room room)

 

 

마이바티스와 스프링 연동

스프링 프레임워크에서 마이바티스를 사용하는 경우 마이바티스 프로젝트에서 제공되는 Mybatis-Spring이라는 라이브러리를 사용한다.

이 라이브러리를 이용해 마이바티스 컴포넌트를 스프링의 DI 컨테이너에서 관리할 수 있게 된다.

 

Mybatis-Spring을 사용함으로써 얻을 수 있는 이점
  • 스프링의 트랜잭션 제어를 이용하기 때문에 마이바티스의 API에 의존한 트랜잭션 제어를 할 필요가 없다.
  • 마이바티스의 초기화 처리를 Mybatis-Spring이 수행하므로 기본적으로 마이바티스의 API를 직접 사용할 필요가 없다.
  • 마이바티스와 JDBC에서 발생한 예외가 스프링이 제공하는 데이터 접근 예외로 변환되기 때문에 마이바티스와 JDBC의 API에 의존한 예외 처리를 할 필요가 없다.
  • 스레드 안전한 Mapper 객체를 생성할 수 있으므로 Mapper 객체를 다른 빈에 DI해서 사용할 수 있다.

 

 

마이바티스와 Mybatis-Spring의 주요 컴포넌트

컴포넌트/설정 파일 설명
마이바티스 설정 파일 마이바티스의 동작 설정을 지정하는 XML 파일이다.
매퍼(Mapper) 인터페이스 매핑 파일이나 어노테이션에 정의한 SQL에 대응하는 자바 인터페이스다.
마이바티스는 실행할 때 Mapper 인터페이스의 구현 클래스를 프락시로 인스턴스화하기 때문에(자동으로 생성) 개발자는 Mapper 인터페이스의 구현 클래스를 작성할 필요는 없다.
매핑 파일 SQL과 객체의 매핑 정의를 기술하는 XML 파일이다.
SQL을 어노테이션에 지정하는 경우에는 사용하지 않는다.
해당 파일을 SqlSession 객체가 참조한다.
org.apache.ibatis.session.SqlSession SQL 발행이나 트랜잭션 제어용 API를 제공하는 컴포넌트다.
마이바티스를 이용해 데이터베이스에 접근할 때 가장 중요한 역할을 하는 컴포넌트다.
스프링 프레임워크에서 사용하는 경우에는 마이바티스 측의 트랜잭션 제어 API는 사용하지 않는다.
SqlSession이 Mapper 파일에서 SQL을 수행하고 결과 데이터를 반환하는 역할을 한다.
org.apache.ibatis.session.SqlSessionFactory SqlSession을 생성하기 위한 컴포넌트다.
org.apache.ibatis.session.SqlSessionFactoryBuilder 마이바티스 설정 파일을 읽어 들여 SqlSessionFactory를 생성하기 위한 컴포넌트다.
빌더 패턴으로 구현되어있다.
org.mybatis.spring.SqlSessionFactoryBean SqlSessionFactory를 구축하고 스프링의 DI 컨테이너에 객체를 저장하기 위한 컴포넌트다.
org.mybatis.spring.SqlSessionTemplate 스프링 트랜잭션 관리하에 마이바티스 표준의 SqlSession을 취급하기 위한 컴포넌트로 스레드 안전하게 구현돼있다.
이 클래스는 SqlSession 인터페이스를 구현하고 있으며 SqlSession으로 동작(실제 처리는 마이바티스의 표준의 SqlSession에 위임)하는 것도 지원한다.
org.mybatis.spring.mapper.MapperFactoryBean 스프링 트랜잭션 관리하에 SQL을 실행하는 Mapper 객체를 빈으로 생성하기 위한 컴포넌트다.
스프링의 DI 컨테이너에서 빈으로 취급할 수 있으므로 임의의 빈에 주입해서 SQL을 실행하는 것이 간단해진다.

 

SqlSession.java
public interface SqlSession extends Closeable {
    <T> T selectOne(String var1);

    <T> T selectOne(String var1, Object var2);

    <E> List<E> selectList(String var1);

    <E> List<E> selectList(String var1, Object var2);

    <E> List<E> selectList(String var1, Object var2, RowBounds var3);

    ...
}

 

SqlSessionFactory.java

SqlSessionFactory 객체를 사용해서 SqlSession 객체를 생성한다.

public interface SqlSessionFactory {
    SqlSession openSession();

    SqlSession openSession(boolean var1);

    SqlSession openSession(Connection var1);

    SqlSession openSession(TransactionIsolationLevel var1);

    SqlSession openSession(ExecutorType var1);

    SqlSession openSession(ExecutorType var1, boolean var2);

    SqlSession openSession(ExecutorType var1, TransactionIsolationLevel var2);

    SqlSession openSession(ExecutorType var1, Connection var2);

    Configuration getConfiguration();
}

 

SqlSessionFactoryBuilder.java

SqlSessionFactoryBuilder 객체를 통해서 SqlSessionFactory 객체를 생성한다.

public class SqlSessionFactoryBuilder {
    public SqlSessionFactoryBuilder() {
    }

    public SqlSessionFactory build(Reader reader) {
        return this.build((Reader)reader, (String)null, (Properties)null);
    }

    public SqlSessionFactory build(Reader reader, String environment) {
        return this.build((Reader)reader, environment, (Properties)null);
    }

    public SqlSessionFactory build(Reader reader, Properties properties) {
        return this.build((Reader)reader, (String)null, properties);
    }

    ...
}

 

SqlSessionTemplate.java

이 클래스는 SqlSession 인터페이스를 구현한다.

public class SqlSessionTemplate implements SqlSession, DisposableBean {
    private final SqlSessionFactory sqlSessionFactory;
    private final ExecutorType executorType;
    private final SqlSession sqlSessionProxy;
    private final PersistenceExceptionTranslator exceptionTranslator;

    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType());
    }

    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) {
        this(sqlSessionFactory, executorType, new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true));
    }

    ...
    
    // sqlSessionProxy
    public <T> T selectOne(String statement, Object parameter) {
        return this.sqlSessionProxy.selectOne(statement, parameter);
    }

    public <K, V> Map<K, V> selectMap(String statement, String mapKey) {
        return this.sqlSessionProxy.selectMap(statement, mapKey);
    }

    public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey) {
        return this.sqlSessionProxy.selectMap(statement, parameter, mapKey);
    }

    public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
        return this.sqlSessionProxy.selectMap(statement, parameter, mapKey, rowBounds);
    }

    public <T> Cursor<T> selectCursor(String statement) {
        return this.sqlSessionProxy.selectCursor(statement);
    }

    public <T> Cursor<T> selectCursor(String statement, Object parameter) {
        return this.sqlSessionProxy.selectCursor(statement, parameter);
    }

    public <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds) {
        return this.sqlSessionProxy.selectCursor(statement, parameter, rowBounds);
    }

    public <E> List<E> selectList(String statement) {
        return this.sqlSessionProxy.selectList(statement);
    }

    public <E> List<E> selectList(String statement, Object parameter) {
        return this.sqlSessionProxy.selectList(statement, parameter);
    }

    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        return this.sqlSessionProxy.selectList(statement, parameter, rowBounds);
    }

    public void select(String statement, ResultHandler handler) {
        this.sqlSessionProxy.select(statement, handler);
    }

    public void select(String statement, Object parameter, ResultHandler handler) {
        this.sqlSessionProxy.select(statement, parameter, handler);
    }

    public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
        this.sqlSessionProxy.select(statement, parameter, rowBounds, handler);
    }
}

 

 

컴포넌트의 작용 과정

▶ 애플리케이션을 시작할때 수행하는 빈 생성 처리

https://dongdd.tistory.com/178

 

1) SqlSessionFactoryBean -> SqlSessionFactoryBuilder

SqlSessionFactoryBean을 빈으로 정의함으로써 스프링의 FactoryBean 구조로 SqlSessionFactoryBuilder를 사용하여 SqlSessionFactory가 빈으로 생성된다. 

 

2) SqlSessionFactoryBuilder -> SqlSessionFactory

SqlSessionFactoryBuilder은 마이바티스 설정 파일의 정의에 따라 SqlSessionFactory를 생성한다. 생성된 SqlSessionFactory는 스프링의 DI 컨테이너에 의해 관리된다.

 

3) MapperFactoryBean -> SqlSessionTemplate

MapperFactoryBean은 SqlSessionTemplate을 생성하고 스프링의 트랜잭션 관리하에 마이바티스 표준의 SqlSession을 취급할 수 있게 한다.

 

4) MapperFactoryBean -> JDK Proxy(Mapper 인터페이스)

MapperFactoryBean은 스프링의 트랜잭션 관리하에 SQL을 실행하는 Mapper 객체를 생성한다.

프록시화된 Mapper 객체는 SqlSessionTemplate을 이용함으로써 스프링의 트랜잭션 관리하에 SQL을 실행한다. 또한 생성된 Mapper 객체는 스프링의 DI 컨테이너에서 싱글턴 빈으로 등록되기 때문에 애플리케이션 측에서 사용하는 경우 Service 클래스 등의 빈에 DI해서 사용한다.

 

 

▶ 요청마다 수행하는 데이터 접근 처리

https://dongdd.tistory.com/178

1) Client Request

애플리케이션은 클라이언트의 요청을 받아 비즈니스 로직을 실행한다.

 

2) Client Request -> JDK Proxy(Mapper 인터페이스)

애플리케이션(비즈니스 로직)은 DI 컨테이너에 의해 DI된 Mapper 객체의 메서드를 호출한다.

 

3) JDK Proxy(Mapper 인터페이스) -> SqlSessionTemplate

Mapper 객체는 호출된 메서드에 대응하는 SqlSession(구현 클래스는 SqlSessionTemplate)의 메서드를 호출한다.

 

4) SqlSessionTemplate -> SqlSessionFactory

SqlSessionTemplate은 SqlSessionFactory를 통해 마이바티스 표준 SqlSession을 취득한다.

마이바티스에서는 여러 SQL을 같은 트랜잭션에서 조작하는 경우 같은 SqlSession을 공유해서 사용해야 한다. SqlSessionTemplate은 SqlSessionFactory를 통해 취득한 SqlSession을 실행중인 트랜잭션에 할당함으로써 같은 트랜잭션에서 같은 SqlSession이 사용되도록 제어하며 이때 JDK 표준의 동적 프락시 구조가 이용된다.

 

5) SqlSessionTemplate -> JDK Proxy(Mapper 인터페이스)

SqlSessionTemplate는 프록시화된 SqlSession를 통해 마이바티스 표준 SqlSession 메서드를 호출해 애플리케이션에서 호출된 Mapper 객체의 메서드에 대응하는 SQL 실행을 의뢰한다.

 

6) JDK Proxy(Mapper 인터페이스) -> 매핑 파일

마이바티스 표준 SqlSession은 Mapper 객체의 메서드에 대응하는 SQL을 매핑 파일에서 취득하고 실행한다.

전달된 이수나 SQL 반환값 등의 변환도 이때 이뤄진다. 또한 매핑 파일에서 읽어들인 SQL과 매핑 정의 정보는 캐시되는 구조로 돼있다.

 

 

전체적인 흐름

위 내용이 쉽게 이해되지 않으므로, 전체적인 흐름을 보자.

https://linked2ev.github.io/mybatis/2019/09/08/MyBatis-1-MyBatis-%EA%B0%9C%EB%85%90-%EB%B0%8F-%EA%B5%AC%EC%A1%B0/

 

※ 위 이미지 출처의 포스팅에서 전체적인 흐름을 잘 설명해놓았다. 참고하자.

컴포넌트의 작용 과정 흐름
응용 프로그램 시작시 수행되는 프로세스 흐름
  1. 응용 프로그램은 SqlSessionFactoryBuilder에 대한 SqlSessionFactory 빌드를 요청한다.
  2. SqlSessionFactoryBuilder는 SqlSessionFactory 생성을 위한 MyBatis 구성 파일을 읽어온다.
  3. SqlSessionFactoryBuilder는 MyBatis 구성 파일 설정 기반으로 SqlSessionFactory를 생성한다.
클라이언트의 각 요청에 대해 수행되는 프로세스 흐름
  1. 클라이언트는 응용 프로그램에 대한 프로세스를 요청한다.
  2. 응용 프로그램은 SqlSessionFactoryBuilder를 사용하여 작성된 SqlSessionFactory에서 SqlSession을 가져온다.
  3. SqlSessionFactorySqlSession을 생성하여 이를 애플리케이션으로 리턴한다.
  4. 응용 프로그램은 SqlSession에서 Mapper Interface의 구현 객체를 가져온다.
  5. 응용 프로그램은 Mapper Interface 메서드를 호출한다.
  6. Mapper Interface의 구현 객체는 SqlSession 메서드를 호출하고 SQL 실행을 요청한다.
  7. SqlSession은 매핑 파일에서 실행할 SQL을 가져 와서 SQL을 실행한다.

 

 

Mybatis-Spring 예외 처리

마이바티스 및 JDBC 드라이버에서 발생한 예외는 org.springframework.dao.DataAccessException을 상속한 비검사 예외로 래핑되어 던져진다. 이 구조에 의해 스프링 표준 JdbcTemplate을 사용해 데이터에 접근할 때와 마찬가지로 예외 처리를 구현할 수 있다. 

 

DataAccessException.java
package org.springframework.dao;

import org.springframework.core.NestedRuntimeException;
import org.springframework.lang.Nullable;

public abstract class DataAccessException extends NestedRuntimeException {
    public DataAccessException(String msg) {
        super(msg);
    }

    public DataAccessException(@Nullable String msg, @Nullable Throwable cause) {
        super(msg, cause);
    }
}

 

NestedRuntimeException.java
public abstract class NestedRuntimeException extends RuntimeException {
    private static final long serialVersionUID = 5439915454935047936L;

    public NestedRuntimeException(String msg) {
        super(msg);
    }

    public NestedRuntimeException(@Nullable String msg, @Nullable Throwable cause) {
        super(msg, cause);
    }

    ...

    static {
        NestedExceptionUtils.class.getName();
    }
}

Mybatis-Spring은 org.mybatis.spring.MybatisExceptionTranslator 클래스에서 DataAccessException으로 변환하며, SqlSessionTemplate이 예외를 포착했을때 호출되는 구조다.

 

MybatisExceptionTranslator.java
public class MyBatisExceptionTranslator implements PersistenceExceptionTranslator {
    private final Supplier<SQLExceptionTranslator> exceptionTranslatorSupplier;
    private SQLExceptionTranslator exceptionTranslator;

    public MyBatisExceptionTranslator(DataSource dataSource, boolean exceptionTranslatorLazyInit) {
        this(() -> {
            return new SQLErrorCodeSQLExceptionTranslator(dataSource);
        }, exceptionTranslatorLazyInit);
    }

    public MyBatisExceptionTranslator(Supplier<SQLExceptionTranslator> exceptionTranslatorSupplier, boolean exceptionTranslatorLazyInit) {
        this.exceptionTranslatorSupplier = exceptionTranslatorSupplier;
        if (!exceptionTranslatorLazyInit) {
            this.initExceptionTranslator();
        }

    }

    public DataAccessException translateExceptionIfPossible(RuntimeException e) {
        if (e instanceof PersistenceException) {
            if (((RuntimeException)e).getCause() instanceof PersistenceException) {
                e = (PersistenceException)((RuntimeException)e).getCause();
            }

            if (((RuntimeException)e).getCause() instanceof SQLException) {
                this.initExceptionTranslator();
                return this.exceptionTranslator.translate(((RuntimeException)e).getMessage() + "\n", (String)null, (SQLException)((RuntimeException)e).getCause());
            } else if (((RuntimeException)e).getCause() instanceof TransactionException) {
                throw (TransactionException)((RuntimeException)e).getCause();
            } else {
                return new MyBatisSystemException((Throwable)e);
            }
        } else {
            return null;
        }
    }

    private synchronized void initExceptionTranslator() {
        if (this.exceptionTranslator == null) {
            this.exceptionTranslator = (SQLExceptionTranslator)this.exceptionTranslatorSupplier.get();
        }

    }
}

위 코드에서 translateExceptionIfPossible()를 통해 org.springframework.dao.DataAccessException 로 래핑되어 던져진다.

 

 

 

반응형

Designed by JB FACTORY