Spring boot – Log 설정

Spring boot에서 사용할 수 있는 대표적인 로그 관리 프레임워크

Log4j

2001년도에 개발된 로그 관리 프레임워크로, XML 설정 파일을 사용하여 로깅 구성을 정의하고, 다양한 출력 형식을 지원한다.

다음과 같은 단점을 가지며, 현재 많이 사용되지는 않는다.

  • 성능이 떨어질 수 있고, 멀티 스레드 환경에서의 안정성이 부족할 수 있다.
  • 보안 취약점 (CVE-2021-44228, “Log4Shel”)이 발견되어 사용이 권장되지 않는다.

Log4j2

2014년도 개발된 로그 관리 프레임워크로, Log4j2의 후속 버전으로 성능과 기능이 크게 개선되었다. 비동기 로깅을 지원하여 높은 성능을 발휘하며, 플러그인 아키텍처를 통해 유연한 확장이 가능하며, 다양한 설정 형식을 지원한다.

주요 특징은 다음과 같다.

  • 높은 성능과 안정성, 더 나은 비동기 지원
  • API 호환성 덕분에 기존 Log4j 사용자들이 쉽게 마이그레이션 가능
  • 멀티 쓰레드 환경에서의 비동기 로거의 경우, 다른 로깅 프레임워크보다 많은 처리량과 짧은 대기 시간을 제공
  • 람다 표현식과 사용자 정의 로그 레벨을 지원
  • 필터링 기능과 자동 리로딩을 지원

Logback

2008년도에 개발된 로그 관리 프레임워크로, Log4j의 창시자가 개발하였다. SLF4J (Simple Logging Façade for Java)와의 통합이 잘 되어 있어, 다양한 로깅 프레임워크와 함께 사용할 수 있다.

주요 특징은 다음과 같다.

  • 성능이 뛰어나고, 비동기 로깅을 지원
  • Log4j2보다 더 간단한 구성 및 설정을 제공
  • 로그 회전 및 압축 기능이 내장되어 있어 관리가 용이
  • Spring boot의 기본 로그 프레임워크로 내장되어 있음

Log4j2 vs Logback

Spring boot 반영

Log 활성화

Slf4j (Simple Logging Facade for Java)

로깅 프레임워크 구현체에 대한 추상 레이어 역할을 수행하는 인터페이스로, slf4j를 사용하며 코드 수정 없이 로깅 프레임워크 구현체를 바꿀 수 있는 조건을 만들 수 있다.

Spring boot Start – web 추가

Spring boot Start는 기본적으로 사용되는 라이브러리들에 대한 의존성들이 포함되어 있다. spring-boot-starter-web에는 로그를 사용하기 위한 spring-boot-starter-logging 라이브러리가 포함되어 있다. 해당 라이브러리를 사용함으로 인해, 기본적으로 slf4j와 그 구현체인 logback이 포함된다.

log4j2 의존성 수정

spring-boot-starter-logging 라이브러리에는 logback이 포함되어 있기 때문에, 아래와 같은 설정으로 logback을 제거하고, log4j2 의존성을 추가해 준다.

dependencies {
    implementation “org.springframework.boot:spring-boot-starter-log4j2”
}
configurations {
    all {
        exclude group: ‘org.springframework.boot’, module: ‘spring-boot-starter-logging’
    }
}

Jackson-dataformat-yaml (옵션)

만약 설정을 yaml 파일 형태를 사용할 것이라면, 아래와 같이 yaml 파일 해석을 위한 라이브러리를 추가해 줘야 한다. 해당 설정은 필수 요소가 아니기에, 해당 의존성이 포함되어 있지 않는 것 같다. (추후 변경될 가능성 있음)

implementation ‘com.fasterxml.jackson.dataformat:Jackson-dataformat-yaml’

콘솔의 로그 레벨에 따른 색깔 표시

