박매일 님의 스프 1탄을 들으면서 정리하고 있다. 이전에는 스프링 레거시 프로젝트를 만들고 Spring MVC까지 적용해 보았다.
이번에는 프로젝트에 DB 및 MyBatis 연결을 해 보았다.
나는 Docker MySQL을 사용할 것이기 때문에 프로젝트 디렉토리에 docker-compose.yml
파일을 만들어주었다.
version: '3'
services:
db:
image: mysql:8.0
container_name: untitled
restart: always
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: "root1234"
MYSQL_DATABASE: "untitled"
MYSQL_USER: "ex"
MYSQL_PASSWORD: "ex"
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
volumes:
- ./database/datadir/:/var/lib/mysql
- ./database/init/:/docker_entrypoint-initdb.d/
설정 파일을 생성하고 터미널에서 docker-compose up
을 통해 빌드하면 컨테이너가 생성되고 실행된다.
인텔리제이에서 DB 연동을 해 준다. 연동을 해두면 직접 쿼리를 실행할 수 있고 스키마나 데이터도 인텔리제이에서 편하게 확인할 수 있다.
User
, Password
는 루트가 아닌 유저의 정보를 입력해야 한다.Database
는 Test Connection
을 해야 완전히 생성된다. 테스트 커넥션이 성공적으로 이루어지면 Database
를 선택해주고 OK
를 눌러 연결을 마친다.entity
패키지에 board.sql
파일을 생성하고 스키마를 구성하고 임시 데이터를 삽입해준다. SQL 파일을 Run
하면 설정 창이 뜨는데 타겟 데이터베이스를 선택해주고 실행하면 쿼리가 실행된다.
자바 애플리케이션에 DB를 연동하기 위해서는 JDBC API가 필요하다. 또한 사용하는 DB에 맞는 JDBC 드라이버도 추가해야 한다. 마이바티스는 커넥션 풀을 사용하기 때문에 데이터소스로 HikariCP
를 사용하겠다.
<!-- DB 관련 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.4.1</version>
</dependency>
톰캣이 실행되면 톰캣은 web.xml
을 읽고 스프링 컨테이너를 생성한다. 이 때 ContextLoaderListener
도 생성되는데 이 객체는 root-context.xml
를 읽고 객체를 생성한다.
<!-- Root Context: defines shared resources visible to all other web components -->
<!-- API(HikariCP) -->
<!-- bean : 객체를 생성하는 태그 -->
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/untitled?serverTimezone=UTC"/>
<property name="username" value="ex"/>
<property name="password" value="ex"/>
</bean>
<!-- HikariDataSource(Connection POOL을 만드는 역할을 한다) -->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<constructor-arg ref="hikariConfig"/>
</bean>
hikariConfig
: 포트 번호, 데이터베이스 이름, 유저 이름과 패스워드를 잘 기재한다.dataSource
: hikariConfig
설정 정보를 참조하여 커넥션 풀을 생성한다.<constructor-arg ref="hikariConfig"/>
destroy-method="close"
: 빈이 다 사용되고 destroy
가 호출될 때 close
메서드로 커넥션을 종료하게끔 설정한다. 개발자가 커넥션을 종료하는 것이 아니라 자동으로 끊어지게 된다.마이바티스는 dataSource
에 의해 생성된 커넥션 풀에서 커넥션을 가져다 사용한다.
DB에서 Board
에 맞는 데이터를 꺼내오기 위해 BoardMapper
인터페이스를 만들어준다. 마이바티스가 매퍼를 인식하기 위해 @Mapper
애노테이션을 달아주어야 한다.
@Mapper
를 인식하지 못한다.<?xml version="1.0" encoding="UTF-8"?>
<!-- xml 파일명은 인터페이스와 같아야 함 -->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.example.mapper.BoardMapper"> <!-- 스프링이 이것을 참고하여 경로를 찾아감 -->
<!-- BoardMapper 인터페이스의 getList 메서드와 매핑 -->
<select id="getLists" resultType="org.example.entity.Board">
select * from myboard order by idx desc
</select>
</mapper>
매퍼 인터페이스만으로도 마이바티스를 사용할 수 있다. getList
메서드에 @Select("select * from myboard order by idx desc")
를 달아주면 된다. 그러나 매퍼가 복잡해지면 XML 파일로 분리하는 것이 좋다.
<mapper namespace>
: XML 파일이 매퍼를 찾을 수 있도록 경로를 적어준다.<select>
: id
: 인터페이스의 메서드명과 일치해야 한다.resultType
: 결과값을 바인딩 할 객체의 경로를 적어준다.스프링에서 마이바티스를 사용하기 위해 필요한 API는 총 다섯 개이다:
1~3은 앞에서 추가했으므로 MyBatis
, Mabatis-spring
만 추가하면 된다.
<!-- MyBatis 관련 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 기존 namespace 아래 정보 추가 -->
<beans xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring">
<beans xsi:schemaLocation="http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">
<!-- BoardMapper interface의 구현클래스 생성
SqlSessionFactoryBean(SQL을 실행하는 API) -->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- Mapper interface들을 메모리에 올리는 방법(scan) -->
<mybatis-spring:scan base-package="org.example.mapper"/>
마이바티스가 매퍼를 인식하기 위해서 매퍼를 빈으로 등록해야 한다. 하나하나 직접 등록하는 것은 번거로우므로 스캔을 통해 베이스 패키지(base-package="kr.board.mapper"
) 내의 매퍼들을 일괄 등록하도록 한다.
BoardMapper
는 인터페이스이기 때문에 사용하려면 구현체가 필요하다. 마이바티스는 내부적으로 SqlSessionFactoryBean
라는 구현체를 만들어준다.
SqlSessionFactoryBean
은 인터페이스 메서드의 내부를 모두 구현해준다. DB에서 쿼리를 실행하고 결과를 지정 타입에 맞는 타입으로 반환하는 것까지 마이바티스에서 해결해주는 것이다.
이 구현체는 개발자에게 보이지는 않지만 자동 생성되지 않고 꼭 필요한 객체이기 때문에 <bean>
으로 등록해주어야 한다.
SqlSessionFactoryBean
은 커넥션 풀을 통해 쿼리를 실행하므로 HikariDataSource
를 참조해야 한다. 객체 명칭(id)인 dataSource
로 가져와준다.@Controller
public class BoardController {
@Autowired
private BoardMapper boardMapper;
@RequestMapping("/boardList.do")
public String boardList(Model model) {
List<Board> list = boardMapper.getLists();
model.addAttribute("list", list);
return "boardList"; // /WEB-INF/views/boardList.jsp -> forward
}
}
boardMapper
프로퍼티를 추가해주고 스프링이 자동 주입을 하도록 @Autowired
를 걸어준다.boardList
에서는 매퍼 인터페이스에 정의된 대로 boardMapper.getLists()
를 호출하여 DB에서 모든 리스트를 가져온다.list
라는 이름으로 담아 bordList
라는 뷰에 전달한다.브라우저에서 /boardList.do
를 실행해 보면 위와 같이 데이터가 잘 가져와진 것을 확인할 수 있다 !
테스트 도중에 오류가 발생했는데 로깅을 설정하지 않아 디버깅이 어려웠다. 이전 포스트에서 로깅 의존성을 추가만 하고 설정을 하지 않았기 때문에 보이지 않았던 것이다.
src/main/resources
에 log4j.xml
을 만들고 다음과 같이 설정해준다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN" "log4j.dtd">
<log4j:configuration>
<!-- Appenders -->
<appender name="console" class="org.apache.log4j.ConsoleAppender">
<param name="Target" value="System.out" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%-5p: %c - %m%n" />
</layout>
</appender>
<!-- Application Loggers -->
<logger name="org.example.controller">
<level value="info" />
</logger>
<!-- 3rdparty Loggers -->
<logger name="org.springframework.core">
<level value="info" />
</logger>
<logger name="org.springframework.beans">
<level value="info" />
</logger>
<logger name="org.springframework.context">
<level value="info" />
</logger>
<logger name="org.springframework.web">
<level value="info" />
</logger>
<!-- Root Logger -->
<root>
<priority value="debug" />
<appender-ref ref="console" />
</root>
</log4j:configuration>
디버깅이 필요한 부분에서 로깅 레벨을 debug
로 높이면 아래와 같이 동작을 상세하게 확인할 수 있다.
Caused by: org.xml.sax.SAXParseException; lineNumber: 34; columnNumber: 61; cvc-complex-type.2.4.c: 일치하는 와일드 카드 문자가 엄격하게 적용되지만 'mybatis-spring:scan' 요소에 대한 선언을 찾을 수 없습니다.
at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException(ErrorHandlerWrapper.java:204)
at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.error(ErrorHandlerWrapper.java:135)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:396)
...
내 오류의 원인은 root-context.xml
에서 mybatis-spring
를 위한 xsi:schemaLocation
이 지정되어있지 않아 XML 파일을 읽어올 수 없던 것이었다. 상단의 root-context.xml
예시에서는 올바르게 수정해 두었다.