Keep going

스프링 MVC 의 Controller(2) 본문

Records/Spring Framework

스프링 MVC 의 Controller(2)

코딩천재홍 2021. 2. 9. 23:21

4. Model이라는 데이터 전달자

 

- Controller의 메서드를 작성할 때는 특별하게 Model이라는 타입을 파라미터로 지정할 수 있다.

- Model 객체는 JSP에 컨트롤러에서 생성된 데이터를 담아서 잔달하는 역할을 하는 존재다.

- 이를 이용해서 JSP와 같은 뷰로 전달해야 하는 데이터를 담아서 보낼 수 있다.

- 메서드의 파라미터에 Model 타입이 지정된 경우에는 스프링은 특별하게 Model 타입의 객체를 만들어서 메서드에 주입하게 된다.

- Model은 모델 2 방식에서 사용하는 request.setAtrribute()와 유사한 역할을 한다.

 

Servlet에서 모델2 방식으로 데이터를 전달하는 방식
request.setAttribute("serverTime"new java.util.Date());
 
RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/jsp/home.jsp");
 
dispatcher.forward(request, response);
cs

 

스프링 MVC에서 Model을 이용한 데이터 전달
public String home(Model model) {
 
model.addAttribute("serverTime"new java.util.Date());
 
return "home";
}
cs

- 메서드의 파라미터를 Model 타입으로 선언하게 되면 자동으로 스프링 MVC에서 Model타입의 객체를 만들어 주기 때문에 개발자의 입장에서는 필요한 데이터를 담아 주는 작업만으로 모든 작업이 완료된다.

- Model을 사용해야 하는 경우는 주로 Controller에 전달된 데이터를 이용해서 추가적인 데이터를 가져와야 하는 상황이다.

- 예를 들어, 1) 리스트 페이지 번호를 파라미터로 전달받고, 실제 데이터를 View로 전달해야 하는 경우 2) 파라미ㅓㅌ들에 대한 처리 후 결과를 전달해야 하는 경우

 

4.1 @ModelAttribute 어노테이션

- 웹페이지의 구조는 Request에 전달된 데이터를 가지고 필요하다면 추가적인 데이터를 생성해서 화면으로 전달하는 방식으로 동작한다.

- Model의 경우는 파라미터로 전달된 데이터는 존재하지 않지만 화면에서 필요한 데이터를 전달하기 위해 사용

- 예를 들어 페이지 번호는 파라미터로 전달, 결과 데이터는 Model에 담아서 전달한다.

- 스프링 MVC의 Controller는 기본적으로 JAVA  Beans 규칙에 맞는 객체는 다시 화면으로 객체를 전달한다. 

- JAVA Beans 규칙은 단순히 생성자가 없거나 빈 생성자를 가져야 하며, getter/setter를 가진 클래스의 객체들을 의미

- 전달될 때에는 클래스명의 앞글자는 소문자로 처리된다. 반면 기본 자료형은 파라미터로 선언해도 화면까지 전달 X

 

SampleController에 추가
@GetMapping("/ex04")
    public String ex04(SampleDTO dto, int page) {
        
        log.info("dto: " + dto);
        log.info("page: "+ page);
        
        return "/sample/ex04";
    }
cs

- ex04()는 SampleDTO 타입과 int 타입의 데이터를 파라미터로 사용한다. 

- 결과를 확인하기 위해서 '/WEB-INF/views' 폴더 아래 sample 폴더를 생성하고 리턴값에서 사용한 'ex04()'에 해당하는 ex04.jsp 를 작성한다.

 

ex04.jsp의 일부
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http:// www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv = "Content-Type" content = "text/html;  charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
 
<h2>SAMPLEDTO ${sampleDTO } </h2>
<h2>PAGE ${page }</h2>
 
</body>
</html>
cs

실행할 URL : 'http://localhost:8080/sample/ex04?name=aaa&age=11&page=9' 

위에 URL 을 호출하면 화면에 SampleDTO 만이 전달된 것을 확인할 수 있다. int 타입으로 선언된 page 전달 X

 

실행 결과

- @ModelAttribute는 강제로 전달받은 파라미터를 Model에 담아서 전달하도록 할 때 필요한 어노테이션이다.

- @ModelAttribute가 걸린 파라미터는 타입에 관계없이 무조건 Model에 담아서 전달되므로, 파라미터로 전달된 데이터를 다시 화면에서 사용해야 할 경우에 유용하게 사용된다.

 

 

