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

Scenario


깃 헙이나, 외국 사이트를 돌아다니다 보면 보일러 플레이트 코드란 말을 많이 들을 수 있다. 이 '보일러 플레이트' 코드란 무엇일까?



Solution


boilerplate code; boilerplate
보일러플레이트 코드; 보일러플레이트, 상용구 코드, 상용구
  • 최소한의 변경으로 재사용할 수 있는 저작품
  • 적은 수정만으로 여러 곳에 활용 가능한 코드, 문구
  • 각종 문서에서 반복적으로 인용되는 문서의 한 부분 

제타위키 펌



Java진영에서 어느날 갑자기 등장하여 개발자들을 모호하게 만들어 버렸던
POJO!!

이 녀석이 당췌 뭐야?
많은 사람들은 그럴싸한 이론으로 POJO를 포장하려 한다.
실제 강의나 책을 통해서 설명되는 POJO는 이해하기 힘듬. ㅜㅜ

본인 또한 처음 POJO란 용어를 접했을때 이게 뭐지? 
직역하면 
명백히 오래된 자바 객체?

아쒸 명백히 오래된 자바객체가 한두개야?
jdk 1.0 버전때 부터 제공되던 수 많은 클래스들을 통해 생성하는 객체들이 그럼 다 POJO야?


POJO는 2000년 9월에 열린 컨퍼런스(어떤 컨퍼런스인지는 모름)에서
Rebecca Parsons, Josh MacKenzie, Martin Fowler 가 처음 사용한 용어이다.

다른 개념 다 버리고

POJO = Java Beans
여기서 Java Beans는 Sun의 Java Beans나 EJB의 Bean을 뜻하는것이 아닌
순수하게 setter, getter 메소드로 이루어진 Value Object성의 Bean을 말한다.


예를 들면 아래와 같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SimpleBean {
    private String name;
    private String age;
 
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return this.name;
    }
 
    public void setAge(String age) {
        this.age = age;
    } 
    public String getAge() {
        return this.age;
    }
 
}
cs

 

우리가 열심히 코딩하거나 이클립스를 통해 자동으로 생성하던
그 깡통 빈 클래스!! 를 통해서 생성된 객체!
맞다 바로 이것이 POJO 다.


그럼 왜 이전 부터 사용하던 Beans라고 말하지 않고
사람들 헤깔리게 POJO 새로이 불렀을까?

이유인즉, Beans라는 용어로 위의 깡통 빈 클래스를 정의하기에는
Java Beans나 EJB의 Beans와 구분이 모호하고 또한 Beans라는 용어로
정의되는 여타 다른 개념들과의 확실한 분리를 위해
POJO라는 용어를 사용한것이라 볼 수 있다.


=========================================================================================

 

 

-- 쉽게 따라하는 자바 웹 개발 중 --

 

POJO라는 용어는 평범한 자바 객체라는 뜻인데 어떤 객체를 평범하다고 지칭하는지

그리고 POJO를 사용해서 개발하는게 왜 중요한지 설명 한다. 

 

먼저 평범하다고 말하는 객체는 다음과 같은 특징. 

 

  • 클래스 상속을 강제하지 않는다. 
  • 인터페이스 구현을 강제하지 않는다. 
  • 애노테이션 사용을 강제하지 않는다.

 

POJO가 아닌 대표적인 객체

public HelloServlet extends HttpServlet{ ... } 

 

자바 서블릿 코드를 작성할 때는 이렇게 반드시 HttpServlet을 상속바아야 한다. 

서블릿 프로그래밍을 한다는 이유로 객체지향 프로그래밍의 핵심적인 기능 중 하나인 상속을 빼앗긴 것이나 마찬가지이다. 코드를 작성하고 있는 개발자가 직접 상속을 사용할 수 있는 기회가 없어졌으니.. 

그리고 extends HttpServlet이 추가되면서 이 코드를 이해하기 어려워 진다. 

HttpServlet에서 어떤 기능을 제공하는지 어떤 코드를 어떻게 재사용해야 할지 판단하기도 어렵다. 

 

