여니의 프로그래밍 study/Spring & Spring Boot

[Spring] MVC 1 : 요청 매핑, 커맨드 객체, 리다이렉트, 폼 태그, 모델 (Chapter 11)

여니's 2023. 12. 2. 15:40

 

참고 자료 : 초보 개발자를 위한 스프링5 프로그래밍 입문

 

 


<Chapter 11. MVC 1 : 요청 매핑, 커맨드 객체, 리다이렉트, 폼 태그, 모델 >

1. 프로젝트 준비

 

1) 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>sp5</groupId>
	<artifactId>sp5-chap11</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>

	<dependencies>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>3.1.0</version>
			<scope>provided</scope>
		</dependency>

		<dependency>
			<groupId>javax.servlet.jsp</groupId>
			<artifactId>javax.servlet.jsp-api</artifactId>
			<version>2.3.2-b02</version>
			<scope>provided</scope>
		</dependency>

		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
			<version>1.2</version>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>5.0.2.RELEASE</version>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>5.0.2.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.apache.tomcat</groupId>
			<artifactId>tomcat-jdbc</artifactId>
			<version>8.5.27</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.45</version>
		</dependency>

		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>1.7.25</version>
		</dependency>

		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>1.2.3</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.7.0</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
					<encoding>utf-8</encoding>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

 

 


 

2) 요청 매핑 애노테이션을 이용한 경로 매핑

웹 애플리케이션을 개발하는 것은

다음 코드를 작성하는 것과 같다. 

 

- 특정 요청 URL을 처리할 코드

- 처리 결과를 HTML과 같은 형식으로 응답하는 코드

 

요청 매핑 어노테이션 중 하나인

@GetMapping 어노테이션은

"/hello" 요청 경로를 hello() 메서드가 처리하도록 설정했다.

(10장 HelloController.java 파일 확인)

 

또한,

요청 매핑 어노테이션을 적용한 메서드를

2개 이상 정의할 수도 있다. 

 

각 요청 매핑 어노테이션의 경로에서

공통되는 부분의 경로를 담은 @RequestMapping 어노테이션을

클래스에 적용하고

각 메서드는 나머지 경로를 값으로 갖는 

요청 매핑 어노테이션을 적용할 수 있다.

 

@Controller
@RequestMapping("/register") // 각 메서드에 공통되는 경로를 지정해준다.
// 이렇게 공통된 부분은 중복으로 기재되는 걸 방지할 수 있음.
public class RegistController { 
	
    @RequestMapping("/step1") // 공통 경로를 제외한 나머지 경로를 지정해준다
    public String handleStep1(){
    	return "register/step1";
    }
    
    @RequestMapping("/step2")
    public String handleStep2(){
    	...
    }
}

 

 

스프링 MVC는

클래스에 적용한 요청 매핑 어노테이션의 경로와 (/register)

메서드에 적용한 요청 매핑 어노테이션의 경로를 (/step1, /step2)

합쳐서 경로를 찾는다. 

 

 

> 회원가입의 첫 번째 과정인

약관을 보여주는 요청 경로를 처리하는 컨트롤러 클래스

 

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class RegisterController {
    
    @RequestMapping("/register/step1")
    public String handleStep1(){
        return "register/step1";
    }
}

 

 


3) GET과 POST 구분 : @GetMapping, @PostMapping

 

스프링 MVC는 GET, POST 방식 상관없이

@RequestMapping 어노테이션에 

지정한 경로와 일치하는 요청을 처리한다. 

 

단, POST 방식 요청만 처리하고 싶다면

@PostMapping 어노테이션을 사용해서

제한이 가능하다.

 


4) 요청 파라미터 접근

컨트롤러 메서트에서 요청 파라미터를 사용하는 방법

1. HttpServletRequest를 직접 이용

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;

@Controller
public class RegisterController {

    @RequestMapping("/register/step1")
    public String handleStep1(){
        return "register/step1";
    }