로그 레벨에 따라 색을 칠하려면 다음과 같이 2가지 설정이 필요하다.

  1. log4j2설정에서 disableAnsi를 비활성화
    Configutaion.Appenders:Console.PatternLayout.disableAnsi: false
  2. 패턴에 highlight 패턴으로 감싸줌
    %highlight{} 패턴으로 다음과 같이 출력된 부분을 감싸 준다.
    %highlight{%-level}

위와 같이 설정하면, 설정된 부분에 로그 레벨에 따라 미리 시스템에 의해, 혹은 사용자 정의로 설정된 색으로 구문을 표시해준다.

Lombok을 통한 Logging

Lombok 추가 후, @Slf4j 어노테이션을 추가하면, 별도의 추가 작업 없이 바로 log 명령어를 통해 로거에 접근할 수 있다.

Logging.Properties를 통한 로깅 사용 자제

Spring boot 에서는 logging.properties를 통한 로그 구성 파일을 제공하는 기능을 지원하지만, jar에서 실행할 때 문제를 일으키는 알려진 클래스 로딩 문제가 있기 때문에 해당 방법을 사용하는 것에 신중해야 한다.

Log4j2 설정

Configuration:
Appenders:
Console:
name: Console_Appender
target: SYSTEM_OUT
PatternLayout:
charset: UTF-8
pattern: “%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} – %msg%n”
disableAnsi: false
RollingFile:
name: RollingFile_Appender
fileName: ./logs/auth-server.log
filePattern: ./logs/auth-server-%d{yyyy-MM-dd}-%i.log
PatternLayout:
charset: UTF-8
pattern: “%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} – %msg%n”
disableAnsi: false
Policies:
TimeBasedTriggeringPolicy:
interval: 1
modulate: true
SizeBasedTriggeringPolicy:
size: 10MB
DefaultRolloverStrategy:
max: 10
Loggers:
Root:
level: debug
AppenderRef:
ref: Console_Appender
ref: RollingFile_Appender

Async Logger 활성화

AsyncLogger설정을 통한 Async Logger 활성화를 수행한다.

Configuration:

...
Loggers:
AsyncLogger:
name: asnyc-logger
level: info
additivity: false
AppenderRef:
- ref: RollingFile_Appender

에러 처리

AsyncLogger를 추가했을 경우, 아래와 같은 에러가 발생할 수 있다. 이 경우엔, 필요로 하는 라이브러리를 참조 추가하여 해결한다.

에러)
Caused by: java.lang.ClassNotFoundException: com.lmax.disruptor.EventHandler
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)

수정 사항)

// Log4j2 설정
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
implementation 'com.lmax:disruptor:3.4.2'

 

 

성능 개선 확인 

Log4j2 – sync 방식의 성능 

관련 코드 

    /* 

     * 테스트 로그를 쌓음 

     */ 

    @GetMapping(“/writeLogAsync”) 

    @Async 

    public CompletableFuture<String> writeLogAsync() { 

        var startDateTime = LocalTime.now(); 

        for (int i = 0; i < 100000 ; i++) { 

            asyncLogger.warn(“LOG – {}”, i); 

        } 

        var endDateTime = LocalTime.now(); 

        asyncLogger.info(“End DateTime : {} ==> {}”, startDateTime, endDateTime); 

        return CompletableFuture.completedFuture(“{ S : ” + startDateTime.toString() + “, E : ” + endDateTime.toString() + ” }”); 

    } 

 

성능 확인 

15:52:07.402 [http-nio-8200-exec-2] INFO  com.wemade.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 9076 ms] 

15:52:27.411 [http-nio-8200-exec-3] INFO  com.wemade.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 9017 ms] 

16:35:45.800 [http-nio-8200-exec-5] INFO  com.wemade.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 9439 ms] 

 

Log4j2 – async 방식의 성능 

