Scenario


원래 Spring Project에서 http request/response를 json 형태로 주고받았으나 jsp를 리턴해줄 경우가 생겼다. 그러나 jsp 리소스를 리턴해주려고하니 몇가지 에러가 나는 것 이었다.

1.jsp 자원을 요청해주는 버튼을 클릭하니
아래와 같은 문구가  chorme console 창에 뜨는 것이 아닌가?

Refused to display 'http://localhost:8080/xxx/xxx' in a frame because it set 
'X-Frame-Options' to 'DENY'.

 

구글링을 해보니 "기본적으로 X-Frame-Options는 클릭 잭킹 공격을 막기 위해 거부 됨으로 설정됨" 이라고 한다.

*클릭재킹: 웹 사용자가 자신이 클릭하고 있다고 인지하는 것과 다른 어떤 것을 클릭하게 속이는 악의적인 기법

 

2. Spring Security 설정을 통해 (1) 번 문제는 고칠 수 있엇으나, 이제는 갑자기 404에러가 났다.

	<mvc:view-resolvers>
    	<bean class="org.springframework.web.servlet.view.UrlBasedViewResolver">
			<property name="order" value="0"/>
	        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />    
	    </bean>
	</mvc:view-resolvers>

 위와 같이 설정된 return values handler 설정을 고친 방법을 알아보자.

Solution


1. X-frame-Options 해결방법.

xml 설정을 사용중이라면 Filter체인에서 아래와 같이 headers 설정을 해주면 된다.

<beans xmlns="http://www.springframework.org/schema/beans" 
       xmlns:security="http://www.springframework.org/schema/security"> 
  <security:http>
      <security:headers>
           <security:frame-options disabled="true"></security:frame-options>
      </security:headers>
  </security:http>
</beans>

 

2. Jsp resource 404 not found 해결방법.

ReturnValuesHandler의 프로젝트 내부의 경로옵션인 prefix와 suffix를 써주지 않아서 jsp 리소스를 써주지 않아서 일어난 일이다. 보통 jsp 리소스를 /WEB-INF/views에 많이 놓지만, 나는 /jsp/ 라는 폴더에 넣어주었다. 

<mvc:view-resolvers>
    <bean class="org.springframework.web.servlet.view.UrlBasedViewResolver">
		<property name="order" value="0"/>
	    <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />    
		<property name="prefix" value="/jsp/" />
		<property name="suffix" value=".jsp" />
	</bean>
</mvc:view-resolvers>

위와 같이 설정해주니 잘 동작하였다.

 

--------------

*Xframe관련 출처:https://cnpnote.tistory.com/entry/SPRING-Spring-Security%EC%97%90%EC%84%9C-X-Frame-Options%EC%9D%91%EB%8B%B5-%ED%97%A4%EB%8D%94%EB%A5%BC-%EB%B9%84%ED%99%9C%EC%84%B1%ED%99%94%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9E%85%EB%8B%88%EA%B9%8C

Scenario


기존 AS-IS 프로젝트의 url pattern이 아래와 같이 .do 로 끝나는 url만 이 action이라는 이름의 dispatcher sertvlet으로 요청을 받게 설정이 되어 있었다.

    <servlet-mapping>
        <servlet-name>action</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>

나는 이를 /* 또는 / 으로 받으려고 했는데, main.html을 못불러오는게 아닌가?

    <servlet-mapping>
        <servlet-name>action</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

 

이유를 알고자 다음과 같이 정리하였다.

 

Solution


위를 알려면 서블릿 컨테이너와 웹 어플리케이션의 연동 방법을 알아야한다.

실제로 WEB-INF 외부에 있는 jsp 파일을 요청하더라도, 직접 접근하는게 아니라 tomcat was라는 미들웨어를 통해  브라우저가 알아볼 수 없는 jsp 파일을 html로 변형해서 서버로 내려준뒤 브라우저로 내려준다.

(was는 동적 파일 처리(jsp 같은 파일) web server는 정적 파일 처리)

위와 같은 내용는 tomcat web.xml에 다음과 같은 설정을 통해 알 수 있다.

<servlet-mapping>
	<servlet-name>jspServlet</servlet-name>
	<url-pattern>*.jsp</url-pattern>
</servlet-mapping>

 

앞의 사전지식을 사용하여 url pattern 문제에 대해 접근해보자.

'/*' 은 요청받는 모든 URL을 처리한다는 의미이다. 모든 요청을 처리하게 되면 .jsp라는 view에 대한 요청도 action 이라는 이름의 Dispatcher Servlet이 처리하겠다는 것이다. 이렇게 되면 .jsp 파일은 tomcat web,xml 에서 정의된 jsp Servlet으로 처리되어지지 않고 .jps 요청에 대한 view를 내려줄 수 없다. (action dispatcher servlet설정에 의해 마스킹)

'/' 로 해준다면 default servlet 타게되는 요청들이 action servlet을 타게 되는데  *.jsp은 톰캣에 설정에 의해 dispatcher servlet에서 처리되지 않고 WAS내 jsp servlet으로 처리되므로 제대로 view를 리턴해줄 수 있으나. 여기서 문제는 톰캣 설정에서 default servlet 은 .html .css .js 같은 정적인 파일들을 처리 해주었으나 이제 처리해 줄 수 없으므로, 정적인 파일들에 대하여 404 not found error가 뜨게된다. (action servlet이 그것을 마스킹 하게 되어버린다.)

이제 여기서 필요한 설정이 context-dispatcherServlet.xml 내에 아래와 같은 설정이다.

<mvc:resources location="/resources/" mapping="/resources/**"/>
//또는
<mvc:default-servlet-handler />

위와  xml을 설정하게 된다면, 정적파일을 호출하게 되는 Request URL에 대하여 default servlet으로 위임 하는 것이다.

Scenario


MockMVC를 활용하여 벤더사 Param을 위해 만든 MessageConverter를 테스트 하려고 하였다. 

Meesage Converter를 Junit에서는 Test하였을때 잘 동작하였는데, 막상 Spring application 에서는 잘 동작하지 않았다. 그 이유는 원래 HttpServletRequest에서는 inputstream을 한번밖에 읽을 수 없지만, MockHttpServletRequest 에서는 getInputStream을 할때마다 Byte[]를 만들어서 준다. 

그래서 실제 스프링 환경과 똑같이 단위테스트를 하기위해 NotReuseHttpServletRequest을 만들게 되었다.

 

 

Solution


1. 먼저,  Junit의 RequestBuilder을 Delegate 하기위해 만든  NotReuseMockHttpServletRequestBuilder가 존재한다.

public class NotReuseMockHttpServletRequestBuilder implements RequestBuilder{

	private RequestBuilder requestBuilder;
	
	public NotReuseMockHttpServletRequestBuilder(RequestBuilder requestBuilder) {
		this.requestBuilder = requestBuilder;
	}
	
	@Override
	public MockHttpServletRequest buildRequest(ServletContext servletContext) {
		MockHttpServletRequest buildRequest = this.requestBuilder.buildRequest(servletContext);
		return new NotReuseHttpServletRequest(buildRequest);
	}
}

 

2.다음으로 실질적으로 MockHttpServletRequest 리턴하는 NotReuseHttpServletRequestNotReuseHttpServletRequest 클래스가 존재한다. 

Java 11.83 KB
/**
 * Movck MVC 테스트시에 실제 Spring MVC 환경과 똑같이   
 * InputStream을 두번 읽지 못하도록 테스트하기위한 클래스
 */