    @RequestMapping("/register/step2")
    public String handleStep2(HttpServletRequest request){
        String agreeParam=request.getParameter("agree");
        if (agreeParam==null || !agreeParam.equals("true")){
            return "register/step1";
        }
        return "register/step2";
    }
}

 

> 메서드의 파라미터는 HttpServletRequest 타입 사용

> 파라미터값 구하기 : HttpServletRequest의 getParameter() 메서드 사용

 

2.@RequestParam 어노테이션 사용

> 요청 파라미터 개수가 몇 개 안되면 간단하게 사용할 수 있음

 @RequestMapping("/register/step2")
public String handleStep2(
    @RequestParam(value="agree",defaultValue="false") Boolean agreeVal){
    if(!agree){
        return "register/step1";
    }
    return "register/step2";
}
 }

 

agree 요청 파라미터의 값을 읽어와서 agreeVal 파라미터에 할당

요청 파라미터의 값이 없으면 "false" 문자열을 값으로 사용

 

> value : HTTP 요청 파라미터의 이름을 지정

> required : 필수 여부를 지정

> defaultValue : 요청 파라미터가 값이 없을 때 사용할 문자열 값을 지정함

 

 

agree 요청 파라미터의 값을 읽어옴 

> Boolean 타입으로 변환하여 agreeVal 파라미터에 전달

> 기본 데이터 타입과 래퍼 타입에 대한 변환을 지원

 


5. 리다이렉트 처리

컨트롤러에서 특정 페이지로 리다이렉트 시키는 방법은

"redirect:경로"를 뷰 이름으로 리턴하면 된다. 

@GetMapping("/register/step2")
public String handleStep2Get(){
    return "redirect:/register/step1";
}

 

rediect: 뒤의 문자열이 /로 시작하면

웹 어플리케이션 기준으로 이동경로를 생성한다. 

 

웹 어플리케이션 경로인 /sp5-chap11 과

/register/step1을 연결한

/sp5-chap11/register/step2가 된다.

 


6. 커맨드 객체를 이용해서 요청 파라미터 사용하기

@PostMapping("/register/step3")
public String handleStep3(HttpServletRequest request){
    String email=request.getParameter("email");
    String name=request.getParameter("name");
    String password=request.getParameter("password");
    String confirmPassword=request.getParameter("confirmPassword");

    RegisterRequest regReq=new RegisterRequest();
    regReq.setEmail(email);
    regReq.setName(name);
    regReq.setPassword(password);
    regReq.setConfirmPassword(confirmPassword);
    ...
}

 

 

위 방식대로 진행하면

파라미터의 개수가 많아지면 그만큼 작성을 해줘야한다

 

스프링은 이러한 불편함을 줄이고자

요청 파라미터의 값을 커맨드 객체에 담아주는 기능을 제공함

 

커맨드 객체는

요청 매핑 어노테이션이 적용된 메서드의 파라미터에 위치함

 

@PostMapping("/register/step3")
public String handleStep3(RegisterRequest regReq){
	...
}

 

RegisterRequest 클래스에는

setEmail, setName... 메서드가 있음

 

스프링은 이 메서드를 이용해서

요청 파라미터의 값을 커맨드 객체에 복사한 후

regReq에 전달함.

 

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import spring.DuplicateMemberException;
import spring.MemberRegisterService;
import spring.RegisterRequest;

@Controller
public class RegisterController {

    private MemberRegisterService memberRegisterService;

    public void setMemberRegisterService(MemberRegisterService memberRegisterService){
        this.memberRegisterService=memberRegisterService;
    }

    @RequestMapping("/register/step1")
    public String handleStep1(){
        return "register/step1";
    }
    
    @RequestMapping("/register/step2")
    public String handleStep2(
            @RequestParam(value="agree", defaultValue = "false") Boolean agree) {
        if (!agree) {
            return "register/step1";
        }
        return "register/step2";
    }

    @GetMapping("/register/step2")
    public String handleStep2Get(){
        return "redirect:/register/step1";
    }