POJO는 그러한 제약이 없는 일반적인 객체를 말한다. 

상속이나 인터페이스 구현을 사용하지 않은 객체를 말하는 것이 아니라, 그런 것을 라이브러리나 프레임워크로부터 강제받지 않는 객체라는 것이다. 

 

public HelloController { .... } 

이런 클래스라면 개발자의 선택에 따라 자신이 만든 다른 Controller클래스를 상속받게 하거나 인터페이스를 구현하게 할 수 있다. 또한 이해하기 쉬운 코드이기도 하다. 무엇보다도 이런 객체가 테스트를 작성하기 편하다. 테스트를 작성하기 쉬운 코드가 곧 좋은 코드이다. 

 

POJO는 자바 표준 스펙이 아니다. 

 

 

[출처] POJO|작성자 난이



Scenario


윈도우에서 BitNami에서 제공하는 패키지로 RedMine을 설치했는데, DB를 MySQL 에서 MariaDB로 변경하고 싶었다.



Solution


1. 레드마인 메인 서버

 -bitnami설치(설치는 설명없음)

 -DB Migtion (처음 설치 시 필요없는 작업)


2. 레드마인 DB 서버

 -mysql 설치 (설치는 설명없음)

 -DB 복원(처음 설치 시 필요없는 작업)


3.mainserver 에서 DB연동

DB를 연결시켜주는 부분

- C:\Bitnami\redmine\apps\redmine\htdocs\config - (본인이 설치한 경로)



4.database.yml  수정

  

  adapter: 해당어댑터(bitnami는 mysql2)

  database: Redmine_DBname 예)bitnami_redmine

  host: DB_Server_IP 예)127.0.0.1 예는 이렇지만 서버 분리할 경우 mysql설치한 서버의 IP겠죠

  username: mysql_ID 예)bitnami *root로 해도 상관 없음

  password: mysql_PW

  encoding: utf8

  port: mysql_port(기본은 3306)


이후 Maria DB에도 레드마인과 관련된 기본 데이터를 넣어주어야한다.


5.    세션 생성

http://dev.mysql.com/downloads/connector/c/6.0.html#downloads 사이트에서

mysql-connector-c-noinstall-6.0.2.zip을 받아서 lib 밑에 있는 libmysql.dll을 넣어 준다.

입력 : 경로\redmine\redmine-2.3.3 -> rake generate_secret_token



6.   DB migrate 실행 (아마 여기서부터 시작하면 된다)

입력 : 경로\redmine\redmine-2.3.3 -> rake db:migrate RAILS_ENV=production




7.  기본 데이터 입력

입력 : 경로\redmine\redmine-2.3.3 -> rake redmine:load_default_data RAILS_ENV=production

(명령어 실행 중 언어선택 메시지가 나오면 ko라고 입력한다.)


8.  서버 기동

입력 : 경로\redmine\redmine-2.3.3 -> ruby script/rails server webrick -e production

 

9.  확인

http://localhost:3000/


BitNami로 설치하면 기본적으로 MySQL이 설치되니, Maria DB로 연동후 MySQL은 삭제할것


참고: http://pinkwony.tistory.com/1

Scenario


ReactNative로 타이머를 만들고 있는데,

Props에 따라 state를 변화시켜야 할때, componentWillReceiveProps(newtProps)를 써야할때가 있다.

*(이 메소드 안에서 this.setState()를 해도 추가적으로 렌더링하지 않는다.)


JavaScript 0.44 KB
  1.     componentWillReceiveProps (nextProps) {
  2.         const currentProps = this.props;
  3.         if(!currentProps.isPlaying && nextProps.isPlaying) {
  4.             const timerInterval = setInterval(() => {currentProps.addSecond();}, 1000);
  5.             this.setState({
  6.                 timerInterval
  7.             })
  8.         } else if(currentProps.isPlaying && !nextProps.isPlaying) {
  9.             clearInterval(this.state.timerInterval);
  10.         }
  11.     }


위 코드가 타이머에서 elapsedTime(지금까지 카운팅한 시간) state를 증가시키는 로직인데,


