스프링과 스프링 Web MVC
- 의존성 주입과 스프링
- 스프링의 시작
- ApplicationContext와 빈
- 인터페이스를 이용한 느슨한 결합
- 웹 프로젝트를 위한 스프링 준비
- MyBatis와 스프링 연동
- 스프링 Web MVC 기초
- 스프링 Web MVC 특징
- 파라미터 자동 수집과 변환
- 스프링 MVC 예외 처리
- 스프링 Web MVC 구현하기
- 프로젝트의 구현 목표와 준비
- 부트스트랩
- MyBatis와 스프링을 이용한 영속 처리
- Todo 기능 개발
- 페이징 처리를 위한 TodoMapper
- 목록 데이터를 위한 DTO와 서비스 계층
- 검색/필터링 조건의 정의
- 검색 조건을 위한 화면 처리
Todo 애플리케이션에 스프링 MVC 적용
1. 스프링 MVC 컨트롤러를 이용해 여러 경로의 호출을 하나의 컨트롤러를 이용해 처리
-GET 방식의 경우와 POST 방식의 경우 처리하는 방법이 다르다.
package org.zerock.springex.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.zerock.springex.dto.PageRequestDTO;
import org.zerock.springex.dto.TodoDTO;
import org.zerock.springex.service.TodoService;
import javax.validation.Valid;
@Controller
@RequestMapping("/todo")
@Log4j2
@RequiredArgsConstructor
public class TodoController {
private final TodoService todoService;
@RequestMapping("/list")
public void list(){
log.info("todo list...");
}
@GetMapping("/register")
public void registerGET() {
log.info("GET todo register.......");
}
@PostMapping("/register")
public String registerPost() {
log.info("POST todo register.......");
}
}
2. 객체 자료형인 TodoDTO를 파라미터로 적용
(1) TodoDTO에 작성자 변수 추가
package org.zerock.jdbcex.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
@ToString
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TodoDTO {
private Long tno;
private String title;
private LocalDate dueDate;
private boolean finished;
private String writer;
}
(2) TodoController의 register() 메서드를 POST방식으로 처리하는 메서드에 TodoDTO를 파라미터로 적용
-자동으로 형변환 처리되어 다양한 타입의 멤버 변수들이 자동 처리된다.
package org.zerock.springex.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.zerock.springex.dto.PageRequestDTO;
import org.zerock.springex.dto.TodoDTO;
import org.zerock.springex.service.TodoService;
import javax.validation.Valid;
@Controller
@RequestMapping("/todo")
@Log4j2
@RequiredArgsConstructor
public class TodoController {
private final TodoService todoService;
@RequestMapping("/list")
public void list(Model model){
log.info("todo list.......");
}
@GetMapping("/register")
public void registerGET() {
log.info("GET todo register.......");
}
@PostMapping("/register")
public String registerPost(TodoDTO todoDTO) {
log.info("POST todo register.......");
log.info(todoDTO);
}
}
(3) register.jsp 추가
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- 기존의 <h1>Header</h1> -->
<div class="row">
<div class="col">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link active" aria-current="page" href="#">Home</a>
<a class="nav-link" href="#">Features</a>
<a class="nav-link" href="#">Pricing</a>
<a class="nav-link disabled">Disabled</a>
</div>
</div>
</div>
</nav>
</div>
</div>
<!-- header end -->
<!-- 기존의 <h1>Header</h1>끝 -->
<div class="row content">
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<form action="/todo/register" method="post">
<div class="input-group mb-3">
<span class="input-group-text">Title</span>
<input type="text" name="title" class="form-control" placeholder="Title">
</div>
<div class="input-group mb-3">
<span class="input-group-text">DueDate</span>
<input type="date" name="dueDate" class="form-control" placeholder="Writer">
</div>
<div class="input-group mb-3">
<span class="input-group-text">Writer</span>
<input type="text" name="writer" class="form-control" placeholder="Writer">
</div>
<div class="my-4">
<div class="float-end">
<button type="submit" class="btn btn-primary">Submit</button>
<button type="result" class="btn btn-secondary">Reset</button>
</div>
</div>
</form>
<script>
const serverValidResult = {}
<c:forEach items="${errors}" var="error">
serverValidResult['${error.getField()}'] = '${error.defaultMessage}'
</c:forEach>
console.log(serverValidResult)
</script>
</div>
</div>
</div>
</div>
</div>
<div class="row content">
<h1>Content</h1>
</div>
<div class="row footer">
<!--<h1>Footer</h1>-->
<div class="row fixed-bottom" style="z-index: -100">
<footer class="py-1 my-1 ">
<p class="text-center text-muted">Footer</p>
</footer>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</body>
</html>
(4) 파라미터로 TodoDTO를 받도록 작성
-Model이라는 파라미터를 이용
-@ModelAttribute 어노테이션을 이용해 자동으로 생성된 변수명이 아니라 명시적으로 이름을 지정한다.
@GetMapping("/ex4_1")
public void ex4Extra(@ModelAttribute("dto") TodoDTO todoDTO, Model model){
log.info(todoDTO);
}
-> JSP에서 @ModelAttribute로 명시적으로 지정한 이름인 ${dto}를 이용해 변수로 처리 가능
의존성 주입
1. SampleService와 SampleDAO 생성
2. XML 설정 파일 추가
(1) 'WEB-INF' 폴더에서 'root-context.xml'이라는 이름의 스프링 설정 XML 생성
(2) 'Configure application context' - 'root-context.xml' 선택해 생성
(3) <bean>태그를 이용해 SampleService와 SampleDAO 빈 설정
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
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/context https://www.springframework.org/schema/context/spring-context.xsd http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">
<bean class="org.zerock.springex.samplex.sample.SampleDAO"></bean>
<bean class="org.zerock.springex.samplex.sample.SampleService"></bean>
</beans>
(4) 빈 설정 테스트 test 폴더에서 동일한 패키지 생성해 SampleTests 클래스 생성
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations="file:src/main/webapp/WEB-INF/root-context.xml")
public class SampleTests {
@Autowired
private SampleService sampleService;
@Test
public void testService1(){
log/info(sampleService);
Assertions.assertNotNull(sampleService);
}
}
-SampleService를 멤버 변수로 선언해 @Autowired을 이용해 해당 타입의 빈이 존재하면 주입하는 의존성 주입 어노테이션
-@ExtendWith(SpringExtension.class) 어노테이션은 테스트를 수행하는 JUnit5을 위한 어노테이션
-@ContextConfiguration 어노테이션은 스프링 설정 정보 로딩 어노테이션 : XML 설정은 locations / 자바 설정은 classes
3. SampleService에 SampleDAO 주입
-@Autowired를 이용해 필요한 타입을 주입시킨다.
@ToString
public class SampleService{
@Autowired
private SampleDAO sampleDAO;
}
-Lombok의 @ToString, SampleDAO를 변수로 선언해 @Autowired 적용하는 방식으로 필드주입 방식 사용
-> SampleService 객체 안에 SampleDAO 객체가 주입
4. <bean>을 이용한 설정 방식에서 어노테이션을 이용한 스캔 방식으로 변경
(1) root-context.xml 변경
- <context:component-scan> 추가
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
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/context https://www.springframework.org/schema/context/spring-context.xsd http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">
<context:component-scan base-package="org.zerock.springex.sample"/>
</beans>
(2) SampleDAO 변경
-@Repository 어노테이션 추가
(3) SampleService 변경
-@Service 어노테이션 추가
5. 필드 주입 방식에서 생성자 주입 방식으로 변경
-SampleService 클래스의 의존성 주입 방식을 생성자 주입 방식으로 변경
@ToString
@RequiredArgsConstructor
public class SampleService{
private final SampleDAO sampleDAO;
}
6. 인터페이스를 통해 유연한 구조로 변경
-SampleDAO를 다른 객체로 변경시 SampleService 역시 수정되어야 한다.
-이같은 단점을 보완하기 위해 실제 객체를 몰라도 타입만 이용해 코드 작성이 가능한 인터페이스를 적용한다.
(1) SampleDAO 변경
public interface SampleDAO{
}
(2) SampleDAO 인터페이스 구현한 SampleDAOImpl 클래스 작성
@Repository
public class SampleDAOImpl implements SampleDAO{
}
(3) @Primary 혹은 @Qualifier를 사용해, 특수한 경우와 일반적 경우의 사용을 분리
7. 애플리케이션 컨텍스트 생성을 위한 리스너 설정
(1) spring-webmvc 라이브러리 추가
(2) web.xml에 <listener> 설정과 <context-param> 추가
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
8. DB 연동 설정
(1) build.gradle에 DB 드라이버와 커네션풀인 HikariCP 라이브러리 추가
(2) root-context.xml에 HikariCP 설정
-HikariConfig 객체와 HikariDataSource 초기화하기 위해 ConnectionUtil 클래스를 작성한 JDBC 프로그래밍
-> HikariConfig와 HikariDataSource 객체를 스프링의 빈으로 등록
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
<property name="driverClassName" value="org.mariadb.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mariadb://localhost:3306/webdb"></property>
<property name="username" value="webuser"></property>
<property name="password" value="webuser"></property>
<property name="dataSourceProperties">
<props>
<prop key="cachePrepStmts">true</prop>
<prop key="prepStmtCacheSize">250</prop>
<prop key="prepStmtCacheSqlLimit">2048</prop>
</props>
</property>
</bean>
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
destroy-method="close">
<constructor-arg ref="hikariConfig" />
</bean>
-HikariConfig에 id 속성이 적용되어 있어서 HikariDataSource에서 해당 id 값을 참조해 사용한다.
(3) javax.sql의 DataSource 인터페이스 구현체인 HikariDataSource 설정 테스트
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations="file:src/main/webapp/WEB-INF/root-context.xml")
public class SampleTests {
@Autowired
private SampleService sampleService;
@Autowired
private DataSource dataSource;
@Test
public void testService1(){
log.info(sampleService);
Assertions.assertNotNull(sampleService);
}
@Test
public void testConnection() throws Exception{
Connection connection = dataSource.getConnection();
log.info(connection);
Assertions.assertNotNull(connection);
connection.close();
}
}
-root-context.xml에 선언된 HikariCp를 주입받기 위해 DataSource 변수 선언해 @Autowired로 주입
-스프링은 필요한 객체를 주입해주기 때문에 빈으로 등록할 경우에 어디서는 해당 객체를 쉽게 사용할 수 있다.
9.MyBatis와 스프링을 연동하고 Mapper 인터페이스만 이용 (@Select 어노테이션 사용)
-데이터베이스의 현재 시각을 문자열로 처리하는 Time 인터페이스
(1) TimeMapper 인터페이스 선언
public interface TimeMapper{
@Select("select now()")
String getTime();
}
(2) root-context.xml에 매퍼 인터페이스를 등록
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
<property name="driverClassName" value="org.mariadb.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mariadb://localhost:3306/webdb"></property>
<property name="username" value="webuser"></property>
<property name="password" value="webuser"></property>
<property name="dataSourceProperties">
<props>
<prop key="cachePrepStmts">true</prop>
<prop key="prepStmtCacheSize">250</prop>
<prop key="prepStmtCacheSqlLimit">2048</prop>
</props>
</property>
</bean>
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
destroy-method="close">
<constructor-arg ref="hikariConfig" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:/mappers/**/*.xml"></property>
</bean>
<mybatis:scan base-package="org.zerock.springex.mapper"></mybatis:scan>
(3) 테스트 코드 작성해 확인
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations="file:src/main/webapp/WEB-INF/root-context.xml")
public class TimeMapperTests {
//해당 객체가 없더라도 예외가 받지 않게 하는 속성
@Autowired(required=false)
private SampleService sampleService;
@Test
public void testGetTime() throws Exception{
log.info(timeMapper.getTime());
}
}
10. @Select 어노테이션 대신 XML로 SQL 분리
-@Select 사용시 SQL이 길어지면 복잡해지고, 어노테이션 변경시 다시 빌드해야하기 때문에
(1) TimeMapper 인터페이스 정의
public interface TimeMapper{
String getNow();
}
(2) main/resources/mappers 폴더 추가 후, 인터페이스와 같은 이름의 TimeMapper.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="org.zerock.springex.mapper.TimeMapper2">
<select id="getNow" resultType="string">
select now()
</select>
</mapper>
-<select>태그는 id값을 이용하고, resultType이나 resultMap 속성 지정해야한다.
-resultType : select 문 결과를 어떤 타입으로 처리할지 결정한다. [자료형 전체 이름을 쓰거나 / 타입 별칭을 이용]
(3) root-context.xml의 MyBatis 설정에 XML 파일 설정
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:/mappers/**/*.xml"></property>
</bean>
-mapperLocations은 XML 매퍼 파일의 위치를 의미
-resources의 경우 claspath: 접우어 이용해 인식되는 경로로 하위 폴더까지 포함하도록 "/mappers/**/*.xml"로 설정한다.
(4) 테스트 코드 작성
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations="file:src/main/webapp/WEB-INF/root-context.xml")
public class TimeMapperTests {
//해당 객체가 없더라도 예외가 받지 않게 하는 속성
@Autowired(required=false)
private SampleService sampleService;
@Test
public void testGetTime() throws Exception{
log.info(timeMapper.getTime());
}
}
11. 스프링 MVC로 변환
(1) spring-webmvc 라이브러리 추가되었는지 확인
(2) webapp/WEB-INF/servelt-context.xml 생성해, 스프링 MVC 관련 설정 추가
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<mvc:annotation-driven></mvc:annotation-driven>
<mvc:resources mapping="/resources/**" location="/resources/"></mvc:resources>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
</beans>
(3) web.xml의 DispatcherServlet 설정
-스프링 MVC의 실행을 위해 프론트 컨트롤러 역할을 하는 DispatcherServlet을 설정한다.
<?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">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/servlet-context.xml</param-value>
</init-param>
<init-param>
<param-name>throwExceptionIfNoHandlerFound</param-name>
<param-value>true</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
<servlet> 설정 : DispatcherServlet을 등록하는데 DispatcherServlet이 로딩할 때 servlet-context.xml 이용하도록 설정
load-on-startup 설정 : 톰캣 로딩 시에 클래스를 미리 로딩해 두기 위한 설정
<servlet-mapping> 설정 : DispatcherServlet이 모든 경로의 요청에 대한 처리를 담당하기 때문에 '/'로 지정
12. 스프링 MVC 컨트롤러 적용
(1) 프론트 컨트롤러인 SampleController 작성
@Controller
@Log4j2
public class SampleController{
@GetMapping("/hello")
pulic void hello(){
log.info("hello...");
}
}
-@Controller 어노테이션 : 해당 클래스가 스프링 MVC에서 컨트롤러 역할을 하게 하고, 빈으로 처리되도록 사용
-@GetMapping 어노테이션 : 해당 url패턴에 대해 GET 방식으로 들어오는 요청을 처리하기 위해 사용
(2) servlet-context.xml에 패키지 스캔해 빈으로 자동 등록되도록 component-scan 적용
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<mvc:annotation-driven></mvc:annotation-driven>
<mvc:resources mapping="/resources/**" location="/resources/"></mvc:resources>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<context:component-scan base-package="org.zerock.springex.controller"/>
</beans>
(3) 해당 컨트롤러에 적용한 URL을 매핑할 페이지 작성
13. 파라미터 자동 수집과 변환
(1) SampleController에 메서드 추가
@Controller
@Log4j2
public class SampleController{
@GetMapping("/ex1")
public void ex1(String name, int age){
log.info("ex1...");
log.info("name: "+name);
log.info("age: "+age);
}
}
(2) @RequestParam의 defaultValue 속성
-파라미터가 전달되지 않았을 때 전달될 기본값 설정
@GetMapping("/ex2")
public void ex2(@RequestParam(name = "name", defaultValue="AAA") String name,
@RequestParam(name = "age", defaultValue="20") int age){
log.info("ex2...");
log.info("name: "+name);
log.info("age: "+age);
}
}
(3) Formatter를 이용한 파라미터의 커스텀 처리
-HTTP는 문자열로 데이터를 전달하므로, 컨트롤러는 문자열 기준으로 특정한 클래스의 객체로 처리한다.
-날짜 관련 타입 처리의 경우 'Date', 'LocalDate', 'LocalDateTime'으로 변환하는데 파라미터 수집의 경우엔 Formatter 이용
문자열을 포맷을 이용해 특정한 객체로 변환하기 위해 Formatter 사용
-Formatter 인터페이스를 구현한 LocalDateFormatter 클래스 작성
-Formatter 인터페이스엔 parse()메서드와 print() 메서드가 존재한다.
package org.zerock.springex.controller.formatter;
import org.springframework.format.Formatter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class LocalDateFormatter implements Formatter<LocalDate> {
@Override
public LocalDate parse(String text, Locale locale) {
return LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}
@Override
public String print(LocalDate object, Locale locale) {
return DateTimeFormatter.ofPattern("yyyy-MM-dd").format(object);
}
}
Formatter를 servlet-context.xml에 적용하는 방법
(1) FormattingConverstionServiceFactoryBean 객체를 스프링의 빈으로 등록하고, 이 안에 작성한 LocalDateFormatter를 추가
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="formatters">
<set>
<bean class="org.zerock.springex.controller.formatter.LocalDateFormatter"/>
<bean class="org.zerock.springex.controller.formatter.CheckboxFormatter"/>
</set>
</property>
</bean>
(2) conversionService라는 빈을 등록하고, <mvc:snnotation-driven>에 이를 이용한다고 지정한다.
<mvc:annotation-driven conversion-service="conversionService" />
14. 스프링 MVC에서 PRG 패턴을 처리
-RedirectAttributes 클래스의 addAttribute()와 addFlashAttribute()메서드
(1) addAttribute()와 addFlashAttribute()를 추가
@GetMapping("/ex5")
public String ex5(RedirectAttributes redirectAttributes){
redirectAttributes.addAttributes("name", "ABC");
redirectAttributes.addFlashAttributes("result", "success");
return "redirect:/ex6";
}
@GetMapping("/ex6")
public void ex6(){
}
(2) ex6.jsp 작성
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
<title>Hello, world!</title>
</head>
<body>
<h1>ADD FLASH ATTRIBUTE: ${result}</h1>
</body>
</html>
15. 스프링 MVC의 예외처리 - 전달되는 Exception 객체 지정
(1) @ControllerAdvice 작성
-CommonExceptionAdvice에 NumberFormatException 예외처리를 위해 exceptNumber() 메서드 생성
package org.zerock.springex.controller.exception;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.util.Arrays;
@ControllerAdvice
@Log4j2
public class CommonExceptionAdvice {
}
(2) @ControllerAdvice을 동작하기 위한 코드를 SampleController에 추가
@GetMapping("/ex7")
public void ex7(String p1, int p2){
log.info("p1..."+p1);
log.info("p2..."+p2);
}
-> 숫자 대신 알파벳 전송시 NumberFormatException 발생
(3) exceptNumber() 메서드 작성
-exceptNumber()메서드에 CommonExceptionAdvice에 NumberFormatException을 처리하도록 지정해 예외처리
@ResponseBody
@ExceptionHandler(NumberFormatException.class)
public String exceptNumber(NumberFormatException numberFormatException){
log.error("-----------------------------------");
log.error(numberFormatException.getMessage());
return "NUMBER FORMAT EXCEPTION";
}
-@ExceptionHandler가 지정되어 있고, NumberFormatException 타입을 지정하므로, 해당 타입의 예외를 파라미터로 전달받는다.
-@ResponseBody를 이용해 만들어진 문자열 그대로 브라우저에 전송한다.
16. 스프링 MVC의 예외처리 - 범용적인 예외처리 수행
CommonExceptionAdvice에 exceptCommon 메서드 작성
-범용적인 예외 처리를 위해 상위 타입인 Exception 타입을 처리하도록 구성
@ResponseBody
@ExceptionHandler(Exception.class)
public String exceptCommon(Exception exception){
log.error("-----------------------------------");
log.error(exception.getMessage());
StringBuffer buffer = new StringBuffer("<ul>");
buffer.append("<li>" +exception.getMessage()+"</li>");
Arrays.stream(exception.getStackTrace()).forEach(stackTraceElement -> {
buffer.append("<li>"+stackTraceElement+"</li>");
});
buffer.append("</ul>");
return buffer.toString();
}
17. 에러페이지 화면 작성
(1) CommonExceptionAdvice에 notFound() 메서드 작성
@ResponseBody
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String notFound(){
return "custom404";
}
(2) common404.jsp 작성
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>Oops! 페이지를 찾을 수 없습니다!</h1>
</body>
</html>
(3) web.xml에 DispatcherServlet 설정
<servlet> 태그 내에 <init-param>을 추가하고 throwExceptionIfNoHandlerFound 파라미터 설정 추가
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/servlet-context.xml</param-value>
</init-param>
<init-param>
<param-name>throwExceptionIfNoHandlerFound</param-name>
<param-value>true</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
의존성 주입과 스프링
객체지향과 설계를 위해 등장한 스프링 프레임워크
스프링의 시작
-경량 프레임워크로 개발과 설계 문제를 해결한 스프링
-코어 라이브러리와 추가적인 라이브러리의 결합으로 프로젝트를 구성
의존성 주입
-객체와 객체 간의 관계의 유연한 유지를 위한 도입한 개념으로, 객체의 생성과 관계를 분리한다.
-XML 설정 / 자바 설정을 통해 필요한 객체를 찾아서 사용한다.
프로젝트 생성
-deployment 설정 : war(exploded), 경로 '/'
-Server 인코딩 설정과 재시작 옵션 : -Dfile.encoding=UTF-8, Update classes and resources
스프링 라이브러리 추가
-라이브러리는 jar파일로 직접 추가하거나, 메이븐 저장소에서 가져온다.
-build.gradle에 스프링 관련 라이브러리인 core, context, test 라이브러리 추가
-build.gradle에 Lombok과 log4j2 라이브러리 추가
-buid.gradle에 JSTL 라이브러리 추가
-resources 폴더에 log4j2.xml 추가
ApplicationContext와 빈
서블릿이 존재하는 공간은 서블릿 컨텍스트
빈이라는 객체를 관리하는 공간이 애플리케이션 컨텍스트
-> root-context.xml에 설정된 해당 클래스의 객체 생성해서 관리한다.
@Autowired와 필드 주입
: 멤버 변수에 직접 @Autowired를 선언하는 방식이 필드 주입 방식
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations="file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoServiceTests {
@Autowired
private TodoService todoService;
}
<context:component-scan>
: 스프링 이용시 클래스를 작성하거나 객체 직접 생성하지 않고, ApplicationContext가 생성된 객체를 관리하는데
<bean>을 이용해 설정하는 방식에서 어노테이션 형태로 변화
스프링 어노테이션
- @Controller
- @Service
- @Repository
- @Component
-> 이같은 어노테이션을 사용하면 해당 패키지를 조사해 클래스의 어노테이션을 이용하는 설정으로 변경된다.
생성자 주입 방식
- 주입 받을 객체의 변수를 final 선언
- 생성자를 이용해 해당 변수를 생성자의 파라미터로 지정
- @RequiredArgsConstructor 어노테이션을 이용한다.
-> 생성자 주입 방식을 사용하면 객체 생성시 문제 발생 여부를 파악할 수 있는 장점이 있다.
인터페이스를 이용한 느슨한 결합
인터페이스를 이용해 변경에 유연한 구조로 전환
-인터페이스를 이용하면 실제 객체를 몰라도 타입만 이용한 코드 작성이 가능하다.
-객체와 객체의 의존 관계를 몰라도 코드 작성이 가능한 '느슨한 결합'
다른 객체로의 변경이 발생한 경우
-두 가지 이상 구현한 클래스가 존재한다면, 스프링에서는 어떤 타입의 객체를 사용할지 선택하지 못한다.
-@Primary 어노테이션을 이용해, 해당 어노테이션을 지정한 클래스를 스프링에서 선택한다.
-Lombok과 @Qualifier 어노테이션을 이용하는 방법
(1) src/main/java 경로에 lombok.config 파일을 생성
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
(2) 지금 사용하고 싶은 클래스에 @Qualifier("event") 를 사용하고, 일반적으로 사용하는 클래스는 @Qualifier("normal")을 사용
(3) 의존성을 주입하는 클래스에 특정한 이름의 객체를 사용하도록 추가
@Qualifier("normal")
private final SampleDAO sampleDAO;
스프링의 빈으로 지정되는 객체들
모든 클래스의 객체가 스프링 빈으로 처리되는 것은 아니다.
'역할' 중심의 객체만 처리하고, 나머지 DTO나 VO같은 데이터 중심 객체의 경우 빈으로 등록하지 않는다.
XML이나 어노테이션으로 처리하는 객체
빈으로 처리할 경우, 코드를 수정할 수 있는지 여부로 판단해 [XML 설정을 이용 / 어노테이션으로 처리] 여부를 선택한다.
jar 파일로 추가되는 경우 - 어노테이션 추가 불가능하므로 XML에서 <bean>으로 처리
직접 작성되는 클래스의 경우 - 어노테이션을 이용
웹 프로젝트를 위한 스프링 준비
애플리케이션 컨텍스트에서 빈으로 등록된 객체를 생성 및 관리
애플리케이션 컨텍스트가 웹 애플리케이션에서 동작하기 위해 애플리케이션 실행시, 애플리케이션 내부에 애플리케이션 컨텍스트을 생성
-> web.xml을 이용해 리스너를 설정하는 작업이 필요하다.
DataSource 구성해 DB 연동
(1) build.gradle에 DB 드라이버와 커네션풀인 HikariCP 라이브러리 추가
(2) root-context.xml에 HikariCP 설정
MyBatis와 스프링 연동
DB 연동을 위한 spring-jdbc 라이브러리를 이용해 구현하거나, Mybatis이나 JPA 프레임워크 이용할 수도 있다.
MyBatis
'Sql Mapping FrameWork'로 SQL 실행 결과를 객체지향으로 매핑해주는 프레임워크로 기존의 SQL을 그래도 사용할 수 있는 장점
- PreparedStatement / ResultSet의 처리
- Connection / PreparedStatement / ResultSet의 close() 처리
- SQL의 분리
Mybatis와 스프링의 연동 방식
스프링에서 제공하는 라이브러리 이용 여부
- MyBatis를 단독으로 개발하고 스프링에서 DAO를 작성해 처리
- MyBatis와 스프링을 연동하고 Mapper 인터페이스만 이용
- @Select 어노테이션 사용
- XML로 SQL 분리
MyBatis를 위한 라이브러리 추가
- 스프링 관련 라이브러리 : spring-jdbc, spring-tx
- MyBatis 관련 라이브러리 : mybatis, mybatis-spring
MyBatis를 위한 스프링 설정
: HikariDataSource를 이용해 SqlSessionFactory라는 빈 등록
'mybatis-spring' 라이브러리에 있는 클래스를 이용해 빈 등록
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
<property name="driverClassName" value="org.mariadb.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mariadb://localhost:3306/webdb"></property>
<property name="username" value="webuser"></property>
<property name="password" value="webuser"></property>
<property name="dataSourceProperties">
<props>
<prop key="cachePrepStmts">true</prop>
<prop key="prepStmtCacheSize">250</prop>
<prop key="prepStmtCacheSqlLimit">2048</prop>
</props>
</property>
</bean>
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
destroy-method="close">
<constructor-arg ref="hikariConfig" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>
MyBatis와 스프링을 연동하고 Mapper 인터페이스만 이용
-개발자가 실제 동작하는 클래스와 객체를 생성하지 않고, 스프링에서 자동으로 생성되는 방식 이용
-인터페이스만으로 개발할 수 있다는 장점과 직접 코드를 수정하지 못하는 단점을 가진다.
- @Select 어노테이션을 이용해 쿼리 작성하는 방식
- 간단하게 어노테이션을 이용해 SQL문 작성 가능
- XML로 SQL을 분리하는 방식
- @Select 사용시 SQL이 길어지면 복잡해지고, 어노테이션 변경시 다시 빌드해야하는 단점 해결
XML을 이용해 매퍼 인터페이스 결합하는 과정
- 매퍼 인터페이스 정의하고 메소드 선언
- 매퍼 인터페이스 이름과 같은 XML 파일 작성해 <select>와 같은 태그로 SQL 작성
- 태그에 id 속성 값을 인터페이스의 메소드 이름과 같게 작성한다.
- root-context.xml에 매퍼 로케이션 설정을 통해 XML 매퍼 파일들의 위치를 지정해준다.
스프링 Web MVC 기초
서블릿 API를 추상화한 형태로 작성된 라이브러리
스프링 Web MVC 특징
- Front-controller 패턴으로 모든 흐름 사전/사후 처리 가능
- 어노테이션의 활용으로 최소한의 코드로 처리 가능
- HttpServletRequest / HttpServletResponse를 이용하지 않고 추상화된 방식으로 개발 가능
DispatcherServlet과 Front Controller
-모든 요청이 반드시 DispatcherServlet으로 실행된다.
-객체지향의 경우 : 퍼사드 패턴 / 웹 구조의 경우 : Front-Controller 패턴
Front-Controller 패턴을 이용하면 모든 요청이 프론트 컨트롤러를 지나서 처리
-> 모든 공통 처리를 프론트 컨트롤러에서 처리할 수 있다.
스프링 MVC에서는 DispatcherServlet이라는 객체가 프론트 컨트롤러 역할을 수행한다.
-> @Controller 어노테이션을 통해서 각각의 다른 처리를 수행하는 부분만 별도로 처리하는 컨트롤러를 작성한다.
스프링 MVC을 사용하기 위한 설정
- servlet-context.xml
- web.xml
1. Servlet-context.xml
<mvc:annotation-driven>
: 스프링 MVC 설정을 annotation 기반으로 처리하고, 스프링 MVC 객체를 자동으로 빈등록하기 위한 어노테이션
<mvc:resources>
: 정적 파일 경로를 지정하는 설정으로, 스프링 MVC에서 처리하지 않는다.
InternalResourceViewResolver
: 스프링 MVC에서 제공하는 뷰 설정을 담당하는 클래스로, 빈으로 설정해야 한다.
-> prefix와 suffix를 이용해 경로와 확장자 설정을 생략할 수 있다.
2. web.xml
: 스프링 MVC을 실행하기 위한 프론트 컨트롤러 역할의 DispatcherServlet 설정
<servlet> 설정
: DispatcherServlet을 등록하는데 DispatcherServlet이 로딩할 때 servlet-context.xml 이용하도록 설정
load-on-startup 설정
: 톰캣 로딩 시에 클래스를 미리 로딩해 두기 위한 설정
<servlet-mapping> 설정
: DispatcherServlet이 모든 경로의 요청에 대한 처리를 담당하기 때문에 '/'로 지정
스프링 MVC 컨트롤러
전통적인 방식의 상속과 인터페이스 기반 구현 방식이 아닌 스프링 MVC 컨트롤러의 특징
- 상속이나 인터페이스 없이 어노테이션만으로 처리 가능
- 오버라이드 없이 필요한 메서드 정의
- 메소드의 파라미터를 기본 자료형이나 객체 자료형을 마음대로 지정
- 메소드의 리턴타입도 void, String, 객체 등 다양한 타입 사용 가능
(1) 프론트 컨트롤러 작성
(2) servlet-context.xml에 패키지 스캔해 빈으로 자동 등록되도록 component-scan 적용
(3) 해당 컨트롤러에 적용한 URL을 매핑할 페이지 작성
@RequestMapping과 파생 어노테이션들
특정한 경로의 요청을 지정하기 위한 어노테이션
[컨트롤러 클래스 선언부 / 컨트롤러 메소드]에 사용 가능
-> 서블릿 중심의 MVC와 다르게 하나의 컨트롤러를 통해 여러 경로 호출 처리 가능
파라미터 자동 수집과 변환
DTO나 VO 등을 메소드의 파라미터로 설정해 자동으로 전달되는 HttpServletRequest의 파라미터를 수집해주는 기능
파라미터 수집 기준
- 기본 자료형의 경우 자동으로 형 변환처리 가능
- 객체 자료형의 경우 setXXX() 동작을 통해 처리
- 객체 자료형의 경우 생성자가 없거나 파라미터가 없는 생성자가 필요 (Java Beans)
객체 자료형의 파라미터 수집
- 객체가 생성되고 setXXX()를 이용해 처리한다.
- Lombok활용시 @Setter나 @Data 이용
Model이라는 특별한 파라미터
setAttribute() : request.setAttribute()를 이용해 데이터를 JSP로 전달
Model :웹 MVC와 동일한 방식으로 모델이라고 부르는 데이터를 JSP까지 전달하는 객체
1. addAttribute()라는 메소드를 이용해 뷰에 전달할 '이름'과 '값(객체)'를 지정
@GetMapping("/ex4")
public void ex4(Model model){
log.info("----");
model.addAttribute("message", "Hello World");
}
2. ex4.jsp 생성
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
<title>Hello, world!</title>
</head>
<body>
<h1>${message}</h1>
<h1><c:out value="${message}"></c:out></h1>
</body>
</html>
Java Beans와 @ModelAttribute
-스프링 MVC의 경우 파라미터로 getter/setter를 이용해 Java Beans의 형식의 사용자 정의 클래스가 파라미터인 경우에는 자동으로 화면까지 객체를 전달한다.
-@ModelAttribute를 이용하면 명시적으로 이름을 지정할 수 있다.
RedirectAttributes와 리다이렉션
PRG패턴을 처리하기 위해서 스프링 MVC에서 지원하는 RedirectAttributes 타입
- addAttribute(키, 값) : 리다이렉트할 때 쿼리 스트링이 되는 값을 지정
- addFlashAttribute(키, 값) : 일회용으로만 데이터를 전달하고 삭제되는 값을 지정
다양한 리턴 타입
스프링 MVC는 파라미터를 자유롭게 지정가능한데, 컨트롤러 내에 선언하는 메소드 리턴타입도 다양하게 사용가능
- 화면이 따로 있는 경우
- void : @RequestMapping, @GetMapping
- 문자열 : redirect, forward
- JSON타입을 활용할 경우
- 객체나 배열, 기본 자료형
- ResponseEntity
스프링 MVC에서 주로 사용하는 어노테이션들
위치별 어노테이션
- 컨트롤러 선언부에 사용하는 어노테이션
- @Controller
- @RestController : @Controller + @ResponseBody
- @ReqeustMapping
- 메소드 선언부에 사용하는 어노테이션
- @GetMapping / @PostMapping / @DeleteMapping / @PutMapping
- @RequestMapping
- @ResponseBody
- 메소드의 파라미터에 사용하는 어노테이션
- @RequestParam
- @PathVariable
- @ModelAttribute
- @SessionAttribute, @Valid, @RequestBody
스프링 MVC 예외 처리
@ControllerAdvice : 스프링 MVC에서 컨트롤러의 예외처리를 하기위한 어노테이션, 이 어노테이션이 선언된 클래스 역시 빈에 등록된다.
@ExceptionHandler : @ControllerAdvice 메소드에 사용하는 Exception 객체들을 지정하고 메소드의 파라미터에서 이용하기 위한 어노테이션
- 특정 Exception을 지정해 예외처리 수행
- 상위 타입을 지정해 범용적인 예외처리 수행
404 에러 페이지와 @ResponseStatus
잘못된 URL을 호출하게 되었을 때 @ControllerAdvice 어노테이션을 지정한 메서드에 @ResponseStatus를 이용해 해당 화면을 작성할 수 있다.
스프링 Web MVC 구현하기
프로젝트의 구현 목표와 준비
부트스트랩
MyBatis와 스프링을 이용한 영속 처리
Todo 기능 개발
페이징 처리를 위한 TodoMapper
목록 데이터를 위한 DTO와 서비스 계층
검색/필터링 조건의 정의
검색 조건을 위한 화면 처리
'Server Programming > Spring Boot Backend Programming' 카테고리의 다른 글
4장-3. 스프링 Web MVC 구현 (2) 페이징 처리 (+ 백틱) (0) | 2022.12.01 |
---|---|
4장-2. 스프링 Web MVC 구현 (1) CRUD (+@Configuration, @Bean, 브라우저 한글 처리) (0) | 2022.11.29 |
3장. 세션과 필터, 쿠키와 리스너 (+ 한글 깨짐 처리 / Optional<> / 옵저버 패턴) (0) | 2022.11.27 |
2장. 웹과 데이터베이스 (0) | 2022.11.25 |
1장. 웹 프로그래밍 시작 (0) | 2022.11.24 |