Last Modified 2023.2.3

테스트 환경
Tomcat 9
Java 17
워크스페이스 C:\www

C:\ Command Prompt
C:\www> mvn archetype:generate -Dfilter=maven-archetype-webapp

Choose archetype:
1: remote -> com.haoxuer.maven.archetype:maven-archetype-webapp
2: remote -> com.lodsve:lodsve-maven-archetype-webapp
3: remote -> org.apache.maven.archetypes:maven-archetype-webapp
4: remote -> org.bytesizebook.com.guide.boot:maven-archetype-webapp
Choose a number or apply filter: : 3
Choose org.apache.maven.archetypes:maven-archetype-webapp version: 
1: 1.0-alpha-1
2: 1.0-alpha-2
3: 1.0-alpha-3
4: 1.0-alpha-4
5: 1.0
6: 1.3
7: 1.4
Choose a number: 7: ↵ 
Define value for property 'groupId': net.java_school
Define value for property 'artifactId': mybatisspring
Define value for property 'version' 1.0-SNAPSHOT: : ↵ 
Define value for property 'package' net.java_school: : ↵
Confirm properties configuration:
groupId: net.java_school
artifactId: mybatisspring
version: 1.0-SNAPSHOT
package: net.java_school
 Y: : ↵

빌드가 완료되면 C:\www에 mybatisspring라는 폴더가 생긴다. C:\www\mybatisspring이 프로젝트 루트 디렉터리이다.

Spring MVC 테스트

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <groupId>net.java_school</groupId>
  <artifactId>mybatisspring</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>mybatisspring Maven Webapp</name>
  <url>http://localhhost:8080</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <spring.version>5.3.33</spring.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api -->
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>javax.servlet.jsp-api</artifactId>
      <version>2.3.3</version>
      <scope>provided</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/javax.servlet/jstl -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
    </dependency>    
  </dependencies>

  <build>
    <finalName>mybatisspring</finalName>
    <pluginManagement>
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
          <configuration>
            <filesets>
               <fileset>
                  <directory>src/main/webapp/WEB-INF/classes</directory>
               </fileset>
               <fileset>
                  <directory>src/main/webapp/WEB-INF/lib</directory>
               </fileset>
            </filesets>
          </configuration>
        </plugin>
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

src/main/webapp/WEB-INF/web.xml

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                      http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
  version="4.0">

  <display-name>Mybatis Spring</display-name>
        
  <filter>
    <filter-name>encodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
      <param-name>forceEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>

  <filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <servlet>
    <servlet-name>mybatisspring</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/app-config.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
    
  <servlet-mapping>
    <servlet-name>mybatisspring</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
  
</web-app>

src/main/webapp/WEB-INF/app-config.xml

app-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc.xsd">

<mvc:resources mapping="/resources/**" location="/resources/" />

<mvc:annotation-driven />

<bean id="internalResourceViewResolver"
      class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
  <property name="prefix" value="/WEB-INF/views/" />
  <property name="suffix" value=".jsp" />
</bean>

</beans>

홈페이지

src/main/webapp/resources/css/styles.css

styles.css
@CHARSET "UTF-8";
@import url(http://fonts.googleapis.com/earlyaccess/notosanskr.css);

html, body {
  margin: 0;
  padding: 0;
  background-color: #FFF;
  font-family: "Noto Sans KR", "Liberation Sans", Helvetica, "돋움", dotum, sans-serif;
}
#wordcard {
  margin: 7px auto;
  padding: 1em;
  border: 3px solid grey;
  width: 600px;
  text-align: center;
}

src/main/webapp/WEB-INF/views/index.jsp

index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>Spring MVC Test</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="Keywords" content="Spring MVC Test" />
<meta name="Description" content="This is web app for Spring MVC Test" />
<link href="./resources/css/styles.css" rel="stylesheet" />
</head>
<body>

<div id="wordcard">
<h1>Vocabulary</h1>
<p>
어휘
</p>
</div>

</body>
</html>

영어 단어와 그 의미를 보여주는 화면이다.

화면이 나오면, 데이터베이스 설계를 시작으로, 자바 빈즈, DAO, 서비스, 컨트롤러 순으로 구현하는 게 일반적이지만, 대부분을 생략하고 컨트롤러와 뷰만으로 동작하는 예제를 만들어 보자.

컨트롤러
src/main/java/net/java_school/english/HomeController.java

package net.java_school.english;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

  @GetMapping("/")
  public String homepage() {
    return "index";
  }
}

