6 minute read

문제 상황

운영 서버를 구축하고 서비스를 배포하기에 앞서 앞으로 서비스에 대한 전반적인 모니터링, 유지보수를 위해 로깅을 설정하려고 한다.

로그를 통해 해결하고자 하는 것

  • 트러블 슈팅
  • 실시간 모니터링
  • 애플리케이션의 성능 향상
  • 서비스 품질 개선과 UX 향상
    • 사용자의 행동, 사용 패턴파악

문제 해결 과정

1. Spring Logging 설정에 대해 알아보기

spring-boot-start-web 의존성이 추가되어 있을 경우 로깅에 관련된 라이브러리를 기본적으로 제공한다.

  • logback-classic
  • log4j-to-slf4j
  • jul-to-slf4j
  • slf4j

2. SLF4J 에 대한 공부

Simple Logging Facade For Java ( 자바를 위한 간단한 logging facade )

( facade 에 대한 설명으로 이동 )

slf4j는 여러 로깅 프레임워크에 대한 추상화를 진행해주는 인터페이스 역할을 하는 라이브러리이다.

SLF4J 에 의존하는 코드로만 구현하여 여러 실제 로깅 프레임워크들을 사용할 수 있다.

동작 과정

1. SLF4J Bridging Modules ( 브릿징 모듈 )

  • 다른 로깅 프레임워크를 사용할 때 로그 호출을 SLF4J 인터페이스로 리다이렉트시키는 역할을 한다.
  • SLF4J 가 실제 로깅 작업을 처리할 수 있도록 하는 어댑터 역할
  • 여러 로깅 라이브러리를 SLF4J 통해 한번에 관리할 수 있다.
브릿징 모듈 목록
  • jul-to-slf4j : 자바 표준 로깅 호출을 SLF4J로 연결해준다. ( spring-boot-start-web 기본 제공 )
  • log4j-over-slf4j : log4j 호출를 SLF4J 로 연결해준다. ( spring-boot-start-web 기본 제공 )
  • logback-classic : logback 호출을 SLF4J로 연결해준다. ( spring-boot-start-web 기본 제공 )

2. SLF4J API

  • 로그 호출에 대한 인터페이스를 제공하며 필요한 메서드를 호출한다.

3. SLF4J Binding

  • SLF4J 인터페이스에서 호출된 메서드를 Binder ( 실제 로깅 프레임워크 ) 의 메서드 호출로 변환한다.
    • Binder 가 없을 경우 SLF4J 는 동작하지 않는다.
    • 로그 메시지의 출력, 포맷, 레벨 등이 실제로 처리가 된다.
  • 하나에 SLF4J API 에는 실제로 처리하는 Binder 는 한개만 설정되어 있어야한다.

Facade ( 정면 )

facade 가 뭔지 궁금하여 찾아보니 facade pattern 이라는 디자인 패턴이 나왔다.

파악한 facade 패턴은 간단하게 말하여 서브 시스템들의 동작을 묶어서 인터페이스로 추상화한 것이다.

클라이언트는 facade 인터페이스의 메서드를 호출할 뿐 내부적으로 어떤 서브 시스템을 사용하고 서브 시스템의 어떤 메서드를 호출하는지 모른다.

즉 내부는 모르는 채로 정면 ( 사용하는 인터페이스 ) 만 보는 것이다.


3. SLF4J 사용해보기

여러가지 해보기 위해 스프링이 없는 playground 에서 진행했다.

1 . buid.gradle에 slf4j 의존성을 추가

dependencies {  
	implementation 'org.slf4j:slf4j-api:1.7.31'  
}

2 . 실행할 수 있는 간단한 코드 작성

import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  
  
public class Logging {  
	  
	private static Logger logger = LoggerFactory.getLogger(Logging.class);  
	  
	public static void main(String[] args) {  
		logger.info("info log");  
	}  
}

3 . 실행 Binder 로드에 실패했다. 실제로 로그를 처리할 로깅 프레임워크가 없는 것이다.

4 . logback-classic 로깅 프레임워크 의존성 추가

dependencies {  
	implementation 'org.slf4j:slf4j-api:1.7.31'  
	implementation 'ch.qos.logback:logback-classic:1.2.11'
}

5 . 다시 실행한다. ( 작동 성공 )

4. Logback 에 대한 공부

