Spring MVC 기반으로 개발 하는 경우, URL 기준이 아닌, Method 기준 으로 AOP 를 사용할 때가 있다. 스타일 차이라고 생각 한다.


보통 Interceptor 를 사용 하면, HttpServletRequest, HttpServletResponse 객체를 기본적으로 사용할 수 있다. Tomcat Filter 의 기능과 비슷하기 때문이다. 처리 방법에 따라 다르겠지만, 시점이 Controller 호출 전 이기 때문에, @ModelAttribute 를 사용하여 개발 한다면, Http 요청 시 사용되는 파라미터가 Model 로 값이 들어가기 전이기 때문에, 원시 적인 방법인 request.getParameter(String key) 함수를 사용해야 한다. 더 나은 방법은 AOP 기법을 활용 하는 것이다.


AOP 는 Method 기준으로 사용되기 때문에 원하는 시점을 선택하여 적용이 가능 하다. Controller 를 호출 하기전에 끼어드는 방식으로 Around 방식을 사용하면 Model 에 값이 들어간 후 이기 때문에, 데이터의 가공을 편하게 할 수 있다. Model은 joinPoint.getArgs() 함수를 사용하여 값 확인이 가능 하다. 아래는 예제 소스 이다.


/**
 * CustomAspect
 *
 * @author whitelife
 * @since 2014.10.05
 * @version 0.1
 */
public class CustomAspect {

    /**
     * Custom Aspect
     *
     * @param joinPoint Proxy Method Info
     * @return joinPoint
     * @throws Throwable
     */
    public Object customAspect(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = null;

        // (Before) doing...        

        for (Object obj : joinPoint.getArgs()) {
            if (obj instanceof HttpServletRequest || obj instanceof MultipartHttpServletRequest) {
                HttpServletRequest request = (HttpServletRequest) obj;

                // Doing...

            }
        }

        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            throw e;
        }

        // (After) doing...     

        return result;
    }
}


ProceedingJoinPoint 클래스는 Target Class, Target Method 정보 를 담고 있다. proceed 메소드 호출 시 Target Method 를 실행 한다. 보통 joinPoint.getArgs() 함수 를 사용하여 Target Method 의 인자 값을 확인하여 사용하지만, 매번 Controller Method 에 인자로 HttpServletRequest 를 넣어줘야 하는 불편함이 있다.


HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();


위 방법을 사용하면, AOP 에서도 HttpServletRequest 객체에 접근이 가능하다. Controller 개발 시 인자로 HttpServletRequest 를 받을 필요가 없다. 편리하게 사용할 수 있겠다.


ServletRequestAttributes 는 HttpRequest 가 오는 경우, RequestContextListener.requestInitialized(ServletRequestEvent requestEvent) 함수에 의해 값이 전달 되기 때문에 값을 받을 수 있는 것이다. RequestContextListener 의 일부 이다. 아래와 같이 구현 되어 있다.


public void requestInitialized(ServletRequestEvent requestEvent) {
    if (!(requestEvent.getServletRequest() instanceof HttpServletRequest)) {
        throw new IllegalArgumentException(
                "Request is not an HttpServletRequest: "
                        + requestEvent.getServletRequest());
    }

    HttpServletRequest request = (HttpServletRequest) requestEvent
            .getServletRequest();
    ServletRequestAttributes attributes = new ServletRequestAttributes(
            request);
    request.setAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE, attributes);
    LocaleContextHolder.setLocale(request.getLocale());
    RequestContextHolder.setRequestAttributes(attributes);
}


ArgumentResolver 를 사용하여 구현도 가능 하지만, 해당 기능은 공통적으로 필요한 값을 추가 하는 용도라고 판단이 된다.


무조건 정답이라고 할 수는 없다. 적합한 방법을 사용하여 작업 하면 된다고 판단 된다.


참고 사이트


WEB 서버를 사용하는 경우 정적 파일 이나, Default 요청에 대한 자원 관리는 WAS 가 처리하지 않고, WEB 서버에게 위임 한다.


WAS(Web Application Server) 만 사용하는 경우 Servlet 등록 하고, 프로그래밍 하여 동적 페이지를 생성 한다. 모든 요청을 받는 Servlet 을 작성 하는 경우 별도 처리를 해줘야 한다.


Spring MVC 를 사용한다고 하면, 대표적으로 DispatcherServlet 를 등록하고, 모든 요청을 받게 설정을 한다.


web.xml


<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:dispatcher-servlet.xml
        </param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>


위와 같이 하는 경우 정적 파일 까지 포함해서 DispatcherServlet 이 요청을 받게 된다. 해당 요청을 처리하기 위해서는 별도의 설정이 필요 하다.


1. mvc:view-controller


dispatcher-servlet.xml


<mvc:view-controller path="/" view-name="index"/>


