스프링프레임워크에서 JUnit 을 이용한 API 테스트 : Spring Framework + JUnit + JNDI

junit 없이 매번 was 재기동 및 postman 호출로 api 결과들을 확인하는 방식으로 테스트를 하다 

junit 을 한 번 사용해 볼까 하여 기존 프로젝트에 설정을 잡고 사용해보았다.

 

혹여 본 포스팅 내용을 참고 하신다면, 프로젝트마다 context 및 프로젝트내에서 사용중인 lib version 등의 설정이 다르므로 제가 작성한 글은 그냥 참고만 하시길 바랍니다. 

실제 프로젝트에서 사용중인 코드 내에서 일부를 발췌하였으므로 github 등에 소스 공유는 힘듭니다.

언제가 될진 모르겠지만 별도의 프로젝트를 만들어 추후 github에 올려놓고 repository 링크를 공유하겠습니다..

출하면 안될 것 같은 패키지 경로, 실제 소스, 결과 등의 코드는 생략하였습니다.

 

[pom.xml]

필요한 의존성 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.7</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>1.9.5</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>3.2.2.RELEASE</version>
    <scope>test</scope>
</dependency>
cs

 

[ApiControllerTest.java]

src/test/java 밑에 패키지 생성 후 아래와 같이 작성한다.

※ 테스트 케이스 생성 방법

테스트를 하고자 하는 클래스 선택 -> new -> JUnit Test Case

실제 소스 경로와 동일한 경로의 테스트 소스 경로가 만들어진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package ~;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import java.nio.charset.Charset;
import java.util.Map;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
import ~.BaseController;
import ~.IdolApiController;
import ~.IdolApiService;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
                  "classpath:bmp/spring/context-*.xml" 
                , "classpath:bmp/sqlmap/mybatis-config.xml" 
                , "classpath:bmp/servlet/servlet-*.xml"
              
                })
@ActiveProfiles("dev")
@TransactionConfiguration(transactionManager="txManager", defaultRollback = false)
@Transactional
@WebAppConfiguration
public class ApiControllerTest3 {
    protected static final Logger logger = LoggerFactory.getLogger(ApiControllerTest3.class);
    
    @Autowired
    private WebApplicationContext context;
    
    private MockMvc mvc;
    
    private MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
            MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));
    
    @Before
    public void setUp() throws Exception {
        mvc = MockMvcBuilders.webAppContextSetup(this.context).build();
    }
    
    
    @Test
    public void testIdolApiController() throws Exception {
        
        JSONObject jobj = new JSONObject();
        jobj.put("id""test");
        jobj.put("clientIp""172.0.0.1");
        jobj.put("content""testcontent.mpg");
        String p = jobj.toString();
        
        MvcResult rs = null;
        
        rs = mvc.perform(post("/getdata")
                                       //.content("{\"said\":\"test\", \"clientIp\":\"172.0.0.1\", \"content\":\"testcontent.mpg\"}")
                                         .content(jobj.toString())
                                         .contentType(contentType)
                        )
        .andDo(print())
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andReturn();
        
        logger.debug("---------------------------");
        logger.debug("result : {}", rs.getModelAndView().getModel());
        
    }
}
 
cs

31 L : junit test 를 위해 클래스에 붙여주는 annotation

32 L : 참조할 bean 들의 설정이 잡혀있는 context 경로 (classpath/file 및 절대/상대 경로 주의)

38 L : 활성시킬 profile 지정 (profile과 관련된 내용은 여기를 참고)

39 L : 트랜잭션 설정

40 L : 트랜잭션 적용

41 L : web controller 테스트를 위한 annotation

53 L : @Before를 메소드에 달아줄 경우 @Test 실행 전에 실행된다.

55 L : MockMvc 빌드를 위한 코드 ( 정확히 모르니 추후 Mock 관련 공부를 하며 보충 )

70~77 L : 

post(url) : post 방식 호출

content(String) : body 로 전송

contentType(contentType) : 호출시 content Type 지정

andDo(print()) : 결과 출력

andReturn() : 결과값 리턴(MvcResult rs 변수에 담기)

 

※ context 가 아닌 특정 controller 만 주입 받아 처리하고 싶은 경우 아래와 같이..

참고 : https://thswave.github.io/java/2015/03/02/spring-mvc-test.html

@Mock
VodApiService vodApiService;
@InjectMocks
VodApiController vodApiController;
private MockMvc mvc;

@Before
public void setUp() throws Exception {
      MockitoAnnotations.initMocks(this);
      mvc = MockMvcBuilders.standaloneSetup(vodApiController).build();
}

[junit 실행 결과]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
MockHttpServletRequest:
         HTTP Method = POST
        Request URI = /getData
          Parameters = {}
             Headers = {Content-Type=[application/json;charset=UTF-8]}
 
             Handler:
               Type = ~
             Method = ~
 
  Resolved Exception:
                Type = null
 
        ModelAndView:
           View name = jsonView
                View = null
           Attribute = result_msg
               value = 
           Attribute = data
              value = {~}
           Attribute = result_cd
               value = 1
 
            FlashMap:
 
MockHttpServletResponse:
              Status = 200
       Error message = null
             Headers = {Content-Type=[application/json;charset=UTF-8], Pragma=[no-cache], Cache-Control=[no-cache, no-store, max-age=0], Expires=[1]}
        Content type = application/json
               Body = {~}
       Forwarded URL = null
      Redirected URL = null
             Cookies = []
DEBUG: ~ - ---------------------------
DEBUG: ~ - result : {~}
cs

 

여기서 log가 debug 레벨로 잘 출력 됐음을 확인 할 수 있는데, 이는 log4j 설정이 잡혀 있는 파일 (본 프로젝트에선 log4j2.xml)을 src/test/resources 밑에 복사 붙여넣기 한 후, log 레벨을 debug 로 내렸기 때문이다.

src/main/resources 내의 log4j 설정 파일을 읽기 전에 src/test/resources 내의 log4j 설정 파일을 우선적으로 읽는다. 관련 내용은 여기를 참고.

log4j 설정 관련 글은 여기를 참고.

 

JNDI 를 사용하고 있는 경우, jUnit test

보통은 위와 같이 테스트 할 수 있지만,

JNDI 를 사용하여 DB 설정이 잡혀 있는 경우, 별도의 처리가 필요하다.

JNDI 설정의 경우 was 에 DB 설정 정보가 존재하며 was 구동시 이를 읽어 들이는 방식인데(정확히 모름+공부필요),

was가 아닌 JUnit 구동시 DB설정정보를 읽어들이지 못해 이와 관련된 Bean을 주입 받을 때 에러가 발생하기 때문이다.

이를 해결하는 방법은 아래와 같이 두가지 정도가 있는 듯 하다.

 

[방법 1 : 테스트용 context 별도 설정]

1) JNDI 설정이 잡혀있는 context-datasource.xml 을 복사하여 src/test/resources 경로 밑에 붙여넣은 후 파일명을 test-datasource.xml 로 바꿔준다.

[example]

아래와 같은 JNDI 설정을

1
2
3
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
  <property name="jndiName" value="java:/jdbc/testJNDI" />
</bean>
cs

아래와 같이 바꿔준다. (was에 있을 DB접속정보(jndi 설정)를 context에 직접 작성)

1
2
3
4
5
6
7
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" >
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/> 
    <property name="url" value="jdbc:mysql://~:3306/~?useSSL=false&amp;serverTimezone=UTC"/> 
    <property name="username" value="~"/> 
    <property name="password" value="~"/>