if(!currentProps.isPlaying && nextProps.isPlaying)

이런식으로 조건을 주는게 직관적이지도 않고 왠지 비효율적으로 보였다.


이 소스를 단지 

if(nextProps.isPlaying

이런식의 조건식으로 바꿀수 없는것일까? 생각해보았다.


Solution


결론부터 말하자면 componentWillReceiveProps(newtProps) 에서 setState를 해야할 경우에는 무조건 이전값과 다음값을 비교한뒤에 해야한다.


현재 props와 next props를 비교하지 않고 setState를 하면 어떻게 되는지 알아보기위해


  1.         if(currentProps.isPlaying) {
  2.             const timerInterval = setInterval(() => {currentProps.addSecond();}, 1000);
  3.             this.setState({
  4.                 timerInterval
  5.             })
  6.         }

이런식으로 소스코드를 바꿨더니, setInterval 함수가 여러번 실행되서 카운팅 속도가 1초주기가 아닌, 점점 빨라졌다.



(카운트가 빨라진 앱의 모습)



왜 그럴까?



그림 출처: https://velopert.com/1130 - velopert님


일단 Reactjs Life Cycle 부터 살펴보자면 위와 같다.


만약 Prop의 변화를 감지해서 State를 변환시켜 줄때는 componentWillReceiveProps에서 해주는게 맞다.


왜냐하면 shouldComponentUpdate 이후에는 setState를 할 수 없다.


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

본론으로 들어가서 왜 그럴까?


Note that React may call this method even if the props have not changed, so make sure to compare the current and next values if you only want to handle changes. This may occur when the parent component causes your component to re-render.

출처:https://reactjs.org/docs/react-component.html


리액트 공식문서를 살펴보면 componentWillReceiveProps에 대해 위와같이 서술하고 있다.

"이 메소드는 props가 변하지 않더라도 Call 할 수 있으므로, 현재 current value와 next value를 꼭 비교해서 써야하며, 이는 상위 component가 너의 컴포넌트를 re-render 시킨다" 고 설명한다


더 이해하기 쉽게 예를들어보자,


예를들어, 타이머가 현재 카운팅을 하고있어서 현재 isPlaying 값이 true라고 가정해보자.

현재 isPlaying이 true 면서 props가 바뀌지 않았을때 componentWillReceiveProps 메소드를 콜 한다면, if(nextProps.isPlaying) 의 조건식은 항상 참 일수 밖에없다.


그러나 if(!currentProps.isPlaying && nextProps.isPlaying) 형식으로 조건문을 걸어준다면, false && true 이므로, props가 변동이 있을때만, 이 조건문이 실행되게 되는 것 이다.


 

 current.isPlaying

nextProps.isPlaying

when props are not Changed

 true

 true

when props are Changed

 true

 false 



Scenario


개인적으로 리액트 네이티브를 공부하다가, Expo 서버 연결 없이 안드로이드 앱을 공기계에서 실행해보고싶었다.

예전 앱개발을 할땐 apk파일이 자동적으로 떨궈졌는데

ReactNative로 apk파일을 얻어보려니, 공식 DOC에도 제공을 하지 않아 이것저것 삽질(?) 을 시작해봤다.



Solution


One of the points of Expo on top of React Native is that you don't go down to android or ios code. Expo deals with those folders for you, you don't need to interact with them. Is there a reason you need those folders? if so, you will have to detach. Here's the documentation to do so


설명: Expo를 React-Native에서 공식적으로 패키징 시키면서, android나 ios 코드로 직접 접근하지 않게 해놨다.

(근데 듀토리얼은 왜 예전 기준일까?)

출처:https://stackoverflow.com/questions/44270504/react-native-ios-and-android-folders-not-present


삽질 좀 하다가 프로젝트 구조를 예전으로 되돌릴 방법을 찾아 냈다.


다음 세가지 순서로 진행된다.

1.expo 에서 expo 프레임워크를 제거 한다.

npm run eject

2.Bundle debug build 를한다 

react-native bundle --dev false --platform android --entry-file index.android.js --bundle-output ./android/app/build/intermediates/assets/debug/index.android.bundle --assets-dest ./android/app/build/intermediates/res/merged/debug

3.Create debug build를 한다

cd android
./gradlew assembleDebug

For Windowsgradlew assembleRelease

Generated apk will be located at android/app/build/outputs/apk


출처:https://stackoverflow.com/questions/35283959/build-and-install-unsigned-apk-on-device-without-the-development-server


Scenario


원래는 Expo를 이용해서 React Native 앱을 핸드폰에서 실행해서 디버깅을 했다.

그런데 방화벽 문제가 생기면서 Expo로 핸드폰에서 앱을 실행 할 수 없었다.

Expo로 App을 Create 하지않고 React Native로 공식 문서를 보며 안드로이드 AVD로 개발환경을 구축하고자 했다.

그렇게 나의 삽질은 시작되었다..



Solution


1. npm install -g create-react-native-app

 일단 RN 앱을 쉽게 만들기 위해 create-react-native-app을 설치한다


2.create-react-native-app AwesomeProject && cd AwesomeProject && npm start

여기까지면 Rn 프로젝트를 만든 것이다.


3. Android Studio 와 JDK를 설치해야한다. (JDK는 환경변수도 설정, 이건 패스)


4.안드로이드 스튜디오를 키고 Tools -> android -> AVD manager 메뉴에 들어가서 nexus 5를 설치한다

 참고

 -Tools -> Android 메뉴조차 활성화가 안돼있을때가 있다. 그럴때는

 4.1.우측 하단에 Event Log 를 누른다

 4.2.클릭하면 에러메세지가 뜨는데 그것을 클릭하면 알아서 인스톨을 해준다 (이거때문에 거의 3시간 삽질함)

 출처: https://stackoverflow.com/questions/46948322/how-to-open-avd-manager-in-android-studio-3-0-version/47143861

 


5.이제 AVD를 켠뒤에 아까 만든 폴더에 들어가서 npm run android 라고 치면 실행이 된다.

만약 안될경우에는 재부팅을 한번 해보자(여기서 1시간 정도 삽질함ㄴㅇ라ㅓㄴ)


6.Ctrl + M 으로 핫로딩 설정도 완료 할 수있다.

(Hot loading도 아주 잘되는걸 볼 수 있다.)

Array(배열)

sort()

배열 안의 원소를 정렬해 반환(알파벳,한글,숫자 모두가능)

[2,3,1].sort() => [1,2,3]

reverse()

배열의 원소 순서를 반대로 정렬해 반환

[1,2,3].reverse() => [3,2,1]

concat(인자,인자,…)

기존 배열에 인자의 내용을 추가해 반환

[1,2].concat(3) => [1,2,3]

[1,2].concat([3,[4]]) => [1,2,3[4]]

slice(시작,끝)

시작 인자부터 끝 인자 전까지 배열을 반환, 문자열의 substring과 비슷함

인자를 1개 입력시 입력값의 인덱스부터 끝까지 반환

음수 입력시 뒤에서부터 반환

[1,2,3,4].slice(1,2) => [2]

[1,2,3,4].slice(2) => [3,4]

[1,2,3,4].slice(-3,-2) => [2]

splice(시작,끝,삽입,삽입,…)

배열의 원소를 삽입, 제거할 때 사용

인자는 순서대로 삭제시작인덱스, 삭제할개수, 삽입할원소, 삽입할원소,…

아래 예제는 실행시마다 변수 a 다시 선언 후 실행한 결과

let a = [1,2,3,4,5]

a.splice(2) => [3,4,5] , a => [1,2]

a.splice(2,1) => [3] , a => [1,2,4,5]

a.splice(2,1,’a’,’b’) => [3] , a => [1,2,”a”,”b”,4,5]

push(인자,인자,…)

인자의 내용을 배열의 뒤에 추가하고 배열의 길이를 반환

let a = [1,2,3]

a.push(4,5) => 5, a => [4,5,1,2,3]

pop()

배열의 마지막 원소를 삭제 후 반환

let a = [1,2,3]

a.pop() => 3 , a => [1,2]

unshift()

배열의 앞에 추가하고 배열의 길이를 반환

let a = [1,2,3]

a.unshift(4,5) => 5, a => [1,2,3,4,5]

shift()

배열의 앞에서 제거하고 배열의 길이를 반환

let a = [1,2,3]

a.shift() => 1, a => [2,3]

toString()

배열의 모든 원소를 쉼표로 구분해 문자열로 반환

[1,2,”a”,”b”].toString() => “1,2,a,b”


ECMA2015(ES6) 배열 메서드


forEach(함수(인자))

배열을 순회하는 메서드

let sum = 0;

[1,2,3,4].forEach((x) => (sum += x)) => 10

map(함수(인자))

배열의 각 원소를 함수의 인자로 전달하고 해당 함수로 연산 후 새로운 배열로 반환

[1,2,3,4].map((x) => { return x*x }) => [1,4,9,16]

filter(함수(인자))

배열의 각 원소를 함수의 인자로 전달하고 해당 함수의 조건식을 만족하는 원소만 새로운 배열로 반환

[1,2,3,4].filter((x) => { return x<4 }) => [1,2,3]

every(함수(조건문)))