위 소스를 추가 하자. DispatcherServlet 에 view-controller 설정을 했다는 것은 해당 path 의 요청이 오면 view-name 으로 페이지를 연결 시켜 준다. 해당 기능은 Spring 의 AbstractController 가 AbstractUrlViewController 에게 위임 하고 있다. 아래 코드는 실제 구현 되어있는 부분이다.


protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) {
    String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
    String viewName = getViewNameForRequest(request);
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Returning view name '" + viewName
                + "' for lookup path [" + lookupPath + "]");
    }
    return new ModelAndView(viewName,
            RequestContextUtils.getInputFlashMap(request));
}


view-controller 기능을 사용하면 직접 구현하지 않아도 편하게 개발이 가능 하다.


2. mvc:resources


dispatcher-servlet


<mvc:resources location="/css/**" mapping="/css/**"/>
<mvc:resources location="/image/**" mapping="/image/**"/>
<mvc:resources location="/js/**" mapping="/js/**"/>


위 소스를 추가 하자. 설명은 Tomcat 으로 한다. location 에 대한 요청이 들어오면 해당 요청을 Tomcat 의 DefaultServlet 에 위임 한다. DefaultServlet 는 tomcat/conf/web.xml 을 참고 하면 된다.


web.xml


<servlet>
    <servlet-name>default</servlet-name>
    <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
    <init-param>
        <param-name>debug</param-name>
        <param-value>0</param-value>
    </init-param>
    <init-param>
        <param-name>listings</param-name>
        <param-value>false</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<!-- The mapping for the default servlet -->
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>


기본적으로 등록 되어 있는 Servlet 이다. webapps 경로를 찾아 mapping 에 대한 파일을 응답 한다.


설명한 두가지 방법을 활용 한다면, WAS 만 사용하는 프로젝트나, 아니면 임시로 설정하여 개발하는대 많은 도움이 될 것이다.


소개


Spring MVC 사용 시 DispatcherServlet 기능을 사용 한다. requestUri 에 따라 Controller 로 분기를 하고, 비지니스 로직 처리 후 Resolver 를 사용하여 해당 JSP 파일을 찾아 응답 하게 되는대 그 사이의 시점을 잡아 처리 하는 부분이 AbstractView 의 기능이다.


범용적으로 사용하는 Resolver 는 InternalResourceViewResolver 이다. 우리는 그 전에 DownloadView 를 구현하여 파일을 다운로드 할 것이다.


적용 방법

1. DownloadView


public class DownloadView extends AbstractView {

    public DownloadView() {
        setContentType("applicaiton/download;charset=utf-8");
    }

    private void setDownloadFileName(String fileName, HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
        String userAgent = request.getHeader("User-Agent");

        boolean isIe = userAgent.indexOf("MSIE") != -1;

        if(isIe){
            fileName = URLEncoder.encode(fileName, "utf-8");
        } else {
            fileName = new String(fileName.getBytes("utf-8"));
        }

        response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\";");
        response.setHeader("Content-Transfer-Encoding", "binary");
    }

    private void downloadFile(File downloadFile, HttpServletRequest request, HttpServletResponse response) throws Exception {
        OutputStream out = response.getOutputStream();
        FileInputStream in = new FileInputStream(downloadFile);

        try {
            FileCopyUtils.copy(in, out);
            out.flush();
        } catch (Exception e) {
            throw e;
        } finally {
            try { if (in != null) in.close(); } catch (IOException ioe) {}
            try { if (out != null) out.close(); } catch (IOException ioe) {}
        }
    }

    @Override
    protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        try {
            this.setResponseContentType(request, response);

            File downloadFile = (File) model.get("downloadFile");

            if (logger.isDebugEnabled()) {
                logger.debug("downloadFile: " + downloadFile);
            }

            this.setDownloadFileName(downloadFile.getName(), request, response);

            response.setContentLength((int) downloadFile.length());
            this.downloadFile(downloadFile, request, response);
        } catch (Exception e) {
            throw e;
        }
    }
}


2. applicationContext.xml


파일 하단에 추가 하자.


<beans:bean id="downloadView" class="kr.co.whitelife.DownloadView" />


3. SampleController


View 를 DownloadView 로 교체 한 후 파일 객체를 넘겨 주자.


@Controller
public class SampleController {

    @Resource(name="downloadView")
    private View downloadView;

    @RequestMapping(value="/sample", method=RequestMethod.GET})
    public ModelAndView sample() {
        ModelAndView mav = new ModelAndView();
        mav.setView(this.downloadView);

        File downloadFile = new File("downloadFile");
        mav.addObject("downloadFile", downloadFile);

        return mav;
    }
}


4. SampleRequest


파일을 첨부 하여 http://localhost:8080/sample 요청 해 보자.