logback-classic

  • logback-core 와 slf4j-api 를 가지고 있다.

logback-core

  • 실제 로깅을 위해 필요한 기능들을 가지고 있다.

Logback 설정해보기

  • slf4j 의 binder 로 logback 을 사용할 계획이기에 logback 설정에 대해서 공부하고 작성해보았다.

작성한 logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <!-- 스프링 profile 설정 -->
  <springProfile name="prod">

    <!-- 콘솔 출력 appender -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
      <encoder>
        <pattern>
          [ %d{yyyy/MM/dd HH:mm:ss.SSS} ] %-5level-- [%thread][%logger][%class][%method:%line] - %msg %n
        </pattern>
      </encoder>
    </appender>

    <!-- 애플리케이션 로그 파일 appender -->
    <appender name="file-application" class="ch.qos.logback.core.rolling.RollingFileAppender">
      <file>/etc/log/app/application.log</file>
      <encoder>
        <pattern>
          [ %d{yyyy/MM/dd HH:mm:ss.SSS} ] %-5level-- [%thread] %logger[%method:%line] - %msg %n
        </pattern>
        <charset>utf8</charset>
      </encoder>
      <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <fileNamePattern>/etc/log/app/application.%d{yyyy-MM-dd}-%i.log</fileNamePattern>
        <maxFileSize>100MB</maxFileSize>
        <maxHistory>15</maxHistory>
        <totalSizeCap>2GB</totalSizeCap>
      </rollingPolicy>
    </appender>

    <!-- WARN 로그 파일 appender -->
    <appender name="file-warn" class="ch.qos.logback.core.rolling.RollingFileAppender">
      <file>/etc/log/warn/warn.log</file>
      <encoder>
        <pattern>
          [ %d{yyyy/MM/dd HH:mm:ss.SSS} ] %-5level-- [%thread] %logger[%method:%line] - %msg %n
        </pattern>
        <charset>utf8</charset>
      </encoder>
      <filter class="ch.qos.logback.classic.filter.LevelFilter">
        <level>WARN</level>
        <onMatch>ACCEPT</onMatch>
        <onMismatch>DENY</onMismatch>
      </filter>
      <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <fileNamePattern>/etc/log/warn/warn.%d{yyyy-MM-dd}-%i.log</fileNamePattern>
        <maxFileSize>100MB</maxFileSize>
        <maxHistory>15</maxHistory>
        <totalSizeCap>1GB</totalSizeCap>
      </rollingPolicy>
    </appender>

    <!-- ERROR 로그 파일 appender -->
    <appender name="file-error" class="ch.qos.logback.core.rolling.RollingFileAppender">
      <file>/etc/log/error/error.log</file>
      <encoder>
        <pattern>
          [ %d{yyyy/MM/dd HH:mm:ss.SSS} ] %-5level-- [%thread] %logger[%method:%line] - %msg %n
        </pattern>
        <charset>utf8</charset>
      </encoder>
      <filter class="ch.qos.logback.classic.filter.LevelFilter">
        <level>ERROR</level>
        <onMatch>ACCEPT</onMatch>
        <onMismatch>DENY</onMismatch>
      </filter>
      <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <fileNamePattern>/etc/log/error/error.%d{yyyy-MM-dd}-%i.log</fileNamePattern>
        <maxFileSize>100MB</maxFileSize>
        <maxHistory>15</maxHistory>
        <totalSizeCap>1GB</totalSizeCap>
      </rollingPolicy>
    </appender>

    <!-- 로그 root 레벨 & appender 설정 -->
    <root level="info">
      <appender-ref ref="console"/>
      <appender-ref ref="file-application"/>
      <appender-ref ref="file-warn"/>
      <appender-ref ref="file-error"/>
    </root>

  </springProfile>

</configuration>

사용한 logback.xml 설정에 대한 설명

1 . <springProfile name="{프로파일명}">

  • 스프링 프로파일에 따른 설정
  • resources/logback.xml 파일에 작성해야한다. ( 자동 스캔 대상에 포함시키기 위함 )

2 . <appender name="{어팬더-이름}" class="{어팬더-타입}">

  • appender 를 생성한다.
  • 설정한 어팬더 타입을 통해 설정한 이름의 appender 를 생성한다.

3 . <encoder>

  • appender 에서 사용할 encoder 를 설정한다.
  • encoder 는 로그를 작성하는 책임을 가지고 있다.