</bean>
 
cs

2) 위에서 작성한 JUnit테스트 소스인 ApiControllerTest.java 를 아래와 같이 수정한다.

@ContextConfiguration 부분에 선언된 context 에 1~2) 번에서 작성한 context (jndi 설정을 걷어낸 DB설정(test-datasource.xml))를 추가한다.

1
2
3
4
5
6
@ContextConfiguration(locations = {
                  "classpath:bmp/spring/context-*.xml"
              , "classpath:test-*.xml"
                , "classpath:bmp/sqlmap/mybatis-config.xml" 
                , "classpath:bmp/servlet/servlet-*.xml"
                })
cs

 

위에서 언급한 log4j 설정과 마찬가지로, src/test/resources 가 src/main/resources 보다 선행돼서 읽히기 때문에 src/main/resources 밑의 context 에 선언된 Bean 과 동일한 이름의 Bean이 src/test/resources 밑의 context 에 선언돼 있어도 상관이 없는 듯 하다.

 

 

[방법 2 : JNDI 바인딩]

1) SpringJUnit4ClassRunner 를 상속받는 ExtSpringJUnit4ClassRunner 작성.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package ~;
 
import org.junit.runners.model.InitializationError;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.jndi.JndiTemplate;
import org.springframework.mock.jndi.SimpleNamingContextBuilder;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
public class ExtSpringJUnit4ClassRunner extends SpringJUnit4ClassRunner {
 
    public ExtSpringJUnit4ClassRunner(Class<?> clazz) throws InitializationError {
        super(clazz);
        // TODO Auto-generated constructor stub
        try {
            bindJndi();
        } catch (Exception e) {
            
        }
    }
    
    private void bindJndi() throws Exception {
        SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
        builder.activate();
        
        JndiTemplate jt = new JndiTemplate();
        
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://~:3306/~?useSSL=false&amp;serverTimezone=UTC");
        ds.setUsername("~");
        ds.setPassword("~");
        
        jt.bind("java:/jdbc/testJNDI", ds);
    }
}
 
cs

위와 같이 Jndi 바인딩을 java 소스로 처리.

관련 부분은 여기를 참고했음.

(이런식으로 JNDI 설정을 잡아줄 수 있는 걸 보면 다른 부분 설정도 이와 비슷하게 처리가 가능할 듯)

 

2) 위에서 작성한 JUnit테스트 소스인 ApiControllerTest.java 를 아래와 같이 수정한다.

1
@RunWith(ExtSpringJUnit4ClassRunner.class)
cs

SpringJUnit4ClassRunner 대신 위에서 작성한 ExtSpringJUnit4ClassRunner를 사용한다.

나머지 부분은 동일하다.

(jndi 설정을 java소스(ExtSpringJUnit4ClassRunner.java)에서 잡아주었으므로 방법 1과 같이 별도의 context 를 @ContextConfiguration에 추가해줄 필요가 없다)

 

JUnit 사용시 Maven Build 주의사항

JUnit을 사용하여 테스트를 하고 있고 Maven Build 로 war 파일을 생성하고 있다면 주의해야 할 점.

테스트 코드 결과가 실패인 경우, Maven Build 의 결과가 Failure (실패)로 떨어진다.

이땐 아래와 같이 해결한다.

 

[방법 1 : UI 에서 build option 지정]

프로젝트 우클릭 > Run Configurations.. > 좌측 리스트에서 Maven Build 중 해당 프로젝트 선택 > skip Tests 옵션 체크

 

[방법 2 : pom.xml 에 build option 수정]

1
2
3
4
5
6
7
8
9
 <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.18.1</version>
    <configuration>
        <useSystemClassLoader>false</useSystemClassLoader>
        <skipTests>true</skipTests>
    </configuration>
</plugin>
cs

위에서 7번라인과 같이 skipTests 옵션 true 지정.

해당 내용은 여기를 참고했음.

 

 

전체적인 JUnit에 대한 설명 및 설정등은 여기를 참고했음.

(이것저것 공부하고 관련 레퍼런스 찾아보고 블로그/깃헙 돌아다니다 보면 이상하리만큼 자주 도달하게 되는 곳이 있는데 그중 하나인 곳.. 항상 감사드립니다..)

 

본 포스팅은 다른 포스팅들과 마찬가지로 수시로 수정 및 업데이트 하겠습니다.

 

참고 :

https://effectivesquid.tistory.com/entry/Spring-test-%EC%99%80-Junit4%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8

반응형

※ 들어가기에 앞서, Spring Framework, maven, jboss(wildfly) 환경에서 log4j설정하는 법은 이곳을 참고.

 

log4j.xml 파일을 아래와 같이 설정한다.

pom.xml 및 기타 설정은 되어있다 가정

 

[log4j.xml]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
 
    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%-5p: %c - %m%n" />
        </Console>
    </Appenders>
    
    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="console" />
        </Root>
    </Loggers>
 
</Configuration>
 
cs

 

위에 설정한 콘솔 로그를 출력하기 위해 아래와 같이 Sample.class 를 만들어준다.

[Sample.class]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.log;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class Sample {
    
    protected static final Logger logger = LoggerFactory.getLogger(Sample.class);
    protected static final Logger fileLogger = LoggerFactory.getLogger("fileLogger");
    
    public static void main(String[] args) {
        
        logger.info("Sample.class logger print with info level");
        
        fileLogger.info("fileLogger logger print with info level");
    }
 
}
 
cs

"fileLogger 를 name 으로 하는 logger 가 없는데?" 라는 생각이 들겠지만 일단 main 메소드를 실행시켜보자.

 

[Sample.class 실행결과]

INFO : com.log.Sample - Sample.class logger print with info level
INFO : fileLogger - fileLogger logger print with info level

Exception을 찍거나, 혹은 아무것도 찍지 않을 줄 알았던 fileLogger.info(~); 가 로그를 정상적으로 찍고 있다.

이를 이해하려면 log4j 의 위계구조 (hierarchy) 에 대한 이해가 필요하다.

 

[log4j 위계구조 (hierarchy)]

1. 모든 로거의 최상위 부모는 Root 로거이다.

2. class 에서 로그를 출력하는데 사용된 logger가 log4j.xml 에 존재하지 않는다면, 부모 로거를 찾는다.

: Sample.class 9번 라인의 fileLogger 는 "fileLogger" 라는 name을 갖는 로거를 log4j.xml 에서 찾았으나, 해당 이름의 로거가 존재하지 않으므로 부모인 Root 로거를 타게된 것. (마찬가지 이유로, Sample.class 에서 8번라인의 ~.getLogger(Sample.class) 부분을 ~.getLogger("") 과 같이 바꿔도 로거는 정상출력된다)

 

 

이번엔 log4j 를 다음과 같이 수정 후, 위의 Sample.class 메인메소드를 다시 실행해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    
    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%-5p: %c - %m%n" />
        </Console>
    </Appenders>
    
    <Loggers>
        <Logger name="com" level="INFO" >
            <AppenderRef ref="console" />
        </Logger>
        
        <Logger name="com.log" level="INFO" >
            <AppenderRef ref="console" />
        </Logger>
        
        <Logger name="com.log.Sample" level="INFO" >
            <AppenderRef ref="console" />
        </Logger>
        
        <Root level="INFO">
            <AppenderRef ref="console" />
        </Root>
    </Loggers>
    