관련 코드 

    /* 

     * 테스트 로그를 쌓음 

     */ 

    @GetMapping(“/writeLog”) 

    public String writeLog() { 

        var startDateTime = LocalTime.now(); 

        for (int i = 0; i < 100000 ; i++) { 

            syncLogger.warn(“LOG – {}”, i); 

        } 

        var endDateTime = LocalTime.now(); 

        syncLogger.info(“End DateTime : {} ==> {}”, startDateTime, endDateTime); 

        return “{ S : ” + startDateTime.toString() + “, E : ” + endDateTime.toString() + ” }”; 

    } 

성능 확인 

15:49:37.397 [http-nio-8200-exec-2] INFO  com.wemade.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 665 ms] 

15:49:56.652 [http-nio-8200-exec-3] INFO  com.wemade.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 632 ms] 

15:50:18.316 [http-nio-8200-exec-4] INFO  com.wemade.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 617 ms] 

 

특이 사항 

  1. Async 방식의 특징에 따라, 결과 리턴 후, 계속 로그가 쌓여가는 현상이 발생함

 

Logback – sync 방식의 성능 

관련 코드 

    /* 

     * 테스트 로그를 쌓음 

     */ 

    @GetMapping(“/writeLog”) 

    public String writeLog() { 

        var startDateTime = LocalTime.now(); 

        for (int i = 0; i < 100000; i++) { 

            logger.warn(“LOG – {}”, i); 

        } 

        var endDateTime = LocalTime.now(); 

        logger.info(“End DateTime : {} ==> {}”, startDateTime, endDateTime); 

        return “{ S : ” + startDateTime.toString() + “, E : ” + endDateTime.toString() + ” }”; 

    } 

성능 확인 

10:24:47.661 [http-nio-8200-exec-2] INFO  c.w.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 11259 ms] 

10:25:06.786 [http-nio-8200-exec-3] INFO  c.w.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 11742 ms] 

10:25:27.619 [http-nio-8200-exec-4] INFO  c.w.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 11049 ms] 

 

 

Logback – async 방식의 성능 

관련 코드 

    /* 

     * 테스트 로그를 쌓음 

     */ 

    @GetMapping(“/writeLog”) 

    public String writeLog() { 

        var startDateTime = LocalTime.now(); 

        for (int i = 0; i < 100000; i++) { 

            logger.warn(“LOG – {}”, i); 

        } 

        var endDateTime = LocalTime.now(); 

        logger.info(“End DateTime : {} ==> {}”, startDateTime, endDateTime); 

        return “{ S : ” + startDateTime.toString() + “, E : ” + endDateTime.toString() + ” }”; 

    } 

 

설정 파일 변경 내역 

Gradle의 logback classic 라이브러리 추가 
implementation ‘ch.qos.logback:logback-classic’; 

AsyncAppender 로 기존 로그 Appender Wrapping 

        <!– Async Appender –> 

        <appender name=”FILE-ASYNC” class=”ch.qos.logback.classic.AsyncAppender”> 

            <appender-ref ref=”FILE” /> 

            <includeCallerData>false</includeCallerData> 

            <neverBlock>false</neverBlock> 

        </appender> 

성능 확인 

10:45:30.398 [http-nio-8200-exec-1] INFO  c.w.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 9704 ms] 

10:45:49.463 [http-nio-8200-exec-2] INFO  c.w.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 8968 ms] 

10:46:07.141 [http-nio-8200-exec-3] INFO  c.w.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 9481 ms] 

 

특이 사항 

비동기 방식이지만, Log4j2 – async 방식과는 다르게 모든 로그가 출력된 이후, 결과가 리턴 되고 있음 

Hyper-V 그래픽 카드 자원에 대한 VM 할당 방법

자원 분할 할당 (MIG)

MIG(Multi-Instance GPU)를 지원하는 그래픽 카드는 자체적으로 GPU를 VM 에 분할 할당할 수 있는 기능을 제공한다. 기기에 따라 다르지만, NVidia의 GB200/B200/B100, H100, H200 같은 기종의 경우, 최대 7대까지 (자원을 분할하여) 그래픽카드를 일정 분배에 따라 나누어, 각각의 인스턴스에 분할하여 할당할 수 있는 기능을 드라이버 차원에서 제공한다.

