AOP pointcut 에 1개 이상의 조건을 사용 하는 경우 AND&&, OR|| 를 사용 한다. Java Annotation 방식으로 사용 하지 않고, XML 에서 사용 하는 경우 예외가 발생 한다.


XML은 기본적으로 <, >, & 등 사용 되고 있으므로, 이스케이프 문자로 변환 해야 한다.


  • &lt; <
  • &gt; >
  • &amp; &


아래와 같이 &amp; 로 수정해서 사용 하자.


<aop:pointcut id="test" expression="execution(public * *..*Sample.*(..)) &amp;&amp; !execution(public * *..*Sample.exclude(..))"/>


참고 사이트


Spring MVC 기능을 기반으로 개발 하면, 핵심 기능인 DI(Dependency Injection) 기능을 많이 사용하게 된다. 의존성 주입 이라고 보면 되겠다. setSample(new Sample()) 강제적으로 setter 할 필요 없이, Spring Container 가 @Inject, @Autowired, @Resource 를 사용하여 대신 setter 를 해준다고 보면 된다.


생각을 반대로 바꿔보자. Spring Container 에게 특정 객체를 달라고 요청 하는 것이다. 반대로 객체를 받아오는 경우를 DL(Dependency Lookup) 이라 한다. Spring ApplicationContext 에게 getBean(String id, class<?> clazz) 함수를 호출하여 반대로 필요한 객체를 받게 된다.


객체는 Bean Scope 의 영향을 받는다.


  • singleton: 그 말대로 하나의 객체만 존재 한다.
  • prototype: 객체를 매번 생성 하기 때문에, 다른 객체가 제공 된다.
  • request: http 요청을 기준으로 객체를 제공 한다. http 요청 생명 주기에 단 하나의 객체만 존재 한다. 다른 http 요청에 영향 받지 않는다.
  • session: session 발생을 기준으로 객체를 제공 한다. session 생명 주기에 단 하나의 객체만 존재 한다.
  • global session: 테스트를 해봐야 할 것 같다. (WAS의 ServletContext 와 같은 개념인듯 싶다.)


DL(Dependency Lookup) 의 사용을 위해서는 ApplicationContextAware 인터페이스 구현을 필요로 한다.


public class Sample implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public void sample() {
        Target target = this.applicationContext.getBean("ID", Target.class); // DL

        // Doing...
    }
}


위 샘플 소스를 참조 하면 ApplicationContext 를 받아 왔다. 이제 반대로 객체를 받아서 사용해 보자. 중앙에서 관리 하는 기능을 구현해야 할 일이 있다면, 편하게 사용되는 것 같다.


Query 실행 시 null parameter 가 있는 경우 발생 한다. 해결 방법은 jdbcType 을 명시 한다. #{value, jdbcType=VARCHAR}, Global 옵션으로 설정 하는 경우 아래와 같이 mybatis-config.xml 파일에 추가 하자.


<setting name="jdbcTypeForNull" value="NULL" />

참고 사이트


Tomcat Work Directory 를 초기화 한다. Eclipsc 의 경우 Servers > 마우스 오른쪽 > Clean Tomcat Work Directory 클릭, 서버를 재 시작 하자.

JSP 페이지 에서 날짜 값을 파라미터로 전달 할 때, 서버에서 String 으로 오는 날짜 값을 파싱하여, Date 객체로 넣는 작업이 예외 발생 시 Bad Request 가 발생 하였다. 이를 해결 하기 위해서는 직접 파싱 후 바인딩 하는 작업을 해줘야 한다. @initBinder 기능을 사용하면 되겠다.


@InitBinder
public void initBinder(WebDataBinder binder) throws Exception {
    binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {

        public void setAsText(String text) throws IllegalArgumentException {
            try {
                setValue(new SimpleDateFormat("yyyy-MM-dd").parse(text));
            } catch (ParseException e) {
                setValue(null);
            }
        }
    });
}