page에 @ModelAttribute 추가한 SampleController
@GetMapping("/ex04")
    public String ex04(SampleDTO dto, @ModelAttribute("page"int page) {
        
        log.info("dto: " + dto);
        log.info("page: "+ page);
        
        return "/sample/ex04";
    }
}
cs

- @ModelAttribute 가 붙은 파라미터는 화면까지 전달되므로 브라우저를 통해서 호출하면 아래와 같이 ${page}가 출력되는 것을 확인할 수 있다. (기본 자료형에 @ModelAttribute를 적용할 경우에는 반드시 @ModelAttribute("page")와 같이 값을 지정한다.)

 

 

 

4.2 RedirectAttributes

- Model 타입과 더불어서 스프링 MVC가 자동으로 전달해 주는 타입 중에는 Redirect Attributes 타입이 존재한다.

- RedirectAttributes는 조금 특별하게도 일회성으로 데이터를 전달하는 용도로 사용된다.

- RedirectAttributes는 기존에 Servlet에서는 response.sendRedirect()를 사용할 때와 동일한 용도로 사용된다.

 

Servlet에서 redirect 방식
response.sendRedirect("/home?name=aaa&age=10");
cs

 

스프링 MVC를 이용하는 redirect 처리
rttr.addFlashAttribute("name""AAA");
    rttr.addFlashAttribute("age"10);
 
    return "redirect:/";
cs

- RedirectAttributes는 Model과 같이 파라미터로 선언해서 사용하고, addFlashAttribute(이름, 값) 메서드를 이용해서 화면에 한 번만 사용하고 다음에는 사용되지 않는 데이터를 전달하기 위해서 사용한다.

 


 

5. Controller의 리턴 타입

- 스프링 MVC의 구조가 기존의 상속과 인터페이스에서 어노테이션을 사용하는 방식으로 변한 이후에 가장 큰 변화 중 하나는 리턴 타입이 자유로워 졌다는 점이다.

- Controller의 메서드가 사용할 수 있는 리턴 타입 종류

  • String : jsp를 이용하는 경우에는 jsp 파일의 경로와 파일이름을 나타내기 위해서 사용
  • void : 호출하는 URL과 동일한 이름의 jsp를 의미
  • VO, DTO 타입 : 주로 JSON 타입의 데이터를 만들어서 반환하는 용도로 사용
  • ResponseEntity 타입 : response 할 때 HTTP 헤더 정보와 내용을 가공하는 용도로 사용
  • Model, ModelAndView : Model로 데이터를 반환하거나 화면까지 같이 지정하는 경우에 사용
  • HttpHeaders : 응답에 내용 없이 Http 헤더 메시지만 전달하는 용도로 사용

5.1 void

- 메서드의 리턴 타입을 void로 지정하는 경우 일반적인 경우에는 해당 URL의 경로를 그대로 jsp 파일의 이름으로 사용한다.

 

SampleController의 일부
@GetMapping("/ex05")
    public void ex05() {
        log.info("/ex05........");
    }
cs

- 에러 메시지를 자세히 보면 에러 메시지의 원인이 'WEB-INF/views/sample/ex05.jsp'가 존재하지 않아서 생기는 문제라는 것을 볼 수 있다.

- 이것은 servlet-context.xml의 아래 설정과 같이 맞물려 URL 경로를 View로 처리하기 때문에 생기는 결과다.

 

servlet-context.xml의 일부
beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <beans:property name="prefix" value="/WEB-INF/views/" />
        <beans:property name="suffix" value=".jsp" />
    </beans:bean>
cs

 

5.2 String 타입

- void 타입과 더불어서 가장 많이 사용하는 것은 String 타입이다.

- String 타입은 상황에 따라 다른 화면을 보여줄 필요가 있을 경우에 유용하게 사용한다. (if~else 같은 처리)

- 일반적으로 String 타입은 현재 프로젝트의 경우 JSP 파일의 이름을 의미한다.

- 프로젝트 시 기본으로 만들어진 HomeController의 코드를 보면 String을 타입으로 사용한다.

 

HomeController의 일부
@RequestMapping(value = "/", method = RequestMethod.GET)
    public String home(Locale locale, Model model) {
        logger.info("Welcome home! The client locale is {}.", locale);
        
        Date date = new Date();
        DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);
        
        String formattedDate = dateFormat.format(date);
        
        model.addAttribute("serverTime", formattedDate );
        
        return "home";
    }
