-
Chapter 4. MyBatis 스프링 연동코드로 배우는 스프링 웹 프로젝트_intellij 2021. 4. 15. 09:44
이번 장에서는 스프링 프레임워크와 MyBatis를 연동해서 좀 더 빠르게 SQL을 처리할 수 있는 구조를 만들어 보겠습니다.
MyBatis란?
객체 지향 언어의 자바의 관계형 데이터베이스 프로그래밍을 보다 쉽게 도와주는 프레임 워크이다.(SQL 매핑 프레임워크), 개발자들이 JDBC 코드의 복잡하고 지루한 작업을 피하는 용도로 사용.
JDBC MyBatis 직접 Connection을 맺고 마지막에 close()
PreparedStatement 직접 생성 및 처리
PreparedStatement setXXX() 등에 대한 작업을 개발자가 직접 처리
SELECT의 경우 직접 ResultSet 처리자동으로 Connection close() 가능
내부적으로 PreparedStatment 처리 가능
#{prop}와 같이 속성을 지정하면 내부적으로 자동 처리
리턴 타입을 지정하는 경우 자동으로 객체 생성 및 ResultSet 처리MyBatis는 기존의 SQL을 그대로 활용할 수 있다는 장점이 있고 진입장벽이 낮아 JDBC의 대안으로 많이 사용합니다. 스프링 프레임워크의 가장 큰 특징으로 다른 프레임워크들과의 연동을 쉽게 하는 추가적인 라이브러리가 많다.
*타 프레임워크는 다른 프레임워크를 배척
*JDBC(Java DataBase Connectivity) : 자바 프로그래밍이 데이터베이스와 연결되어 데이터를 주고 받을 수 있게 해주는 프로그래밍 인터페이스
(DriverClass, Connecotion, PerparedStatement, ResultSet etc...)
MyBatis는 mybatis-spring이라는 라이브러리를 통해서 쉽게 연동작업 처리 가능
MyBatis는 보통 이러한 관계를 가진다.
MyBatis의 특징
- SQL문이 코드로부터 완전히 분리
- 기존에는 DAO 파일에 모든 SQL문을 작성하였으나 MyBatis에서는 Mapper파일에 SQL코드를 입력해 놓고 DAO 파일에 필요할 때마다 가져와서 사용
- XML, 어노테이션 방식으로 SQL문을 별도로 처리하는 것이 가능하고, 필요한 경우에는 두가지 방식 혼용도 가능하다.(간단한 SQL은 어노테이션으로 복잡하고 많은 SQL은 XML로)
- 코드가 짧아짐
- Spring과 연동으로 자동화 처리
- 스프링과 MyBatis를 연계하는 MyBatis-Spring 라이브러리를 이용하면 개발자는 직접 SQL문의 호출업싱도 원하는 결과를 얻을 수 있음
- MyBatis는 단독으로 사용하는 것보다 스프링과 연계해서 사용하는 것이 코드를 줄여 생산성을 높일 수 있다.
- 유지보수성 향상(동적 SQL을 이용한 제어)
- Mapper파일에만 SQL 코드를 입력하고 나중에 SQL 코드를 변경할 일이 발생한다면 이곳에서 유지보수만 하면 DAO는 아무런 영향을 받지 않는다.
- MyBatis는 기본적으로 SQL문을 처리하기는 하지만 약간의 제어문이나 루프 등의 처리 기능을 가지고 있습니다. 이를 통해 SQL과 관련된 처리를 Java코드에서 분리시킬 수 있습니다
MyBatis의 구성
1. SqlSessionConfig.xml : MyBatis 환경설정 파일 / MyBatis가 JDBC 코드를 실행하는데 필요한 전반에 걸친 세팅을 한다.
- TypAlias 설정 : 사용할 모델 클래사에 대한 별칭 설정 \<typeAlias>
- DB연동을 위한 설정 : DataBase에 어떻게 접속할 것인지에 대한 설정 \<environment>
- Mapper 설정 파일 등록 : 매핑 설정이 어디 있는지 \<mapper>
2. Mapper 설정 파일 ex) member.xml ,company.xml / SQL문과 관련된 설정을 하는 파일로서 MyBatis 설정파일(SqlSessionConfig.xml)에 등록하여야 한다.
- 주요 구성 요소
- SQL문 등록 태그
- SQL문 태그의 구성요소 : Result, Parameter, Sql문 등록
- SQL 태그 : insert, update, select
- 공통 SQL문 설정 태그 : \<sql>
- SELECT 결과 처리 설정
- \<resultMap>
- SQL문 등록 태그
MyBatis 관련 라이브러리 추가
MyBatis와 mybatis-spring을 사용하기 위해서 pom.xml 파일에 추가적인 라이브러리를 설정해야함
pom.xml 추가
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.0.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.3.18.RELEASE</version> </dependency>
spring-jdbc/spring-tx : 스프링에서 데이터베이스 처리와 트랜잭션 처리
mybatis/mybatis-spring : Mybatis와 스프링 연동용 라이브러리
XML 방식 ) applicationContext.xml 추가
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"></property> </bean>
MyBatis에서 가장 핵심적인 객체라 불리는 SQLSession이라는 존재와 SQLSessionFactory입니다. SQLSessionFactory의 이름에서 보듯이 내부적으로 SQLSession이라는 것을 만들어 내는 존재인데, 개발에서는 SQLSession을 통해서 Connection을 생성하거나 원하는 SQL을 전달하고 결과를 리턴 받는 구조로 작성하게 됨.
스프링에 SQLSessionFactory를 등록하는 작업은 SqlSessionFactoryBean을 이용합니다.
*org.mybatis.spring.~~ : MyBatis의 패키지가 아니라 스프링과 연동 작업을 처리하는 mybatis-spring 라이브러리 클래스
Java 방식 ) RootConfig.class에 추가
@Bean public SqlSessionFactory sqlSessionFactory() throws Exception{ SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dataSource()); return sqlSessionFactory.getObject(); }
DataSourceTests에 추가
import lombok.Setter; import lombok.extern.log4j.Log4j; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.junit.Test; import org.junit.runner.RunWith; import org.mido.config.RootConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import javax.sql.DataSource; import java.sql.Connection; @RunWith(SpringJUnit4ClassRunner.class) // @ContextConfiguration("file:/src/web/WEB-INF/applicationContext.xml") //Java 설정을 사용하는 경우 @ContextConfiguration(classes={RootConfig.class}) @Log4j public class DataSourceTests { @Setter(onMethod_={@Autowired}) private DataSource dataSource; @Setter(onMethod_=@Autowired) private SqlSessionFactory sqlSessionFactory; @Test public void testConnection(){ try(Connection con = dataSource.getConnection()){ log.info(con); }catch (Exception e){ org.junit.Assert.fail(e.getMessage());; } } public void testMyBatis(){ try(SqlSession session = sqlSessionFactory.openSession(); Connection con = session.getConnection(); ) { log.info(session); log.info(con); } catch (Exception e) { org.junit.Assert.fail(e.getMessage()); } } }
SqlSessionFactoryBean을 이용해서 SqlSession을 사용해 보는 테스틑 기존의 DataSourceTests 클래스에 추가해서 확인합니다. testMyBatis()는 설정된 SqlSessionFactory 인터페이스 타입의 SqlSessionFactoryBean을 이용해서 생성하고, 이를 이용해서 Connection까지를 테스트합니다.
이렇게 정상 출력된다면 정상적으로 수행된 것입니다.
스프링과의 연동처리
SQLSessionFactory를 이용해서 코드를 작성해도 직접 Connection 을 얻어서 JDBC 코딩이 가능하다. 하지만 편하게 작업하기 위해서 SQL에 관한 처리를 분리하고 자동으로 처리되는 방식을 사용해야 합니다. 그래서 우리는 MyBatis의 Mapper를 사용합니다. Mapper는 SQL과 그에 대한 처리를 지정하는 역할을 수행합니다. mybatis-spring을 이용하는 경우에는 Mapper를 XML과 인터페이스 + 어노테이션 형태로 작성할 수 있습니다.
TimeMappper 작성
Mapper를 작성하는 작업은 XML을 이용할 수도 있지만 이번에는 최소한의 코드로 작성하는 Mapper 인터페이스를 사용해 보겠습니다. src>main>java에 org.mido.mapper를 만들고 TimeMapper.interface를 추가하도록 하겠습니다.
package org.mido.mapper; import org.apache.ibatis.annotations.Select; public interface TimeMapper { @Select("SELECT sysdate FROM dual") public String getTime(); }
MyBatis 어노테이션을 사용해 SQL을 메서드에 추가합니다. Mapper 작성 후 MyBatis가 동작할 때 Mapper를 인식할 수 있도록 applicationContext.xml에 추가적인 설정이 필요합니다. 가장 간단한 방식인 <mybatis:scan>을 사용하도록 하겠습니다.
<mybatis-spring:scan base-package="org.mido.mapper.TimeMapper"></mybatis-spring:scan>
<mybatis-spring:scan>태그의 base-package 속성은 지정된 패키지의 모든 MyBatis 관련 어노테이션을 찾아서 처리 Mapper를 처리하는 방법은 XML이나 Mapper 인터페이스를 사용할 수도 있지만 매번 너무 번잡하기에 패키지를 자동으로 인식하는 방법으로 하였습니다.(이것이 가장 편리)
만약 applicationContext에 추가하는 것이 아니라 Java의 설정을 이용하고 싶은 경우에는
@Configuration @ComponentScan(basePackages = {"org.mido.sample"}) @MapperScan(basePackages = {"org.mido.mapper"}) public class RootConfig {
이러한 것을 추가합니다.
(MapperScan을 이용해서 처리)
Mapper 테스트
MyBatis와 Spring은 Mapper 인터페이스를 이용해서 실제 SQL 처리가 되는 클래스를 자동으로 생성합니다. 그 결과 개발자들은 인터페이스와 SQL만을 작성하는 방식으로도 모든 JDBC를 처리를 끝낼 수 있게 되었습니다.
테스트 코드는 src>test>java>org.mido.persistence.TimeMapperTest.class를 생성합니다.
package org.mido.persistence; import lombok.Setter; import lombok.extern.log4j.Log4j; import org.junit.Test; import org.junit.runner.RunWith; import org.mido.mapper.TimeMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @Log4j @RunWith(SpringJUnit4ClassRunner.class) //@ContextConfiguration("file:web/WEB-INF/applicationContext.xml") //Java 설정인 경우 @ContextConfiguration(classes = {org.mido.config.RootConfig.class}) public class TimeMapperTest { @Setter(onMethod_=@Autowired) private TimeMapper timeMapper; @Test public void testGetTime(){ log.info(timeMapper.getClass().getName()); log.info(timeMapper.getTime()); } }
이 또한 정상 수행이 된다. TimeMapperTest는 TimeMapper가 정상적으로 사용이 가능한지를 알아보는 테스트 코드이기에 위의 코드가 정상적으로 동작한다면 스프링 내부에는 TimeMapper 타입으로 만들어진 빈이 존재한다는 뜻이 됩니다. 위의 코드에서 timeMapper.getClass().getName()은 실제 동작하는 클래스의 이름을 확인해 주는데 우리는 인터페이스만을 만들었지만 적당한 클래스가 만들어진 것을 알 수 있습니다.(스프링 AOP)
-> 스프링이 인터페이스를 이용해서 객체를 생성한다
XML 매퍼와 같이 쓰기
MyBatis를 이용해서 SQL을 처리할 때 어노테이션을 이용하는 방식이 압도적으로 편리하기는 하나 SQL문이 복잡하거나 길어지는 경우에는 어노테이션보다 XML을 이용하는 방식을 더 선호하게 됩니다. mybatis-spring은 인터페이스와 XML방식을 같이 사용할 수 있습니다.
XML을 사용할 때에는 XML 파일의 위치와 XML 파일에 지정하는 namespace속성이 중요하다. XML 파일의 위치의 경우 Mapper 인터페이스가 있는 곳에 같이 작성거나 src>main>resources에 XML을 저장할 폴더를 생성해 저장하면 됩니다. (XML 파일의 이름은 Mapper 인터페이스와 같은 이름을 만드는 것이 좋음)
src>main>resources 폴더 내에 org.mido.mapper 폴더를 만든 후 TimeMapper.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="org.mido.mapper.TimeMapper"> <select id="getTime2" resultType="string"> SELECT sysdate FROM dual </select> </mapper>
XML 매퍼를 사용할 때는 <mapper>태그의 namespace 속성값이 중요한데 MyBatis는 Mapper 인터페이스와 XML을 인터페이스의 이름과 namespace 속성값을 가지고 판단합니다.
위와 같이 org.mido.mapper.TimeMapper 인터페이스와 xml의 <mapper namespace="org.mido.mapper.TimeMapper">가 같은 이름이면 이를 병합해 처리합니다. 또한 위의 방식은 메서드 선언은 인터페이스에서 하고 SQL에 대한 처리는 XML을 이용하는 방식입니다.
<select>태그의 id 속성의 값은 메서드의 이름과 동일하게 맞춰야합니다. <select>태그는 resultType 속성을 가지는데 이 값은 인터페이스에 선언된 메서드의 리턴 타입과 동일하게 작성
최종적인 확인을 위해서 TimeMapperTest 클래스를 이용해서 테스트 작업을 해보겠습니다.
그 전에 필요한 것을 추가하겠습니다.
TimeMapper 추가
package org.mido.mapper; import org.apache.ibatis.annotations.Select; public interface TimeMapper { @Select("SELECT sysdate FROM dual") public String getTime(); public String getTime2(); }
TimeMapperTest 추가
package org.mido.persistence; import lombok.Setter; import lombok.extern.log4j.Log4j; import org.junit.Test; import org.junit.runner.RunWith; import org.mido.mapper.TimeMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @Log4j @RunWith(SpringJUnit4ClassRunner.class) //@ContextConfiguration("file:web/WEB-INF/applicationContext.xml") //Java 설정인 경우 @ContextConfiguration(classes = {org.mido.config.RootConfig.class}) public class TimeMapperTest { @Setter(onMethod_=@Autowired) private TimeMapper timeMapper; @Test public void testGetTime(){ log.info(timeMapper.getClass().getName()); log.info(timeMapper.getTime()); } @Test public void testGetTime2(){ log.info("getTime2"); log.info(timeMapper.getTime2()); } }
getTime2()와 getTime()과 동일합니다.
log4jdbc-log4j2 설정
MyBatis는 JDBC의 PreparedStatement를 이용해서 SQL을 처리합니다. 따라서 SQL에 전달되는 파라미터는 JDBC에서와 같이 ?로 치환되어서 처리됩니다. 복잡한 SQL의 경우 ?로 나는 값이 제대로 되었는지 확인하기 쉽지 않고 실행된 SQL의 내용을 정확히 확인하기는 어렵다. 이런 문제를 해결하기 위해 우리는 ?가 어떤 값으로 처리되었는지 확인하는 기능을 추가할 필요가 있고 log4jdbc-log4j2 라이브러리를 추가하도록 한다.
pom.xml에 추가
<dependency> <groupId>org.bgee.log4jdbc-log4j2</groupId> <artifactId>log4jdbc-log4j2-jdbc4</artifactId> <version>1.16</version> </dependency>
라이브러리를 추가한 후 두 단계를 지나면 완성되는데
- 로그 설정 파일을 추가하는 작업
- JDBC 연결 정보를 수정
우선 src>main>resources 밑에 log4jdbc.log4j2.properties를 생성한다
loh4jdbc-log4j2.properties 생성
log4jdbc-spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4jdbc를 이용하는 경우는 JDBC 드라이버와 URL을 수정해야 합니다. 그러기에 우리는 applicationContext.xml을 수정합니다.
XML 방식)applicationContext.xml 수정
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig"> <property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy"></property> <property name="jdbcUrl" value="jdbc:log4jdbc:oracle:thin:@localhost:1521:XE"></property> <property name="username" value="system"></property> <property name="password" value="root"></property> </bean>
dataSource() 메서드에서 변경되는 부분은 JDBC 드라이버의 클래스, url 부분이 바뀌게 됩니다. 이 두 설정이 제대로 되어 있지 않으면 데이터베이스 로그가 정상적으로 기록되지 않습니다.
설정을 변경한 후 기존의 테스트 코드를 실행해보면 이전과 달리 JDBC와 관련된 로그들이 출력이 됩니다.
Java 방식) RootConfig.class 수정
package org.mido.config; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; @Configuration @ComponentScan(basePackages = {"org.mido.sample"}) @MapperScan(basePackages = {"org.mido.mapper"}) public class RootConfig { @Bean public DataSource dataSource(){ HikariConfig hikariConfig = new HikariConfig(); //hikariConfig.setDriverClassName("oracle.jdbc.OracleDriver"); //hikariConfig.setJdbcUrl("jdbc:oracle:thin:@localhost:1521:XE"); hikariConfig.setDriverClassName("net.sf.log4jdbc.sql.jdbcapi.DriverSpy"); hikariConfig.setJdbcUrl("jdbc:log4jdbc:oracle:thin:@localhost:1521:XE"); hikariConfig.setUsername("system"); hikariConfig.setPassword("root"); HikariDataSource dataSource = new HikariDataSource(hikariConfig); return dataSource; } @Bean public SqlSessionFactory sqlSessionFactory() throws Exception{ SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dataSource()); return sqlSessionFactory.getObject(); } }
위와 같이 정상적으로 수행이 되게 됩니다.
로그의 레벨 설정
테스트 코드를 실행하면 상당히 많은 양의 로그가 출력되기 때문에 처음 개발할 때는좋지만 시간이 지나면 불편하다고 느낄 수 있습니다. 이런 경우 로그의 레벨을 조금 수정할 필요가 있습니다.
테스트 코드와 관련된 설정은 src>test>resources 밑에 log4j.xml을 이용합니다.
<!-- Root Logger --> <root> <priority value="info" /> <appender-ref ref="console" /> </root>
-> 테스트 코드가 실행될 떄 보여지는 'INFO' 메세지는 log4j.xml의 윗부분에 영향을 받기 때문이다.
만일 log4jdbc에서 출력되는 로그를 조절하고 싶다면 추가적인 logger를 지정해서 처리합니다.
<logger name="jdbc.audit"> <level value="warn"></level> </logger> <logger name="jdbc.resultset"> <level value="warn"></level> </logger> <logger name="jdbc.connection"> <level value="warn"></level> </logger>
기본 설정인 로그는 info 레벨이기 때문에 warn과 같이 좀 더 높은 레벨의 로그만 기록하게 수정하면 테스트 코드를 실행할 때 이전에 비해 로그의 양이 줄어드는 것을 확인할 수 있습니다.
저는 로그의 줄어짐을 체감상 느끼지 못하긴 했습니다.
<참고>
일반적인 스프링 웹 프로젝트의 구성
일반적으로 웹 프로젝트는 3개의 레이어로 구성합니다.
출처 : https://velog.io/@wimes/2.-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD%EC%84%A4%EC%A0%95-Spring-MyBatis-MySQL%EC%9D%98-%EC%84%A4%EC%A0%95-2zk4cf5gof Presentation Layer : UI를 담당하는 구성 요소들이 들어갑니다.
- JSP와 같은 View와 Controller부분으로 분리되어 작성
Business Layer : 고객의 요구사항을 반영하는 계층, 사용자의 환경이 아닌 기능적인 요구사항을 구현한 곳입니다.
(비즈니스 계층은 어떤 형태의 데이터가 필요하고 반환될 것인지 결정)
Data Access Layer(Persistence Layer) : 데이터 처리를 전문으로 담당.
- MyBatis / DAO(Data Access Object)의 경우 MyBatis를 호출하고 사용하는 구조
출처 - 출처 : https://velog.io/@wimes/2.-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD%EC%84%A4%EC%A0%95-Spring-MyBatis-MySQL%EC%9D%98-%EC%84%A4%EC%A0%95-2zk4cf5gof DAO의 구성은 이런식으로 이루어집니다. 앞서 말했듯이 Spring-mybatis 라이브러리를 통해 MyBatis를 사용하는 그러한 과정을 나타냅니다.
MyBatis 연동 준비물
이 장에서는 spring-jdbc 모듈, spring-test 모듈, MyBatis, MyBatis-Spring을 사용할 것인데 이를 위해 pom.xml을 이용해서 jar파일을 다우로드하고 스프링의 설정파일을 수정하는 작업을 수행할 것입니다.
에러코드
org.springframework.beans.factory.BeanCreationException:
코딩을 하던 중 이러한 에러코드가 발생하였는데 자세히 읽어보니 driverClassName에서 에러를 뱉어 내는 것. 하지만 오타는 없었고 다음 내용에는 log4j 라이브러리를 찾을 수 없다고 나와있어 log4j는 api와 core로 나누어져 있기에 아래 내용을 추가한다.
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.0.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.0.1</version> </dependency>
'코드로 배우는 스프링 웹 프로젝트_intellij' 카테고리의 다른 글
Chapter 3. 스프링과 Oracle Database 연동 (0) 2021.04.14 Chapter 2. 스프링의 특징과 의존성 주입 (0) 2021.04.13 Chapter 1. 스프링 개발 환경 구축 (0) 2021.04.12 코드로 배우는 스프링 웹 프로젝트 1) 개발환경 (0) 2021.04.12 - SQL문이 코드로부터 완전히 분리