요청을 받는 Controller 에 해당 코드를 추가 한다. setAsText(String text) 구현은 자유롭게 하면 되겠다. 위 코드는 샘플 이다. String 으로 전달되는 값을 Date 객체로 변경하여 넣어준다. 위 코드를 작성 했다면, Date 로 전달 되는 값을 받아볼 수 있게 된다.


서버에서 Paging 에 대한 로직 처리 후, jsp include, param 기능을 사용하여 구현 한다.


1. Paging

Paging 기능을 구현하는 VO Class 이다. setTotalCount(int totalCount) 호출 시 makePaging() 함수 호출 한다.


/**
 * Paging
 *
 * @author whitelife
 * @since 2014.10.05
 * @version 0.1
 */
public class Paging {
    private int pageSize; // 게시 글 수
    private int firstPageNo; // 첫 번째 페이지 번호
    private int prevPageNo; // 이전 페이지 번호
    private int startPageNo; // 시작 페이지 (페이징 네비 기준)
    private int pageNo; // 페이지 번호
    private int endPageNo; // 끝 페이지 (페이징 네비 기준)
    private int nextPageNo; // 다음 페이지 번호
    private int finalPageNo; // 마지막 페이지 번호
    private int totalCount; // 게시 글 전체 수

    /**
     * @return the pageSize
     */
    public int getPageSize() {
        return pageSize;
    }

    /**
     * @param pageSize the pageSize to set
     */
    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }

    /**
     * @return the firstPageNo
     */
    public int getFirstPageNo() {
        return firstPageNo;
    }

    /**
     * @param firstPageNo the firstPageNo to set
     */
    public void setFirstPageNo(int firstPageNo) {
        this.firstPageNo = firstPageNo;
    }

    /**
     * @return the prevPageNo
     */
    public int getPrevPageNo() {
        return prevPageNo;
    }

    /**
     * @param prevPageNo the prevPageNo to set
     */
    public void setPrevPageNo(int prevPageNo) {
        this.prevPageNo = prevPageNo;
    }

    /**
     * @return the startPageNo
     */
    public int getStartPageNo() {
        return startPageNo;
    }

    /**
     * @param startPageNo the startPageNo to set
     */
    public void setStartPageNo(int startPageNo) {
        this.startPageNo = startPageNo;
    }

    /**
     * @return the pageNo
     */
    public int getPageNo() {
        return pageNo;
    }

    /**
     * @param pageNo the pageNo to set
     */
    public void setPageNo(int pageNo) {
        this.pageNo = pageNo;
    }

    /**
     * @return the endPageNo
     */
    public int getEndPageNo() {
        return endPageNo;
    }

    /**
     * @param endPageNo the endPageNo to set
     */
    public void setEndPageNo(int endPageNo) {
        this.endPageNo = endPageNo;
    }

    /**
     * @return the nextPageNo
     */
    public int getNextPageNo() {
        return nextPageNo;
    }

    /**
     * @param nextPageNo the nextPageNo to set
     */
    public void setNextPageNo(int nextPageNo) {
        this.nextPageNo = nextPageNo;
    }

    /**
     * @return the finalPageNo
     */
    public int getFinalPageNo() {
        return finalPageNo;
    }

    /**
     * @param finalPageNo the finalPageNo to set
     */
    public void setFinalPageNo(int finalPageNo) {
        this.finalPageNo = finalPageNo;
    }

    /**
     * @return the totalCount
     */
    public int getTotalCount() {
        return totalCount;
    }

    /**
     * @param totalCount the totalCount to set
     */
    public void setTotalCount(int totalCount) {
        this.totalCount = totalCount;
        this.makePaging();
    }

    /**
     * 페이징 생성
     */
    private void makePaging() {
        if (this.totalCount == 0) return; // 게시 글 전체 수가 없는 경우
        if (this.pageNo == 0) this.setPageNo(1); // 기본 값 설정
        if (this.pageSize == 0) this.setPageSize(10); // 기본 값 설정

        int finalPage = (totalCount + (pageSize - 1)) / pageSize; // 마지막 페이지
        if (this.pageNo > finalPage) this.setPageNo(finalPage); // 기본 값 설정

        if (this.pageNo < 0 || this.pageNo > finalPage) this.pageNo = 1; // 현재 페이지 유효성 체크

        boolean isNowFirst = pageNo == 1 ? true : false; // 시작 페이지 (전체)
        boolean isNowFinal = pageNo == finalPage ? true : false; // 마지막 페이지 (전체)

        int startPage = ((pageNo - 1) / 10) * 10 + 1; // 시작 페이지 (페이징 네비 기준)
        int endPage = startPage + 10 - 1; // 끝 페이지 (페이징 네비 기준)

        if (endPage > finalPage) { // [마지막 페이지 (페이징 네비 기준) > 마지막 페이지] 보다 큰 경우
            endPage = finalPage;
        }

        this.setFirstPageNo(1); // 첫 번째 페이지 번호

        if (isNowFirst) {
            this.setPrevPageNo(1); // 이전 페이지 번호
        } else {
            this.setPrevPageNo(((pageNo - 1) < 1 ? 1 : (pageNo - 1))); // 이전 페이지 번호
        }

        this.setStartPageNo(startPage); // 시작 페이지 (페이징 네비 기준)
        this.setEndPageNo(endPage); // 끝 페이지 (페이징 네비 기준)

        if (isNowFinal) {
            this.setNextPageNo(finalPage); // 다음 페이지 번호
        } else {
            this.setNextPageNo(((pageNo + 1) > finalPage ? finalPage : (pageNo + 1))); // 다음 페이지 번호
        }

        this.setFinalPageNo(finalPage); // 마지막 페이지 번호
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
}