</Configuration>
 
cs

 

[실행 결과]

INFO : com.log.Sample - Sample.class logger print with info level
INFO : com.log.Sample - Sample.class logger print with info level
INFO : com.log.Sample - Sample.class logger print with info level
INFO : com.log.Sample - Sample.class logger print with info level
INFO : fileLogger - fileLogger logger print with info level

com.log 패키지 경로에 있는 Sample.class (Sample.class 의 1번라인 참고)는 자신의 클래스명과 일치하는 name을 가진 Logger를 찾는다.

com.log.Sample 로거의 appender 로 로그가 찍히고, 

com.log 로거의 appender 로 로그가 찍히고,

com 로거의 appender 로 로그가 찍히고,

Root 로거의 appender 로 로그가 찍혀 총 4번의 로그가 찍혔다. (fileLogger 제외)

(부모 > Root > com > com.log > com.log.Sample > 자식)

 

이번엔 아래와 같이 com.log 로거에 additivity="false" 속성을 추가해보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    
    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%-5p: %c - %m%n" />
        </Console>
    </Appenders>
    
    <Loggers>
        <Logger name="com" level="INFO" >
            <AppenderRef ref="console" />
        </Logger>
        
        <Logger name="com.log" level="INFO" additivity="false">
            <AppenderRef ref="console" />
        </Logger>
        
        <Logger name="com.log.Sample" level="INFO" >
            <AppenderRef ref="console" />
        </Logger>
        
        <Root level="INFO">
            <AppenderRef ref="console" />
        </Root>
    </Loggers>
    
</Configuration>
cs

 

[실행결과]

INFO : com.log.Sample - Sample.class logger print with info level 
INFO : com.log.Sample - Sample.class logger print with info level 
INFO : fileLogger - fileLogger logger print with info level

com.log.Sample 로거의 appender 로 로그가 찍히고,

com.log 로거의 appender 로 로그가 찍히고 끝이난다.

additivity 값을 false 로 주면 부모 로거로의 전달을 막게된다. (default 값은 true)

 

 

[실제 응용]

1. 특정 레벨의 로그는 별도의 파일로 관리

콘솔로그는 INFO 레벨 이상(INFO < WARN < ERROR...)인 경우 출력,

ERROR(ERROR < FATAL) 레벨 이상의 로그는 별도의 파일에 작성하여 관리하고자 하는 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    
    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%-5p: %c - %m%n" />
            <ThresholdFilter level="INFO"/>
        </Console>
        
        <RollingFile name="logFile" fileName="/logs/api.log" filePattern="/logs/$${date:yyyyMM}/app_%d{yyyyMMdd}-%i.log.gz">
            <PatternLayout pattern="%-5p: %m%n" />
            <Policies>
                <TimeBasedTriggeringPolicy interval="1" />
                <SizeBasedTriggeringPolicy size="100MB"/>
            </Policies>
            <ThresholdFilter level="ERROR"/>    
        </RollingFile>    
    </Appenders>
    
    
    <Loggers>
        <Logger name="com" additivity="false">
            <AppenderRef ref="console" />
            <AppenderRef ref="logFile" />
        </Logger>
          
        <Root level="INFO">
            <AppenderRef ref="console" />
        </Root>
    </Loggers>
</Configuration>
 
cs

FileAppender 는 로그를 파일에 작성하기 위해,

RollingFileAppender 는 로그를 파일에 저장 + 롤아웃(백업) 하기 위해 사용한다.

이외에도 다양한 Appender 들이 지원되니 다른 appender 가 필요하다면 공식 document 를 참고하면된다.

 

TimeBasedTriggeringPolicy 는 시간에 의한 백업

SizeBasedTriggeringPolicy 는 로거 파일 용량에 의한 백업

filePattern 은 백업 파일형식

ThresholdFilter 는 appender 의 로그 레벨을 지정

 

위 설정은 아래와 같다.

com 밑의 모든 class 에서 출력하는 INFO 레벨 이상의 콘솔 로그를 %-5p: %c - %m%n 포맷으로 찍는다.

com 밑의 모든 class 에서 출력하는 ERROR 레벨 이상의 콘솔 로그를 %-5p: %m%n 포맷으로 /logs/api.log 파일에 기록한다.

용량이 100MB 초과할 경우, 혹은 하루가 지난 경우(매일) 로그 파일을 /logs/년월/app_년월일-백업시퀀스.log.gz 이름으로 압축한다.

 

 

※ logback에서의 설정은 아래와 같다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- doc : http://logback.qos.ch/documentation.html -->
<!-- xml example : https://mkyong.com/logging/logback-xml-example/ -->
   <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
      <encoder>
         <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
      </encoder>
   </appender>
 
   <appender name="FILE_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
      <file>/boot_jpa_log/log_stats.log</file>
      <encoder>
         <pattern>%m%n</pattern>
      </encoder>
      <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
         <fileNamePattern>/boot_jpa_log/old/log_stats_%d{yyyyMMdd}.log</fileNamePattern>
      </rollingPolicy>   
   </appender>
   
   <logger name="com" level="DEBUG" additivity="false">
      <appender-ref ref="STDOUT" />
   </logger>
   
   <logger name="org.springframework.boot.autoconfigure" level="WARN">
      <appender-ref ref="STDOUT"/>
   </logger>
   
   <logger name="fileLogger" level="INFO" additivity="false">
      <appender-ref ref="FILE_APPENDER" />
   </logger>
   
   <root level="INFO">
      <appender-ref ref="STDOUT" />
   </root>
 
</configuration>
cs

 

 

[위와 같이 설정한 경우 Sample.class]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.sample.controller;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class Sample {
    
    protected static final Logger logger = LoggerFactory.getLogger(Sample.class);
    
    public static void main(String[] args) {
        
        logger.info("Sample.class logger print with info level");
        
        logger.error("fileLogger logger print with error level");
    }
 
}
 
cs

main 메소드를 실행시 logger.error 로 찍은 로그는 /logs/api.log 파일에 write 되어 있으며, 콘솔에선 찍히지 않는다.

 

[실행 결과 : 콘솔]

INFO : com.sample.controller.Sample - Sample.class logger print with info level

[실행 결과 : /logs/api.log 파일]

ERROR : fileLogger - fileLogger logger print with error level

 

2. 특정 로거에서 출력한 로그는 파일로 저장하기

기존의 console 로그 이외에 로거를 하나 추가로 두고,

추가한 로거를 사용하여 로그를 찍는 경우, 별도의 파일로 저장하고자 하는 경우

fileLogger 를 로거로 추가하고, 파일 어팬더를 주입.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    
    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%-5p: %c - %m%n" />
        </Console>
        
        <RollingFile name="logFile" fileName="/logs/api.log" filePattern="/logs/$${date:yyyyMM}/app_%d{yyyyMMdd}-%i.log.gz">
            <PatternLayout pattern="%-5p: %c - %m%n" />
            <Policies>
                <TimeBasedTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="100MB"/>
            </Policies>
        </RollingFile>
    </Appenders>
    
    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="console" />
        </Root>
        
        <Logger name="fileLogger" level="INFO" additivity="false">
             <AppenderRef ref="logFile" />
        </Logger>
    </Loggers>
    
</Configuration>
 
cs

 

Sample.class는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.sample.controller;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class Sample {
    
    protected static final Logger logger = LoggerFactory.getLogger(Sample.class);
    protected static final Logger fileLogger = LoggerFactory.getLogger("fileLogger");
 
    public static void main(String[] args) {
        
        logger.info("Sample.class logger print with info level");
        
        fileLogger.info("fileLogger logger print with info level");
    }
 
}
 