( OutputStream 에 궁극적으로 event(로그) 를 작성하는 것은 encoder 라는 설명 )

4 . <pattern>

  • encoder 에서 사용할 pattern 을 설정한다.

  • 사용한 패턴 형식 목록

    • %d: 로그 이벤트 날짜, 시간 출력
    • %level: 로그 레벨 출력 (ex. TRACE, DEBUG, INFO .. )
      • %-5level : 최대 길이인 5로 출력 영역을 고정하여 로그의 출력 형식 동일하게 지정
    • %thread: 로그 이벤트 생성 스레드 이름 출력
    • %logger: 로그 이벤트를 관리한 객체 이름 출력
    • %method: 로그 이벤트 생성 메서드 이름 출력
    • %line: 로그 이벤트 생성 라인 번호 출력
    • %msg: 실제 로그 메시지
    • %n : 줄바꿈 문자 출력

5 . <file>

  • 파일로 저장하는 appender 설정에서 사용가능 하며 저장할 파일 위치를 설정한다.
  • 절대경로를 사용할 경우 미리 디렉토리를 생성해야놔야 한다.
  • 로그파일 저장을 위해 어플리케이션을 실행한 사용자에게 디렉토리에 대한 권한이 있어야 한다.

6 .<filter class="{사용할 filter 타입}">

  • appender 에서 필터링을 위한 filter 를 설정한다.

7 . <level>

  • LevelFilter 에서 필터링할 레벨을 설정한다.

8 . <onMatch>

  • level에 해당 될 때 동작 방식을 설정한다.

9 . <onMismatch>

  • level 에 해당되지 않을 때 동작 방식을 설정한다.

10 . <rollingPolicy class="{사용할 rollingPolicy 타입}">

  • RollingFileAppender 에서 사용할 rollingPolicy 를 설정한다.

11 . <fileNamePattern>

  • RollingFileAppender 에서 저장될 로그파일의 이름을 설정한다.
  • 사용한 패턴 형식
    • %d: dateTime 을 뜻한다.
      • default : { yyyy-MM-dd } : 매 일 자정에 새로운 로그로 rollover
      • %d{yyyy-MM-dd_HH-mm} : 매 분 새로운 로그로 rollover
      • %d{yyyy/MM} : 매 월 새로운 로그로 rollover
    • %i: SizeAndTimeBasedRollingPolicy 를 사용할 때 로그가 최대크기가 될 경우 자동으로 인덱스를 사용하여 로그 파일을 저장해주는 옵션 ( 없을 시 에러가 난다. )

12 . <maxFileSize>

  • 한 로그 파일 크기의 최대치를 설정한다.

13 . <maxHistory>

  • 로그 기록 파일의 총 개수의 최대치를 설정한다.

14 . <totalSizeCap>

  • 로그 파일들의 총 크기의 최대치를 설정한다.
설정 전체 설명
  • 해당 설정은 스프링의 profile 이 prod 일 때만 적용된다.
  • Appender 설정
    • 로그 출력 형식
      • [ %d{yyyy/MM/dd HH:mm:ss.SSS} ] %-5level-- [%thread] %logger[%method:%line] - %msg %n
    • 콘솔 출력을 위해 ConsoleAppender 타입의 appender 생성
    • 로그 파일을 저장을 위해 RollingFileAppender 타입의 appender 생성
      • utf-8 형식으로 저장한다.
      • 로그 파일 저장되는 때
        • 매일 자정
        • 로그파일이 100MB 가 넘어가는 경우
      • 오래된 로그 파일이 삭제되는 때
        • 파일들의 총크기가 2GB를 넘을 때
        • 파일들의 갯 수가 15개를 넘을 때
      • appender 별 필터링
        • file-application : 필터링 없음 ( root 로그 레벨에 해당되는 모든 로그 저장 )
        • file-warn : WARN 레벨의 로그만 저장
        • file-error : ERROR 레벨의 로그만 저장
      • appender 별 저장위치
        • file-application : /etc/log/app/application.log
        • file-warn : /etc/log/warn/warn.log
        • file-error : /etc/log/error/error.log
      • 로그 파일 저장 파일명
        • file-application : application.%d{yyyy-MM-dd}-%i.log
        • file-warn : warn.%d{yyyy-MM-dd}-%i.log
        • file-error : error.%d{yyyy-MM-dd}-%i.log
  • root 설정
    • info 로그 레벨 이상의 로그만 출력하게 설정
    • 위에 생성한 모든 appender 사용하게 설정