class NotReuseHttpServletRequest extends MockHttpServletRequest {
 
    private MockHttpServletRequest buildRequest;
    private ServletInputStream servletInputStream;
   
    public NotReuseHttpServletRequest(MockHttpServletRequest buildRequest) {
        this.buildRequest = buildRequest;
    }
   
    public ServletInputStream getInputStream() {
        if(this.servletInputStream == null) {
            this.servletInputStream = this.buildRequest.getInputStream();
        }
        return this.servletInputStream;
    }
 
    public int hashCode() {
        return buildRequest.hashCode();
    }
 
    public boolean equals(Object obj) {
        return buildRequest.equals(obj);
    }
 
    public ServletContext getServletContext() {
        return buildRequest.getServletContext();
    }
 
    public boolean isActive() {
        return buildRequest.isActive();
    }
 
    public void close() {
        buildRequest.close();
    }
 
    public void invalidate() {
        buildRequest.invalidate();
    }
 
    public String toString() {
        return buildRequest.toString();
    }
 
    public Object getAttribute(String name) {
        return buildRequest.getAttribute(name);
    }
 
    public Enumeration<String> getAttributeNames() {
        return buildRequest.getAttributeNames();
    }
 
    public String getCharacterEncoding() {
        return buildRequest.getCharacterEncoding();
    }
 
    public void setCharacterEncoding(String characterEncoding) {
        buildRequest.setCharacterEncoding(characterEncoding);
    }
 
    public void setContent(byte[] content) {
        buildRequest.setContent(content);
    }
 
    public int getContentLength() {
        return buildRequest.getContentLength();
    }
 
    public long getContentLengthLong() {
        return buildRequest.getContentLengthLong();
    }
 
    public void setContentType(String contentType) {
        buildRequest.setContentType(contentType);
    }
 
    public String getContentType() {
        return buildRequest.getContentType();
    }
 
 
 
    public void setParameter(String name, String value) {
        buildRequest.setParameter(name, value);
    }
 
    public void setParameter(String name, String[] values) {
        buildRequest.setParameter(name, values);
    }
 
    public void setParameters(Map params) {
        buildRequest.setParameters(params);
    }
 
    public void addParameter(String name, String value) {
        buildRequest.addParameter(name, value);
    }
 
    public void addParameter(String name, String[] values) {
        buildRequest.addParameter(name, values);
    }
 
    public void addParameters(Map params) {
        buildRequest.addParameters(params);
    }
 
    public void removeParameter(String name) {
        buildRequest.removeParameter(name);
    }
 
    public void removeAllParameters() {
        buildRequest.removeAllParameters();
    }
 
    public String getParameter(String name) {
        return buildRequest.getParameter(name);
    }
 
    public Enumeration<String> getParameterNames() {
        return buildRequest.getParameterNames();
    }
 
    public String[] getParameterValues(String name) {
        return buildRequest.getParameterValues(name);
    }
 
    public Map<String, String[]> getParameterMap() {
        return buildRequest.getParameterMap();
    }
 
    public void setProtocol(String protocol) {
        buildRequest.setProtocol(protocol);
    }
 
    public String getProtocol() {
        return buildRequest.getProtocol();
    }
 
    public void setScheme(String scheme) {
        buildRequest.setScheme(scheme);
    }
 
    public String getScheme() {
        return buildRequest.getScheme();
    }
 
    public void setServerName(String serverName) {
        buildRequest.setServerName(serverName);
    }
 
    public String getServerName() {
        return buildRequest.getServerName();
    }
 
    public void setServerPort(int serverPort) {
        buildRequest.setServerPort(serverPort);
    }
 
    public int getServerPort() {
        return buildRequest.getServerPort();
    }
 
    public BufferedReader getReader() throws UnsupportedEncodingException {
        return buildRequest.getReader();
    }
 