cs

 

[실행결과 : 콘솔]

INFO : com.sample.controller.Sample - Sample.class logger print with info level

[실행결과 : /logs/api.log]

INFO : fileLogger - fileLogger logger print with info level

 

참고:

https://logging.apache.org/log4j/2.x/manual/architecture.html

https://logging.apache.org/log4j/2.x/manual/appenders.html

 

반응형

스프링 빈은 Thread safe 하나,

스프링 빈 내에서의 멤버변수는 Thread safe 하지 않다.

--> race condition 을 유의해야 한다.

--> stateless 하게 개발해야 한다.

 

※ race condition : 

두 개 이상의 프로세스가 공통 자원을 병행적으로(concurrently) 읽거나 쓸 때,

공용 데이터에 대한 접근이 어떤 순서에 따라 이루어졌는지에 따라 그 실행 결과가 달라지는 상황을 말한다.

※ stateful :

상태 변화에 의존적인 상태를 의미.

ex)서버 session 은 stateful, 클라이언트가 토큰을 들고있는 방식은 stateless (공부중)

 

[Example]

1
2
3
4
5
6
7
8
9
10
11
@Service
public class TestServiceImpl implements TestService {
  
    private int idx = 0;
 
    @Override
    public void test(){
        System.out.println("idx : " + idx);
        idx++;
    }
}
cs

ControllerA  에서 TestService를 주입받아 test() 메소드 실행시 idx 값이 호출될 때마다 증가하며,

ControllerB 에서도 TestService를 주입받아 test() 메소드 실행시 idx 값이 호출될 때마다 증가함을 확인 할 수 있다.

 

만약 멤버변수를 단순 읽기 전용으로 사용한다면 문제되지 않지만, 위와 같이 읽기 뿐만 아닌 쓰는 용도로 사용한다면 문제가 된다.

 

이처럼 스프링 Bean 은 Default로 싱글톤 디자인패턴을 따르므로, 

멤버변수에 값을 저장(쓰는 행위)하는 식의 stateful한 설계는 피해야 한다.

(가급적 스프링 빈으로 관리되는 객체만 멤버변수로 사용하는게 좋다)

 

 

[코드 개선]

위와 같은 코드는 멤버변수를 제거하고, 메소드 내에서 지역변수로 선언 및 파라미터로 값을 들고 다니는 방식으로 수정을 하거나(권장), 빈의 scope 를 prototype 으로 주어 호출시마다 새로운 인스턴스를 생성하도록 해야 한다.

@Scope("prototype") annotation 을 class 선언 부에 달아주면 해당 객체는 싱글톤이 아닌 프로토타입 객체가 된다.

※ 싱글톤 빈 내에서 프로토타입 빈 생성을 할 경우 정상적으로 프로토타입 빈 동작이 되지 않는다(호출마다 빈이 생성되야 하나 그렇지 못함)

 

[prototype Scope를 적용한 코드]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
@Scope("prototype")
public class TestServiceImpl implements TestService, InitializingBean, DisposableBean {
  
    private int idx = 0;
 
    @Override
    public void test(){
        System.out.println("idx : " + idx);
        idx++;
    }
 
    @Override 
    public void afterPropertiesSet() throws Exception { 
        logger.info("created service! "); 
    }
 
    @Override 
    public void destroy() throws Exception { 
        logger.info("destroyed service! "); 
    }
 
}
cs

※ InitializingBean, DisposableBean 인터페이스의 afterPropertiesSet(), destroy() 메소드는 빈이 생성, 제거 될 때 각각 호출된다.

 

코드를 위와 같이 수정한 후, 이전과 동일하게

1) ControllerA  에서 TestService를 주입받아 test() 메소드 실행,

2) ControllerB 에서도 TestService를 주입받아 test() 메소드 실행한 경우 결과는 아래와 같다.

[실행 결과]

created service!
idx : 0
created service!
idx : 1

 

참고:

토비의 스프링 3.1(도서)

https://gmlwjd9405.github.io/2018/11/10/spring-beans.html

https://beyondj2ee.wordpress.com/2013/02/28/%EB%A9%80%ED%8B%B0-%EC%93%B0%EB%A0%88%EB%93%9C-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B9%88-%EC%A3%BC%EC%9D%98%EC%82%AC%ED%95%AD/

https://gmlwjd9405.github.io/2018/11/10/spring-beans.html

 

 

반응형

1. component-scan

- spring 2.5 버전에 출시

- 2.5 버전 이전엔 xml file 에 모든 bean 을 설정해야 했는데, component-scan 이 xml 내의 bean 설정 양을 줄여줬다

- @Component, @Repository, @Service, @Controller 어노테이션을 스캔한다

- @Autowired, @Qualifier 어노테이션도 해석

- component-scan 이 선언되어있다면 annotation-config 는 필요없음

 

2.annotation-driven

- annotation-driven 은 spring mvc 컴포넌트를 활성화

- annotation-driven 을 포함하지 않아도 component-scan을 사용하여 Bean을 생성한 경우 mvc application은 동작

- annotation-driven은 구성되지 않은 특정 빈을 구성하는데 추가 작업을 수행

- @Controller 에 요청을 하기 위해 필요한 HandlerMapping, HandlerAdapter 을 등록

- @Number로 숫자 필드 지원 , classPath에 Jackson이 있는 경우 JSON 쓰기 읽기를 지원 와 같은 작업 적용

 

3. annotation-config

- 어플리케이션 context에 이미 등록되어 있는 (xml에 선언돼있거나 package scanning 으로 검색된) 어노테이션 빈을 활성화

- 스프링 컨텍스트에 의해 생성 및 저장된 빈에 대해 @Autowired, @Qualifier 어노테이션을 해석

- component-scan 또한 같은 일을 하지만, annotation-config 는 빈을 찾아 등록해주진 않는다. 이미 등록이 끝난 bean 들을 활성화할 할 뿐이다.

 

※ annotation-config 만 설정에 잡혀있을 경우, Bean 이 스캔에 의해 자동으로 등록되지 않으므로 xml 에 Bean 설정을 해주어야 함. 현재 개발중인 배치서버 설정에 빈이 자동으로 잡히지 않아 찾아보니 annotation-config 설정만 잡혀있고, component-scan 혹은 annotation-driven 은 잡혀있지 않았음.

 

참고 및 해석 : https://javabeat.net/spring-mvc-component-scan-annotations/ 

반응형

[Spring property 두가지 설정 방법]

1. context:property-placeholder 태그 사용

<context:property-placeholder location="classpath:프로퍼티파일명"/>

* 빈팩토리 후처리기로써 빈 설정 메타정보 모두 준비됐을 때 메타정보 자체를 조작 : 빈 설정이 끝난 이후 ${} 표현을 찾아 해당하는 프로퍼티로 치환해준다는 의미인 듯

* ${} 사용 : "${프로퍼티key}" 와 같이 사용

* @Value annotation 에서도 사용이 가능 : @Value("${프로퍼티key}")

 

2. bean 으로 등록

<util:properties id="빈아이디" location="classpath:프로퍼티파일명" />

* Spring 3.0 이상부터 지원

* 프로퍼티 파일 내용을 Properties 타입의 빈으로 생성

* spEL(Spring Expression Language) 사용 : #{빈아이디['프로퍼티Key']} 와 같이 사용