Spring MVC 사용 시 SimpleMappingExceptionResolver 설정을 해놓았다. Controller 에서 공통으로 필요한 부분을 AOP 로 만들어서 사용 하는대, Exception 발생 시 AOP Proxy 에서 예외가 발생 했다. 정작 필요한 에러 메시지는 볼 수 없었다.


일반적으로 AOP 설정을 하는 경우 아래와 같이 선언을 했었다.


<aop:config>    
    // Doing...
</aop:config>


<aop:config> 에 아무런 설정 값을 주지 않은 경우 기본적으로 JDK Proxy 로 동작 한다. Spring 에서 제공 하는 InvocableHandlerMethod.invoke(Object[] arg) 메소드를 사용 하여 실제 targetClass를 호출 한다. Java Reflection 기반 이라고 생각 하면 된다.


에러 메시지의 일부 이다. invoke 호출 시 예외가 발생 한다. targetClass 가 Interface 를 구현하고 있는 경우 instance 를 인식하지 못해 예외가 발생 하는 것 같다.


java.lang.IllegalStateException: The mapped controller method class 'com.xxx.xxx' is not an instance of the actual controller bean instance '$Proxy22'. If the controller requires proxying (e.g. due to @Transactional), please use class-based proxying.
HandlerMethod details: 
Controller [$Proxy22]
Method [public org.springframework.web.servlet.ModelAndView com.xxx.xxx.xxxxxx() throws java.lang.Exception]
// 생략...


문제 해결을 위해서는 Class 기반 proxy 사용을 해야 한다.
please use class-based proxying. 이 메시지가 친절하게 방법을 알려 준다.


CGLIB Proxy 를 사용하려면 아래와 같이 설정 값을 변경 한다.


<aop:config proxy-target-class="true">
    // Doing...
</aop:config>


테스트를 해보자. 정상적으로 동작 할 것 이다.


참고 사이트


소개


Spring 3.0 이상 부터는 Ajax 처리 시 아래와 같은 방법으로 쉽게 처리가 가능 하다.


RequestMapping(value="/sample", method=RequestMethod.GET})
public @ResponseBody String sample() {
    return "sample";
}


하지만 범용적으로 사용하고 싶을 때가 있을 것 이다. 그런 경우 사용하는 방법 이다. 아래 설명을 참고하여 적용 해 보자.


적용 방법

1. AjaxView


public class AjaxView extends AbstractView {

    public AjaxView() {
        setContentType("text/json;charset=utf-8");
    }

    @Override
    protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        try {
            this.setResponseContentType(request, response);

            String reponseString = "";

            // responseString Doing...

            PrintWriter writer = response.getWriter();
            writer.print(reponseString);
            writer.flush();

            try { if (writer != null) { writer.close(); } } catch (IOException ioe) {}
        } catch (Exception e) {
            throw e;
        }
    }
}


2. applicationContext.xml


파일 하단에 추가 하자.


<beans:bean id="ajaxView" class="kr.co.whitelife.AjaxView" />


3. SampleController


View 를 ajaxView 로 교체 하자.


@Controller
public class SampleController {
    @Resource(name="ajaxView")
    private View ajaxView;

    @RequestMapping(value="/sample", method=RequestMethod.GET})
    public ModelAndView sample() {
        ModelAndView mav = new ModelAndView();
        mav.setView(this.ajaxJsonView);

        // Doing...

        return mav;
    }
}


4. Sample Request


http://localhost:8080/sample 요청 해 보자.


설명


Spring MVC 에서 Excel 기능을 제공하는 View 가 있다.


  • AbstractExcelView (라이브러리에서 제공 하는 소스 일부 이다.)
@Component("AbstractExcelView")
public abstract class AbstractExcelView extends AbstractView {

    @Override
    protected final void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // doing
    }

    /**
     * Subclasses must implement this method to create an Excel HSSFWorkbook document,
     * given the model.
     * @param model the model Map
     * @param workbook the Excel workbook to complete
     * @param request in case we need locale etc. Shouldn't look at attributes.
     * @param response in case we need to set cookies. Shouldn't write to it.
     */
    protected abstract void buildExcelDocument(
            Map<String, Object> model, HSSFWorkbook workbook, HttpServletRequest request, HttpServletResponse response)
            throws Exception;
}


AbstractView 기능을 구현하기 위해서는 기본적으로 renderMergedOutputModel 메소드를 구현해야 한다. 시점은 Controller 처리 후 라고 보면 쉽게 이해 할 수 있다. model 값은 Controller 에서 modelMap, ModelAndView 에 넣었던 데이터들 이라고 생각 하면 된다.


AbstractExcelView 의 경우에는 Excel 생성 부분 buildExcelDocument 메소드만 Override 하여 구현 하면 됬는대, 인자로 넘어오는 HSSFWorkbook 는 Excel2003 버전을 지원 한다. 확장자 (.xls) 그 이상 버전을 지원하는 poi 라이브러리를 사용 하는 경우 AbstractView 를 직접 구현해야 한다.