    // step3 폼 전송 요청을 처리하는 코드
    @PostMapping("/register/step3")
    public String handleStep3(RegisterRequest regReq){
        try {
            memberRegisterService.regist(regReq);
            return "register/step3";
        } catch (DuplicateMemberException ex){
            return "register/step2";
        }
    }
}

 

 

handleStep3() 메서드는 

MemberRegisterService를 이용해 회원가입을 처리함

회원가입에 성공하면 step3 뷰를 리턴하고

실패하면 step2 뷰를 리턴한다 

 

package config;

import controller.HelloController;
import controller.RegisterController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.MemberRegisterService;


@Configuration
public class ControllerConfig {

    @Autowired
    private MemberRegisterService memberRegSvc;

    @Bean
    public HelloController helloController(){
        return new HelloController();
    }

    @Bean
    public RegisterController registerController(){
        RegisterController controller =new RegisterController();
        controller.setMemberRegisterService(memberRegSvc);
        return controller;
    }

}

 

RegisterController 클래스는

MemberRegisterService 타입의 빈을 의존한다.

따라서 ControllerConfig.java 파일에

의존 주입을 설정해줘야 한다. 

 


7. 뷰 JSP 코드에서 커맨드 객체 사용하기

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
    <title>회원가입</title>
</head>
<body>
    <p><strong>${registerRequest.name}님</strong> 
        회원 가입을 완료했습니다.</p>
    <p><a href="<c:url value='/main'/>">[첫 화면 이동]</a></p>
</body>
</html>

 

커맨드 객체를 사용해서 정보를 표시한다. 

registerRequest가 커맨드 객체에 접근할 떄 사용한 속성 이름

${registerRequest.name}

 


8. @ModelAttribute 어노테이션으로 커맨드 객체 속성 이름 변경

> 커맨드 객체 접근 시 사용할 속성 이름 변경을 하려면

파라미터에 해당 어노테이션을 적용해주면 된다.

 

public String handleStep3(@ModelAttribute("formData") RegisterRequest regReq){

...

}

 

formData라는 이름으로 뷰 코드에서 커맨드 객체에 접근할 수 있음!

 


9. 커맨드 객체와 스프링 폼 연동

<input type="text" name="name" id="name" value="${registerRequest.name}">

 

 

> 다시 폼을 보여줄 때 커맨드 객체의 값을 폼에 채워주면

다시 입력해야 하는 불편함을 해소할 수 있다.

 


10. 컨트롤러 구현 없는 경로 매핑

 

WebMvcConfigurer 인터페이스의 

addViewControllers() 메서드를 사용하면

컨트롤러 구현 없이 요청 경로와 뷰 이름을 연결할 수 있다.

@Override
public void addViewControllers(ViewControllerRegistry registry){
	registry.addViewController("/main").setViewName("main");
}

 

 

/main 요청 경로에 대해

뷰 이름으로 Main을 사용한다고 설정하는 것임.

MvcConfig 파일에 해당 설정을 추가해준다. 

 

main.jsp

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
    <title>메인</title>
</head>
<body>
    <p>환영합니다.</p>
    <p><a href="<c:url value="/register/step1" />">[회원 가입하기]</a>
</body>
</html>

 


11. 주요 에러 발생 상황

1) 요청 매핑 어노테이션과 관련된 주요 익셉션

 

404 에러 발생

- 요청 경로 확인

- 컨트롤러에 설정한 경로 확인

- 컨트롤러 클래스를 빈으로 등록했는지 확인

- 컨트롤러 클래스에 @Controller 어노테이션 적용했는지 확인

- JSP 파일이 존재하지 않아도 확인

 

405 에러 발생

- 지원하지 않는 전송 방식을 사용한 경우

예를 들어 POST 방식만 처리하는 요청 경로를 

GET 방식으로 연결하면 에러가 난다

 

2) @RequestParam이나 커맨드 객체와 관련된 익셉션

 

 


12. 커맨드 객체 : 중첩 , 콜렉션 프로퍼티

 

Respondent.java

package survey;

public class Respondent {

	private int age;
	private String location;

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

	public String getLocation() {
		return location;
	}