cs

- home() 메서드는 'home'이라는 문자열을 리턴했기 때문에 경로는 'WEB-INF/views/home.jsp' 경로가 된다.

 

String 타입에는 키워드를 붙여서 사용 할 수 있다.

- redirect : 리다이렉트 방식으로 처리하는 경우

- forward : 포워드 방식으로 처리하는 경우

 

5.3 객체 타입

- Controller의 메서드 리턴 타입을 VO나 DTO 타입 등 복합적인 데이터가 들어간 객체 타입으로 지정할 수 있는데 , 이 경우는 주로 JSON 데이터를 만들어 내는 용도로 사용한다.

 

porm.xml에 추가되는 Jackson-databind 라이브러리
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.4</version>
        </dependency>
cs

 

SampleController의 일부
@GetMapping("/ex06")
    public @ResponseBody SampleDTO ex06() {
        log.info("/ex06.......");
        
        SampleDTO dto = new SampleDTO();
        dto.setAge(10);
        dto.setName("홍길동");
        
        return dto;
    }
cs

 

 

5.4 ResponseEntity 타입

- Web을 다루다 보면 HTTP 프로토콜의 헤더를 다루는 경우도 종종 있다.

- 스프링 MVC 의 사상 HttpServletResponse나 HttpServletResponse를 직접 핸들링하지 않아도 이런 작업이 가능하도록 작성되었기 때문에 이러한 처리를 위해 ResponseEntity를 통해서 원하는 헤더 정보나 데이터를 전달할 수 있다.

 

SampleController의 일부
@GetMapping("/ex07")
    public ResponseEntity<String> ex07() {
        System.out.println("ex07...............");
        // {"name":"홍길동"}
        String msg = "{\"name\":\"홍길동\"}";
 
        HttpHeaders header = new HttpHeaders();
        header.add("Content-Type""application/json;charset=UTF-8");
        return new ResponseEntity<>(msg, header, HttpStatus.OK);
    }
cs

ReponseEntity는 HttpHeaders 객체를 같이 전달할 수 있고, 이를 통해서 원하는 HTTP 헤더 메시지를 가공하는 것이 가능하다.

ex07()의 경우 브라우저에는 JSON 타입이라는 헤더 메시지와 200 OK라는 상태 코드를 전송한다.

 

 

5.5 파일 업로드 처리

Controller의 많은 작업은 스프링 MVC를 통해서 처리하기 때문에 개발자는 자신이 해야 하는 역할에만 집중해서 코드를 작성할 수 있지만, 조금 신경 써야 하는 부분이 있따면 파일을 업로드하는 부분에 대한 처리이다.

파일 업로드를 하기 위해서는 전달되는 파일 데이터를 분석해야 하는데, 이를 위해서 Servlet3.0전까지는 commons의 파일업로드를 이용하거나 cos.jsr 등을 이용해서 처리를 해 왔습니다. Servlet 3.0이후 (Tomcat7.0)에는 기본적으로 업로드되는 파일을 처리할 수 있는 기능이 추가 되어 있으므로 더 이상 추가적인 라이브러리가 필요하지 않습니다.

조금아쉬운 점은 'Spring Legacy Project'로 생성되는 프로젝트의 경우 Servlet 2.5를 기준으로 생성되기 때문에 3.0이후에 지원되는 설정을 사용하기 어렵다는 점이다.

 

servlet-context.xml 설정

- servlet-context.xml은 스프링 MVC의 특정한 객체(빈)를 설정해서 파일을 처리한다.

- 다른 객체를 설정하는 것과 달리 파일 업로드의 경우에는 반드시 id 속성의 값을 'multipartResolver'로 정확하게 지정해야 하므로 주의가 필요하다.

 

servlet-context.xml의 일부
<beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
 
    <beans:property name="defaultEncoding" value="utf-8"></beans:property>
 
    <!-- 1024 * 1024 * 10 bytes 10 MB -->
 
    <beans:property name="maxUploadSize" value="104857560"></beans:property>
 
    <!--1024 * 1024 * 2 bytes 2MB  -->
 
    <beans:property name="maxUploadSizePerFile" value="2097152"></beans:property>
 
    <beans:property name="uploadTempDir" value="file:/C:/upload/tmp"></beans:property>
 
    <beans:property name="maxInMemorySize" value="10485756"></beans:property>
    
</beans:bean>
cs

