아래 글은 SKT 소액 결재 개발팀 손정호 님이 작성하신 글임을 알려 드립니다.
참고로 preparedStatement에서 적용한 예 입니다.
====================================================================
Log4j Summary
이번 WebChannel 개발시에 적용된 Log4j 환경을 바탕으로 작성한 간단한 summary입니다.
1. 다운로드
다운로드http://logging.apache.org/log4j/docs/download.html 매뉴얼http://logging.apache.org/log4j/docs/documentation.html API spechttp://logging.apache.org/log4j/docs/api/index.html
2. 구조 Log4j는 크게 3가지 요소로 구성되어 있습니다. ① Logger : logging 메시지를 Appender에 전달합니다. ② Appender : 전달받은 logging 메시지를 원하는 곳으로 보내는 매개체의 역할을 합니다. 아래 표는 Appender의 종류입니다. API에서 보고 이해가 된 선에서 적었습니다. ConsoleAppender 로그 메시지를 콘솔에 출력합니다. DailyRollingFileAppender 로그 메시지를 파일로 저장합니다. DatePattern 옵션에 따라 원하는 기간마다 로그파일을 갱신합니다. ExternallyRolledFileAppender FileAppender 직접적으로 사용되지 않고 DailyRollingFileAppender와 RollingFileAppender의 superclass로 사용되는듯 합니다. JDBCAppender 로그 메시지를 DB에 저장합니다. 현재는 완벽하지 않으니 왠만하면 차기 버전에서 사용하라고 하는 것 같습니다. JMSAppender 로그 메시지를 JMS Topic으로 보냅니다. NTEventLogAppender NT 이벤트 로그를 위한 Appender. 윈도우에서만 사용가능합니다. NullAppender 내부적으로만 사용되는 Appender입니다. RollingFileAppender 로그 메시지를 파일로 저장합니다. 설정된 size를 초과하면 로그파일이 갱신됩니다. SMTPAppender 로그 메시지를 지정된 이메일로 발송합니다. SocketAppender 로그 메시지를 socket을 이용해서 지정된 곳으로 보냅니다. SocketHubAppender 위와 비슷하게 사용하는듯 합니다. SyslogAppender 로그 메시지를 원격 syslog deamon으로 보냅니다. TelnetAppender 로그 메시지를 telnet을 통해 보낸다는 것 같습니다. 원격 모니터링, 특히 servlet의 모니터링에 유용하다고 합니다. WriterAppender FileAppender처럼 주로 superclass로서 사용되는듯 합니다. ③ Layout : logging 메시지의 출력 형식을 지정합니다. - 아래에서 설명.
3. 로깅레벨 FATAL : 가장 크리티컬한 에러가 발생했을 때 사용합니다. ERROR : 일반적인 에러가 발생했을 때 사용합니다. WARN : 에러는 아니지만 주의가 필요할 때 사용합니다. INFO : 일반적인 정보가 필요할 때 사용합니다. DEBUG : 일반적인 정보를 상세히 나타낼 때 사용합니다.
로깅레벨의 우선순위는 FATAL이 가장 높고 DEBUG가 가장 낮습니다. 예를 들어 레벨을 WARN으로 설정하면 WARN이상되는 로그(FATAL, ERROR, WARN)만 출력합니다.
4. 환경설정 - Log4j의 환경설정은 직접 코드에서 메서드를 이용하는 방법과 properties 파일을 이용하는 방법, XML파일을 이용하는 방법이 있습니다. ① 코드에서 설정 String layout = "%d %-5p [%t] %-17c{2} (%13F:%L) %3x - %m%n"; String logfilename = "DailyLog.log"; String datePattern = ".yyyy-MM-dd ";
PatternLayout patternlayout = new PatternLayout(layout); DailyRollingFileAppender appender = new DailyRollingFileAppender(patternlayout, logfilename, datePattern); logger.addAppender(appender); logger.setLevel(Level.INFO); logger.fatal("fatal!!");
위 코드처럼 설정하시면 됩니다.
② properties 파일로 설정 #---------- file logging ---------- log4j.rootLogger=INFO, rolling #---------- consol logging ----------- #log4j.rootLogger=INFO, stdout #---------- file, console logging ----------- #log4j.rootLogger=INFO, stdout, rolling log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=[%d] %-5p at %C{3}.%M(%13F:%L) %3x - %m%n log4j.appender.rolling=org.apache.log4j.DailyRollingFileAppender log4j.appender.rolling.File=/WEB_BACKUP1/pgw_log/webchannel.log log4j.appender.rolling.Append=true #---------- every day renew ------------ log4j.appender.rolling.DatePattern='.'yyyy-MM-dd #---------- every month renew ------------ #log4j.appender.rolling.DatePattern='.'yyyy-MM #---------- every week renew ------------ #log4j.appender.rolling.DatePattern='.'yyyy-MM-ww #---------- every 12hours renew ------------- #log4j.appender.rolling.DatePattern='.'yyyy-MM-dd-a #---------- every hour renew -------------- #log4j.appender.rolling.DatePattern='.'yyyy-MM-dd-HH #---------- every min renew -------------- #log4j.appender.rolling.DatePattern='.'yyyy-MM-dd-HH-mm log4j.appender.rolling.layout=org.apache.log4j.PatternLayout log4j.appender.rolling.layout.ConversionPattern=[%d] %-5p at %C{3}.%M(%13F:%L) %3x - %m%n
위 properties 파일은 실제 WebChannel에 적용한 파일입니다. - log4j.rootLogger=INFO, rolling : 로깅레벨을 ‘INFO’로 하고 ‘rolling’이라는 이름의 Appender를 사용한다. 위 properties파일에는 ConsoleAppender(stdout)와 DailyRollingFileAppender(rolling)가 정의되어 있습니다. - log4j.rootLogger=INFO, stdout : console에만 출력 - log4j.rootLogger=INFO, stdout, rolling : console과 file 로 출력 위처럼 설정이 가능합니다. - log4j.appender.stdout=org.apache.log4j.ConsoleAppender : ConsoleAppender의 이름은 ‘stdout’으로 한다. - log4j.appender.rolling=org.apache.log4j.DailyRollingFileAppender : DailyRollingFileAppender의 이름은 ‘rollong’으로 한다. - log4j.appender.rolling.File=/WEB_BACKUP1/pgw_log/webchannel.log : 로그파일의 위치와 파일명을 지정한다. - log4j.appender.rolling.Append=true : 서버 restart시에도 파일이 reset되지 않는다. - log4j.appender.rolling.DatePattern='.'yyyy-MM-dd : DatePattern 을 ‘매일갱신’으로 설정. 매일 자정이 지나면 파일명 뒤에 날짜가 붙는다. ex) webchannel.log.2005-11-21 - log4j.appender.rolling.layout=org.apache.log4j.PatternLayout : layout을 PatternLayout으로 설정. - log4j.appender.rolling.layout.ConversionPattern=[%d] %-5p at %C{3}.%M(%13F:%L) %3x - %m%n : 로그의 출력 형식을 설정. 아래 설명.
# log4j.appender.rolling.MaxFileSize=500KB : 파일의 최대size 설정하는 부분인데 서버 기동시 최초에 이 부분의 property를 읽지 못했다는 경고가 자꾸 떠서 삭제 했습니다. 설정하지 않으면 Default로 10MB가 설정된다고 합니다.
#### properties 파일의 변경사항은 server restart시에 적용됩니다. #### ③ XML 파일로 설정 현재 잘 모르니 넘어가겠습니다.-_-
5. 설정 포맷 ① DatePattern 설정 포맷 '.'yyyy-MM 매달 첫번째날에 로그파일을 변경합니다 '.'yyyy-ww 매주의 시작시 로그파일을 변경합니다. '.'yyyy-MM-dd 매일 자정에 로그파일을 변경합니다. '.'yyyy-MM-dd-a 자정과 정오에 로그파일을 변경합니다. '.'yyyy-MM-dd-HH 매 시간의 시작마다 로그파일을 변경합니다. '.'yyyy-MM-dd-HH-mm 매분마다 로그파일을 변경합니다.
② PatternLayout 설정 포맷 %p debug, info, warn, error, fatal 등의 로깅레벨이 출력된다. %m 로그내용(코드상에서 설정한 내용)이 출력됩니다. ex) logger.info("log"); 라고 코딩했다면 ‘log’가 로그 내용임. %d 로깅 이벤트가 발생한 시간을 기록합니다. 포맷은 %d{HH:mm:ss, SSS}, %d{yyyy MMM dd HH:mm:ss, SSS} 같은 형태로 사용하며 SimpleDateFormat에 따른 포맷팅을 하면 된다 %t 로그이벤트가 발생된 쓰레드의 이름을 출력합니다. %% % 표시를 출력하기 위해 사용한다. %n 플랫폼 종속적인 개행문자가 출력된다. \r\n 또는 \n 일것이다. %c 카테고리를 표시합니다. ex) 카테고리가 a.b.c 처럼 되어있다면 %c{2}로 설정하면 b.c 가 출력됩니다. %C 클래스명을 포시합니다. ex) 클래스구조가 org.apache.xyz.SomeClass 처럼 되어있다면 %C{2}는 xyz.SomeClass 가 출력됩니다 %F 로깅이 발생한 프로그램 파일명을 나타냅니다. %l 로깅이 발생한 caller의 정보를 나타냅니다 %L 로깅이 발생한 caller의 라인수를 나타냅니다 %M 로깅이 발생한 method 이름을 나타냅니다. %r 어플리케이션 시작 이후 부터 로깅이 발생한 시점의 시간(milliseconds) %x 로깅이 발생한 thread와 관련된 NDC(nested diagnostic context)를 출력합니다. %X 로깅이 발생한 thread와 관련된 MDC(mapped diagnostic context)를 출력합니다.
ex) [%d] %-5p at %C{3}.%M(%13F:%L) %3x - %m%n [2005-11-23 10:43:21,560] INFO at pgw.database.PGWBoardDAO.selectList(PGWBoardDAO.java:146) - ========== PGWBoardDAO#selectList ==========
포맷의 각 색깔별로 출력되는 실제 예입니다. 포맷 중간에 원하는 단어(at)나 기호(`.` , `-`)등을 넣으면 그대로 출력됩니다.
6. 실제 적용 예
다운받은 log4j.jar파일을 원하는 디렉토리에 복사하고 weblogic의 startWebLogic.cmd내의 classpath에 잡아줍니다. buildpath도 잡아주셔야 합니다.
① PGWBoardDAO.java //import 해줍니다. import org.apache.log4j.Logger; . //중략/
public class PGWBoardDAO extends PGWDAO { //parameter로 받은 이름의 instance를 생성합니다. static Logger logger = Logger.getLogger("PGWBoardDAO"); . //중략/ . public PGWBean selectList(HashMap hashMap) throws Exception { . /중략/ . // pstmt = conn.prepareStatement(sql.toString()); //LoggableStatement instance생성. LoggableStatement는 아래에서 설명. pstmt = new LoggableStatement(conn, sql.toString());
. //중략/ . pstmt.setInt(nIdx++, ((curPage-1)*listSize) + 9); pstmt.setInt(nIdx++, (curPage-1)*listSize); //주어진 로그내용을 ‘INFO’레벨로 출력합니다. getQueryString()으로 ‘?’가 실제데이터로 치환된 query를 출력합니다. logger.info("\n======== PGWBoardDAO#selectList ========\n " + ((LoggableStatement)pstmt).getQueryString() + "\n========================================\n");
. //중략/ .
} catch (SQLException se) { System.out.println("PGWBoardDAO.selectList SQLException ====" + se); //Exception은 ‘ERROR’레벨로 출력합니다. logger.error("\n==== PGWBoardDAO#selectList Exception ====" , se ); return null; } catch (Exception e) { System.out.println("PGWBoardDAO.selectList Exception ====" + e); logger.error("\n==== PGWBoardDAO#selectList Exception ====" , e ); return null; } finally { . //중략/
- 위 코드에서 INFO 레벨의 로그는 주어진 로그를 출력하고, ERROR 레벨의 로그는 발생한 Exception을 로그로 출력합니다.
로그의 출력메서드는 2가지 형식을 지원합니다. logger.fatal(Object message) logger.fatal(Object message, Throwable t) logger.error(Object message) logger.error(Object message, Throwable t) logger.warn(Object message) logger.warn(Object message, Throwable t) logger.info(Object message) logger.info(Object message, Throwable t) logger.debug(Object message) logger.debug(Object message, Throwable t)
- Throwble 타입의 변수를 parameter로 받는 메서드를 이용하면 원하는 위치에서 원하는 Exception을 발생시킬 수도 있습니다.
- 위 코드에서 INFO 레벨의 로그는 주어진 내용를 출력하고, ERROR 레벨의 로그는 발생한 Exception을 로그로 출력합니다.
② LoggableStatement.java - 이 클래스는 query를 로그로 출력할 때 부가적으로 필요한 클래스로 PreparedStatement의 ‘?’를 실제 데이터로 치환해서 출력하는 기능을 합니다. 이 클래스는 Interface인 PreparedStatement를 구현하는 클래스로 파일이름은 임의로 정하셔도 됩니다. 클래스내에는 PrepareddStatement의 메서드를 오버라이딩한 메서드와 넘어온 데이터를 ArrayList에 넣어주는 메서드, 그리고 query의 ‘?’를 치환해 리턴해주는 메서드를 구현합니다.
//PreparedStatement 와 ArrayList를 import 해줍니다. //메서드 오버라이딩시에 필요한 클래스도 추가적으로 import 해줍니다. import java.sql.PreparedStatement; import java.util.ArrayList;
public class LoggableStatement implements PreparedStatement {
private ArrayList parameterValues; private String sqlTemplate;
private PreparedStatement wrappedStatement;
//connection.prepareStatement(String sql) 대신에 사용할 생성자 입니다. //PreparedStatement Object를 생성, query를 String에 담고 ArrayList를 생성합니다. public LoggableStatement(Connection connection, String sql) throws SQLException { wrappedStatement = connection.prepareStatement(sql); sqlTemplate = sql; parameterValues = new ArrayList(); }
. //중략/ .
//실제로 필요한 메서드만 오버라이딩 하고, 나머지는 auto generate하시면 됩니다. //여기서는 query문 실행관련 메서드와 setInt, setString, setDate, setCharacterStream 을 오버라이딩 했습니다. public boolean execute() throws java.sql.SQLException { return wrappedStatement.execute(); }
public boolean execute(String sql) throws java.sql.SQLException { return wrappedStatement.execute(sql); }
public int[] executeBatch() throws java.sql.SQLException { return wrappedStatement.executeBatch(); }
public java.sql.ResultSet executeQuery() throws java.sql.SQLException { return wrappedStatement.executeQuery(); }
public java.sql.ResultSet executeQuery(String sql) throws java.sql.SQLException { return wrappedStatement.executeQuery(sql); }
public int executeUpdate() throws java.sql.SQLException { return wrappedStatement.executeUpdate(); }
public int executeUpdate(String sql) throws java.sql.SQLException { return wrappedStatement.executeUpdate(sql); }
public java.sql.Connection getConnection() throws java.sql.SQLException { return wrappedStatement.getConnection(); }
public void setCharacterStream( int parameterIndex, java.io.Reader reader, int length) throws java.sql.SQLException { wrappedStatement.setCharacterStream(parameterIndex, reader, length); saveQueryParamValue(parameterIndex, reader);
}
public void setDate(int parameterIndex, java.sql.Date x) throws java.sql.SQLException { wrappedStatement.setDate(parameterIndex, x); saveQueryParamValue(parameterIndex, x); }
public void setDate( int parameterIndex, java.sql.Date x, java.util.Calendar cal) throws java.sql.SQLException { wrappedStatement.setDate(parameterIndex, x, cal); saveQueryParamValue(parameterIndex, x); }
public void setInt(int parameterIndex, int x) throws java.sql.SQLException { wrappedStatement.setInt(parameterIndex, x); saveQueryParamValue(parameterIndex, new Integer(x)); }
public void setString(int parameterIndex, String x) throws java.sql.SQLException { wrappedStatement.setString(parameterIndex, x); saveQueryParamValue(parameterIndex, x); } //넘어온 데이터를 ArrayList에 담아주는 메서드입니다. private void saveQueryParamValue(int position, Object obj) { String strValue; if (obj instanceof String || obj instanceof Date) { strValue = "'" + obj + "'"; } else { if (obj == null) { strValue = "null"; } else { strValue = obj.toString(); } } while (position >= parameterValues.size()) { parameterValues.add(null); } parameterValues.set(position, strValue); } //instance생성시 String에 넣어둔 query의 ‘?’를 ArrayList에 담긴 실제 데이터로 //치환해서 리턴해 줍니다. public String getQueryString() { //여기서 query를 String에도 담아준 이유는 webLogic의 jdk가 1.3 버전으로 //StringBuffer의 indexOf(String str) 메서드를 사용할 수 없었기 때문입니다. //다른 방법이 있으시면 알려주세요.. String sql = sqlTemplate; StringBuffer query = new StringBuffer(sqlTemplate); int idx = 0; if(!parameterValues.isEmpty()) { for(int i=1;i < parameterValues.size();i++) { idx = sql.indexOf("?"); query.replace(idx, idx+1, (String)parameterValues.get(i)); sql = query.toString(); } parameterValues = null; return query.toString(); } else { parameterValues = null; return query.toString(); } } }
- 다음은 실제 출력문입니다.
[2005-11-23 13:50:19,030] INFO at pgw.database.listDAO.modify(listDAO.java:543) - ========== llistDAO#modify#if Customer ========== UPDATE ACKLIST SET NM_USER = 'aaaaaaaaaa', NO_SSN = '2222222222222', NO_MINHEADER = '222', NO_MINNUMBER = '22222222', REASON = '22222222222222222444444444444444444444', ID_MODIFY = 'pbadmin', DT_MODIFY = SYSDATE WHERE SEQ_NUM = '460' AND TYPE = '2' | |