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 8: 1.5 Choose a number: 8: ↵ 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>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
    <spring.version>6.2.8</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/jakarta.servlet/jakarta.servlet-api -->
    <dependency>
      <groupId>jakarta.servlet</groupId>
      <artifactId>jakarta.servlet-api</artifactId>
      <version>6.1.0</version>
      <scope>provided</scope>
    </dependency>       
    <!-- https://mvnrepository.com/artifact/jakarta.servlet.jsp.jstl/jakarta.servlet.jsp.jstl-api -->
    <dependency>
      <groupId>jakarta.servlet.jsp.jstl</groupId>
      <artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
      <version>3.0.2</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.glassfish.web/jakarta.servlet.jsp.jstl -->
    <dependency>
      <groupId>org.glassfish.web</groupId>
      <artifactId>jakarta.servlet.jsp.jstl</artifactId>
      <version>3.0.1</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>
        <!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-maven-plugin -->
        <plugin>
          <groupId>org.eclipse.jetty</groupId>
          <artifactId>jetty-maven-plugin</artifactId>
          <version>11.0.25</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="https://jakarta.ee/xml/ns/jakartaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                      https://jakarta.ee/xml/ns/jakartaee/web-app_6_1.xsd"
  version="6.1"
  metadata-complete="true">
  <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" />
테스트
루트 디렉터리에서 다음을 실행한다.
mvn clean jetty:run
http://localhost:8080을 요청한다.
컨트롤러가 어떻게 동작하는지 확인했다.
다음은 컨트롤러가 뷰에 데이터를 어떻게 세팅하는지 알아보자.
예제를 간단하게 유지하기 위해 데이터베이스는 사용하지 않겠다.
자바 빈즈
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
데이터베이스 다루기
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/ojdbc11 -->
<dependency>
  <groupId>com.oracle.database.jdbc</groupId>
  <artifactId>ojdbc11</artifactId>
  <version>23.7.0.25.01</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-dbcp2 -->
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-dbcp2</artifactId>
  <version>2.13.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.5.19</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
  <version>3.0.4</version>
</dependency>
스프링 설정 파일에 데이터소스와 SqlSessionFactoryBean을 추가한다.
<bean id="dataSource" class="org.apache.commons.dbcp2.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="maxTotal" value="100" />
  <property name="maxWaitMillis" 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>
자바 빈즈
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;
import org.springframework.web.bind.annotation.RequestParam;
@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(@RequestParam(name="word") String word, @RequestParam(name="definitions") 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.dbcp2.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="maxTotal" value="100" />
  <property name="maxWaitMillis" 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 jetty:run
로깅
참고: https://mybatis.org/mybatis-3/ko/logging.html
log4j 2 라이브러리를 의존성에 추가한다.
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.24.3</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.24.3</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.24.3</version> </dependency>
log4j2.xml라는 이름으로 log4j 2 설정 파일을 src/main/resources/ 디렉터리에 생성한다.
log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration>
<Configuration xmlns="http://logging.apache.org/log4j/2.0/config">
  <Appenders>
    <File name="MyBatisSpring" fileName="MyBatisSpring.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="MyBatisSpring" />
    </Logger>
    <Root level="INFO">
      <AppenderRef ref="STDOUT" />
    </Root>
  </Loggers>
</Configuration>
mvn clean jetty:run 실행해 컴파일하고 로그 메시지를 확인한다.
매퍼 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.dbcp2.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="maxTotal" value="100" />
  <property name="maxWaitMillis" 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.dbcp2.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="maxTotal" value="100" />
  <property name="maxWaitMillis" 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.setMaxTotal(100);
  dataSource.setMaxWaitMillis(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.setMaxTotal(100);
    dataSource.setMaxWaitMillis(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 jakarta.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() };
  }
}
화면이 나오면, 데이터베이스 설계를 시작으로, 자바 빈즈, DAO, 서비스, 컨트롤러 순으로 구현하는 게 일반적이지만, 대부분을 생략하고 컨트롤러와 뷰만으로 동작하는 예제를 만들어 보자.
컨트롤러
다음처럼 설정해도 된다.
참고: https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-servlet-context-hierarchy
package net.java_school.config;
import jakarta.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="https://jakarta.ee/xml/ns/jakartaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                      https://jakarta.ee/xml/ns/jakartaee/web-app_6_1.xsd"
  version="6.1"
  metadata-complete="true">
</web-app>
https://github.com/kimjonghoon/mybatisspring
참고