java-school logo
Last Modified 2017.6.26

로깅(Logging)

로그(Log)란 프로그램 개발이나 운영 시 발생하는 문제점을 추적하거나 운영 상태를 모니터링하기 위한 텍스트다.
지금까지 우리는 System.out.println();문을 사용하여 로그를 기록했다.
이것이 로그를 남기기 위한 가장 쉬운 방법이다.
이보다 로그를 기록하는 클래스를 만들어 사용하는 게 더 나은 방법이다.
다음 클래스는 사용자 정의 로그 클래스이다.

Log.java
package net.java_school.util;

import java.io.*;
import java.util.Date;

public class Log {
  String logFile = "C:/debug.log";
  FileWriter fw;
  static final String ENTER = System.getProperty("line.separator");
  
  public Log() {
    try {
      fw = new FileWriter(logFile, true);
    } catch (IOException e){}
  }

  public void close() {
    try {
      fw.close();
    } catch (IOException e){}
  }

  public void debug(String msg) {
    try {
      fw.write(new Date()+ " : ");
      fw.write(msg + ENTER);
      fw.flush();
    } catch (IOException e) {
      System.err.println("IOException!");
    }
  }
}

아래 파일을 만들어 Log 클래스를 테스트한다.

LogTest1.java
package net.java_school.logtest;

import net.java_school.util.Log;

public class LogTest1 {

	public void someMethod() {
		Log log = new Log(); //출력스트림 생성 
		log.debug("로그 테스트!"); //로그 기록하기
		log.close(); //출력스트림 닫기
	}
  
	public static void main(String[] args) {
		LogTest1 test = new LogTest1();
		test.someMethod();
	}
  
}

Log4j 2

로그를 전담하는 프레임워크가 있다.
프레임워크란 공통적인 작업을 자동화하고, 개발자로 하여금 빨리 애플리케이션을 개발하도록 하기 위한 노력의 산물을 말한다.
첫 번째로 설명할 프레임워크는 Log4j 2이다.
Log4j 2를 사용하기 위해서는 아래 경로에서 Log4j 2 바이너리 파일을 내려받는다.
http://logging.apache.org/log4j/2.x/download.html
파일 압축을 풀면 생기는 디렉터리에서 log4j-api-2.8.jar 와 log4j-core-2.8.jar을 복사하고 클래스로더가 찾을 수 있는 경로에 붙여넣는다.
그다음 아래 프로퍼티 파일을 역시 클래스로더가 찾을 수 있는 경로에 생성한다.

log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
	<Appenders>
		<File name="A1" fileName="A1.log" append="true">
			<PatternLayout pattern="%t %-5p %c{2} - %m%n" />
		</File>
		<Console name="STDOUT" target="SYSTEM_OUT">
			<PatternLayout pattern="%d %-5p [%t] %C{2} (%F:%L) - %m%n" />
		</Console>
	</Appenders>
	<Loggers>
		<Logger name="net.java_school" level="debug">
			<AppenderRef ref="A1" />
		</Logger>
		<Root level="debug">
			<AppenderRef ref="STDOUT" />
		</Root>
	</Loggers>
</Configuration>

이클립스에서 Log4j 2 테스트

  1. Java Project 퍼스펙티브에서 새로운 프로젝트 생성한다.
  2. Log4j 2 라이브러리를 프로젝트 빌드패스에 추가한다.
    1. Package Explorer 뷰에서 프로젝트를 선택하고 마우스 오른쪽 버튼을 클릭한다.
    2. Build Path - Configure Build Path...를 선택한다.
    3. Libraires 탭에서 Add External JARs...를 선택한다.
    4. log4j-api-2.8.jar 와 log4j-core-2.8.jar를 추가하고 OK 버튼을 클릭한다.
  3. 아래 LogTest2.java 파일을 생성한다.
LogTest2.java
package net.java_school.logtest;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

public class LogTest2 {
	//private Logger log = LogManager.getLogger(LogTest2.class); //ROOT 로거만 작동
	private Logger log = LogManager.getLogger("net.java_school");

	public void xxx() {
		if (log.isDebugEnabled()) {
			log.debug("debug message");
		}
	}

	public void yyy() {
		if (log.isInfoEnabled()) {
			log.info("info message");
		}
	}

	public static void main(String[] args) {
		LogTest2 test = new LogTest2();
		test.xxx();
		test.yyy();
	}

}