	public void setLocation(String location) {
		this.location = location;
	}

}

 

 

AnsweredData.java

package survey;

import java.util.List;

public class AnsweredData {

	private List<String> responses; // 답변 목록을 저장하기 위해 List 타입의 responsed 프로퍼티 사용
	private Respondent res; // 응답자 정보를 담기 위해 Respondent 타입의 res 프로퍼티 사용

	public List<String> getResponses() {
		return responses;
	}

	public void setResponses(List<String> responses) {
		this.responses = responses;
	}

	public Respondent getRes() {
		return res;
	}

	public void setRes(Respondent res) {
		this.res = res;
	}

}

 

 

Respondent : 응답자 정보를 담는 클래스

AnsweredData : 설문 항목에 대한 답변과 응답자 정보를 함께 담는다. 

 

res 프로퍼티는 Respondent 타입이며

res 프로퍼티는 다시 age와 location  프로퍼티를 갖는다. 

중첩 프로퍼티를 의미함 

 

 

프로퍼티 : 변수와 유사하지만

일반적으로 변수보다 높은 수준의 추상화를 제공함

값을 가져오거나 설정할 때 추가적인 로직이 실행될 수 있다.

 

변수 : 데이터를 저장하고 참조하기 위한 메모리 위치를 나타낸다. 

단순히 값을 간단히 저장하고 읽기 위한 용도로 사용됨

 

 

HTTP 요청 파라미터 이름이 

프로퍼티이름[인덱스] > List 타입 프로퍼티의 값 목록을 처리

프로퍼티이름.프로퍼티이름 > 중첩 프로퍼티 값을 처리

 

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import survey.AnsweredData;

@Controller
@RequestMapping("/survey")
public class SurveyController {
    @GetMapping
    public String form(){
        return "survey/surveyForm";
    }

    @PostMapping
    public String submit(@ModelAttribute("ansData")AnsweredData data){
        return "survey/submitted";
    }
}

 

 

ControllerConfig.java

 

package config;

import controller.HelloController;
import controller.RegisterController;
import controller.SurveyController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.MemberRegisterService;


@Configuration
public class ControllerConfig {

    @Autowired
    private MemberRegisterService memberRegSvc;

    @Bean
    public HelloController helloController(){
        return new HelloController();
    }

    @Bean
    public RegisterController registerController(){
        RegisterController controller =new RegisterController();
        controller.setMemberRegisterService(memberRegSvc);
        return controller;
    }

    @Bean
    public SurveyController surveyController(){
        return new SurveyController();
    }

}

 

 

surveyForm.jsp > 폼 전송을 위한 뷰

submitted.jsp > 폼 전송 후 결과를 보여주기 위한 뷰

 


13. Model을 통해 컨트롤러에서 뷰에 데이터 전달하기

> 컨트롤러는 뷰가 응답 화면을 구성하기 위해 필요한 데이터를 생성 및 전달해야한다. 

이때 사용하는 것이 Model. 

 

- 요청 매핑 어노테이션이 적용된 메서드의 파라미터로 Model을 추가

- Model 파라미터의 addAttribute() 메서드로 뷰에서 사용할 데이터 전달

 

model.addAttribue("greeting","안녕하세요, " + name);

> 속성이름 (이 이름을 사용해 데이터에 접근함)

${greeting} > 이런식으로

 

1) ModelAndView를 통한 뷰 선택과 모델 전달

> Model을 이용해서 뷰에 전달할 데이터 설정

> 결과를 보여줄 뷰 이름 리턴

ModelAndView를 사용하면 위 2가지를 모두 한 번에 처리할 수 있다. 

 

@GetMapping
public String form(Model model) {
    List<Question> questions = createQuestions();
    model.addAttribute("questions", questions);
    return "survey/surveyForm";
}

 

>

 

@GetMapping
public ModelAndView form() {
    List<Question> questions = createQuestions();
    ModelAndView mav=new ModelAndView();
    mav.addObject("questions",questions);
    mav.setViewName("survey/surveyForm");
	return mav;
}