* @Value annotation 에서도 사용이 가능 : @Value("빈아이디['프로퍼티Key']")

 

[Ex]

1. Comm.properties 파일 생성 (resources/sample/properties/Comm.properties)

api.server.url="https://sample.api.com"
db.driverClass="sampleDriverClass"
db.url="sampleDBUrl"
db.username="sampleDBUrl"
db.password="sampleDBUrl"

2. property 설정(Bean 주입, context.xml 설정)

<context:property-placeholder location="classpath:sample/properties/Comm.properties" />
혹은
<util:properties id="properties" location="classpath:sample/properties/Comm.properties" />

※서버 환경별(로컬, 개발, 운영 등) property 파일을 동적으로 다르게 사용하고자 할 경우 이곳 참고

 

3. 스프링설정파일 (~context.xml) 에서의 사용

context:property-placeholder 태그 사용한 property 설정시 :

1
2
3
4
5
6
<bean id=""dataSource" class="~">
   <property name="driverClass" value="${db.driverClass}" />
   <property name="url" value="${db.url}" />
   <property name="username" value="${db.username}" />
   <property name="password" value="${db.password}" />
</bean>
cs

util:properties 와 같이 bean 으로 property 설정시 :

1
2
3
4
5
6
7
<bean id=""dataSource" class="~">
   <property name="driverClass" value="#{properties['db.driverClass']}" />
   <property name="url" value="#{properties['db.url']}" />
   <property name="username" value="#{properties['db.username']}" />
   <property name="password" value="#{properties['db.password']}" />
</bean>
 
cs

 

4. 소스에서의 사용

context:property-placeholder 태그 사용한 property 사용시 :

@Value("${api.server.url}") 

private String apiServerUrl;

 

util:properties 와 같이 bean 으로 property 사용시 :

@Value("#{properties['api.server.url']}") 

private String apiServerUrl;

 

 

[ property 의 형변환 ]

property 값은 기본적으로 String 형태의 Text로 선언 및 사용되는게 default.

String 이 아닌 다른 기타 자료형을 바꿔서 사용해야하는 경우

1. PropertyEditor : 스프링이 default로 해당 에디터를 사용하여 property를 형변환 함 

: boolean, short, int, long, double 등의 기본 자료형 및 Boolean, Short, Integer, Long, Double 등의 wrapper클래스의 오브젝트 타입도 지원

ex) @Value("1.2") double num;

: array 도 사용 가능

ex) @value("1,2,3") int[] arr;

 

* PropertyEditor interface 직접구현하여 사용자가 직접 정의한 형태로 값을 리턴하도록 사용 가능 (Thread safe X)

 

2. ConversionService : 스프링 3.0부터 지원 (Thread safe O)

bean 으로 선언하여 사용.

 

 

공부를 더 해봐야 하겠지만 지금 당장 String 및 기본자료형 정도로만 property 사용하면 되므로

나중에 필요할 때 공부하는걸로..

 

참고 :

토비의스프링 3.1 (도서)

https://whiteship.tistory.com/2563

반응형

* Spring 5.1.2

* jdk 1.8

* jboss/wildfly 9

* jndi 

 

1. xml + aop pointcut 을 사용한 설정 (context xml 설정)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!-- transactionManager 적용 범위 지정(advice) -->
<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
        <tx:method name="get*" read-only="true" rollback-for="Exception" />
        <tx:method name="select*" read-only="true" rollback-for="Exception" />
        <tx:method name="insert*" read-only="false" propagation="REQUIRED" rollback-for="Exception"/>
        <tx:method name="update*" read-only="false" propagation="REQUIRED" rollback-for="Exception"/>
        <tx:method name="delete*" read-only="false" propagation="REQUIRED" rollback-for="Exception"/>
    </tx:attributes>
</tx:advice>
 
<!-- transactionManager pointcut  -->
<aop:config>
    <aop:pointcut id="txMethod" expression="execution(* com.sample..*Service.*(..))" />
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txMethod" />
</aop:config>
 
<!-- transactionManager bean -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>
 
<!-- datasource bean (jndi 사용) -->
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="java:/jdbc/AdmJNDI" />
</bean>
     
cs

* aop pointcut 으로 선언 및 설정할 경우 위와 같이 적용할 pointcut 범위를 지정.

(* com.sample..*Service.*(..)) : 모든 접근제한자의 com 패키지 밑의 sample 패키지 밑의 ~Service로 끝나는 모든 클래스 밑의 모든(*) 메소드

* advice 설정에 메소드별로 제약 및 옵션을 걸어 줄 수 있다.

  get* : get으로 시작하는 모든 메소드

  select* : select 로 시작하는 모든 메소드

* read-only = true : insert / update / delete 쿼리를 내부적으로 실행시 exception 을 뱉는다.

* roll-backfor = Exception : Exception (Exception 밑의 모든 Exception 포함) 이 발생할 경우 rollback 처리. (no rollback for 옵션으로 특정 Exception이 발생시 rollback 처리를 하지 않게 처리 가능)

propagation : 트랜잭션의 전파속성으로써 메소드 내에서 다른 메소드를 사용할 때 하나의 트랜잭션으로 묶을지, 별도의 트랜잭션으로 분류할지 등 과 같은 설정을 지정하는 옵션. default 값이 required 로 알고 있어 위와 같이 선언할 필요는 없는 걸로 알고 있다.. 여러가지 설정이 있는데 나중에 정리하는걸로. 

 

>> com.sample..*Service 밑의 get*, insert*, delete*, select*, update* 이름을 가진 모든 메소드는 위의 트렌잭션 설정에 의해 관리된다.

 

 

2. annotation 을 사용한 설정

1
2
3
4
5
6
7
8
9
10
11
12
<!-- transactional annotation 설정 사용 -->
<tx:annotation-driven transaction-manager="txManager" />
 
<!-- transactionManager bean -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>
 
<!-- datasource bean (jndi 사용) -->
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="java:/jdbc/AdmJNDI" />
</bean>
cs

* txManager 와 관련된 database 설정, transaction manager bean 선언 부분은 1번과 동일.

 

>> Transaction 으로 관리하고 싶은 메소드위에 @Transactional 어노테이션을 달아주어 사용.

 

 

* 주의사항

1. @Transactional 은 public 메소드에서만 정상 작동한다.

 

2. @Transactional 을 달아놓은 메소드가 동일한 클래스 내의 다른 메소드에 의해 호출된다면 트랜잭션이 정상 작동하지 않는다.

ex: 퍼사드 패턴이나 템플릿 패턴처럼 여러 메소드를 내부적으로 묶어 사용하고 있는 메소드가 있다면 구성요소 메소드에 @Transactional 를 달지 않고 구성요소를 묶고 있는 상위개념의 메소드에 @Transactional 을 달아주어야 한다. 구성요소 메소드에 @Transactional 을 달아 주어 트랜잭션으로 관리 할 경우 rollback 이 정상적으로 작동하지 않는 경우가 발생한다.

 

위 내용과 같은 경우가 아래에 정리되어 있다.

https://woowabros.github.io/experience/2019/01/29/exception-in-transaction.html

propagation 속성이 required 인 경우, 트랜잭션안에서 호출되는 메소드가 트랜잭션으로 같이 묶이게 되어 예상치 못한 결과가 나올 수 있다는 내용이다.

※ required propagation 속성은 트랜잭션이 이미 존재하면 append를 하고, 트랜잭션이 존재하지 않다면 새로 생성한다. (공식 doc)

 