실행하여 콘솔과 A1.log 파일에서 로그 메시지를 확인한다.
LogTest1.java처럼 파일에 로그를 쓰지만 출력 스트림을 닫는 코드가 없음에 주목하자.

Log4j2.xml 설정 파일에서 주요 엘리먼트 설명

Log4j 2는 Logger, Appender, Layout라는 세 개의 주요 엘리먼트가 있다. 이들의 협력으로 로그를 기록한다.

  1. Logger : 로그의 주체로 로그 메시지를 Appender에 전달한다.
  2. Appender : 로그 메시지를 출력할 대상을 지정한다. Appenders
  3. Layout : 로그의 포맷을 지정한다. Layouts

루트 로거는 언제나 존재하며, 모든 로거 중 가장 위에 있다.

자세한 설명은 http://logging.apache.org/log4j/2.x/manual/configuration.html에서 찾을 수 있다.

Log Level

로그 레벨은 아래와 같이 계층적으로 구성되어 있다.
TRACE > DEBUG > INFO > WARN > ERROR > FATAL
INFO로 셋팅하면, INFO, INFO, WARN, ERROR, FATAL은 기록된다.

  1. FATAL : 프로그램이 중지될 수도 있는 치명적인 에러 이벤트
  2. ERROR : 프로그램이 중지될 정도는 아닌 에러 이벤트
  3. WARN : 잠재적인 위험
  4. INFO : 대략적으로 프로그램의 진행 상황을 강조
  5. DEBUG : 응용 프로그램을 디버깅하는 데 가장 유용한 세밀한 정보 이벤트
  6. TRACE : DEBUG보다 세분화 된 정보 이벤트

Commons Logging와 Log4j 2 연동

아파치 그룹의 commons-logging은 개발자들에게 공통 로깅 API를 제공하기 위해 만들어진 프레임워크로 애플리케이션이 특정 로깅 프레임워크에 종속되지 않게 한다.
현재 많은 서드 파티 로깅 프레임워크들이 commons-logging 기반으로 구현되어 있다.

commons-logging 사용법

아래 주소에서 바이너리 배포본을 내려받는다.
http://apache.mirror.cdnetworks.com/commons/logging/binaries/
다운로드하고 압축을 풀면 서브 폴더에 commons-logging-1.2.jar 파일이 생긴다.
이 파일을 클래스 패스에 복사한다.
commons-logging은 자체적으로 로깅을 지원한다기보다는 여러 로깅 API를 표준화된 방법으로 사용할 수 있게 해주는 개념이기 때문에, 실제 로깅 처리를 위한 별도의 로깅 구현체가 필요하다.
여기서는 로깅 구현체로 Log4j 2를 사용하는 방법을 설명한다.

아래 프로퍼티 파일을 클래스 패스에 생성한다.

commons-logging.properties
org.apache.commons.logging.Log = org.apache.commons.logging.impl.Log4JLogger

테스트 파일을 만들고 테스트한다.

LogTest3.java
package net.java_school.logtest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class LogTest3 {
	private Log log = LogFactory.getLog(LogTest3.class);
	// 또는 private Log log = LogFactory.getLog(this.getClass());
	
	public void xxx() {
		if (log.isDebugEnabled()) {
			log.debug("debug message");
		}
	}
	
	public void yyy() {
		if (log.isInfoEnabled()) {
			log.info("info message");
		}
	}
  
	public static void main(String[] args) {
		LogTest3 test = new LogTest3();
		test.xxx();
		test.yyy();
	}
  
}

commons-logging-1.2.jar와 log4j-jcl-2.8.jar를 빌드 패스에 추가한다. log4j-jcl-2.8.jar는 압축을 푼 Log4j 2 디렉터리에서 찾을 수 있다.

slf4j

다음 주소에서 최신 배포본을 내려받는다.
http://www.slf4j.org/download.html
압축을 풀고 slf4j-api-1.7.25.jar 와 slf4j-simple-1.7.25.jar 파일을 클래스 패스에 추가한다.
다음 예제를 작성하고 테스트한다.

LogTest4.java
package net.java_school.bank;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogTest4 {
	
	public static void main(String[] args) {
		Logger logger = LoggerFactory.getLogger(Test.class);
		int amount = 1000;
		int balance = 10000;
		logger.info("amount: {}, balance: {}", amount, balance);
	}

}

slf4j의 디폴트 레벨은 INFO이다.
로깅 레벨을 DEBUG로 변경하려면 클래스 패스에 simplelogger.properties 파일을 아래 내용으로 만든다.