적용 방법

1. Dependency Library 추가


<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>3.10.1</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>3.10.1</version>
</dependency>


2. SampleController 구현 하기


  • SampleController
@Controller("SampleController")
public class SampleController {

    @RequestMapping(value="/sample", method=RequestMethod.POST)
    public ModelAndView sample () {
        ModelAndView mav = new ModelAndView();
        mav.setViewName("SampleExcelView");

        return mav;
    }

}


3. SampleExcelView 구현 하기


  • SampleExcelView
@Component("SampleExcelView")
public class SampleExcelView extends AbstractView {

    /** The content type for an Excel response */
    private static final String CONTENT_TYPE = "application/vnd.ms-excel";

    public AdminExcelView() {
        setContentType(CONTENT_TYPE);
    }

    protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        try {
            Workbook workbook = new XSSFWorkbook();
            XSSFSheet sheet = (XSSFSheet) workbook.createSheet();

            // Doing

            ServletOutputStream out = response.getOutputStream();
            workbook.write(out);

            if (out != null) out.close();
        } catch (Exception e) {
            throw e;
        }
    }
}


Doing 부분을 AbstractExcelView 의 buildExcelDocument 구현하는 것 처럼 작성 하면 Excel2007 이상 지원하는 (.xlsx) 파일을 작성 할 수 있다.


참고 사이트


1. Dependency Library 추가


Java Poi 라이브러리를 사용 Maven pom.xml 에 추가 하자.


<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>3.0.2-FINAL</version>
</dependency>


2. AbstractExcelView 구현 하기


Spring 에서 제공 하는 AbstractView 기능을 확장하여 사용 한다.

TestExcelView.java
@Component("TestExcelView")
public class TestExcelView extends AbstractExcelView {

    @Override
    protected void buildExcelDocument(Map<String, Object> model, HSSFWorkbook workbook, HttpServletRequest request, HttpServletResponse response) throws Exception {

        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        HSSFSheet sheet = workbook.createSheet();
        HSSFRow header = sheet.createRow(0);
        header.createCell((short) 0).setCellValue("Test1");
        header.createCell((short) 1).setCellValue("Test2");
        header.createCell((short) 2).setCellValue("Test3");
        header.createCell((short) 3).setCellValue("Test4");
        header.createCell((short) 4).setCellValue("Test5");

        HSSFRow row = sheet.createRow(1);
        row.createCell((short) 0).setCellValue("Test1");
        row.createCell((short) 1).setCellValue("Test2");
        row.createCell((short) 2).setCellValue("Test3");
        row.createCell((short) 3).setCellValue("Test4");
        row.createCell((short) 4).setCellValue("Test5");
    }

}


3. Controller 구현 하기


ModelAndView Return 시 앞서 구현한 TestExcelView 로 변경 하자.

TestController.java
@RequestMapping(value="/testExcel.xls", method=RequestMethod.GET)
public ModelAndView testExcel(HttpServletRequest request) {
    ModelAndView mav = new ModelAndView();

    mav.setViewName("TestExcelView");

    return mav;
}


4. 요청 하기


http://localhost:8080/testExcel.xls

참고 서적
  • Spring Recipes - A Problem-Solution Approach, 2nd Edition (Apress, 2010) (364~365)


설명


Controller 에 @InitBinder 기능을 추가로 사용 한다. WebDataBinder.class 를 인자로 받게 된다.

Sample


binder.registerCustomEditor(requiredType, field, propertyEditor);
  • requireType: 파라미터 Class
  • field: 파라미터 명
  • propertyEditor: 파라미터 처리 핸들러

해당 코드는 Integer[] 타입의 tests=123&tests=124&tests=125 요청을 처리 하는 핸들러 이다. 필요에 따라 setAsText 함수를 Override 하여 값 수정 후 setValue(text) 처리를 하자.


@InitBinder
protected void initBinder(WebDataBinder binder) {

    binder.registerCustomEditor(Integer[].class, "tests", new PropertyEditorSupport() {

        public void setAsText(String text) throws IllegalArgumentException {
            // text: field value

            if (text.indexOf("&") == -1) {
                setValue(Integer.valueOf(text.split("=")[1]));
                return;
            }

            List<Integer> tests = new ArrayList<Integer>();

            for (String test : text.split("&")) {
                tests.add(Integer.valueOf(test.split("=")[1]));
            }

            setValue(tests.toArray());
        }
    });
}


확인 하기


@RequestMapping(value="/request", method=RequestMethod.POST)
public @ResponseBody String request(Test test) throws Exception {
    return Arrays.toString(test.getTests());
}

참고 사이트


+ Recent posts