    public void setRemoteAddr(String remoteAddr) {
        buildRequest.setRemoteAddr(remoteAddr);
    }
 
    public String getRemoteAddr() {
        return buildRequest.getRemoteAddr();
    }
 
    public void setRemoteHost(String remoteHost) {
        buildRequest.setRemoteHost(remoteHost);
    }
 
    public String getRemoteHost() {
        return buildRequest.getRemoteHost();
    }
 
    public void setAttribute(String name, Object value) {
        buildRequest.setAttribute(name, value);
    }
 
    public void removeAttribute(String name) {
        buildRequest.removeAttribute(name);
    }
 
    public void clearAttributes() {
        buildRequest.clearAttributes();
    }
 
    public void addPreferredLocale(Locale locale) {
        buildRequest.addPreferredLocale(locale);
    }
 
    public void setPreferredLocales(List<Locale> locales) {
        buildRequest.setPreferredLocales(locales);
    }
 
    public Locale getLocale() {
        return buildRequest.getLocale();
    }
 
    public Enumeration<Locale> getLocales() {
        return buildRequest.getLocales();
    }
 
    public void setSecure(boolean secure) {
        buildRequest.setSecure(secure);
    }
 
    public boolean isSecure() {
        return buildRequest.isSecure();
    }
 
    public RequestDispatcher getRequestDispatcher(String path) {
        return buildRequest.getRequestDispatcher(path);
    }
 
    public String getRealPath(String path) {
        return buildRequest.getRealPath(path);
    }
 
    public void setRemotePort(int remotePort) {
        buildRequest.setRemotePort(remotePort);
    }
 
    public int getRemotePort() {
        return buildRequest.getRemotePort();
    }
 
    public void setLocalName(String localName) {
        buildRequest.setLocalName(localName);
    }
 
    public String getLocalName() {
        return buildRequest.getLocalName();
    }
 
    public void setLocalAddr(String localAddr) {
        buildRequest.setLocalAddr(localAddr);
    }
 
    public String getLocalAddr() {
        return buildRequest.getLocalAddr();
    }
 
    public void setLocalPort(int localPort) {
        buildRequest.setLocalPort(localPort);
    }
 
    public int getLocalPort() {
        return buildRequest.getLocalPort();
    }
 
    public AsyncContext startAsync() {
        return buildRequest.startAsync();
    }
 
    public AsyncContext startAsync(ServletRequest request, ServletResponse response) {
        return buildRequest.startAsync(request, response);
    }
 
    public void setAsyncStarted(boolean asyncStarted) {
        buildRequest.setAsyncStarted(asyncStarted);
    }
 
    public boolean isAsyncStarted() {
        return buildRequest.isAsyncStarted();
    }
 
    public void setAsyncSupported(boolean asyncSupported) {
        buildRequest.setAsyncSupported(asyncSupported);
    }
 
    public boolean isAsyncSupported() {
        return buildRequest.isAsyncSupported();
    }
 
    public void setAsyncContext(MockAsyncContext asyncContext) {
        buildRequest.setAsyncContext(asyncContext);
    }
 
    public AsyncContext getAsyncContext() {
        return buildRequest.getAsyncContext();
    }
 
    public void setDispatcherType(DispatcherType dispatcherType) {
        buildRequest.setDispatcherType(dispatcherType);
    }
 
    public DispatcherType getDispatcherType() {
        return buildRequest.getDispatcherType();
    }
 
    public void setAuthType(String authType) {
        buildRequest.setAuthType(authType);
    }
 
    public String getAuthType() {
        return buildRequest.getAuthType();
    }
 
    public void setCookies(Cookie... cookies) {
        buildRequest.setCookies(cookies);
    }
 
    public Cookie[] getCookies() {
        return buildRequest.getCookies();
    }
 
    public void addHeader(String name, Object value) {
        buildRequest.addHeader(name, value);
    }
 
    public long getDateHeader(String name) {
        return buildRequest.getDateHeader(name);
    }
 
    public String getHeader(String name) {
        return buildRequest.getHeader(name);
    }
 
    public Enumeration<String> getHeaders(String name) {
        return buildRequest.getHeaders(name);
    }
 
    public Enumeration<String> getHeaderNames() {
        return buildRequest.getHeaderNames();
    }
 
    public int getIntHeader(String name) {
        return buildRequest.getIntHeader(name);
    }
 
    public void setMethod(String method) {
        buildRequest.setMethod(method);
    }
 
    public String getMethod() {
        return buildRequest.getMethod();
    }
 
    public void setPathInfo(String pathInfo) {
        buildRequest.setPathInfo(pathInfo);
    }
 
    public String getPathInfo() {
        return buildRequest.getPathInfo();
    }
 
    public String getPathTranslated() {
        return buildRequest.getPathTranslated();
    }
 
    public void setContextPath(String contextPath) {
        buildRequest.setContextPath(contextPath);
    }
 
    public String getContextPath() {
        return buildRequest.getContextPath();
    }
 
    public void setQueryString(String queryString) {
        buildRequest.setQueryString(queryString);
    }
 
    public String getQueryString() {
        return buildRequest.getQueryString();
    }
 
    public void setRemoteUser(String remoteUser) {
        buildRequest.setRemoteUser(remoteUser);
    }
 
    public String getRemoteUser() {
        return buildRequest.getRemoteUser();
    }
 
    public void addUserRole(String role) {
        buildRequest.addUserRole(role);
    }
 
    public boolean isUserInRole(String role) {
        return buildRequest.isUserInRole(role);
    }
 
    public void setUserPrincipal(Principal userPrincipal) {
        buildRequest.setUserPrincipal(userPrincipal);
    }
 