배열의 각 원소에 대해 조건식을 모두 만족하면 true를 반환, 빈 배열일 경우 true 반환

[1,2,3].every((x) => { return x<4 }) => true

some(함수(조건문)))

배열의 일부 원소에 대해 조건식을 만족하면 true를 변환, 반환값이 true로 결정되면 중단, 빈 배열일 경우 항상 false 반환

[1,2,3].some((x) => { return x<2 }) => true

reduce(함수(인자,인자)), reduceRight()

배열의 각 원소를 순차적으로 두개씩 비교해 함수에 의해 연산, reduce()는 오름차순(왼쪽>오른쪽), reduceRight()는 내림차순(오른쪽>왼쪽)으로 진행

[1,2,3,4,5].reduce((x,y) => { return x-y }) => 1-2-3-4-5 = -13

[1,2,3,4,5].reduceRight((x,y) => { return x-y }) => 5-4-3-2-1 = -5

indexOf(인자), lastIndexOf(인자)

배열의 원소중 특정 값을 찾음, indexOf는 오름차순(왼쪽>오른쪽), lastIndexOf는 내림차순(오른쪽>왼쪽) 순으로 검색하며 가장 먼저 찾은 값의 인덱스를 반환, 값이 없으면 -1 반환

[2,4,6,4,8].indexOf(4) => 1