다만, 일반 그래픽 카드의 경우는, 해당 기능을 제공하는 그래픽 카드는 존재하지 않으며, 가상 환경에 그래픽 카드를 할당 하려면, DDA (Discrete Device Assignment)와 같은 방법으로 디바이스 자체를 할당 시키는 방법을 통해 활용하는 방법을 선택해야 한다. 이때에도, MIG처럼 디바이스를 분할 하거나, 공유하지 못하고, 디바이스 전체를 공유해야 한다.

DDA 기능 활성화 방법

DDA 제한 해제

DDA를 설정하기 위해서는 해당 하드웨어 리소스를 가상 머신에 할당하기 위해 필요한 권한과 구성을 활성화 하기 위한 DDA 제한을 해제하는 과정을 진행해야 한다. DDA 제한 해제 설정의 주요 이유는 다음과 같다.

장치 접근 허용

기본적으로 Hyper-V는 특정 장치에 대한 직접 접근을 제한한다. DDA 제한 해제를 통해 Hyper-V가 GUP와 같은 장치를 VM에서 직접 할당할 수 있도록 함

리소스 분리

가상 머신이 호스트와는 독립적으로 하드웨어 리소스를 사용할 수 있도록 해주어 성능을 최적화 한다.

호환성

일부 GPU는 DDA를 통해 가상 머신에 할당되기 위해 추가적인 설정이나 드라이버 구성 요소가 필요할 수 있다. 이 설정을 통해 이러한 요구사항을 충족시킬 수 있다.

보안 및 격리

DDA 제한 해제를 통해 각 VM이 하드웨어 리소스를 독립적으로 사용할 수 있게 하여, 보안과 격리성을 강화한다. VM 간의 리소스 경합을 줄이는 데에도 도움이 된다.

제한 해제 방법

PowerShell – cmdlet을 사용하여 TurnOff를 Automatic Stop Action 에 대하여 설정한다.

Set-VM –Name VMName –AutomaticStopAction TurnOff

ex) Set-VM –Name TEST-MACHINE –AutomaticStopAction TurnOff

 

그래픽 장치를 위한 VM 준비

CPU에서 쓰기 결합을 활성화

Set-VM –GuestControlledCacheTypes $true –VMName VMName

32비트 메모리 맵핑 IO(MMI) 공간 구성

Set-VM –LowMemoryMappedIoSpace 3Gb –VMName VMName

32비트 이상의 MMIO 공간 구성

Set-VM –HighMemoryMappedIoSpace 33280Mb –VMName VMName

MMIO 메모리 Mapping

위에서 설정한 LowMamoryMappedIoSpace와 HighMemoryMappedIoSpace는 메모리 맵 I/O 공간을 정의하는 매개 변수 이다. 이 주소는 PCIe 디바이스와 관련된 메모리 주소 공간을 설정하는데 필요하다.

LowMomoryMappedIOSpace

이 값은 가상 머신이 사용하는 낮은 주소의 메모리 맵 I/O공간을 지정한다. 일반적으로 이 공간은 1GB 미만으로 설정되며, GPU와 같은 장치에 대한 기본 I/O 맵핑을 위해 사용된다.

HighMemoryMappedIOSpace

dl 값은 가상 머신이 사용하는 높은 주소의 메모리 맵 I/O공간을 지장한다. 일반적으로 1GB 이상의 주소를 포함하며, 더 많은 I/O 리소스를 요구하는 장치에 사용된다.

적절한 사이즈

적절한 사이즈를 설정하려면, 다음과 같은 사항을 확인하여 설정을 진행해야 한다.

장치 문서 참조 : 사용하려는 GPU나 PCIe 디바이스 문서를 참조하여 해당 장치가 요구하는 메모리 맵 I/O 주소 범위를 확인

쉬운 사이즈 설정 (ChatGPT)

NVidia의 Geforce 1050 ti의 경우 아래와 같은 값을 권장 값으로 제시한다.