3. Spring Transaction 은 기본적으로 unchecked Exception (RuntimeException) 만 관리하며 checked Exception (IOException, SQLException 등) 은 관리하지 않는다.

처리방법 1: @Transactional(rollbackFor=Exception.class) 와 같이 설정하여 모든 Exception 발생시 rollback 이 발생하게 처리하거나(unchecked Exception, checked Exception 모두 Exception.class 밑에 있다.)

처리방법 2: checked Exception 이 발생할 가능성이 있는 부분을 try ~ catch (SQLException se){throw new RuntimeException(); } 과 같이 처리(checked Exception 발생시 unchecked Exception 으로 예외를 바꾸어 던지게 처리)하여 Transaction의 관리대상으로 묶어버릴 수 있다.

 

4. pointcut 을 사용한 Transaction 설정과 어노테이션을 사용한 Transaction 설정을 동시에 사용하게 될 경우, 어노테이션이 우선적용(precedence)되는 것 같다. (https://stackoverflow.com/questions/32057910/custom-spring-aop-around-transactional/33509777#33509777)

 

 

* 회고.

몇일 전, 정상적으로 트랜잭션이 관리되고 있다고 믿고 있던 내 소스가 특정 케이스에서 rollback 이 이뤄지지 않고 있다는 걸 발견했다.멘탈폭발

개발중인 소스는 4번과 같이 두 설정 모두를 사용하고 있던터라 advisor 와 어노테이션을 함께 사용하여 발생하는 문제인가 싶어 트랜잭션 관련 설정을 이렇게도 바꿔보고 저렇게도 바꿔보았다.

(stackoverflow 에서 두가지 설정 모두를 사용하여 충돌이 발생할 경우 order 설정에 의한 우선순위에 의해 트랜잭션이 관리되고, order 설정이 존재하지 않는다면 마지막에 선언된 설정을 따른다는 글을 봤던게 설정에 의심을 갖게된 결정적인 계기였다. (답변자가 부정확한 답변을 했거나, 내 해석이 잘못됐거나, 개발환경 차이에 의한 차이거나... 그중 하나겠지..))

퇴근 후 몇일간 탐구삽질해본 결과

동일한 프로젝트 내의 타 메소드에선 트랜잭션 관리가 정상적으로 되고 있다는 걸 알 수 있었고, 주말까지 잡아먹은 끝에 4번의 문제가 아닌, 2번의 문제였다는 걸 깨닫게 되었다..(어설프게 디자인 패턴 써보겠다고 소스 리팩토링 하면서 스스로 만들어낸 참사)

애초에 @Transactional 자체가 정상 작동할 수 없는 케이스에서 rollback이 이뤄지지 않는다고 다른 부분들을 전부 의심하기 시작한 셈이다.

덕분에? 관련 docs 도 많이 찾아 읽어보고, transaction 관련 설정부분 및 기본이자 핵심 부분들을 조금이나마 더 깊이있게 공부할 수 있었던 의미 있는 삽질의 시간이었다.

 

* 3번의 경우 위와 같이 설명되어 있는 글들을 여럿 봤는데, 내 설정과 코드에선 굳이 위처럼 처리하지 않더라도(단순히 @Transactional 어노테이션만 붙여도) rollback 이 잘 이루어 지고 있다.. 스프링 버전이나 jdbc 트랜잭션매니저 버전이 올라가면서 SQLException 을 스프링이 트랜잭션 관리 대상에 포함을 시킨건지.. (잘 모르겠으니 다시 구글링 좀 해보고, 테스트 코드도 작성해봐야겠다..)

 

 

반응형

POI (Excel) + Spring + JSP

 

POI lib 을 사용한 excel 파일 생성 및 다운로드

 

 

1. dependency 추가(HSSF 예제)

1
2
3
4
5
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>3.15</version>
</dependency>
cs

* HSSF이외에 XSSF, SXSSF 라이브러리 존재. 

HSSF : excel 의 .xls 파일 포맷을 위한 POI

XSSF : .xls 및 .xlsx 파일 포맷을 위한 POI

SXSSF : 이미지 및 대용량 엑셀 데이터를 위한 POI

SXSSF 의 경우 HSSF, XSSF에 비해 성능이 떨어지는 거라는 글을 봤음, 대용량 처리가 아닌 이상 XSSF 혹은 HSSF 사용

 

2. 엑셀 파일 생성(HSSF 사용 예제)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Override
    public HSSFWorkbook listExcelDownload(VO param) throws Exception {
        
        
        HSSFWorkbook workbook = new HSSFWorkbook();
        
        HSSFSheet sheet = workbook.createSheet("엑셀시트명");
        
        HSSFRow row = null;
        
        HSSFCell cell = null;
        
        param.setPager(false);
        param.setNullText(NULL_TEXT);
        param.setSeparator(DELI_EXCEL);
        List<VO> list = Dao.selectList(param);
        
        row = sheet.createRow(0);
        String[] headerKey = {"칼럼1""칼럼2""칼럼3""칼럼4"};
        
        for(int i=0; i<headerKey.length; i++) {
            cell = row.createCell(i);
            cell.setCellValue(headerKey[i]);
        }
        
        for(int i=0; i<list.size(); i++) {
            row = sheet.createRow(i + 1);
            StbcsTaskHstVO vo = list.get(i);
            
            cell = row.createCell(0);
            cell.setCellValue(vo.getEx1());
            
            cell = row.createCell(1);
            cell.setCellValue(vo.getEx2());
            
            cell = row.createCell(2);
            cell.setCellValue(vo.getEx3());
            
            cell = row.createCell(3);
            cell.setCellValue(vo.getEx4());
 
        }
        
        return workbook;
    }
    
cs

* HSSF lib 사용시

HSSFWorkbook 생성 > HSSFWorkbook에 HSSFSheet 생성 > HSSFSheet에 HSSFRow 생성 > HSSFRow에 HSSFCell 생성

* XSSF lib 사용시

흐름은 위와 동일, HSSF을 XSSF로만 바꿔주면 됨(ex: HSSFWorkbook >> XSSFWorkbook).

 

 

3. Controller (엑셀 다운로드)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RequestMapping(value="/exceldownload.do")
    public void excelDownload( HttpServletRequest request ,HttpServletResponse response ,HttpSession session, VO param) throws Exception {
        
        OutputStream out = null;
        
        try {
            HSSFWorkbook workbook = Service.listExcelDownload(param);
            
            response.reset();
            response.setHeader("Content-Disposition""attachment;filename=stbcs_history.xls");
            response.setContentType("application/vnd.ms-excel");
            out = new BufferedOutputStream(response.getOutputStream());
            
            workbook.write(out);
            out.flush();
            
        } catch (Exception e) {
            logger.error("exception during downloading excel file : {}", e);
        } finally {
            if(out != nullout.close();
        }    
    }
cs

 

4. jsp

1
2
3
4
5
6
7
8
9
/* 엑셀다운로드 */
function downloadExcel(){
 
    var f = document.frmSearch;
    f.action = "/exceldownload.do";
    f.submit();
    
    return;
}
cs

 

 

 

 

반응형

스프링프레임워크 log4j2 설정 : Spring Framework + log4j 2 + slf4j + jboss/wildfly(ver 9)

 

*  jdk : 1.7

*  framework : spring

*  was : jboss/wildfly 9

 

#기본개념

slf4j 는 interface, 

log4j 는 구현체

 

slf4j 사용시 소스에서 interface , imple 사용하듯 구현체만 바꾸면 돼서 

log4j 를 나중에 걷어내고 다른 log lib을 사용해도 소스레벨에서 수정할게 없음. 그래서 slf4j 사용.

 

log4j 2는 log4j 1랑 다른거 없이 log4j 2.x 대 버전을 의미.

(log4j 1.x 는 log4j 라고부르고 log4j 2 는 log4j 2.x 를 말하는 듯)

* log4j2 사용시 slf4j는 1.7.x 이상 사용

 

참고 : https://goddaehee.tistory.com/45

(위 고수님의 블로그에 log4j, slf4j, logback 에 관한 내용이 상세히 기술되어 있음)

 

[ # log4j 2 및 slf4j maven dependency 설정 ]

log4j-slf4j-impl jar(actifactId) 사용시 1.7.x 이하 version 사용

log4j-slf4j18-impl jar(actifactId) 사용시 1.8.x 이상 version 사용

참고 : https://logging.apache.org/log4j/2.x/log4j-slf4j-impl/index.html

 

[ pom.xml ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<modelVersion>4.0.0</modelVersion>
    <groupId>com.sample</groupId>
    <name>SAMPLE</name>
    <packaging>war</packaging>
    <version>0.1</version>
    <properties>
        <!-- 생략 -->
        <org.slf4j-version>1.7.5</org.slf4j-version>
        <org.log4j-version>2.9.0</org.log4j-version>
    </properties>
 
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>${org.springframework-version}</version>
    <exclusions>
        <!-- Exclude Commons Logging in favor of slf4j -->
        <exclusion>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
         </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>${org.slf4j-version}</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>${org.log4j-version}</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>${org.log4j-version}</version>
    <exclusions>
         <!-- Exclude Commons Logging in favor of log4j-core -->
         <exclusion>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
         </exclusion>
    </exclusions>
</dependency>
cs

* spring 의 경우 default 로 jcl(commons-logging)사용하므로 이를 slf4j 로 대체하기 위해 jcl-over-slf4j 사용

* jcl(commons-logging) 대신 slf4j 를 사용하므로 spring-context 에서 commons-logging 제거(exclude)

* jcl-over-slf4j 내부적으로 slf4j-api dependency 를 포함하고 있으므로 slf4j-api 는 별도의 dependency 로 추가 하지 않음

* log4j-core 내에 log4j-api dependency 포함하므로 log4j-slf4j-impl 에서 해당 의존성 제거

 

[ #log4j2.xml 설정 ]

src/main/resources/ 경로에 생성(/WEB-INF/classes/).

해당 위치에 파일을 위치시킬 경우 log4j 가 초기화 될 때 알아서 해당 파일을 읽어들임

 

* src/main/resource/ 밑이 아닌 기타 경로에 해당 파일을 위치시킬 경우

web.xml 에 아래와 같이 추가

참고 : https://okky.kr/article/282263

 

[ web.xml ]

1
2
3
4
5
6
7
8
9
  <context-param>
    <param-name>log4jConfigLocation</param-name>
    <param-value>classpath:~~~/log4j2.xml</param-value>
  </context-param>
  
  <listener>
    <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
  </listener>
 
cs

 

[ log4j2.xml ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>    
    <Appenders>
        <Console name="console_root" target="SYSTEM_OUT">
            <PatternLayout pattern="%d %5p %m%n" />
        </Console>
         <Console name="console_com" target="SYSTEM_OUT">
            <PatternLayout pattern="%d %5p [%c] %m%n" />
        </Console>       
    </Appenders>
    
    
    <Loggers>
         <Logger name="java.sql" level="INFO" additivity="false">
            <AppenderRef ref="console_com" />
        </Logger>
        <Logger name="jdbc.sqltiming" level="INFO" additivity="false">
            <AppenderRef ref="console_com" />
        </Logger>
        <Logger name="egovframework" level="INFO" additivity="false">
            <AppenderRef ref="console_com" />
        </Logger>
        <Logger name="com" level="INFO" additivity="false">
            <AppenderRef ref="console_com" />
        </Logger>
        <Logger name="org.springframework" level="INFO" additivity="false">
            <AppenderRef ref="console_com" />
        </Logger>
        <Root level="INFO">
            <AppenderRef ref="console_root" />
        </Root>
    </Loggers>
</Configuration>
 
cs

 

[* 어펜더 (Appender) ]

어펜더는 로그이벤트를 목적지에 전달하는 역할을 한다.(타겟 목적지에 이벤트 데이터를 쓰는 역할)

어펜더는 Appender interface 를 구현한다.

name 속성을 가지며 이를 통해 로거에서 참조한다.

아래와 같은 어펜더가 존재.

ConsoleAppender : Sysout 을 찍는 용도로 사용되는 어펜더

FileAppender : 로그를 파일로 저장할 용도로 사용되는 어펜더

RollingFileAppender : FileAppender 에 Rollover 기능(파일 백업)을 추가한 어펜더

 

[* 로거 (Logger) ]

로거의 위계구조(hierarchy)에 대한 이해가 필요하다. 여기를 읽어보길 바란다.

아래는 로거 위계구조의 예이다.

com.sample.sampleController 클래스 내에서
protected Logger logger = LoggerFactory.getLogger(this.getClass());
logger.info("샘플로깅!");
과 같이 로그를 출력할 경우,
this.getClass()com.sample.sampleController를 반환하고,
해당 클래스는 com 패키지 하위의 클래스(com.*) 이므로 "com"이란 이름(name)으로 명명된 logger설정을 따른다.

* 만약 23~25line 의 "com" logger 를 제거한다면 com 패키지 하위 소스내(com.sample.sampleController)에서 찍은 로그는 Root 로거의 console_root 어펜더에 의해 출력된다. ("com" logger 있을 경우 : console_com(%d %5p [%c] %m%n) appender 에 의해 출력, 없을 경우 root logger인 console_root(%d %5p %m%n)  에 의해 출력

* 이와 같은 위계구조를 활용하여 라이브러리 내부에서 불필요 혹은 원치않는 로그를 출력하고 있는 경우, 해당 라이브러리 패키지 경로를 별도의 logger 로 잡아 로그 출력을 제거 할 수 있다.

* Root 로거는 매핑되는 logger 를 찾지 못하는 경우 가장 마지막에 사용되는 최상위 로거이며, Root 로거는 반드시 설정해주어야 한다.

 

[* 패턴 (PatternLayout) ]

%d 날짜 년월일시

%c : 클래스명

%5p : 로그레벨(5자리 길이 맞춰서 출력(5자리 이하일경우 공백열로 채워넣음))

%m : 메시지

%n : 줄바꿈

%t : 스레드명

 

[ # jboss 에서 log4j 사용시 필요한 설정 ]

: jboss 내부적으로 slf4j, log4j 라이브러리를 내장하고 있기 때문에

해당 부분을 exclude 하여 충돌요소를 제거

 

[ jboss-deployment-structure.xml ]

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<jboss-deployment-structure>
    <deployment>
        <exclusions>
            <module name="org.apache.log4j" />
            <module name="org.apache.commons.logging" />
            <module name="org.slf4j"/>
        </exclusions>
    </deployment>
</jboss-deployment-structure>
cs

참고 : http://blog.naver.com/PostView.nhn?blogId=djawl_2&logNo=220665045226&redirect=Dlog&widgetTypeCall=true
(위 고수님의 블로그에 jboss 환경변수를 이용하여 충돌을 막는 방법도 기재되어 있음)

 

[ # jboss 에서 log4j 2.10 버전 이상 사용시 발생하는 문제 ]

jboss 에서 log4j 2 2.10 이상 버전 사용시 발생하는 warn 로그 :

WARN  [org.jboss.as.server.deployment] (MSC service thread 1-5) WFLYSRV0003: Could not index class META-INF/versions/9/module-info.class at /content/example.war/WEB-INF/lib/log4j-api-2.11.1.jar: java.lang.IllegalStateException: Unknown tag! pos=4 poolCount = 32

서버동작에 문제는 없으나 was 기동시 WARN 로그 찍히는게 보기가 싫으니,  log4j 버전을 살짝 내려서 사용.

참고 : https://issues.jboss.org/browse/JBEAP-15261

(module-info.class 가 log4j 2.10 이상 버전에 존재하므로 해당 경고가 발생)

 

참고 : log4j 2 user guide

반응형

운영, 테스트서버, 개발서버 와 같이 서버가 3개가 구성되어있고,

각각의 서버별로 사용하는 jar가 달라야 할 때, 서버 환경에 맞게 jar를 빌드하여 서버에 배포해보자.

 

* 서버 환경별로 jar 내부적으로 소켓통신을 하는 목적지 ip가 달라 jar가 총 3개 존재했고..

   war 를 어떤 서버에 배포하느냐에 따라 jar를 바꿔서 빌드 후, 서버에 배포해야 했던 상황

** 소스내에서 갖다 쓰는 jar 내부의 메소드 자체를 수정하여 목적지 서버 ip를 파라미터로 받게끔 처리해주는게 베스트..

    하지만 jar 만드신 분이 본인보다 상급자라면 별 수 없다.

 

 

1) pom.xml 에 profiles 설정 및 local repository 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<repositories>
      <repository>
         <id>local-repository</id>
         <url>file:${basedir}/repo</url>
      </repository>
       </repositories>
    
    <profiles>
      <profile>
         <id>dev</id>
         <dependencies>
            <dependency>
               <groupId>chat</groupId>
               <artifactId>chatmodule-dev</artifactId>
               <version>0.0.6</version>
            </dependency>
         </dependencies>
      </profile>
      <profile>
         <id>tb</id>
         <dependencies>
            <dependency>
               <groupId>chat</groupId>
               <artifactId>chatmodule-tb</artifactId>
               <version>0.0.6</version>
            </dependency>
         </dependencies>
      </profile>
      <profile>
         <id>live</id>
         <dependencies>
            <dependency>
               <groupId>chat</groupId>
               <artifactId>chatmodule-</artifactId>
               <version>0.0.6</version>
            </dependency>
         </dependencies>
      </profile>
   </profiles>
cs

 

2) 프로젝트 내에 local repository 구성

repo > chat > chatmodule/chatmodule-dev/chatmodule-tb > 0.0.6 > chatmodule-~.jar 와 같이 구성

chatmodule-~.jar : 운영서버 배포시 사용될 jar

chatmodule-tb~.jar : 테스트서버 배포시 사용될 jar

chatmodule-dev~.jar : 개발서버 배포시 사용될 jar

 

 

3) Maven Profile 선택

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Maven > Select Maven Profiles 클릭.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

활성화 시킬 profile(dev/tb/live) 선택

 

 

4) Maven update

 

5) Maven Build (war 파일 생성)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

profile(dev/tb/live) 선택

 

 

6) war 파일 및 jar 확인

빌드가 성공적으로 끝났으면,

workspace/프로젝트/war파일명/target/WEB-INF/lib/

경로로 이동하여 지정한 profile 의 jar가 잘 들어갔는지 확인.

 

 

위와 같이 profile 을 사용하여 환경별 jar 를 동적으로 사용하는게 아닌,

단순히 로컬(프로젝트 내)에 존재하는 jar 1개를 사용하고자 할 경우

local repository 를 잡을 필요 없이 아래와 같이 scope 를 system 으로 주고 프로젝트 내에서 jar 를 가져다 사용할 수 있다.

1
2
3
4
5
6
7
<dependency>
    <groupId>chat</groupId>
    <artifactId>mobile</artifactId>
    <version>0.0.1</version>
    <scope>system</scope>
    <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/chat_0.0.1.jar</systemPath>
</dependency>
cs

 

반응형

기존에 interceptor 로 로그를 출력하도록 소스를 짜놓았지만 리소스를 너무 잡아먹는 듯 하여

AOP에서 로그를 출력하도록 바꾸었다.

 

설명은 나중에 채워넣겠다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package ;
 
import java.util.HashMap;
import java.util.Map;
 
import javax.servlet.http.HttpServletRequest;
 
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.ModelAndView;
 
 
@Component
@Aspect
public class ControllerAOP {
 
    protected static final Logger logger = LoggerFactory.getLogger(ControllerAOP.class);
 
    @Around("execution(public org.springframework.web.servlet.ModelAndView com.sample.bmp.mobile..*Controller.*(..))")
    public ModelAndView around(ProceedingJoinPoint pjp) {
        ModelAndView mav = null;
 
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
 
        Object inputParam = null;
        for (Object obj : pjp.getArgs()) {
            if (obj instanceof Map) {
                inputParam = obj;
            }
        }
 
        long start = System.currentTimeMillis();
 
        String controller = (String) pjp.getTarget().getClass().getSimpleName();
 
        String path = request.getRequestURI();
        String addr = request.getRemoteAddr();
        int port = request.getRemotePort();
 
        logger.info("##########################################################################");
        logger.info("# REQUEST | CONTROLLER = {} | METHOD = {} | REMOTEADDR = {} | PORT = {} | IN_PARAMS = {}",
                controller, path, addr, port, inputParam == null ? "": inputParam);
        logger.info("##########################################################################");
 
        Map<String, Object> result = null;
        
        try {
            mav = (ModelAndView) pjp.proceed();
            result = mav.getModel();
        } catch (APIException e) {
            mav = new ModelAndView("jsonView");
            result = new HashMap<String, Object>();
//            e.printStackTrace();
            result.put("result_cd", e.getErrCode());
            result.put("result_msg", e.getMessage());
 
            mav.addAllObjects(result);
        } catch (Throwable e) {
            mav = new ModelAndView("jsonView");
            result = new HashMap<String, Object>();
//            e.printStackTrace();
            result.put("result_cd"0);
            result.put("result_msg""");
 
            mav.addAllObjects(result);
        } finally {
            String success = "fail";
            
            if (result.containsKey("result_cd")) {
                try {
                    if ("1".equals(result.get("result_cd").toString())) {
                        success = "success";
                    }
                } catch (ClassCastException | NumberFormatException e) {
                    logger.debug("SessionInterceptor error == {}", e);
                }
            }
            
 
            long end = System.currentTimeMillis();
            logger.info("##########################################################################");
            logger.info("# RESPONSE | CONTROLLER = {} | METHOD = {} | RESULT = {} | REMOTEADDR = {} | PORT = {} | TIME = {} ms | IN_PARAMS = {} | OUT_PARAMS = {}",
                    controller, path, success, addr, port, end - start,
                    inputParam == null ? "" : inputParam, 
                    result == null ? "" : result.toString());
            logger.info("##########################################################################");
 
        }
 
        return mav;
    }
 
}
cs
반응형

+ Recent posts