    public Principal getUserPrincipal() {
        return buildRequest.getUserPrincipal();
    }
 
    public void setRequestedSessionId(String requestedSessionId) {
        buildRequest.setRequestedSessionId(requestedSessionId);
    }
 
    public String getRequestedSessionId() {
        return buildRequest.getRequestedSessionId();
    }
 
    public void setRequestURI(String requestURI) {
        buildRequest.setRequestURI(requestURI);
    }
 
    public String getRequestURI() {
        return buildRequest.getRequestURI();
    }
 
    public StringBuffer getRequestURL() {
        return buildRequest.getRequestURL();
    }
 
    public void setServletPath(String servletPath) {
        buildRequest.setServletPath(servletPath);
    }
 
    public String getServletPath() {
        return buildRequest.getServletPath();
    }
 
    public void setSession(HttpSession session) {
        buildRequest.setSession(session);
    }
 
    public <T extends HttpUpgradeHandler> T upgrade(Class<T> handlerClass) throws IOException, ServletException {
        return buildRequest.upgrade(handlerClass);
    }
 
    public HttpSession getSession(boolean create) {
        return buildRequest.getSession(create);
    }
 
    public HttpSession getSession() {
        return buildRequest.getSession();
    }
 
    public String changeSessionId() {
        return buildRequest.changeSessionId();
    }
 
    public void setRequestedSessionIdValid(boolean requestedSessionIdValid) {
        buildRequest.setRequestedSessionIdValid(requestedSessionIdValid);
    }
 
    public boolean isRequestedSessionIdValid() {
        return buildRequest.isRequestedSessionIdValid();
    }
 
    public void setRequestedSessionIdFromCookie(boolean requestedSessionIdFromCookie) {
        buildRequest.setRequestedSessionIdFromCookie(requestedSessionIdFromCookie);
    }
 
    public boolean isRequestedSessionIdFromCookie() {
        return buildRequest.isRequestedSessionIdFromCookie();
    }
 
    public void setRequestedSessionIdFromURL(boolean requestedSessionIdFromURL) {
        buildRequest.setRequestedSessionIdFromURL(requestedSessionIdFromURL);
    }
 
    public boolean isRequestedSessionIdFromURL() {
        return buildRequest.isRequestedSessionIdFromURL();
    }
 
    public boolean isRequestedSessionIdFromUrl() {
        return buildRequest.isRequestedSessionIdFromUrl();
    }
 
    public boolean authenticate(HttpServletResponse response) throws IOException, ServletException {
        return buildRequest.authenticate(response);
    }
 
    public void login(String username, String password) throws ServletException {
        buildRequest.login(username, password);
    }
 
    public void logout() throws ServletException {
        buildRequest.logout();
    }
 
    public void addPart(Part part) {
        buildRequest.addPart(part);
    }
 
    public Part getPart(String name) throws IOException, IllegalStateException, ServletException {
        return buildRequest.getPart(name);
    }
 
    public Collection<Part> getParts() throws IOException, IllegalStateException, ServletException {
        return buildRequest.getParts();
    }
}

 

 

3.실질적인 사용법은 다음과 같다.

 

@Test
public void example() throws Exception {

        logger.debug("############# Post Test #################");

        MockHttpServletRequestBuilder builder =
				  MockMvcRequestBuilders.post("/test")
							.contentType(MediaType.TEXT_XML)
							.content(content);
                                        
		//Spring MVC에서는 스트림을 한번 읽으면 Flush되나 MockMVC에서는 Stream을 Reuse를 할 수 있으므로 
		//실제 Spring MVC와 똑같이 Reuse 할 수 없게 하기위해서 delegator를 만들었다. 
		NotReuseMockHttpServletRequestBuilder delegator = new NotReuseMockHttpServletRequestBuilder(builder);

		MvcResult result = mockMvc.perform(delegator).andExpect(MockMvcResultMatchers.status().isOk())
													.andDo(MockMvcResultHandlers.print())
													.andReturn();
}

 

[그림] 전에 썼던 web.xml에 context 설정


[1]스프링 어플리케이션에 application context는 2개가 들어간다.

- ContextLoaderListener에 의해서 만들어지는 Root WebApplicationContext
- DispatcherServlet에 의해서 만들어지는 WebApplicationContext

1. Root WebApplicationContext 이름 그대로 최상단에 위치한 Context 이다
1.1서비스 계층이나 DAO를 포함한, 웹 환경에 독립적인 빈들을 담아둔다.
1.2서로 다른 서블릿컨텍스트에서 공유해야 하는 빈들을 등록해놓고 사용할 수 있다.
1.3Servlet context에 등록된 빈들을 이용 불가능하고 servlet context와 공통된 빈이 있다면 servlet context 빈이 우선된다.
1.4WebApplication 전체에 사용가능한 DB연결, 로깅 기능들이 이용된다.

2. WebApplicationContext 서블릿에서만 이용되는 Context이다
2.1DispatcherServlet이 직접 사용하는 컨트롤러를 포함한 웹 관련 빈을 등록하는 데 사용한다.
2.2DispatcherServlet은 독자적인 WebApplicationContext를 가지고 있고, 모두 동일한 Root WebApplicationContext를 공유한다.


[2]WebApplicationContext 객체 얻어내기

Java 0.41 KB
  1. //접근 방법1
  2. public class A  {
  3.     @Autowired
  4.     WebApplicationContext applicationContext;
  5.    
  6.     //Container 내에 객체얻기
  7.     public method() {
  8.         B b = context.getBean(B.class);    
  9.     }
  10. }
  11.  
  12.  
  13. //접근 방법2
  14. public class A implements ApplicationContextAware {
  15.   private ApplicationContext context;
  16.  
  17.   public void setApplicationContext(ApplicationContext context) {
  18.       this.context = context;
  19.   }
  20. }