[2,4,6,4,8].lastIndexOf(4) => 3

isArray(인자)

해당 인자가 배열인지 확인

let a = [1,2,3]

Array.isArray(a) => true


String(문자열)


charAt(인자)

지정한 인덱스의 문자값을 반환

“hello”.charAt(str.length ### 1) => “o”

indexOf(인자)

인자의 내용을 탐색해 index 반환

“hello”.indexOf(“h”) => 4

match(인자)

인자의 내용을 탐색해 반환, 여러개 있으면 배열로 반환

보통 인자는 정규식으로 작성 /정규식/플래그 형태로 쓴다

“hello”.match(/l/g) => [“l”,”l”]

replace(인자,인자)

앞의 인자에 문자열에서 찾을 내용을 적고, 뒤의 인자에 변경할 내용을 적는다

“hello”.replace(“ello”,”i”) => “hi”

split(인자) <=> join(인자)

split: 인자의 내용을 기준으로 문자열을 잘라 배열로 반환

join: 인자의 내용을 기준으로 배열을 문자열로 합침(미입력시 ,로 연결)

“hello world”.split(“ “) => [“hello”,”world”]

[“hello”,”world”].join(“ “) => “hello world”

substring(시작,끝)

문자열에서 시작과 끝 인덱스로 문자열 반환

“hello”.substring(1,2) => “e”

substr(시작,길이)

문자열에서 시작 인덱스와 길이로 문자열 반환

“hello”.substr(1,2) => “el”

출처



출처: http://takeuu.tistory.com/102 [워너비스페셜]

+ Recent posts