LowMemoryMappedIOSpace: 0x00000000 (1GB)

HighMemoryMappedIOSpace: 0x800000000 (1GB)

호스트 기기에서 대상 장치를 분리

장치의 위치 경로 확인

장치의 위치 경로는 다음과 같은 형태로 구성된다.

PCIROOT(20)#PCI(0300)#PCI(0000)#PCI(0800)#PCI(0000)

장치 경로는 다음과 같은 방법으로 확인할 수 있다.

  1. 장치 관리자를 열고 장치를 찾음
  2. 장치를 오른쪽 마우스 버튼을 통해 속성 창으로 이동
  3. 세부 정보 탭에서 속성 드롭 다운 메뉴를 확장하고 위치 경로를 선택
  4. PCIROOT로 시작하는 장치의 위치 경로를 확인

장치의 비활성화

장치 관리자를 통해 해당 장치를 비활성화 함

장치 분리

해당 장치가 완화 드라이버를 제공했는지 여부에 따라서 적절하게 강제 명령 옵션을 선택하여 장치를 분리한다.

Dismount –VMHostAssignableDevice (-Force) –LocationPath $locationPath

 

만약 해당 설정이 아래와 같은 에러를 발생하며 실패할 경우, 다음과 같은 BIOS 설정을 진행한다.

에러) 현재 구성에서는 PCI Express 버스의 OS 제어가 허용되지 않습니다. …

해결 방법)

BIOS에 접근하여, Above 4G Decoding 옵션과 VT-d 또는 AMD-Vi 와 같은 가상화 옵션을 활성화 한다.

 

게스트 VM에 해당 장치 할당

VM에 할당하여 해당 디바이스에 접근할 수 있도록 설정함

Add-VMAssignableDevice –LocationPath $locationPath –VMName VMName

 

VM 에서 작업

이렇게 할당이 완료되면, 해당 디바이스는 VM에서 전용으로 점유하여 작업을 수행할 수 있는 상태가 된다. 드라이버를 별도로 설치하고, 해당 디바이스를 활용하여 작업을 수행할 수 있다.

 

장치를 제거하고 호스트로 반환

장치를 원래 상태로 되돌리려면 VM을 중지하고 다음 명령을 실행해야 한다.

Remote-VMAssignableDevice –LocationPath $locationPath –VMName VMName

Mount-VMHostAssignableDevice –LocationPath $locationPath

이후, 장치 관리자를 통해 해당 장치를 활성화 하면 호스트 운영 체제가 다시 장치를 사용할 수 있는 상태가 된다.

 

자동화된 스크립트 예시

# Configure the VM for a Discrete Device Assignment
$vm = “ddatest1”
# Set automatic stop action to TurnOff
Set-VM -Name $vm -AutomaticStopAction TurnOff
# Enable Write-Combining on the CPU
Set-VM -GuestControlledCacheTypes $true -VMName $vm
# Configure 32 bit MMIO space
Set-VM -LowMemoryMappedIoSpace 3Gb -VMName $vm
# Configure Greater than 32 bit MMIO space
Set-VM -HighMemoryMappedIoSpace 33280Mb -VMName $vm

# Find the Location Path and disable the Device
# Enumerate all PNP Devices on the system
$pnpdevs = Get-PnpDevice –presentOnly
# Select only those devices that are Display devices manufactured by NVIDIA
$gpudevs = $pnpdevs | Where-Object {$_.Class -like “Display” -and $_.Manufacturer -like “NVIDIA”}
# Select the location path of the first device that’s available to be dismounted by the host.
$locationPath = ($gpudevs | Get-PnpDeviceProperty DEVPKEY_Device_LocationPaths).data[0]
# Disable the PNP Device
Disable-PnpDevice -InstanceId $gpudevs[0].InstanceId

# Dismount the Device from the Host
Dismount-VMHostAssignableDevice -Force -LocationPath $locationPath

# Assign the device to the guest VM.
Add-VMAssignableDevice -LocationPath $locationPath -VMName $vm