simplelogger.properties
org.slf4j.simpleLogger.defaultLogLevel=DEBUG

자바은행 예제에 slf4j 적용하기

slf4j-api-1.7.25.jar 와 slf4j-simple-1.7.25.jar 파일을 자바은행 클래스 패스에 추가한다.
Account, NormalAccount, MinusAccount 클래스의 입금과 출금 메서드에 로깅을 적용한다.

Account.java
package net.java_school.bank;

//..중간 생략..

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class Account implements Serializable {
	Logger logger = LoggerFactory.getLogger(Account.class);
	
	//..중간 생략..
	
	public synchronized void deposit(long amount) {
		balance = balance + amount;
		Transaction transaction = new Transaction();
		Calendar cal = Calendar.getInstance();
		Date date = cal.getTime();
		transaction.setTransactionDate(Account.DATE_FORMAT.format(date));
		transaction.setTransactionTime(Account.TIME_FORMAT.format(date));
		transaction.setAmount(amount);
		transaction.setKind(DEPOSIT);
		transaction.setBalance(balance);
		transactions.add(transaction);
		logger.debug("AccountNo:{} Amount:{} DEPOSIT/WITHDRAW:{} NORMAL/MINUS:{}", 
			this.accountNo, amount, Account.DEPOSIT, this.kind);
	}
	
	//..중간 생략..

}
NormalAccount.java
@Override
public synchronized void withdraw(long amount) {
	if (amount > balance) {
		throw new InsufficientBalanceException("잔액이 부족합니다.");
	}
	balance = balance + amount;
	Transaction transaction = new Transaction();
	Calendar cal = Calendar.getInstance();
	Date date = cal.getTime();
	transaction.setTransactionDate(Account.DATE_FORMAT.format(date));
	transaction.setTransactionTime(Account.TIME_FORMAT.format(date));
	transaction.setAmount(amount);
	transaction.setKind(Account.WITHDRAW);
	transaction.setBalance(balance);
	transactions.add(transaction);
	logger.debug("AccountNo:{} Amount:{} DEPOSIT/WITHDRAW:{} NORMAL/MINUS:{}", 
		this.getAccountNo(), amount, Account.WITHDRAW, this.getKind());
}
MinusAccount.java
@Override
public synchronized void withdraw(long amount) {
	balance = balance - amount;
	Transaction transaction = new Transaction();
	Calendar cal = Calendar.getInstance();
	
	Date date = cal.getTime();
	transaction.setTransactionDate(Account.DATE_FORMAT.format(date));
	transaction.setTransactionTime(Account.TIME_FORMAT.format(date));
	transaction.setAmount(amount);
	transaction.setKind(Account.WITHDRAW);
	transaction.setBalance(balance);
	transactions.add(transaction);
	logger.debug("AccountNo:{} Amount:{} DEPOSIT/WITHDRAW:{} NORMAL/MINUS:{}", 
		this.getAccountNo(), amount, Account.WITHDRAW, this.getKind());
}

로그 메시지를 콘솔이 아닌 파일에 출력하기를 원한다면 simplelogger.properties에 아래 설정을 추가한다.

simplelogger.properties
org.slf4j.simpleLogger.logFile=C:/java/Bank/javabank.log
org.slf4j.simpleLogger.defaultLogLevel=DEBUG

javabank.log 파일에 로그가 기록된다.
하지만 simpleLogger는 로그 파일에 로그 메시지를 축적하지 못한다.

logback

다음 주소에서 최신 배포본을 내려받는다.
http://logback.qos.ch/download.html
logback-core-x.x.x.jar, logback-classic-x.x.x.jar, logback-access-x.x.x.jar 파일을 클래스 패스에 추가한다.
slf4j-simple-x.x.x.jar을 빌드 패스에서 제거한다.

다음과 같이 logback.xml 파일을 생성한다.
(logbkack.xml은 웹 애플리케이션의 WEB-INF/classes 아래 생성되도록 작성한다)

logback.xml
<configuration>
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>javabank.log</file>
        <encoder>
            <pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- encoders are assigned the type
            ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="debug">
        <appender-ref ref="FILE" />
        <appender-ref ref="STDOUT" />
    </root>
    
</configuration>

LogTest4.java를 다시 실행한다.
이번에는 프로젝트 루트 디렉터리에 javabank.log 파일이 생성되고, 이 파일에 로그가 축적된다.

참고