실제 코드상에서 WebApplicationContext 객체를 얻어낼 방법은 다양하다.

그 중 위와같은 예가 가장 흔하게 쓰이는 예이다.

컨테이너 내의 Bean은 Singleton 이므로 Static 속성과 유사하게 사용될 수 있다.

예시) 객체 내에 설정값 변수를 선언하여 어느 객체에서든지 참조 가능하다.


-------------------추가 좋은글-----------------
*WebAppliCationContext vs ApplicationContext

 스프링에서 말하는 "애플리케이션 컨텍스트"는 스프링이 관리하는 빈들이 담겨 있는 컨테이너라고 생각하시면 됩니다. 스프링 안에는 여러 종류의 애플리케이션 컨텍스트 구현체가 있는데, ApplicationContext라는 인터페이스를 구현한 객체들이 다 이 애플리케이션 컨텍스트입니다. 웹 애플리케이션 컨텍스트는 ApplicationContext를 확장한 WebApplicationContext 인터페이스의 구현체를 말합니다. WebApplicationContext는 ApplicationContext에 getServletContext() 메서드가 추가된 인터페이스입니다. 이 메서드를 호출하면 서블릿 컨텍스트를 반환됩니다. 결국 웹 애플리케이션 컨텍스트는 스프링 애플리케이션 컨텍스트의 변종이면서 서블릿 컨텍스트와 연관 관계에 있다는 정도로 정리가 됩니다. 이 메서드가 추가됨과 동시에 서블릿 컨텍스트를 이용한 몇가지 빈 생애 주기 스코프(애플리케이션, 리퀘스트, 세션 등)가 추가되기도 합니다.


*Child WAC은 여러개가 생성될수있다.

 이론적으로 DispatcherServlet는 여러 개 등록될 수 있다. 왜 그래야 하는지는.. 생각해보면 많은 이유가 있겠지만, 아무튼 기술적으로 가능하고 그런 의도를 가지고 설계되었다. 그리고 각각 DispatcherServlet은 독자적인 WAC를 가지고 있고 모두 동일한 Root WAC를 공유한다. 스프링의 AC는 ClassLoader와 비슷하게 자신과 상위 AC에서만 빈을 찾는다. 따라서 같은 레벨의 형제 AC의 빈에는 접근하지 못하고 독립적이다.


출처

https://www.slipp.net/questions/166

http://toby.epril.com/?p=934

Scenario


Spring AOP를 설정하다. AOP 관련 설정을 Context 파일로 따로 빼서 설정하려고 했다.

Eclipse 상의 AOP Marker도 다 뜨고, 설정이 된 줄 알았는데 막상 서버를 돌릴때 Service를 호출하면 AOP LOG가 뜨지 않는 것이었다.

그렇게 삽질의 서막이 시작되었다.......


Solution



일단 Context가 무엇인지 부터 알아보자.


1.Context 란?

 지금 다루고 있는 웹환경에서의 컨텍스트란 (Spring 상의), 웹환경상에서의 웹호출처리를 위한 환경설정에 관한 각종 정보들을 갖고있는 것이다.

 환경설정파일과 다른것은, 웹 실행시에 실시간으로 생성되어 참조되는 문맥정보?? 인 것이다.



2.Root plication Context vs Servlet Context


[그림] web.xml에 context 설정


Root context xml 파일들은 Tomcat 실행시에 로드 되며, Dispatcher Servlet Context xml은 클라이언트 요청시 dispatcher Servlet 객체 생성시에 로드 된다.


Root Application Context
  - 전체 계층구조에서 최상단에 위치한 컨텍스트
  - 서로 다른 서블릿 컨텍스트에서 공유해야하는 Bean들을 등록해놓고 사용할 수 있다.
  - 웹 어플리케이션 전체에 적용가능한 DB 연결, 로깅기능등에 이용
  - Servlet Context에 등록된 Bean 이용 불가능 하다
  - Servlet Context와 동일한 Bean이 있을 경우 Servlet Context Bean이 우선된다.
  - 하나의 컨텍스트에 정의된 AOP 설정은 다른 컨텍스트의 빈에는 영향을 미치지 않는다.

 

Servlet Context
  - 서블릿에서만 이용되는 컨텍스트
  - 타 서블릿과 공유하기 위한 Bean들은 루트 웹 어플리케이션 컨텍스트에 등록해놓고 사용해야 한다
  - DispatcherServlet은 자신만의 컨텍스트를 생성,초기화하고
    동시에 루트 어플리케이션 컨텍스트를 찾아서 자신의 부모 컨텍스트로 사용




3.결론: 나는 왜 AOP가 동작이 안됐었을까?


결론부터 말하자면 root context 에는 Controller를 제외한 Component 를 스캔해주어야 하고 dispathcer servlet에는 Controller Component만 스캔을 해주어야 한다.


[그림] 잘못된 Dispathcer Servlet에서 Component Scan 방법 (dispatcher-servlet.xml)


[그림] Aop 적용을 위한 Context 설정  (aop-context.xml)


위와같이 패키지 전체를 Component 스캔을 해주었었는데 이는 옳지 않은 방식이다.

패키지 전체를 Servlet Context에서 스캔을 하면 Root Context에서 AOP를 위해 Component 스캔을 한것은 스캔이 되지 않는 것이다. (root와 dispatcher 두 Context에서 빈을 명시하면 root Context는 무시된다)


[그림] 올바르게 Dispathcer Servlet에서 Component Scan 방법 (dispatcher-servlet.xml)


위와같이 설정을 바꾸고 나니 잘 작동 되었다