src/main/webapp/index.jsp 파일 삭제
컨트롤러를 통해 홈페이지("/")가 서비스되도록 하려면 홈페이지 경로로 매핑된 다른 서블릿이 없어야 한다. src/main/webapp/index.jsp 파일을 삭제한다.

스프링 설정 파일에 컨트롤러 추가

<bean id="homeController" class="net.java_school.english.HomeController" />

ROOT 애플리케이션 변경
톰캣을 중지하고 ROOT.xml 파일을 아래처럼 작성한 후 {Tomcat}\Catalina\localhost 디렉터리 복사한다. --{Tomcat}은 톰캣이 설치된 디렉터리를 의미--

ROOT.xml
<?xml version="1.0" encoding="UTF-8"?>
<Context
  docBase="C:/www/mybatisspring/src/main/webapp"
  reloadable="true">
</Context>

WEB-INF 바로 위 디렉터리가 도큐먼트베이스다.

빌드와 배치
루트 디렉터리에서 다음을 실행한다.
mvn compile war:inplace
war:inplace 옵션은 src/main/WEB-INF/classes에 바이트코드를, src/main/WEB-INF/lib에 의존 라이브러리를 생성시킨다.

테스트
톰캣을 시작한다.
http://localhost:8080을 요청한다.
Spring MVC 테스트 화면

컨트롤러가 어떻게 동작하는지 확인했다.
다음은 컨트롤러가 뷰에 데이터를 어떻게 세팅하는지 알아보자.
예제를 간단하게 유지하기 위해 데이터베이스는 사용하지 않겠다.

자바 빈즈
src/main/java/net/java_school/english/WordCard.java

WordCard.java
package net.java_school.english;

public class WordCard {
  private String word;
  private String definitions;

  public WordCard() {}

  public WordCard(String word, String definitions) {
    this.word = word;
    this.definitions = definitions;
  }

  public String getWord() {
    return word;
  }

  public void setWord(String word) {
    this.word = word;
  }

  public String getDefinitions() {
    return definitions;
  }

  public void setDefinitions(String definitions) {
    this.definitions = definitions;
  }
}

DAO
src/main/java/net/java_school/english/WordCardDao.java

WordCardDao.java
package net.java_school.english;

public class WordCardDao {

  private final String[][] words = {
    {"illegible","읽기 어려운"}, 
    {"vulnerable","취약한"}, 
    {"abate","감소시키다"}, 
    {"clandestine","은밀한"}, 
    {"sojourn","잠시 머무름, 체류"}, 
    {"defiance","도전, 저항, 반항"}, 
    {"infraction","위반"}
  }; 
	
  public WordCard selectOne() {
    int no = (int)(Math.random() * 7) + 1;
    WordCard wordCard = new WordCard();
    wordCard.setWord(words[no - 1][0]);
    wordCard.setDefinitions(words[no - 1][1]);
    return wordCard;
  }
}
<bean id="wordCardDao" class="net.java_school.english.WordCardDao" />

서비스
src/main/java/net/java_school/english/WordCardService.java

WordCardService.java
package net.java_school.english;

public class WordCardService {
  private WordCardDao wordCardDao;

  public WordCardService(WordCardDao wordCardDao) {
    this.wordCardDao = wordCardDao;
  }

  public WordCard getWordCard() {
    return wordCardDao.selectOne();
  }
}
<bean id="wordCardService" class="net.java_school.english.WordCardService">
  <constructor-arg ref="wordCardDao" />
</bean>

컨트롤러 수정

package net.java_school.english;

import org.springframework.ui.Model;

import org.springframework.web.bind.annotation.GetMapping;

public class HomeController {

  private WordCardService wordCardService;

  public HomeController(WordCardService wordCardService) {
    this.wordCardService = wordCardService;
  }

  @GetMapping("/")
  public String homepage(Model model) {
    WordCard wordCard = wordCardService.getWordCard();
    model.addAttribute("wordCard", wordCard);
    return "index";
  }
}
<bean id="homeController" class="net.java_school.english.HomeController">
  <constructor-arg ref="wordCardService" />
</bean>

뷰 수정
src/main/webapp/WEB-INF/views/index.jsp

body 엘리먼트 내용을 아래처럼 수정한다.

<div id="wordcard">
<h1>${wordCard.word }</h1>
<p>
${wordCard.definitions }
</p>
</div>

테스트
mvn clean compile war:inplace
http://localhost:8080
테스트 화면2

데이터베이스 다루기

