메이븐으로 스프링 MVC 개발
아래 글은 워크스페이스를 C:\www라고 가정합니다.
아키타입 생성
원형이란 사전적 의미를 가진 아키타입은, 자바에선 프로젝트 프로토타입을 뜻한다.
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': spring-bbs Define value for property 'version' 1.0-SNAPSHOT: : ↵ Define value for property 'package' net.java_school: : ↵ Confirm properties configuration: groupId: net.java_school artifactId: spring-bbs version: 1.0-SNAPSHOT package: net.java_school Y: : ↵
빌드가 완료되면 C:\www에 spring-bbs라는 폴더가 생긴다. C:\www\spring-bbs가 프로젝트 루트 디렉터리이다.
Spring MVC 테스트
pom.xml 수정
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>spring-bbs</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>spring-bbs 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>spring-bbs</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>
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>Spring BBS</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>spring-bbs</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring-bbs</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>-1</session-timeout>
</session-config>
<error-page>
<error-code>404</error-code>
<location>/WEB-INF/views/404.jsp</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/WEB-INF/views/500.jsp</location>
</error-page>
</web-app>
모든 요청에 대해 setCharacterEncoding("UTF-8");를 호출하는 필터가 작동하도록 설정했다. 비영어권 웹 사이트에선 이 필터가 필요하다. 이 필터는 다른 필터보다 먼저 작동하도록 선언한다.
web.xml에서 DispatcherServlet의 설정 파일을 /WEB-INF/spring/mvc.xml이라 정했다. /WEB-INF/spring/ 폴더에 mvc.xml 파일을 아래 내용으로 생성한다.
mvc.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: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/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" />
<style>
@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;
}
</style>
</head>
<body>
<div id="wordcard">
<h1>Vocabulary</h1>
<p>
어휘
</p>
</div>
</body>
</html>
영어 단어와 그 의미를 보여주는 화면이다.
화면이 나오면, 데이터베이스 설계를 시작으로, 자바 빈즈, DAO, 서비스, 컨트롤러 순으로 구현하는 게 일반적이지만, 대부분을 생략하고 컨트롤러와 뷰만으로 동작하는 예제를 만들어 보자.
컨트롤러
src/main/java/net/java_school/english/HomeController.java
src/main/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 index() {
return "index";
}
}
@Controller, @GetMapping은 스프링 어노테이션로, 스프링에 정보를 전달하기 위해 사용된다.
클래스 선언부 위에 위치한 @Controller는 해당 자바 빈이 컨트롤러임을 스프링에게 알린다.
컨트롤러의 메소드 위에 위치하는 @GetMapping("/")은 이 메소드가 GET 방식의 "/" 요청--ROOT 애플리케이션이라면 http://localhost:8080/--을 처리함을 나타낸다.
컨트롤러를 통해 홈페이지("/")가 서비스되도록 하려면 홈페이지 경로로 매핑된 다른 서블릿이 없어야 한다.
하지만 아키타입은 src/main/webapp 디렉터리에 index.jsp 파일을 가지고 있다. 그냥 두면 홈페이지 요청을 이 파일이 응답하게 된다.
src/main/webapp/index.jsp 파일을 삭제한다.
루트 애플리케이션의 홈페이지 요청 시나리오
사용자가 홈페이지("/")를 요청한다.
서블릿 컨테이너는 요청을 DispatcherServlet에 전달한다.
DispatcherServlet은 컨트롤러의 홈페이지 경로로 매핑된 메소드에 요청을 전달한다.
메소드는 홈페이지에 보일 데이터를 요청에 세팅하고--이 부분은 첫 번째 테스트에선 생략하고, 두 번째 테스트에서 다룬다--, "index" 문자열을 반환한다.
DispatcherServlet은 뷰 리졸버가 해석한 /WEB-INF/views/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/spring-bbs/src/main/webapp" reloadable="true"> </Context>
WEB-INF 바로 위 디렉터리가 도큐먼트베이스다.
docBase 값으로 메이븐 프로젝트 루트 디렉터리(C:/www/spring-bbs)를 추가하지 않도록 주의한다.
빌드와 배치
메이븐 루트 디렉터리에서 다음을 실행한다.
mvn compile war:inplace
war:inplace 옵션은 src/main/WEB-INF/classes에 바이트코드를, src/main/WEB-INF/lib에 의존 라이브러리를 생성시킨다.
테스트
톰캣을 시작한다.
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 index(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 compile war:inplace
http://localhost:8080
MyBatis-Spring 연동 모듈
아직 데이터베이스를 다루지 않았다.
모델 2 게시판은 JDBC를 그대로 사용했다. JDBC를 그대로 사용하는 코드는 반복되는 코드가 많아 생산성이 떨어진다.
퍼시스턴스 프레임워크--내부 소스에서 JDBC 사용--를 사용하면 사용자가 작성해야 할 코드의 양을 줄여준다.
MyBatis는 SQL 매핑 퍼시스턴스 프레임워크이다.
MyBatis-Spring는 스프링에서 마이바티스를 편리하게 사용하기 위한 연동 모듈이다.
spring-bbs 프로젝트에 MyBatis-Spring을 사용할 것이다.
MyBatis-Spring를 파악하기 위한 간단한 예제를 준비했다.
https://mybatis.org/spring/ko/getting-started.html
아래 내용을 공식 사이트의 시작하기와 순서를 같게 했다.
JAVA 계정에 접속해 다음 테이블과 시퀀스를 생성한다.
create table photo ( no number, content varchar2(4000) not null, constraint pk_photo primary key(no), constraint uq_photo unique(content) ); create sequence seq_photo increment by 1 start with 1;
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을 추가한다.
<?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: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="wordCardService" class="net.java_school.english.WordCardService">
<constructor-arg ref="wordCardDao" />
</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="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>
commons-dbcp2를 사용한다면, maxActive와 maxWait 파라미터는 maxTotal과 maxWaitMillis로 바꿔야 한다.
자바 빈즈
src/main/java/net/java_school/photostudio/Photo.java
Photo.java
package net.java_school.photostudio;
public class Photo {
private int no;
private String content;
public Photo() {}
public Photo(int no, String content) {
this.no = no;
this.content = content;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
매퍼 인터페이스
매퍼 인터페이스를 다른 스프링 컴포넌트와 구별되는 패키지로 만드는 게 좋다.
src/main/java/net/java_school/mybatis/PhotoMapper.java
PhotoMapper.java
package net.java_school.mybatis;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Insert;
public interface PhotoMapper {
@Insert("INSERT INTO photo VALUES (seq_photo.nextval, #{content})")
public void insert(@Param("content") String content);
}
매퍼 인터페이스를 MapperFactoryBean을 사용해 스프링에 추가한다.
<bean id="photoMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="mapperInterface" value="net.java_school.mybatis.PhotoMapper" /> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean>
서비스
서비스 인터페이스와 서비스 구현 클래스를 작성한다.
src/main/java/net/java_school/photostudio/PhotoService.java
PhotoService.java
package net.java_school.photostudio;
public interface PhotoService {
public void add(String content);
}
src/main/java/net/java_school/photostudio/PhotoServiceImpl.java
PhotoServiceImpl.java
package net.java_school.photostudio;
import net.java_school.mybatis.PhotoMapper;
import org.springframework.stereotype.Service;
@Service
public class PhotoServiceImpl implements PhotoService {
private PhotoMapper photoMapper;
public PhotoServiceImpl(PhotoMapper photoMapper) {
this.photoMapper = photoMapper;
}
@Override
public void add(String content) {
photoMapper.insert(content);
}
}
서비스를 스프링에 추가한다.
<bean id="photoService" class="net.java_school.photostudio.PhotoServiceImpl"> <constructor-arg ref="photoMapper" /> </bean>
컨트롤러
클래스 선언 위에 @RequestMapping("photo") 어노테이션을 두어, ContextPath 다음에 photo가 붙는 요청을 담당하는 컨트롤러를 작성한다.
src/main/java/net/java_school/photostudio/PhotoController.java
PhotoController.java
package net.java_school.photostudio;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
@RequestMapping("photo")
public class PhotoController {
private PhotoService photoService;
public PhotoController(PhotoService photoService) {
this.photoService = photoService;
}
@GetMapping
public String index() {
return "photo/index";
}
@PostMapping
public String add(String content) {
photoService.add(content);
return "redirect:/photo/?page=1";
}
}
컨트롤러를 스프링에 추가한다.
<bean id="photoController" class="net.java_school.photostudio.PhotoController"> <constructor-arg ref="photoService" /> </bean>
뷰
src/main/webapp/WEB-INF/views/photo/index.jsp
index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>mybatis-spring Test</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="Keywords" content="MyBatis Spring Test" />
<meta name="Description" content="This is web app for mybatis-spring Test" />
<style>
html, body {
margin: 0;
padding: 0;
background-color: #FFF;
font-family: "Liberation Sans", Helvetica, sans-serif;
}
</style>
</head>
<body>
<form id="addForm" method="post">
<input type="text" name="content" />
<input id="submit" type="submit" value="Send" />
</form>
</body>
</html>
테스트
mvn compile war:inplace
http://localhost:8080/photo
웹 상의 이미지 링크를 텍스트필드에 붙여 넣고 전송
SQL*PLUS로 인서트가 되었는지 확인
로깅
아래 내용은 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/ 디렉터리에 생성한다.
src/main/resources 디렉터리는 메이븐 기본 디렉터리이니 없으면 생성한다.
log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration>
<Configuration>
<Appenders>
<File name="A1" fileName="A1.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="A1" />
</Logger>
<Root level="INFO">
<AppenderRef ref="STDOUT" />
</Root>
</Loggers>
</Configuration>
mvn compile war:inplace 실행해 컴파일하고 로그 메시지를 확인한다.
http-nio-8080-exec-14 DEBUG PhotoMapper.insert - ==> Preparing: INSERT INTO photo VALUES (seq_photo.nextval, ? http-nio-8080-exec-14 DEBUG PhotoMapper.insert - ==> Parameters: https://cdn.pixabay.com/house_720.jpg(String)
insert 구문에 오른쪽 괄호가 빠져있다.
괄호를 추가하고 다시 컴파일한다.
로그 메시지를 확인한다.
http-nio-8080-exec-26 DEBUG PhotoMapper.insert - ==> Preparing: INSERT INTO photo VALUES (seq_photo.nextval, ?) http-nio-8080-exec-26 DEBUG PhotoMapper.insert - ==> Parameters: https://cdn.pixabay.com/house_720.jpg(String) http-nio-8080-exec-26 DEBUG PhotoMapper.insert - <== Updates: 1
XML 매퍼
여러 줄로 구성된 복잡한 쿼리는 XML 파일로 따로 분리하면 유지 보수에 좋다.
이 파일이 XML 매퍼 파일이다.
지금까지 마이바티스 설정 파일을 만들지 않았다.
XML 매퍼 파일과 매퍼 인터페이스를 같은 클래스패스에 둔다면, 마이바티스 설정 파일이 필요 없다.
참조: https://mybatis.org/spring/ko/mappers.html#register
PhotoMapper 인터페이스와 같은 클래스패스에 파일이 위치하도록, src/main/resources/net/java_school/mybatis 디렉터리에, PhotoMapper.xml 매퍼 파일을 생성한다.
src/main/resources/net/java_school/mybatis/PhotoMapper.xml
PhotoMapper.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.PhotoMapper">
<insert id="insert">
INSERT INTO photo VALUES (seq_photo.nextval, #{content})
</insert>
</mapper>
PhotoMapper 인터페이스에서 쿼리를 제거한다.
PhotoMapper.java
package net.java_school.mybatis;
import org.apache.ibatis.annotations.Param;
public interface PhotoMapper {
public void insert(@Param("content") String content);
}
저장된 이미지 보이기
저장된 이미지를 4개씩 묶어 뷰에 보여주려 한다.
그룹별로 레코드를 가져오는 여러 줄로 구성된 쿼리를 사용해야 한다.
index.jsp 파일의 body 엘리먼트 내용을 아래처럼 수정한다.
index.jsp
<div id="photos">
<img width="640" alt="p_1" src="https://cdn.pixabay.com/photo/2022/10/09/02/16/haunted-house-7508035_960_720.jpg" />
<img width="640" alt="p_2" src="https://cdn.pixabay.com/photo/2022/10/09/02/16/haunted-house-7508035_960_720.jpg" />
<img width="640" alt="p_3" src="https://cdn.pixabay.com/photo/2022/10/09/02/16/haunted-house-7508035_960_720.jpg" />
<img width="640" alt="p_4" src="https://cdn.pixabay.com/photo/2022/10/09/02/16/haunted-house-7508035_960_720.jpg" />
</div>
<div id="paging">
<a href="?page=10" title="10">◁ back</a>
<a href="?page=1" title="1">1</a>
<a href="?page=10" title="10">...</a>
<strong>11</strong>
<a href="?page=12" title="12">12</a>
<a href="?page=13" title="3">3</a>
<a href="?page=14" title="4">4</a>
<a href="?page=15" title="5">5</a>
<a href="?page=16" title="6">6</a>
<a href="?page=17" title="7">7</a>
<a href="?page=18" title="8">8</a>
<a href="?page=19" title="9">9</a>
<a href="?page=20" title="10">10</a>
<a href="?page=21" title="11">...</a>
<a href="?page=407" title="407">407</a>
<a href="?page=12" title="12">next▷ </a>
<form id="addForm" method="post">
<input type="text" name="content" width="500" />
<input id="submit" type="submit" value="Send" />
</form>
</div>
index.jsp 파일의 style 엘리먼트에 다음을 추가한다.
#paging {
width: 640px;
float: left;
font-size: 1em;
}
a:link {
color: #2C80D0;
text-decoration: none;
}
a:visited {
color: #2C80D0;
text-decoration: none;
}
a:active {
color: #2C80D0;
text-decoration: none;
}
a:hover {
color: #2C80D0;
text-decoration: underline;
}
PhotoMapper 인터페이스에 총 레코드 수와 페이지에 보여줄 아이템을 가져오는 메소드를 추가한다.
PhotoMapper.java
package net.java_school.mybatis;
import net.java_school.photostudio.Photo;
import java.util.HashMap;
import java.util.List;
import org.apache.ibatis.annotations.Param;
public interface PhotoMapper {
public void insert(@Param("content") String content);
public int selectCountOfPhotos();//총 레코드 수
public List<Photo> selectPhotos(HashMap<String, String> hashmap);//뷰에 보여줄 아이템
}
매퍼 인터페이스에 추가한 메소드 이름을 id 값으로 갖는 엘리먼트를 XML 매퍼 파일에 추가한다.
PhotoMapper.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.PhotoMapper">
<insert id="insert">
NSERT INTO photo VALUES (seq_photo.nextval, #{content})
</insert>
<select id="selectCountOfPhotos" resultType="int">
SELECT count(*) FROM photo
</select>
<select id="selectPhotos" parameterType="hashmap" resultType="net.java_school.photostudio.Photo">
SELECT no,content
FROM (
SELECT rownum R,A.*
FROM (
SELECT
no,content
FROM
photo
ORDER BY no DESC
) A
)
WHERE R BETWEEN #{start} AND #{end}
</select>
</mapper>
마이바티스 설정 파일에서 typeAlias 요소를 사용하면 XML 매퍼 파일에서 resultType="net.java_school.photostudio.Photo"를 resultType="Photo"처럼 간단히 줄일 수 있다.
<-- XML 설정 파일에서 --> <typeAlias type="net.java_school.photostudio.Photo" alias="Photo"/>
하지만 지금껏 생략했던 마이바티스 설정 파일을 추가해야 한다.
참고: https://mybatis.org/mybatis-3/ko/sqlmap-xml.html
자바 빈즈가 하나뿐이니, 마이바티스 설정 파일을 만들지 않고 진행한다.
서비스 수정
총 레코드 수와 페이지에 보일 레코드를 구하는 메소드를 서비스에 추가한다.
PhotoService.java
package net.java_school.photostudio;
import java.util.List;
public interface PhotoService {
public void add(String content);
public int getTotalRecordCount();
public List<Photo> getPhotos(Integer startRecord, Integer endRecord);
}
PhotoServiceImpl.java
package net.java_school.photostudio;
import java.util.List;
import java.util.HashMap;
import net.java_school.mybatis.PhotoMapper;
public class PhotoServiceImpl implements PhotoService {
private PhotoMapper photoMapper;
public PhotoServiceImpl(PhotoMapper photoMapper) {
this.photoMapper = photoMapper;
}
@Override
public void add(String content) {
photoMapper.insert(content);
}
@Override
public int getTotalRecordCount() {
return photoMapper.selectCountOfPhotos();
}
@Override
public List<Photo> getPhotos(Integer startRecord, Integer endRecord) {
HashMap<String, String> hashmap = new HashMap<String, String>();
hashmap.put("start", startRecord.toString());
hashmap.put("end", endRecord.toString());
return photoMapper.selectPhotos(hashmap);
}
}
컨트롤러 수정
컨트롤러에 총 레코드 수와 페이지에 보일 레코드를 구하는 메소드를 추가한다.
페이지 분할 기능에 필요한 숫자를 구하는 기능을 분리하여 메소드에 구현했다.
PhotoController.java
package net.java_school.photostudio;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
@Controller
@RequestMapping("/photo")
public class PhotoController {
private PhotoService photoService;
public PhotoController(PhotoService photoService) {
this.photoService = photoService;
}
@PostMapping
public String add(String content) {
photoService.add(content);
return "redirect:/photo/?page=1";
}
private Map<String, Integer> getNumbersForPaging(int totalRecordCount, int page, int recordsPerPage, int pagesPerBlock) {
Map<String, Integer> map = new HashMap<String, Integer>();
int totalPageCount = totalRecordCount / recordsPerPage;
if (totalRecordCount % recordsPerPage != 0) totalPageCount++;
int totalBlockCount = totalPageCount / pagesPerBlock;
if (totalPageCount % pagesPerBlock != 0) totalBlockCount++;
int block = page / pagesPerBlock;
if (page % pagesPerBlock != 0) block++;
int firstPage = (block - 1) * pagesPerBlock + 1;
int lastPage = block * pagesPerBlock;
int prevBlock = 0;//previus block's last page
if (block > 1) prevBlock = firstPage - 1;
int nextBlock = 0;//next blcok's first page
if (block < totalBlockCount) nextBlock = lastPage + 1;
if (block >= totalBlockCount) lastPage = totalPageCount;
int listItemNo = totalRecordCount - (page - 1) * recordsPerPage;
int startRecord = (page - 1) * recordsPerPage + 1;
int endRecord = page * recordsPerPage;
map.put("finalPage", totalPageCount);
map.put("firstPage", firstPage);
map.put("lastPage", lastPage);
map.put("prevBlock", prevBlock);
map.put("nextBlock", nextBlock);
map.put("startRecord", startRecord);
map.put("endRecord", endRecord);
return map;
}
@GetMapping
public String index(Integer page, Model model) {
if (page == null) return "redirect:/photo/?page=1";
int recordsPerPage = 4;
int pagesPerBlock = 10;
int totalRecordCount = photoService.getTotalRecordCount();
Map<String, Integer> map = getNumbersForPaging(totalRecordCount, page, recordsPerPage, pagesPerBlock);
Integer startRecord = map.get("startRecord");
Integer endRecord = map.get("endRecord");
List<Photo> photos = photoService.getPhotos(startRecord, endRecord);
Integer prevBlock = map.get("prevBlock");
Integer nextBlock = map.get("nextBlock");
Integer firstPage = map.get("firstPage");
Integer lastPage = map.get("lastPage");
Integer finalPage = map.get("finalPage");
model.addAttribute("photos", photos);
model.addAttribute("prevBlock", prevBlock);
model.addAttribute("nextBlock", nextBlock);
model.addAttribute("firstPage", firstPage);
model.addAttribute("lastPage", lastPage);
model.addAttribute("finalPage", finalPage);
return "photo/index";
}
}
뷰 수정
JSTL을 사용하기 위해선 index.jsp 파일에 다음을 추가한다.
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
JSTL 코드를 사용하여 전달된 데이터와 페이지 분할 기능에 필요한 숫자를 뷰에 세팅한다.
index.jsp
<div id="photos">
<c:forEach var="photo" items="${photos }" varStatus="status">
<img width="640" alt="p_${photo.no }" src="${photo.content }" />
</c:forEach>
</div>
<div id="paging">
<c:if test="${param.page > 1 }">
<a href="?page=${param.page - 1 }" title="${param.page - 1}">◁ Back</a>
</c:if>
<c:if test="${prevBlock > 0}">
<a href="?page=1" title="1">1</a>
<a href="?page=${prevBlock }" title="${prevBlock }">...</a>
</c:if>
<c:forEach var="i" begin="${firstPage }" end="${lastPage }" varStatus="status">
<c:choose>
<c:when test="${param.page == i}">
<strong>${i }</strong>
</c:when>
<c:otherwise>
<a href="?page=${i }" title="${i }">${i }</a>
</c:otherwise>
</c:choose>
</c:forEach>
<c:if test="${nextBlock > 0 }">
<a href="?page=${nextBlock }" title="${nextBlock }">...</a>
<a href="?page=${finalPage }" title="${finalPage }">${finalPage }</a>
</c:if>
<c:if test="${param.page < finalPage }">
<a href="?page=${param.page + 1 }" title="${param.page + 1 }">Next▷ </a>
</c:if>
<form id="addForm" method="post">
<input type="text" name="content" width="500" />
<input id="submit" type="submit" value="Send" />
</form>
</div>
스프링 자동 스캔
스프링의 자동 스캔 기능을 사용하면 설정 파일 내용을 줄일 수 있다.
컨트롤러와 서비스는 <context:component-scan ... />로 스캔할 수 있다.
매퍼 인스턴스는 마이바티스가 생성하는 것이기에 위 설정으로 스캔할 수 없다.
<mybatis:scan ... />로 매퍼 인스턴스를 스캔한다.
mvc.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"
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:annotation-driven />
<context:component-scan base-package="net.java_school.english,
net.java_school.photostudio" />
<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 어노테이션도 함께 사용할 수 있다.
PhotoController.java
import org.springframework.beans.factory.annotation.Autowired;
@Controller
public class PhotoController {
@Autowired
private PhotoService photoService;
/* 생성자 제거
public PhotoController(PhotoService photoService) {
this.photoService = photoService;
}
*/
//..omit..
}
PhotoServiceImpl.java
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
@Service
public class PhotoServiceImpl implements PhotoService {
@Autowired
private PhotoMapper photoMapper;
/* 생성자 제거
public PhotoServiceImpl(PhotoMapper photoMapper) {
this.photoMapper = photoMapper;
}
*/
//..omit..
}
자동 스캔 적용할 때는 스프링 컴포넌트는 클래스 선언 어노테이션을 생략해선 안 된다.
홈페이지 요청에 동작하는 WordCardDao, WordCardService, HomeController를 수정한다.
src/main/java/net/java_school/english/WordCardDao.java
WordCardDao.java
import org.springframework.stereotype.Repository;
@Repository
public class WordCardDao {
//..omit..
}
src/main/java/net/java_school/english/WordCardService.java
WordCardService.java
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
@Service
public class WordCardService {
@Autowired
private WordCardDao wordCardDao;
//..omit..
}
src/main/java/net/java_school/english/HomeController.java
HomeController.java
import org.springframework.beans.factory.annotation.Autowired;
@Controller
public class HomeController {
@Autowired
private WordCardService wordCardService;
//..omit..
}
이클립스 작업환경 구축
이클립스를 시작하고 워크스페이스를 C:\www로 선택한다.
Project Explorer 뷰에서 마우스 오른쪽 버튼을 사용하여 컨텍스트 메뉴를 보이게 한다.
Import를 사용하여 spring-bbs 프로젝트를 이클립스로 불러온다.


진행하면서 pom.xml 파일이 바뀌면 이클립스 설정과 동기화를 해주어야 한다.

- http://stackoverflow.com/questions/14004308/spring-autowiring-not-able-to-hit-my-dao-class-method
- http://static.springsource.org/spring/docs/current/spring-framework-reference/pdf/
- Guide to naming conventions on groupId, artifactId and version
- 4 ways to use the WAR Plugin
- 4 ways to use the WAR Plugin 한글 해설
- 스프링 웹 애플리케이션을 위한 pom.xml 참조
- 메이븐의 만들어준 web.xml 파일을 쓰면 EL이 해석되지 않는 경우