********************************************************************************

[참고]

검색하다보니 Root Application Context와 Servlet Context 설정시 도움이 될만한 정보가 있어서 퍼온다.

출처는 http://bumsgy-innori.tistory.com/tag/%EC%8A%A4%ED%94%84%EB%A7%812.5%EC%84%A4%EC%A0%95

이곳인데 이곳 쥔장도 퍼온글인것 같다.

********************************************************************************

 

 

SpringMVC를 이용한 개발에서 Controller류는 xxx-servlet.xml에 그 외의 Service, Dao류는 applicationContext*.xml에 등록해야 합니다. 직접 등록이 아닌 Scanner을 사용할 경우 중복하지 않도록 해야 합니다. 따라서 처음 만드신 것처럼 xxx-servlet.xml에서 모든 Stereotype 빈을 모두 다 검색하는 component-scan 설정은 잘못됐습니다.

 

이 원칙만 잘 준수하시면 아무 문제없을 것입니다.

 

이번엔 좀 복잡하게 설명해보죠.

 

세가지 사용하신 stereotype이 있습니다. @Repository, @Service, @Controller죠.

 

제가 이전에 작성한 viewtopic.php?f=5&t=20#p33 을 읽어보시면 잘 설명이 되어있는 내용이 있습니다. 일반 applicationContext*.xml에 설정한 내용과 xxx-servlet.xml에 설정한 내용은 두개의 다른 ApplicationContext(이하 AC)로 만들어지고 xxx-servlet.xml의 AC는 applicationContext*.xml에 의해서 만들어진 AC의 child context로 등록이 됩니다.

 

상위 AC를 ac, 웹에서 정의된 AC를 web-ac라고 불러보죠.
 
원래 하신 설정에 따르면 ac에서는 controller,form류(@Controller)를 제외한 나머지만 scan하도록 되어있습니다. 따라서 @Repostiory, @Service가 붙은 bean들이 등록이 되겠죠.

 

web-ac는 exclude조건없이 full-scan을 했습니다. 따라서 @Repository, @Service, @Controller가 붙은 빈들이 다 등록이 됐죠.

 

그리고 ac와 web-ac가 상-하위 ac관계를 가집니다. 이때 같은 bean이 두번 등록되도 문제는 없습니다. 하위 context에서는 상위 context에 등록된 bean을 override합니다. 하위 context에서 먼저 bean을 찾고, 없으면 그때 상위를 찾기 때문에 override한 것과 마찬가지입니다.

 

따라서
ac - @Service, @Repository
web-ac - @Service, @Repository, @Controller


이렇게 각각 빈들을 가지고 있는 상태입니다.

 

스프링이 transaction을 적용하는 방법은 AOP를 이용해서 해당 빈의 proxy를 만들어서 tx가 적용되는 bean을 바꿔치기 하는 방법을 이용합니다. 그러면 원래 @Service(@Transactional)이 붙은 빈은 뒤로 숨고 tx가 적용된 proxy bean이 @Service가 붙은 bean으로 대치됩니다. tx설정은 ac에만 되어있기 때문에 이 것이 적용되면

 

ac - tx적용(@Service), @Repository
web-ac - @Service, @Repository, @Controller

 

이런 형태가 됩니다.

web-ac쪽은 tx가 적용되지 않은 @Service가 그대로 남아있습니다.

 

그리고 @Controller가 동작하고 여기에 @Autowired를 걸면, 우선 같은 context에서 검색을 합니다. 여기서도 @Service가 지정된 같은 bean이 등록되어있기 때문에 web-ac에 등록된 @Service bean을 사용합니다. 하지만 이 bean은 tx를 위한 proxy bean이 아니기 때문에 tx가 동작하지 않습니다. 따라서 처음과 같은 문제가 발생합니다.

 

다음은 @Service를 떼고 <bean>으로 등록하니 tx가 적용된 상황을 살펴보죠.
이때는 @Service에 의해서 scan되지는 않고 bean에 의해서 명시적으로 service bean이 등록됩니다. 물론 <bean>을 등록한 ac에서만 되겠죠. web-ac에서는 full-scan을 하지만, @Service가 없기 때문에 등록되지 않습니다.

 

ac - Service, @Repository
web-ac - @Repository, @Controller

 

이런 상황이 되는거죠.

여기에 tx설정이 적용되면 @Transactional이 붙은 것을 ac 내에서 찾아서 적용하므로

 

ac - tx적용(Service), @Repository
web-ac - @Repository, @Controller

이런 구조가 됩니다.

 

이제 Controller가 @Autowired에 의해서 service를 찾으면 이때는 ac에 등록된 tx적용(Service) 빈이 호출됩니다. 그러니 당연히 tx가 먹겠죠.

따라서 앞에서 하신 모든 가정들(@Service, @Transactional은 같이 쓰면 안된다, 인터페이스에 적용을 안해서 그렇다거나.. 기타등등)은 다 틀렸습니다.

마지막으로 제가 알려드린대로 하시면

 

ac - tx적용(@Service), @Repository
web-ac -@Controller

 

이렇게 깔끔하게 설정되겠죠.

 

Web을 사용하는 스프링 애플리케이션의 설정의 기본원칙만 지키시면 아무 문제가 없는 것입니다. 보통 component-scan을 사용하면 exclude, include등을 잘 적용하지 못해서 이런 문제가 발생할 수 있습니다. 편리한만큼 신경쓸 것도 있는거죠.

 

그리고 exclude 하실때 이름을 가지고 regex를 적용하셨는데 그보다는 annotation filter을 이용하시는 것이 더 깔끔할 것 같습니다. 아예 web쪽과 나머지를 root경로부터 분리하면 간단하긴 하지만, 보통 모듈별로 몰아두기 때문에 annotation 방식이 편합니다.

