• [Java] 기본적인 Log 남기고 활용하기 - java.util.logging.Logger

    2021. 5. 4.

    by. SDev

    728x90

     

    +++ 2021.12.13일 내용 추가

    이 글은 java의 내장 라이브러리인 java.util.logging.Logger를 주 내용으로 설명하고 있지만, 중간에 log4j라는 오픈소스를 언급하고 있습니다. log4j는 Apache 재단에서 제공하는 로깅 관련 오픈소스이며 전세계의 Java 유저들이 즐겨 사용해오고 있습니다. 그러나 지난주 금요일(2021.12.10)부터 log4j와 관련된 보안 취약점이 발견되어 이슈가 되고 있습니다. 현재 시점에서 이미 log4j를 사용하고 있거나 도입을 검토하신다면 각별한 유의가 필요합니다. 

     

     

     

    Logging

    WAS를 구축하고 운영하면서 특정 기능의 소요 시간이나, 운영상에서의 예외상황, 에러를 확인하고 싶은 상황이 자주 있다. 콘솔에 print해 확인하는 방법도 있겠으나, 지속적으로 관리하고 운영하기에 콘솔은 불편한 부분이 많다. 때문에 이를 Log 형태로 파일 형태로 저장해 관리하는 것이 일반적이다.

    • 시스템 운영에 대한 기록
    • 디버깅, 시스템 에러 추적, 성능, 문제점 향상 등의 목적으로 사용
    • 어느정도 까지 로그를 남길 것인가?
      • 너무 적은 로그: 정확한 시스템의 상황을 파악하기 어려움
      • 너무 많은 로그: 빈번한 file I/O의 오버헤드와 로그 파일의 백업 문제등 파생 문제 발생 가능성

     

     

     

    Java.util.logging

    • Java에서 기본적으로 제공되는 Log package
    • 파일이나 콘솔에 로그 내용을 출력할 수 있음
    • jre/lib/logging.properties 파일을 편집해 로그의 출력 방식 로그 레벨을 변경할 수 있음(IDE console)
    • severe, warning, info, config, fine, finer, finest의 로그 레벨 제공
    • 오픈소스로 log4j를 많이 사용함

    패스트캠퍼스 박은종  Java - Logger 강의 중 발췌

     

    그림에서 Logger 클래스가 Handler를 참조하고 있음을 볼 수 있다. Logger 클래스는 Handler 객체에서 Console Handler를 사용할 것인지 File Handler를 사용할 것인지 선택할 수 있다. 

     

     

    Logger 기본 원리 살펴보기

    MyLogger.java

    package log;
    
    import java.io.IOException;
    import java.util.logging.FileHandler;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    import java.util.logging.SimpleFormatter;
    
    public class MyLogger {
    	// String을 기준으로 Logger 클래스 인스턴스 할당
    	Logger logger = Logger.getLogger("mylogger");
    	private static MyLogger instance = new MyLogger();
    	
    	// Level별 Log를 생성할 파일 지정
    	public static final String errorLog = "log.txt";
    	public static final String warningLog = "warning.txt";
    	public static final String fineLog = "fine.txt";
    	
    	private FileHandler logFile = null;
    	private FileHandler warningFile = null;
    	private FileHandler fineFile = null;
    	
    	private MyLogger() {
    		try {
    			// path, append 방식으로 생성
    			logFile = new FileHandler(errorLog, true);
    			warningFile = new FileHandler(warningLog, true);
    			fineFile = new FileHandler(fineLog, true);
    		}catch(SecurityException e) {
    			e.printStackTrace();
    		}catch(IOException e) {
    			e.printStackTrace();
    		}
    		
    		logFile.setFormatter(new SimpleFormatter());
    		warningFile.setFormatter(new SimpleFormatter());
    		fineFile.setFormatter(new SimpleFormatter());
    		
    		logFile.setLevel(Level.ALL);
    		fineFile.setLevel(Level.FINE);
    		warningFile.setLevel(Level.WARNING);
    		
    		logger.addHandler(logFile);
    		logger.addHandler(fineFile);
    		logger.addHandler(warningFile);
    	}
    	
    	public static MyLogger getLogger() {
    		return instance;
    	}
    	
    	public void log(String msg) {
    		logger.finest(msg);
    		logger.finer(msg);
    		logger.fine(msg);
    		logger.config(msg);
    		logger.info(msg);
    		logger.warning(msg);
    		logger.severe(msg);
    	}
    	
    	public void fine(String msg) {
    		logger.fine(msg);
    	}
    	
    	public void warning(String msg) {
    		logger.warning(msg);
    	}
    }

    차근차근 살펴보면 아래와 같은 구성을 갖는다.

    • Logger.getLogger(String) 메서드는 싱글턴 패턴으로 해당 String을 기반으로 호출했을 때 고유한 Logger 객체를 반환해주는 역할을 한다. 
    • 스스로와 같은 MyLogger 클래스 attribute(instance)에 생성자를 통해 만드려는 log file의 구조를 설정시킨다.
    • 생성자에서는 FileHandler 클래스를 이용해 만드려는 log file 구조의 포맷을 설정하고, 각 파일에 저장할 Log Level을 설정한다.
      • 지정된 Log Level부터 상위 레벨까지의 로그를 저장한다.(지정된 Log Level의 Log만 저장하는 것이 아니다. -> log4j)
    • 다양한 레벨의 로그를 발생시키는 여러 유형 메소드를 정의했다. log(String msg), fine(String msg), warning(String msg)

     

     

    LoggerTest.java

    package log;
    
    public class LoggerTest {
    
    	public static void main(String[] args) {
    		MyLogger logger = MyLogger.getLogger();
    		logger.log("log test");
    	}
    
    }

     

     

    LoggerTest에서 log("log test")로 로그를 발생시키면, 앞서 정의한대로 모든 레벨의 로그가 발생한다.

    그 결과 아래와 같은 결과를 볼 수 있다.

    fine.txt

     

    warning.txt

     

    log.txt

     

    세팅 환경이 한국어로 되어 있어서 Level이 자동으로 번역되어 로그가 생성되었는데, 약간은 오역이 있다. fine...

     

    확인해보면 각 파일의 로그 레벨을 모두 다르게 설정해놓았는데 log.txt는 ALL로 설정해 놓았기 때문에 모든 레벨의 로그가 모두 찍혔고, warning.txt는 warning, severe 레벨의 로그만, fine.txt는 fine, config, info, warning, severe까지 5개의 레벨 로그가 찍혀 있다.

     

    Eclipse IDE Console

     

    또, 위처럼 IDE console에도 log까 찍혀있는데 info 레벨부터 log가 표시되는 것을 볼 수 있다. 이는 현재 Eclipse에서 내가 사용하고 있는 JRE에서 Info부터 찍도록 default 설정이 되어 있기 때문이다.

     

     

    logging.properties

    Java가 설치된 디렉토리에서 jdk*(버전)/Contents/Home/jre/lib/logging.properties에 가보면 이를 확인할 수 있는데, 위 사진에서 43 라인에 INFO로 설정되어 있음을 알 수 있다. 변경해서 쓸 수도 있다.

     

    여기까지 해보면서 어느정도 이해는 할 수 있었는데, 로그 파일들에 level을 설정하면 해당 레벨의 로그만 설정하면 좋겠는데 해당 레벨부터 상위 레벨의 로그를 모두 저장하는 것이 불편이 예상됐다. 이런 문제때문에 log4j를 사용하는 것이 간편하면서 유용한 기능이라고 볼 수 있겠다.

     

     

     

    Logger 활용해보기

    이름을 갖는 학생 클래스를 생성하고, 생성자로 학생의 이름을 인자로 받을 때 제약조건을 설정하는 상황을 가정해본다. 이름 String이 null이면 직접 정의한 에러 로그를 띄우고, String이 공백이 3개 이상이면 또 너무 길이가 긴 이름으로 판정해 에러 로그를 띄워본다. (본래 생성자에 throw 구문을 넣는 것은 어색한 구조이지만 실습에서는 간편함을 위해 그냥 넣었다.)

     

     

    StudentNameFormatException.java

    package log;
    
    public class StudentNameFormatException extends IllegalArgumentException{
    	
    	public StudentNameFormatException(String message) {
    		super(message);
    	}
    }
    

     

    Student.java

    package log;
    
    public class Student {
    	private String studentName;
    	MyLogger myLogger = MyLogger.getLogger();
    	
    	public Student(String studentName) {
    		if(studentName == null) {
    			throw new StudentNameFormatException("name must not be null");
    		}
    		if(studentName.split(" ").length > 3) {
    			throw new StudentNameFormatException("name is too long");
    		}
    		this.studentName = studentName;
    	}
    
    	public String getStudentName() {
    		myLogger.fine("begin getStudentName()");
    		return studentName;
    	}
    }

     

    StudentTest.java

    package log;
    
    public class StudentTest {
    
    	public static void main(String[] args) {
    		MyLogger myLogger = MyLogger.getLogger();
    		
    		String name = null;
    		try {
    			Student student = new Student(name);
    		}catch(StudentNameFormatException e) {
    			myLogger.warning(e.getMessage());
    		}
    		
    		try {
    			Student student = new Student("Edward Jon Kim Test");
    		}catch(StudentNameFormatException e) {
    			myLogger.warning(e.getMessage());
    		}
    		
    		Student student = new Student("James");
    		
    		name = student.getStudentName();
    	}
    
    }

     

     

    Test에서 첫 번째 student는 name이 null이기 때문에 "name must not be null" 에러 로그가 발생하고, 두 번째 student는 name이 공백 3개 이상이기 때문에 "name is too long" 에러 로그가 발생할 것이다. 세 번째 student는 문제 없이 잘 생성될 것이고, 마지막 name = student.getStudentName() 메서드는 fine로그를 지정해두었기 때문에 해당 로그가 예상된다.

     

    실제로 이를 실행해본 결과는 아래와 같다.

     

    fine.txt

     

    log.txt

     

    warning.txt

     

     

    예상한대로 총 3개의 log가 발생했으며 log.txt에는 모든 로그가 찍혀 있고, warning.txt에는 2개의 warning 로그만 있는 것을 확인할 수 있다. 또 fine.txt는 fine 레벨 이상의 로그를 저장하므로 모든 로그가 찍혔다.

     

    댓글