MyBatis-Spring을 사용하여 앱이 데이터베이스를 사용하도록 수정해 보자.
MyBatis-Spring은 스프링에서 MyBatis를 편리하게 사용하기 위한 연동 모듈이다.
MyBatis는 SQL 매핑 퍼시스턴스 프레임워크이다.

https://mybatis.org/spring/ko/getting-started.html
아래 내용은 공식 사이트 시작하기와 순서를 같게 했다.

홈페이지 디자인 수정
새 단어를 추가하기 위한 폼을 #wordcard 다음에 추가한다.

<form id="new-form" method="post">
  <input type="text" name="word" />
  <input type="text" name="definitions" />
  <input type="submit" value="Add" style="color: grey;" />
</form>

다음 스타일을 추가한다.

src/main/webapp/resources/css/styles.css

#new-form {
  margin: 7px auto;
  padding-left: 2em;
  width: 600px;
  text-align: right;
  font-size: 0.8em;
}

scott 계정에 접속해 다음 테이블을 생성한다.

create table wordcard (
word varchar2(45),
definitions varchar2(4000),
constraint pk_wordcard primary key(word)
);

mybatis-spring과 함께 스프링 JDBC, 오라클 JDBC 드라이버, 아파치 DBCP 그리고 MyBatis 라이브러리를 함께 추가한다.