제가 사용하는 방법은

ac쪽은

<context:component-scan base-package="base.package...">
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>

 

 web-ac쪽은

<context:component-scan base-package="base.package..." use-default-filters="false">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>


 입니다.



---------------------------------------------------------------------------------------------------------------------------------

참고글: http://www.mungchung.com/xe/spring/21220

서론


 스프링에서는 총 3개의 컨테이너가 구동된다고 할 수있다. 컨테이너란 서버(정적리소스 관리)내에서 Client의 요청을 동적으로 처리하기 위한 웹 서버의 한 부분 이다.


(사진)Servlet Container 


본론




톰캣 서버를 처음 구동하면, 

1. Web.xml 파일을 로딩하여 서블릿 컨테이너가 구동된다.

2. 서블릿컨테이너는 web.xml 파일에 등록된 ContextLoaderListener객체를 생성 한다.

3. 이때 ContextLoaderListener는 applicationContext.xml 파일을 로딩하여 스프링을 구동하는데 이를 'Root 컨테이너'라고 한다.

4. 동시에 Service 구현 클래스나 DAO 객체들이 메모리에 생성된다.

5. Clinet가 서버에 요청을 하게 되면, 서블릿 컨테이너는 DispathcerServlet 객체를 생성하고,

6. Presnetation-layer.xml 파일을 로딩하여 두 번쨰 스프링 컨테이너를 구동한다. 이때 Controller 객체들이 메모리에 올라가게 된다.

 

(+ 추가사항) 

web.xml에서 <servlet><init-param> 과 

<context-param>

<param-name>contextConfigLocation</param-name> 정확히 이해해서 추가내용 쓰기

개요


일반적으로 프레임워크 기반의 웹프로젝트를 보면 아래 그림과 같이 2개의 레이어로 시스템을 나누어 개발한다.



위는 스프링의 프레임워크의 대략적인 구조를 나타낸 것이다. 특히 2-Layered 아키텍쳐에따라 컴포넌트를 분류 해 놓았다.


컴포넌트 설명


(클라이언트의 요청이 들어온 뒤 흐름에 따라 기술)

1.Dispathcer Servlet : 프론트 컨트롤러로써 클라이언트의 요청은 무조건 여기를 거친다.

2.Handler Mapping : 사용자의 요청에 따라 어느컨트롤러로 분기할지 맵핑을 시켜준다.

3.Controller : 기능마다 어느 비즈니스 로직을 태울지 관장하는 컨트롤러이다. 만약 스프링 프레임워크를 쓴다면 실질적으로 여기서부터 프로그래밍을 시작한다.

4.ServiceImpl : 서비스 인터페이스를 상속받아 작성한 비즈니스 로직을 구체적으로 기술 한 곳이다.

5. DAO는 직접 DB에 접근한뒤 DTO 객체에 담아온다.

6.View Resolver : 뷰 리졸버는 이제 다음 *.do URL로 분기할지 아니면, 어떤 jsp로 분기할지 관장하는 컴포넌트이다. 

 *책에서 실제로 구현을 해봣는데 prefix와 suffix를 붙여줘서 실질적인 파일 jsp 이름으로 맵핑시켜줬었다.

7. View : 클라이언트가 마주하는 화면으로써, jsp파일이나 html 페이지에 해당하는 부분이다.


상세


# src/main/resources 폴더에는 비즈니스 레이어에 해당하는 설정파일인 applicationContext.xml (AOP,JDBC등 설정) 파일이 있으며, /WEB-INF/config 폴더에는 프레젠테이션 레이어에 해당하는 설정파일인 presentation-layer.xml 이 있다. 

# DispatcherServlet이 생성되면 presentation-layer.xml 파일을 읽고, 스프링 컨테이너를 구동하면 Controller 객체들이 메모리에 생성된다. 하지만 Controller 들이 생성되기전에, src/main/resources 소스 폴더에 있는 applicationContext.xml 파일을 읽어 비즈니스 컴포넌트 들을 먼저 메모리에 생성해야 한다. 이때 사용하는 클래스가 스픵에서 제공하는 ContextLoaderListener 


->ContextLoaderListener는 다음 글에서 자세히설명 할것 



ps. 본 내용은 스프링 퀵스타트 책을보며 공부한내용을 요약 한 것 입니다.(루비페이퍼, 채규태 저)



Scenario: 이미지 파일첨부를 하려는데 기존 소스는 다중 파일첨부 였다, (multipartHttpServletRequest 리스트를 받아서 리스트를 돌리는 구조의 코드였음.) 이것을 살짝 변형 시켜서 단일 파일첨부구조와 재활용성이 가능하게 바꾸려고 고쳐보았다.


Solutions: 다음



