메이븐으로 스프링 MVC 개발
아래 글은 워크스페이스를 C:\www라고 가정합니다.
아키타입 생성
원형이란 사전적 의미를 가진 아키타입은, 자바에선 프로젝트 프로토타입을 뜻한다.
C:\ Command PromptC:\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>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> <!-- Servlet JSP --> <!-- 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>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="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>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이 해석되지 않는 경우