pom.xml
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-jdbc</artifactId>
  <version>${spring.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc6 -->
<dependency>
  <groupId>com.oracle.database.jdbc</groupId>
  <artifactId>ojdbc6</artifactId>
  <version>11.2.0.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-dbcp/commons-dbcp -->
<dependency>
  <groupId>commons-dbcp</groupId>
  <artifactId>commons-dbcp</artifactId>
  <version>1.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.5.11</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
  <version>2.1.0</version>
</dependency>

commons-dbcp는 아파치에서 제공하는 데이터베이스 커넥션 풀이다.
ojdbc6.jar는 JDBC 4 드라이버이므로 DBCP 1.4 버전을 추가했다.
참고: https://dlcdn.apache.org//commons/dbcp/

스프링 설정 파일에 데이터소스와 SqlSessionFactoryBean을 추가한다.

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
    destroy-method="close">
  <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
  <property name="url" value="jdbc:oracle:thin:@localhost:1521:XE" />
  <property name="username" value="scott" />
  <property name="password" value="tiger" />
  <property name="maxActive" value="100" />
  <property name="maxWait" value="1000" />
  <property name="poolPreparedStatements" value="true" />
  <property name="defaultAutoCommit" value="true" />
  <property name="validationQuery" value=" SELECT 1 FROM DUAL" />
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
</bean>

commons-dbcp2를 사용한다면, maxActive와 maxWait 파라미터는 maxTotal과 maxWaitMillis로 바꿔야 한다.

자바 빈즈
src/main/java/net/java_school/english/WordCard.java

WordCard.java
package net.java_school.english;

public class WordCard {
  private String word;
  private String definitions;

  public WordCard() {}

  public WordCard(String word, String definitions) {
    this.word = word;
    this.definitions = definitions;
  }

  public int getWord() {
    return word;
  }

  public void setWord(String word) {
    this.word = word;
  }

  public String getDefinitions() {
    return definitions;
  }

  public void setDefinitions(String definitions) {
    this.definitions = definitions;
  }
}

매퍼 인터페이스
매퍼 인터페이스를 다른 스프링 컴포넌트와 구별되는 패키지로 만드는 게 좋다.

src/main/java/net/java_school/mybatis/WordMapper.java

WordMapper.java
package net.java_school.mybatis;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;

public interface WordMapper {
  @Insert("INSERT INTO wordcard VALUES (#{word}, #{definitions})")
  public void insert(@Param("word") String word, @Param("definitions") String definitions);

  @Select("select * from (select * from wordcard order by dbms_random.value) where rownum = 1")
  public WordCard selectOne();
}

매퍼 인터페이스를 MapperFactoryBean을 사용해 스프링에 추가한다.

<bean id="wordMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
  <property name="mapperInterface" value="net.java_school.mybatis.WordMapper" />
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

서비스

WordCardService 클래스를 인터페이스로 바꾼다.
src/main/java/net/java_school/english/WordCardService.java

WordCardService.java
package net.java_school.english;

public interface WordCardService {
  public void add(String word, String definitions);
  public WordCard getWordCard();
}

src/main/java/net/java_school/english/WordCardServiceImpl.java

WordCardServiceImpl.java
package net.java_school.english;

import net.java_school.mybatis.WordMapper;

import org.springframework.stereotype.Service;

@Service
public class WordCardServiceImpl implements WordCardService {

  private WordMapper wordMapper;

  public WordCardServiceImpl(WordMapper wordMapper) {
    this.wordMapper = wordMapper;
  }

  @Override
  public void add(String content) {
    wordMapper.insert(content);
  }

  @Override
  public WordCard getWordCard() {
    return wordMapper.selectOne(); 
}
<bean id="wordCardService" class="net.java_school.english.WordCardServiceImpl">
  <constructor-arg ref="wordMapper" />
</bean>

컨트롤러

src/main/java/net/java_school/english/HomeController.java

HomeController.java
package net.java_school.english;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class HomeController {

  private WordCardService wordCardService;

  public HomeController(WordCardService wordCardService) {
    this.wordCardService = wordCardService;
  }
  
  @GetMapping("/")
  public String homepage(Model model) {
    WordCard wordCard = wordCardService.getWordCard();
    model.addAttribute("wordCard", wordCard);
    return "index";
  }

  @PostMapping("/")
  public String add(String word, String definitions) {
    wordCardService.add(word, definitions);	
    return "redirect:/";
  }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc.xsd">

<mvc:resources mapping="/resources/**" location="/resources/" />

<mvc:annotation-driven />

<bean id="internalResourceViewResolver" 
    class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
  <property name="prefix" value="/WEB-INF/views/" />
  <property name="suffix" value=".jsp" />
</bean>

<!-- <bean id="wordCardDao" class="net.java_school.english.WordCardDao" /> -->

<bean id="wordMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
  <property name="mapperInterface" value="net.java_school.mybatis.WordMapper" />
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

<bean id="wordCardService" class="net.java_school.english.WordCardServiceImpl">
  <constructor-arg ref="wordMapper" />
</bean>

<bean id="homeController" class="net.java_school.english.HomeController">
  <constructor-arg ref="wordCardService" />
</bean>

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
    destroy-method="close">
  <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
  <property name="url" value="jdbc:oracle:thin:@localhost:1521:XE" />
  <property name="username" value="scott" />
  <property name="password" value="tiger" />
  <property name="maxActive" value="100" />
  <property name="maxWait" value="1000" />
  <property name="poolPreparedStatements" value="true" />
  <property name="defaultAutoCommit" value="true" />
  <property name="validationQuery" value=" SELECT 1 FROM DUAL" />
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
</bean>

</beans>

빌드와 배치
mvn clean compile war:inplace
테스트
http://localhost:8080

로깅

참고: https://mybatis.org/mybatis-3/ko/logging.html

아파치 commons-logging과 log4j 2 라이브러리를 의존성에 추가한다.

<!-- Logging -->                
<!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
<dependency>
  <groupId>commons-logging</groupId>
  <artifactId>commons-logging</artifactId>
  <version>1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-api</artifactId>
  <version>2.19.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-core</artifactId>
  <version>2.19.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-jcl -->
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-jcl</artifactId>
  <version>2.19.0</version>
</dependency>

log4j2.xml라는 이름으로 log4j 2 설정 파일을 src/main/resources/ 디렉터리에 생성한다.

log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration>
<Configuration>
  <Appenders>
    <File name="WordCard" fileName="WordCard.log" append="false">
      <PatternLayout pattern="%t %-5p %c{2} - %m%n" />
    </File>
    <Console name="STDOUT" target="SYSTEM_OUT">
      <PatternLayout pattern="%d %-5p [%t] %C{2} (%F:%L) - %m%n" />
    </Console>
  </Appenders>
  <Loggers>
    <Logger name="net.java_school" level="DEBUG">
      <AppenderRef ref="WordCard" />
    </Logger>
    <Root level="INFO">
      <AppenderRef ref="STDOUT" />
    </Root>
  </Loggers>
</Configuration>

mvn clean compile war:inplace 실행해 컴파일하고 로그 메시지를 확인한다.

매퍼 XML 파일
여러 줄로 구성된 복잡한 쿼리는 XML 파일로 따로 분리하면 유지 보수에 좋다.
이 파일이 매퍼 XML 파일이다.

지금까지 마이바티스 설정 파일을 만들지 않았다.
매퍼 XML 파일과 매퍼 인터페이스를 같은 클래스패스에 둔다면, 마이바티스 설정 파일이 필요 없다.
참고: https://mybatis.org/spring/ko/mappers.html#register

WordMapper 인터페이스와 같은 클래스패스에 파일이 위치하도록, src/main/resources/net/java_school/mybatis 디렉터리에, WordMapper.xml 매퍼 파일을 생성한다.

src/main/resources/net/java_school/mybatis/WordMapper.xml

WordMapper.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="net.java_school.mybatis.WordMapper">

  <insert id="insert">
    insert into wordcard values (#{word}, #{definitions})
  </insert>

  <select id="selectOne" resultType="net.java_school.english.WordCard">
    select *
    from
    (select * from wordcard order by dbms_random.value)
    where rownum = 1
  </select>
	
</mapper>

마이바티스 설정 파일에서 typeAlias 요소를 사용하면 매퍼 XML 파일에서 resultType="net.java_school.english.WordCard"를 resultType="WordCard"처럼 간단히 줄일 수 있다.

<-- XML 설정 파일에서 -->
<typeAlias type="net.java_school.english.WordCard" alias="WordCard"/>

하지만 지금껏 생략했던 마이바티스 설정 파일을 추가해야 한다.
참고: https://mybatis.org/mybatis-3/ko/sqlmap-xml.html
자바 빈즈가 하나뿐이니, 마이바티스 설정 파일을 만들지 않고 진행한다.

WordMapper 인터페이스에서 쿼리를 제거한다.

WordMapper.java
package net.java_school.mybatis;

import net.java_school.english.WordCard;

import org.apache.ibatis.annotations.Param;

public interface WordMapper {
  public void insert(@Param("word") String word, @Param("definitions") String definitions);
  public WordCard selectOne();
}

스프링 자동 스캔

스프링의 자동 스캔 기능을 사용하면 설정 파일 내용을 줄일 수 있다.
컨트롤러와 서비스는 <context:component-scan ... />로 스캔할 수 있다.
매퍼 인스턴스는 마이바티스가 생성하는 것이기에 위 설정으로 스캔할 수 없다.
<mybatis:scan ... />로 매퍼 인스턴스를 스캔한다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    http://mybatis.org/schema/mybatis-spring
    http://mybatis.org/schema/mybatis-spring.xsd">

<mvc:resources mapping="/resources/**" location="/resources/" />

<mvc:annotation-driven />

<context:component-scan base-package="net.java_school.english" />

<mybatis:scan base-package="net.java_school.mybatis" />

<bean id="internalResourceViewResolver" 
    class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
  <property name="prefix" value="/WEB-INF/views/" />
  <property name="suffix" value=".jsp" />
</bean>

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
    destroy-method="close">
  <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
  <property name="url" value="jdbc:oracle:thin:@localhost:1521:XE" />
  <property name="username" value="java" />
  <property name="password" value="school" />
  <property name="maxActive" value="100" />
  <property name="maxWait" value="1000" />
  <property name="poolPreparedStatements" value="true" />
  <property name="defaultAutoCommit" value="true" />
  <property name="validationQuery" value=" SELECT 1 FROM DUAL" />
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
</bean>

</beans>

<context:component-scan />를 사용하면 @Autowired 어노테이션도 함께 사용할 수 있다.

HomeController.java
import org.springframework.beans.factory.annotation.Autowired;

@Controller
public class HomeController {

  @Autowired
  private WordCardService wordCardService;

  /* 생성자 제거
  public HomeController(WordCardService wordCardService) {
    this.wordCardSerivce = wordCardService;
  }
  */

  //..omit..
}
WordCardServiceImpl.java
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;

@Service
public class WordCardServiceImpl implements WordCardService {

  @Autowired
  private WordMapper wordMapper;

  /* 생성자 제거
  public WordCardServiceImpl(WordMapper wordMapper) {
    this.wordMapper = wordMapper;
  }
  */

  //..omit..
}

자바 기반 스프링 설정

다음 자바 파일을 생성한다.

AppConfig.java
package net.java_school.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class AppConfig implements WebMvcConfigurer {
  //TODO
}

아래를 참고해 스프링 자바 설정 파일을 완성한다.

<mvc:annotation-driven />

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
<mvc:resources mapping="/resources/**"
    location="/resources/"
    cache-period="31556926" />

@Configuration
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {
  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/resources/**")
            .addResourceLocations("/resources/")
            .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
  }
}
<context:component-scan 
    base-package="net.java_school.english" />

@Configuration
@EnableWebMvc
@ComponentScan("net.java_school.english")
public class AppConfig implements WebMvcConfigurer {
<mybatis:scan base-package="net.java_school.mybatis" />

@Configuration
@EnableWebMvc
@ComponentScan("net.java_school.english")
@MapperScan("net.java_school.mybatis")
public class AppConfig implements WebMvcConfigurer {
<bean id="internalResourceViewResolver" 
      class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="prefix" value="/WEB-INF/views/" />
  <property name="suffix" value=".jsp" />
  <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
</bean>

@Bean
public ViewResolver configureViewResolver() {
  InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
  viewResolver.setPrefix("/WEB-INF/views/");
  viewResolver.setSuffix(".jsp");
  viewResolver.setViewClass(JstlView.class);
  return viewResolver;
} 
<bean id="dataSource"
    class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
  <property name="driverClassName"
    value="oracle.jdbc.driver.OracleDriver" />
  <property name="url"
    value="jdbc:oracle:thin:@localhost:1521:XE" />
  <property name="username" value="scott" />
  <property name="password" value="tiger" />
  <property name="maxActive" value="100" />
  <property name="maxWait" value="1000" />
  <property name="poolPreparedStatements" value="true" />
  <property name="defaultAutoCommit" value="true" />
  <property name="validationQuery" value=" SELECT 1 FROM DUAL" />
</bean>

<bean id="sqlSessionFactory"
    class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
</bean>

@Bean(destroyMethod = "close")
public DataSource dataSource() {
  BasicDataSource dataSource = new BasicDataSource();
  dataSource.setDriverClassName("oracle.jdbc.driver.OracleDriver");
  dataSource.setUrl("jdbc:oracle:thin:@localhost:1521:XE");
  dataSource.setUsername("scott");
  dataSource.setPassword("tiger");
  dataSource.setMaxActive(100);
  dataSource.setMaxWait(1000);
  dataSource.setPoolPreparedStatements(true);
  dataSource.setDefaultAutoCommit(true);
  dataSource.setValidationQuery("SELECT 1 FROM DUAL");
  return dataSource;
}

@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
  SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
  sessionFactory.setDataSource(dataSource());
  return sessionFactory.getObject();
}
AppConfig.java
package net.java_school.config;

import java.time.Duration;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSource;
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 org.springframework.http.CacheControl;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

@Configuration
@EnableWebMvc
@ComponentScan("net.java_school.english")
@MapperScan("net.java_school.mybatis")
public class AppConfig implements WebMvcConfigurer {

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/resources/**")
            .addResourceLocations("/resources/")
            .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
  }

  @Bean
  public ViewResolver configureViewResolver() {
    InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
    viewResolver.setPrefix("/WEB-INF/views/");
    viewResolver.setSuffix(".jsp");
    viewResolver.setViewClass(JstlView.class);
    return viewResolver;
  }

  @Bean(destroyMethod = "close")
  public DataSource dataSource() {
    BasicDataSource dataSource = new BasicDataSource();
    dataSource.setDriverClassName("oracle.jdbc.driver.OracleDriver");
    dataSource.setUrl("jdbc:oracle:thin:@localhost:1521:XE");
    dataSource.setUsername("scott");
    dataSource.setPassword("tiger");
    dataSource.setMaxActive(100);
    dataSource.setMaxWait(1000);
    dataSource.setPoolPreparedStatements(true);
    dataSource.setDefaultAutoCommit(true);
    dataSource.setValidationQuery("SELECT 1 FROM DUAL");

    return dataSource;
  }

  @Bean
  public SqlSessionFactory sqlSessionFactory() throws Exception {
    SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
    sessionFactory.setDataSource(dataSource());
    return sessionFactory.getObject();
  }
}

web.xml 파일을 대신할 자바 설정 파일을 생성한다.

MyWebAppInitializer.java
package net.java_school.config;

import javax.servlet.Filter;

import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return null;
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] { AppConfig.class };
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] { "/" };
  }

  @Override
  protected Filter[] getServletFilters() {
    return new Filter[] { new HiddenHttpMethodFilter() };
  }
}

다음처럼 설정해도 된다.
참고: https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-servlet-context-hierarchy

package net.java_school.config;

import javax.servlet.Filter;

import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new Class<?>[] { AppConfig.class };
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return null;
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] { "/" };
  }

  @Override
  protected Filter[] getServletFilters() {
    return new Filter[] { new HiddenHttpMethodFilter() };
  }
}

에러 페이지 매핑은 자바 설정으로 할 수 없기에, 추후 에러 페이지 매핑을 위해, web.xml 파일을 최소한의 내용으로 남겨둔다.
참고: https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-customer-servlet-container-error-page

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                      http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
  version="4.0">

</web-app>
참고