maxUploadSize는 한 번의 Request로 전달될 수 있는 최대의 크기를 의미하고,

maxUploadSizePerFile은 하나의 파일 최대 크기, 

maxInMemorySize는 메모리상에서 유지하는 최대의 크기를 의미한다. 만일 이 크기 이상의 데이터는 uploadTempDir에 임시 파일의 형태로 보관된다.

uploadTempDir에서 절대 경로를 이용하려면 URL 형태로 제공해야 하기 때문에 'file:/'로 시작한다.

defaultEncoding 은 업로드 하는 파일의 이름이 한글일 경우 깨지는 문제를 처리한다.

 

SampleController의 일부
@GetMapping("/exUpload")
    public void exUpload() {
        log.info("/exUpload.........");
    }
cs

 

exUpload.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
 
<!DOCTYPE HTML>
 
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>title is title</title>
</head>
 
<body>
    <form action="/sample/exUploadPost" method="post"
        enctype="multipart/form-data">
        <div>
            <input type='file' name='files'>
        </div>
 
        <div>
            <input type='file' name='files'>
        </div>
 
        <div>
            <input type='file' name='files'>
        </div>
 
        <div>
            <input type='file' name='files'>
        </div>
 
        <div>
            <input type='file' name='files'>
        </div>
 
        <div>
            <input type='submit'>
        </div>
    </form>
</body>
</html>
 
cs

exUpload.jsp는 여러 개의 파일을 한꺼번에 업로드하는 예제로 작성해 보겠습니다.

<form>태그의 action속성, method속성, enctype속성에 주의 해서 작성해야한다.

브라우저는 아래와 같은 모습입니다.

 

 

exUpload.jsp의 action 속성값은 '/sample/exUploadPost'로 작성되었으므로, 이에 맞는 메서드를 SampleController에 추가한다.

 

SampleController의 일부
@PostMapping("/exUploadPost")
    public void exUploadPost(ArrayList<MultipartFile> files) {
        
        files.forEach(file -> {
            log.info("---------------------");
            log.info("name: " +file.getOriginalFilename());
            log.info("size: " +file.getSize());
        });
    }
cs

- 스프링 MVC는 전달되는 파라미터가 동일한 이름으로 여러 개 존재하면 배열로 처리가 간으하므로 파라미터를 MultipartFile의 배열 타비으로 작성한다.

- 현재 설정은 한 파일의 최대 크기가 2MB이므로 그보다 작은 크기의 파일을 지정해서 업로드를 테스트한다.

- 실제로 파일을 업로드해 보면 아래와 같은 결과를 볼 수 있다.

 

위의 그림에서 중간에 보이는 로그는 SampleController에서 업로드 정보가 올바르게 처리되는 것을 보여주고 있다.

최종 업로드를 하려면 byte[]를 처리해야 하는데 위에는 아직 처리하지 않은 상태다.

 

 


6. Controller의 Exception 처리

- Controller를 작성할 때 예외 상황을 고려하면 처리해야 하는 작업이 엄청나게 늘어날 수 밖에 없다.

- 스프링 MVC에서는 이러한 작업을 다음과 같은 방식으로 처리한다.

  • @ExceptionHandler와 @ControllerAdvice를 이용한 처리
  • @ResponseEntity를 이용하는 예외 메시지 구성

 

6.1 @ControllerAdvice

- @ControllerAdvice는 뒤에서 배우게 되는 AOP를 이용하는 방식이다.

- AOP는 핵심적인 로직은 아니지만 프로그램에서 필요한 '공통적인 관심사는 분리' 하자는 개념이다.

- Controller를 작성할 때는 메서드의 모든 예외상황을 전부 핸들링해야 한다면 중복적이고 많은 양의 코드를 작성해야 하나, AOP 방식을 이용하면 공통적인 예외상황에 대해서는 별도로 @ControllerAdvice를 이용해서 분리하는 방식이다.

 

실습을 위해 프로젝트에 org.zerock.exception이라는 패키지를 생성하고, CommonExceptionAdvice 클래스를 생성한다.

 

CommonExceptionAdvice는 @ControllerAdvice 어노테이션을 적용하지만 예외 처리를 목적으로 생성하는 클래스이므로 별도의 로직을 처리하지는 않는다.

 

CommontExceptionAdvice 클래스
package org.zerock.exception;
 
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
 
import lombok.extern.log4j.Log4j;
 
@ControllerAdvice
@Log4j
 
public class CommonExceptionAdvice {
    