1. 가장먼저 Multipart 객체를 받아서 파일과, 함께보내진 폼 요소를 파싱해줄  자바의 파일유틸부터 만들어줌

  1.     //파일 하나만 업로드시킬려고만듬 FileUtil.java (클래스가 없을경우 만들어 줘야함)
  2.     public Map<String,Object> parseInsertFileInfoOne(Map<String,Object> map, HttpServletRequest request) throws Exception{
  3.        
  4.         MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest)request;
  5.         Iterator<String> iterator = multipartHttpServletRequest.getFileNames();
  6.        
  7.         MultipartFile multipartFile = null;
  8.         String originalFileName = null;
  9.         String originalFileExtension = null;
  10.         String storedFileName = null;
  11.        
  12.         Map<String, Object> FileInfoMap = null;
  13.        
  14.        
  15.         //폴더가 없으면 해당 폴더 생성
  16.         File file = new File(CommonUtils.filePath);
  17.         if(file.exists() == false){
  18.             file.mkdirs();
  19.         }
  20.        
  21.         while(iterator.hasNext()){
  22.             multipartFile = multipartHttpServletRequest.getFile(iterator.next());
  23.             if(multipartFile.isEmpty() == false){
  24.                 originalFileName = multipartFile.getOriginalFilename();
  25.                 originalFileExtension = originalFileName.substring(originalFileName.lastIndexOf("."));
  26.                 storedFileName = CommonUtils.getRandomString() + originalFileExtension;
  27.                
  28.                 file = new File(CommonUtils.filePath + storedFileName);
  29.                 multipartFile.transferTo(file);
  30.                
  31.                 FileInfoMap = new HashMap<String,Object>();
  32.                 //인터셉트 맵 자체를 리턴해줄 파일맵에다 넣어줌. 그렇다면 구지 뉴 객체를 생성안해도 될듯
  33.                 FileInfoMap = map;
  34.                
  35.                 FileInfoMap.put("ORIGINAL_FILE_NAME", originalFileName);
  36.                 FileInfoMap.put("STORED_FILE_NAME", storedFileName);
  37.                 FileInfoMap.put("FILE_SIZE", multipartFile.getSize());
  38. //              FileInfoMap.put("REFER_IDX", (String)map.get("IDX"));
  39. //              FileInfoMap.put("NAME", (String)map.get("NAME"));
  40. //              FileInfoMap.put("PJT_SCH_TITLE", (String)map.get("PJT_SCH_TITLE"));
  41. //              FileInfoMap.put("PJT_SCH_CONTENT", (String)map.get("PJT_SCH_CONTENT"));
  42.             }
  43.         }
  44.         return FileInfoMap;
  45.     }


2.Service 객체에서 쓰는방법 예시 (컨트롤러에서 서비스로 보내준뒤 서비스에서 파일유틸 호출)

Java 0.27 KB
  1.     //serviceImpl.java
  2.     @Override
  3.     public void insertProjectSchedule(Map<String, Object> map, HttpServletRequest request) throws Exception {
  4.         Map<String, Object> resultMap = fileUtils.parseInsertFileInfoOne(map, request);
  5.         projectDAO.insertProjectSchedule(resultMap);
  6.     }


3.마이바티스 예시 (많은 파일 속성을 갖고있지만, 이 프로젝트에선 저장된 파일명만 쓴다고 가정

SQL 0.32 KB
  1.         <!-- 예시 sql -->
  2.         INSERT INTO PJT_SCHEDULE
  3.         (
  4.             PJT_SCH_IDX,
  5.             PJT_IDX,
  6.             SCH_TITLE,
  7.             SCH_CONTENT,
  8.             CREA_DATE,
  9.             DEL_FLAG,
  10.             IMG_NAME
  11.         )
  12.         VALUES
  13.         (
  14.             PJT_SCHEDULE_IDX_SEQ.NEXTVAL,
  15.             #{REFER_IDX},
  16.             #{PJT_SCH_TITLE},
  17.             #{PJT_SCH_CONTENT},
  18.             SYSDATE,
  19.             'N',
  20.             #{STORED_FILE_NAME}
  21.         )



4.로컬에 저장된 사진들을 띄울때. (파일패스 경로 전역변수는 적절한곳에 추가해줌)

Java 1.01 KB
  1.  
  2.     /* 이미지 파일불러올때 */
  3.     //Commoncontroller.java
  4.     //이미지 파일 띄울때 컨트롤러를 경유해서 로컬 저장소에 접근하는 것
  5.     @RequestMapping(value="common/getImage.do")
  6.     protected void getImageFile(CommandMap commandMap, HttpServletResponse response) throws Exception
  7.     {
  8.         String fileName = (String) commandMap.get("IMG_NAME");
  9.  
  10.         if(!fileName.isEmpty())
  11.         {
  12.             File file = new File(CommonUtils.filePath+fileName);
  13.             response.setHeader("Content-Type""image/*");
  14.             response.setHeader("Content-Length"String.valueOf(file.length()));
  15.             response.setHeader("Content-Disposition""inline; filename=\"" + file.getName() + "\"");
  16.             Files.copy(file.toPath(), response.getOutputStream());
  17.            
  18.             response.getOutputStream().flush();
  19.             response.getOutputStream().close();
  20.         }
  21.     }
  22.  
  23.  
  24.     //common utils.java에 변수추가(파일패스)
  25.     public static final String filePath = "C:\\dev\\upload\\";



Scenario: 집에서 만든 웹프로젝트를 학원어서 해볼려니 Could not load JDBC driver class [oracle.jdbc.driver.OracleDriver]과 같은 오류가 뜨는 것 이었다.

1.dependency 부분에서 JDBC Template 버젼을 잘못썻나 싶어서 Pome.xml 을 뒤져바도 아무래도 없다...

2.인터넷 검색을 해보니 

<repository>

<id>mesir-repo</id>

<url>http://mesir.googlecode.com/svn/trunk/mavenrepo</url>

</repository>



<dependency>

<groupId>com.oracle</groupId>

<artifactId>ojdbc14</artifactId>

<version>10.2.0.4.0</version>

</dependency>


-------이런식으로 직접 repo를 추가하라고 했지만 안되는 것이었다.----------


Solution:

C:\oraclexe\app\oracle\product\11.2.0\server\jdbc\lib 안에있는 jdbc 파일을 

C:\Program Files\Java\jdk1.8.0_112\jre\lib\ext에 넣어주니 해결 완료.. 환경이 바뀌다보니 기본적인 것 을 안해준 것이었다..




+ Recent posts