2. Controller

setTotalCount(totalCount) 를 호출 한다. CustomServlet 으로 처리도 가능 하다.


/**
 * 리스트 조회
 *
 * @param request
 * @return
 * @throws Exception
 */
@RequestMapping(value="/list", method=RequestMethod.GET)
public ModelAndView list(Sample sample) throws Exception {
    try {
        // (Before) Doing...

        Paging paging = new Paging();
        paging.setPageNo(1);
        paging.setPageSize(10);
        paging.setTotalCount(totalCount);


        // (After) Doing...
    } catch (Exception e) {
        throw e;
    }
}


3. paging.jsp

View 스타일에 따라 수정이 가능 하다.


<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<div class="paginate">
    <a href="javascript:goPage(${param.firstPageNo})" class="first">처음 페이지</a>
    <a href="javascript:goPage(${param.prevPageNo})" class="prev">이전 페이지</a>
    <span>
        <c:forEach var="i" begin="${param.startPageNo}" end="${param.endPageNo}" step="1">
            <c:choose>
                <c:when test="${i eq param.pageNo}"><a href="javascript:goPage(${i})" class="choice">${i}</a></c:when>
                <c:otherwise><a href="javascript:goPage(${i})">${i}</a></c:otherwise>
            </c:choose>
        </c:forEach>
    </span>
    <a href="javascript:goPage(${param.nextPageNo})" class="next">다음 페이지</a>
    <a href="javascript:goPage(${param.finalPageNo})" class="last">마지막 페이지</a>
</div>


4. list.jsp

paging.jsp 를 호출 하는 부분이다.


// (Before) Doing...
<jsp:include page="paging.jsp" flush="true">
    <jsp:param name="firstPageNo" value="${paging.firstPageNo}" />
    <jsp:param name="prevPageNo" value="${paging.prevPageNo}" />
    <jsp:param name="startPageNo" value="${paging.startPageNo}" />
    <jsp:param name="pageNo" value="${paging.pageNo}" />
    <jsp:param name="endPageNo" value="${paging.endPageNo}" />
    <jsp:param name="nextPageNo" value="${paging.nextPageNo}" />
    <jsp:param name="finalPageNo" value="${paging.finalPageNo}" />
</jsp:include>
// (After) Doing...


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 만 사용하는 프로젝트나, 아니면 임시로 설정하여 개발하는대 많은 도움이 될 것이다.


+ Recent posts