    @ExceptionHandler(Exception.class)
    public String except(Exception ex, Model model) {
        
        log.error("Exception ......" + ex.getMessage());
        model.addAttribute("exception", ex);
        log.error(model);
        return "error_page";
    }
}
 
cs

CommonExceptionAdvice 클래스에는 @ControllerAdvice라는 어노테이션과 @ExceptionHandler라는 어노테이션을 사용하고 있다.

- @ControllerAdvice는 해당 객체가 스프링의 컨트롤러에서 발생하는 예외를 처리하는 존재임을 명시하는 용도로 사용하고

- @ExceptionHandler는 해당 메서드가 ( ) 들어가는 예외 타입을 처리한다는 것을 의미한다.

- @ExceptionHandler 어노테이션 속성으로는 Exception클래스 타입을 지정할 수 있다.

- 위에 Exception.class 지정하였으므로 모든 예외에 대한 처리가 except()만을 이용해서 처리할 수 있다.

- 만일 특정한 타입의 예외를 다루고 싶다면 Exception.class 대신에 구체적인 예외의 클래스를 지정해야 한다.

-JSP 하면에서도 구체적인 메시지를 보고 싶다면 Model을 이용해서 전달하는 것이 좋다.

- org.zerock.exception 패키지는 servlet-context.xml에서 인식하지 않기 때문에 <component-scan>을 이용해서 해당 패키지의 내용을 조사하도록 한다.

<context:component-scan base-package="org.zerock.exception"/>
cs

 

CommonExceptionAdvice의 except()의 리턴값은 문자열이므로 JSP 파일의 경로가 된다.

JSP는 error_page.jsp이므로 /WEB-INF/views 폴더 내에 작성해야 한다.

 

error_page.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
   <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
   <%@ page session="false" import="java.util.*" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-KR">
<title>Insert title here</title>
</head>
<body>
 
<h4><c:out value="${exception.getMessage()}"></c:out></h4>
 
<ul>
<c:forEach items="${exception.getStackTrace() }" var="stack">
<li><c:out value="${stack }"></c:out></li>
</c:forEach>
 
</ul>
 
</body>
</html>
cs

 

예외의 메시지가 정상적으로 출력되는지 확인해 보려면 고의로 숫자나 날짜 등의 파라미터 값을 변환에 문제 있게 만들어서 호출해 볼 수 있다.

 

age에 숫자가 아닌 문자열로 전송한 경우

 

6.2 404 에러 페이지

WAS의 구동 중 가장 흔한 에러와 관련된 HTTP 상태 코드는 '404' 와 '500' 에러 코드이다.

500 메시지는 'Internal Server Error'이므로 @ExceptionHandler를 이용해서 처리되지만, 잘 못된 URL을 호출할 때 보이는 404 에러 메시지의 경우는 조금 다르게 처리하는 것이 좋다.

서블릿이나 JSP를 이용했던 개발 시에는 web.xml을 이용해서 별도의 에러 페이지를 지정할 수 있다.

에러 발생 시 추가적인 작업을 하기는 어렵기 때문에 스프링을 이용해서 404와 같이 WAS 내부에서 발생하는 에러를 처리하는 방식을 알아두는 것이 좋다.

 

스프링 MVC의 모든 요청은 DispatcherServlet을 이용해서 처리되므로 404 에러도 같이 처리할 수 있도록 web.xml을 수정한다. 

 

web.xml의 일부
<!-- Processes application requests -->
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>throwExceptionIfNoHandlerFound</param-name>
            <param-value>true</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
cs

 

org.zerock.exception.CommonExceptionAdvice에는 다음과 같이 메서드를 추가한다.

 

@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handle404(NoHandlerFoundException ex) {
        
        return "custom404";
}
cs

에러 메세지는 custom404.jsp를 작성해서 처리한다.

 

custom404.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>해당 URL은 존재하지 않습니다.</h1>
</body>
</html>
cs

 

 


출처 : 코드로 배우는 스프링 웹 프로젝트 [구멍가게 코딩단]

'Records > Spring Framework' 카테고리의 다른 글

영속/비즈니스 계층의 CRUD 구현  (0) 2021.02.15
스프링 MVC 프로젝트의 기본 구성  (0) 2021.02.15
스프링 MVC의 Controller  (0) 2021.02.08
스프링 MVC의 기본 구조  (0) 2021.02.05
MyBatis와 스프링 연동  (0) 2021.02.03
Comments