다른 로깅 프레임워크 요청 SLF4J 로 연결하기

1 . build.gradle 에 log4j 로깅 프레임워크 의존성을 추가한다.

dependencies {    
	implementation 'org.apache.logging.log4j:log4j-core:2.17.1'  
}

2 . 실행할 수 있는 간단한 클래스를 만든다. ( log4j 를 사용한 코드 )

  
import org.apache.logging.log4j.LogManager;  
import org.apache.logging.log4j.Logger;  
  
public class Logging {  
  
	private final static Logger logger = LogManager.getLogger(Logging.class);  
	  
	public static void main(String[] args) {  
		logger.fatal("fatal log");  
	}  
}

3 . 실행해본다.

잘 작동하는 것을 확인할 수 있다.

4 . SLF4J, logback 의존성과 설정을 추가한뒤 실행한다.

dependencies {  
	implementation 'org.slf4j:slf4j-api:1.7.31'
	implementation 'org.apache.logging.log4j:log4j-core:2.17.1'  
	implementation 'ch.qos.logback:logback-classic:1.2.11'
}
<?xml version="1.0" encoding="UTF-8"?>  
<configuration>  
  
	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">  
		<encoder>  
			<pattern>%d{yyyy/MM/dd HH:mm:ss.SSS} %-5level --- [%thread] %logger[%method:%line] - %msg %n  
			</pattern>  
		</encoder>  
	</appender>  
	  
	<root level="info">  
		<appender-ref ref="console"/>  
	</root>  
  
</configuration>

실행 결과

> Task :noSpringZone:Logging.main()
02:11:38.086 [main] FATAL split.Logging - fatal log

작동은 하지만 SLF4J에서 설정한 로그 형태 포맷로 출력되지 않는다.
SLF4J 가 요청을 처리하지 않고 있는 것이다.

5 . Bridging Module 의존성을 추가한다. ( log4j-to-slf4j )

dependencies {  
	implementation 'org.slf4j:slf4j-api:1.7.31'
	implementation 'org.apache.logging.log4j:log4j-core:2.17.1'  
	implementation 'org.apache.logging.log4j:log4j-to-slf4j:2.13.3'
	implementation 'ch.qos.logback:logback-classic:1.2.11'
}

log4j-to-slf4j 는 log4j 요청을 SLF4J 에 연결하는 Bridging Module 이다.

6 . 다시 실행한다.

설정이 적용된 로그가 출력이 되는 것을 확인할 수 있다.

결론

Bridging Module, Binder 를 사용하여 기존에 로깅 프레임워크를 사용하고 있는 코드를 유지한 상태에서 SLF4J API 를 사용하게 할 수 있다.

SLF4J 동작과정으로 돌아가기


마무리

사실 이번 문제는 빠르게 해결하기 위해서는 기본에 사람들이 올려놓은 logback.xml 을 사용해도 됐다.

내가 전반적인 파악을 위해서 시간을 투자한 이유들

  • 운영서버에서 돌아갈 인프라 설정이니 만큼 운영에 문제가 일어나면 안되기에 더욱 더 꼼꼼하게 파악해야 된다고 생각했다.
  • 개인적으로 위에 적어놓은 로그로 해결하려는 것 에 있는 목록들이 앞으로 우테코 프로젝트를 레벨 4까지 운영하는데 굉장히 중요하다고 생각이 들었다.
  • 프롤로그 프로젝트에서 코드외적인 트러블슈팅할 때 로그를 굉장히 유용하게 썼다. 사실 유용을 넘어 로그가 없었다면 트러블슈팅이 불가능해보였던 것들도 있었다.

프롤로그 서비스와 s-hook 프로젝트를 하면서 코드를 치는 일도 있지만 코드 외적인 인프라나 여러 것들에 대해서 많이 공부하는 일들을 스스로 많이 하려고 하기도하고 실제로 해야 되는 일들도 많이 생기고 있다.

코드를 치는 기술직이 아닌 문제를 해결하는 개발자의 관점에서 앞으로 내가 개발자로 일을 하기 위해 굉장히 도움이 되는 기간이 될 것 같다!!