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

Google Play Store – 인앱 결제(IAP) 구현

서비스 활성화

IAP 서비스 설치

  1. 서비스 구현에 앞서, Unity 에서, IAP (In App Purchase) 기능을 활성화 한다.
  2. Windows > General > Services 메뉴로 접근
  3. Game Economy > In App Purchasing 을 선택하여 Install 을 진행한다.

Adaptive Performance 설치

IAP를 설치하고, 서비스를 활성화 하게 되면, 필수적으로 Adaptive Performance 모듈을 필요로 하게 되며 해당 모듈이 설치되어 있지 않으면, 빌드 과정에서 에러를 발생하게 된다.

아래 과정을 거쳐 해당 모듈도 설치를 진행한다.

  1. IAP 와 동일하게, Windows > General > Services 메뉴에서.
  2. All 탭을 선택한 후, Packages > Adaptive Performance 메뉴로 이동하여
  3. 해당 모듈을 설치하거나, Unlock 을 하여 활성화 해 준다.

Adaptive Performance – Samsung Android Provider 활성화

Adaptive Performance 가 활성화 되면, Samsung Android Provider도 활성화 시켜 줘야 한다.

  1. Edit > Project Settings 로 이동
  2. Adaptive Performance 메뉴로 이동하여, Android 탭에서, Samsung Android Provider 를 체크하여 활성화 함

Android 환경 설정

Adaptive Performance 를 사용하기 위해서는,

  1. Android OS: 7.0 이상 (Project Settings > Players >
  2. Scripting Backend: IL2CPP 로 설정되어 있어야 함

Allow downloads over HTTP 설정

Purchase 라이브러리 활성화 하는 과정 중에 HTTP로 라이브러리를 다운로드 하는 경우가 발생하므로, 에러를 방지하기 위해 해당 옵션을 Always allowed로 설정

프로젝트 서비스 설정

  1. 서비스를 활성화 하고, Edit > Project Settings 창을 연다.
  2. Services > In-App Purchasing 메뉴로 접근한다. (만약 해당 메뉴가 보이지 않는다면, 서비스 활성화를 통해 IAP 모듈을 설치를 다시 진행한다.)

Purchases 활성화

In-App Purchases 메뉴 우측의 활성화 Toggle Button을 클릭하여 모듈을 활성화 한다.

License key 입력

In-App Purchases 활성화 창에서, 하단 Receipt Obfuscator 부분에 라이선스 키를 입력

라이선스 키는 Play Store Console 의 Play를 통한 수익 창출 > 수익 창출 설정 메뉴 하단의 라이선스 에서 확인할 수 있다.

만약 해당 컨트롤을 통해 등록되지 않을 경우, Unity Cloud 를 통해 등록을 시도하고, 다시 시도함

구매 처리 클래스 구현

구매 처리 클래스 구현

구매 처리 클래스는 IStoreListener를 상속받는 클래스를 생성함

  1. IStoreController와 IExtensionProvider를 내부 변수로 생성

생성자

생성자 에서는, 구매될 제품의 정보를 구현한다.

var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());

builder.AddProduct(“제품명”, ProductType.Consumable, new IDs

{

        {“제품명_구글버전”, GooglePlay.Name},

        {“제품명_맥버전”, MacAppStore.Name}

}

UnityPurchsing.Initialize(this, builder);

OnInitialized 메소드 (IStoreListener)

해당 메소드에서는, 내부 변수들을 초기화 한다.

public void OnInitialized(IStoreController controller, IExtensionProvider extensions) {

    this.controller = controller;

    this.extensions = extensions;

}

OnIntializeFailed 메소드 구현 (IStoreListener)

초기화 실패에 대한 처리 메소드 구현

public void OnInitializeFailed(InitializationFailureReason error) {

    … 에러 처리

}

public void OnInitializeFailed(InitializationFailureReason error, string message) {

    … 에러 처리

}

PrucessPurchase 메소드 구현 (IStoreListener)

구매 처리 메소드로 구매 결과를 리턴

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e) {

    // product Id

    string productId = e.purchasedProduct.definition.id;

    return PurchaseProcessingResult.Complete;

}

OnPurchaseFailed 메소드 구현 (IStoreListener)

구매 실패에 대한 처리 메소드 구현

public void OnPurchaseFailed(Product I, PurchaseFailureReason p) {

    … 에러 처리

}

Store 에 상품 등록

각 Store 상품 등록 절차에 맞춰 상품을 등록함

또한 등록된 아이디는 생성자 부분에 등록해 주어야 함

구매 처리 메소드 구현

public void Purchase(string productId) {

    Product product = controller.products.WithID(productId);

    if (product != null && product.availableToPurchase) {

        controller.InitiatePurchase(product);

    }

    else {

        … 구매 상품이 없음에 대한 에러 처리

    }

}

Unity – Plugin (Game Object) 구성

Game Object로 등록되는 클래스 생성 법

직접 적으로 Game Object로 등록할 수 있는 클래스 생성 법

MonoBehavior 생성

MonoBehavior를 상속받는 클래스를 생성하여, 해당 Object의 동작을 구현한다.

public class MySpecialObject : MonoBehavior
{
void Start() { … }
}

 

GameObject 메뉴 생성

MenuItem Attribute를 통해 클래스를 생성하여 등록 가능한 클래스를 생성한다.

[MenuItem(“메뉴 위치”, bool isValidateFunction, int priority)]

  1. 메뉴 위치 : 메뉴에서 표출될 위치로 GameObject를 가장 최 상위로 두고, 서브 메뉴로 접근 (GameObject/Sub Menu)
  2. isValidationFunction : 해당 메뉴가 호출되기 전에 해당 함수를 검증함
  3. priority : 메뉴의 우선순위를 설정함

public class MySpecialObjectCreator
{
[MenuItem(“GameObject/My Special Object”, false, 0)]
public static void CreateMySpecialObject()
{
// 새로운 게임 오브젝트 생성
GameObject newObject = new GameObject(“MySpecialObject”);

// MySpecialObject 컴포넌트 추가
newObject.AddComponent<MySpecialObject>();

// 새로 생성한 게임 오브젝트를 선택
Selection.activeGameObject = newObject;
}
}

 

하위 GameObject 추가

GameObject 하위에 자식 GameObject를 생성하려면, transform.parent를 통해 설정하면 된다. 코드는 아래와 같다.

GameObject parent = new GameObject(“TITLE”);
GameObject subObject = new GameObject(“TITLE”);
subObject.transform.parent = parent.transform;

Unity – Plugin (Menu & Setting Windows) 구성

개요

해당 방법을 통해 Unity 에서, 사용자 정의 메뉴를 추가하고, 데이터를 관리할 수 있는 인터페이스를 생성할 수 있음.

구성은 크게,

  1. 메뉴를 추가,
  2. 메뉴 선택 시, 띄울 화면 구성
  3. 메뉴에서 관리할 데이터의 관리 (저장하기, 불러오기)

로 구성된다.

 

메뉴 구성 방법

메뉴는 Static Function 에 MenuItem Attribute 를 통해 구성 가능하다.

  1. Static Method를 생성함

    public static void MyMenu() { … }

  2. Menu Item Annotation을 통해 해당 메뉴를 구성한다.

    [MenuItem(“MyMenu/ My Settings”)]

  3. 화면 표출 코드

    화면을 노출 시키기 위해 아래 코드를 추가함

     

    EditorWindow EditorWindow.GetWindow(System.Type windowType, bool utility, string title) (+ 12 overloads)

     

    MySettingWindow window = (MySettingWindow)GetWindow(typeof(MySettingWindow), false, “My Settings”);

[전체 코드]

[MenuItem(“MyMenu/My Settings”)]

public static void MyMenu() {

    MySettingWindow window = (MySettingWindow)GetWindow(typeof(MySettingWindow), false, “My Settings”);

}

 

화면 구성

띄워질 화면에서 구성할 요소들을 설정한다.

OnGUI() 메소드

해당 메소드는, 매 프레임 마다, 호출되는 메소드로, GUI 핸들링 기능을 제공하는 메소드

데이터 변수

데이터를 관리할 변수들을 추가한다.

private bool toggleValue;

 

화면에 요소를 노출하고, 변수에 대입

EditorGUILayout 메소드 들을 통해, 화면에 UI를 배치하고, 데이터를 대입한다. 해당 메소드는, 이름으로 지정된 인스턴스가 생성되어 있는지 확인하고, 생성되지 않았으면, 2번째 인자의 초기값을 통해 인스턴스를 생성하고 초기값을 그대로 리턴 한다. 만약 인스턴스가 존재하면, 해당 인스턴스의 현재 값을 리턴 한다.

toggle = EditorGUILayout.Toggle(“Enable Feature”, toggleValue);

 

Toggle

EditorGUILayout.Toggle(string title, bool value);

Silder

EditorGUILayout.Slider(string title, float value);

Button

버튼의 경우는, 클릭 되었을 경우 true를 리턴 하고, 그렇지 않으면, false를 리턴 한다.

if (GUILayout.Button(“Button Title”)) { … }

 

데이터 관리

설정된 데이터는 Unity가 재 시작 되면, 기존에 입력했던 데이터가 모두 초기화 된다. 해당 데이터는 다음과 같은 방법으로 저장 및 로드 할 수 있다.

데이터 저장

EditorPrefs.SetBool(“TITLE”, toggleValue);

EditorPrefs.SetFloat(“TITLE”, sliderValue);

데이터가 저장되는 TITLE 구분자는 유니크한 값이어야 한다. 그렇지 않아 중복될 경우, 이전 값이 덮어쓰여짐에 주의해야 함

데이터 로딩

toggleValue = EditorPrefs.GetBool(“TITLE”);

silderValue = EditorPrefs.GetFloat(“TITLE”);

구현 위치

데이터 로딩은, OnGUI() 에서, Window를 구성한 이후, 바로 호출해 주면, 화면이 구성되면서, 이전 데이터를 로딩할 수 있으며,

데이터 저장은, Apply 버튼을 구성하여 저장하는 방법(위에서 설명한 Button)이나, 아래와 같이 실시간으로 저장되도록 구성함

private void OnGUI() {

    …

    bool newToggleValue = EditorGUILayout.Toggle(“TITLE”, toggleValue);

    if (newToggleValue != toggleValue) {

        toggleValue = newToggleValue;

        ApplySettings();

    }

    …

}

다른 클래스나 컴포넌트에서 데이터 가져오기

Singleton Pattern을 통한 데이터 공유

Singleton Pattern의 클래스를 생성하여 데이터를 입력한 후, 다른 클래스나 컴포넌트에서 데이터를 호출할 수 있는 인터페이스를 제공한다.

public class SettingsManager : MonoBehavior
{
    public static SettingsManager Instance { get; set; }
    // … 관리될 변수 정의

    private void Awake() {
        if (Instance == null) {
            Instance = this;
            DontDestroyOnLoad(gameObject); // 씬 간 데이터 유지
        }
        else {
            Destroy(gameObject);
        }
    }
}

 

ScriptableObject

ScriptableObject 를 통해 데이터에 접근

아래와 같은 ScriptableObject 클래스를 생성하면, 해당 데이터는 Create Assets 메뉴를 통해 컴포넌트를 생성할 수 있고, 해당 Assets 은 단순히 Project 에 Asset 을 생성해 놓는 것 만으로도 인스턴스 화 되어 데이터에 접근할 수 있다. 또한 Assets으로 생성된 객체에 접근하여 데이터를 수정하면, 수정된 데이터가 파일로 저장되어 지속적으로 영속화 되어 프로젝트 재 시작 시에도 이전 설정된 데이터 값이 계속 유지된다.

이는 Object에 Component로 등록 되어야만 인스턴스 화 되는 MonoBehavior와는 다른 형태로 동작하며, Settings 데이터등을 관리하기에 최적화 되어 있는 클래스 타입으로 볼 수 있다.

[CreateAssetMenu(fileName = “SettingsData”, menuName = “Settings/SettingsData”, order = 1)]
public class SettingsData : ScriptableObject
{
public bool toggleValue = false;
public float sliderValue = 1.0f;
}

이 경우는 간혹 Windows 에서 변경한 데이터의 변경 사항을 감지하지 못하여 데이터가 저장되지 않는 경우가 발생한다. 이를 방지하기 위하여 GUI 에서, Changed를 감지하여 저장하는 로직을 추가함

if (GUI.changed) {
EditorUtility.SetDirty(settinsData);     // 데이터 변경을 강제로 설정
AssetDatabase.SaveAssets();        // Asset Database 에 데이터 저장 명령을 직접 호출
}

 

Static Object

전역 데이터로 변수를 생성하여 관리하는 방식으로, 추천하지 않는다.

Firebase Notification 연동

Firebase 활성화

Firebase 계정 및 프로젝트 생성

Firebase 계정 등록 절차에 따라 계정 및 프로젝트를 생성한다.

Firebase 구성 파일 추가

Firebase Console 을 통해 플랫폼별 Firebase 구성 파일을 가져와 Unity 프로젝트에 추가한다.

Android 의 경우

  1. google-services.json 다운로드를 클릭하여 파일을 다운로드 함
  2. 다운로드 한 파일을 Assets 폴더로 이동한다.

Android SDK Version

Android 의 경우, 필수적으로 SDK 24 이상을 사용해야 함.

Firebase Unity SDK 추가

  1. 아래 파일을 다운로드 함

    https://firebase.google.com/download/unity?hl=ko&_gl=1*1god004*_up*MQ..*_ga*MTk2MjQxMTE5My4xNzI4NTQwMDA0*_ga_CW55HF8NVT*MTcyODU0MDAwNC4xLjAuMTcyODU0MDAwNC4wLjAuMA..

  2. 압축 해제 후, 다음 패키지를 설치한다.
    1. Assets > Import Package > Custom Package 를 선택하거나,

      해당 패키지를 더블클릭하여 Import 창을 띄움

    2. FirebaseMessaging.unitypackage (클라우드 메시징용 패키지)

코드 작성

Handler 추가

Token Receive Handler 설정 코드 추가

FirebaseMessaging.TokenReceived += OnTokenReceived;

private void OnTokenReceived(object sender, TokenReceivedEventArgs e) { … }

 

Message Receive Handler 설정 코드 추가

FirebaseMessaging.MessageReceived += OnMessageReceived;

private void OnMessageReceived(object sender, MessageReceivedEventArgs e) { … }

 

메시지 초기화 설정

FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(task => {

    var dependencyStatus = task.Result;

    if (dependencyStatus == DependencyStatus.Available) {

        // 설정 코드 추가

}

else {

    …

}

});

 

문제 해결

Crash Error 관련

특이하게, Android 35 버전으로 구동된 에뮬레이터에서는 Crash Error 가 발생한다. 해당 문제는 다른 버전을 사용하면 해결되는 것으로 보인다. 구체적인 문제 원인은 확인되지 않고 있다.

2024/10/16 14:34:18.174 1725 1725 Error CRASH *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

2024/10/16 14:34:18.175 1725 1725 Error CRASH Version ‘2022.3.50f1 (c3db7f8bf9b1)’, Build type ‘Development’, Scripting Backend ‘il2cpp’, CPU ‘arm64-v8a’

2024/10/16 14:34:18.175 1725 1725 Error CRASH Build fingerprint: ‘google/sdk_gphone16k_x86_64/emu64xa16k:15/AP31.240617.003/12088229:user/dev-keys’

2024/10/16 14:34:18.176 1725 1725 Error CRASH Revision: ‘0’

2024/10/16 14:34:18.176 1725 1725 Error CRASH ABI: ‘arm64’

2024/10/16 14:34:18.182 1725 1725 Error CRASH Timestamp: 2024-10-16 14:34:18.176553076+0900

2024/10/16 14:34:18.182 1725 1725 Error CRASH pid: 1725, tid: 1725, name: .gamesdk_client >>> com.gravity.gamesdk.client.gamesdk_client <<<

2024/10/16 14:34:18.182 1725 1725 Error CRASH uid: 10201

2024/10/16 14:34:18.183 1725 1725 Error CRASH signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr ——–

2024/10/16 14:34:18.185 1725 1725 Error CRASH x0 0000000000000001 x1 0000000000000000 x2 0000000000010006 x3 0000745433259e30

2024/10/16 14:34:18.186 1725 1725 Error CRASH x4 0000000000000008 x5 0000000000000000 x6 0000000000000010 x7 7f7f7f7f7f7f7f7f

2024/10/16 14:34:18.186 1725 1725 Error CRASH x8 61b222bc3e0efb77 x9 61b222bc3e0efb77 x10 0000000000000000 x11 0000000000000000

2024/10/16 14:34:18.186 1725 1725 Error CRASH x12 00000000ffffffbf x13 0000000000000020 x14 0000000000000400 x15 0000000000000000

2024/10/16 14:34:18.187 1725 1725 Error CRASH x16 000074542deb7d88 x17 0000745434d6e968 x18 0000745431a5c000 x19 0000000000000000

2024/10/16 14:34:18.187 1725 1725 Error CRASH x20 0000000000000000 x21 0000000000000000 x22 0000000000000000 x23 0000000000000000

2024/10/16 14:34:18.187 1725 1725 Error CRASH x24 0000000000000000 x25 0000000000000000 x26 0000000000000000 x27 0000000000000000

2024/10/16 14:34:18.187 1725 1725 Error CRASH x28 0000000000000000 x29 0000000000000000

2024/10/16 14:34:18.187 1725 1725 Error CRASH lr 0000000000000000 sp 0000745433259ff0 pc 0000000000000000 pst 0000000000000000

2024/10/16 14:34:21.044 1725 1725 Error CRASH Forwarding signal 11

0001/01/01 00:00:00.000 -1 -1 Info ——— beginning of crash

2024/10/16 14:34:21.045 1725 1725 Fatal libc Fatal signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x7454a0ac38ec in tid 1725 (.gamesdk_client), pid 1725 (.gamesdk_client)

Docker 메모리 설정

설정 프로그램 설치

WSL 을 활성화 하면, 같이 “WSL Settings” 라는 설정 프로그램이 같이 설치된다. 다만, 환경이나, 설치 방법에 따라 해당 프로그램이 설치되지 않는 경우도 있으니, 이럴 경우에는, 아래 경로를 통해 설치 파일을 다운로드 받아 설치를 진행한다.

Releases · microsoft/WSL (github.com)

 

메모리 설정

설정 프로그램 (WSL Settings) 를 실행하여, 메모리 및 프로세스 탭을 통해 해당 설정을 수행할 수 있다. 메모리 크기에서 사이즈를 설정하며, MB 단위로 수정할 수 있다.

이 때 주의할 점은, 수정된 메모리 사이즈는 현재 메모리 가용 범위 내에서 설정을 해 주어야 한다. 그렇지 않을 경우, 설정이 저장되지 않고, 관련 오류가 표시되니, 재부팅 등을 활용하여 가용 메모리를 확보 한 후 수행하여야 한다.

또 한가지 주의할 점은, 해당 메모리 사이즈는, 운영체제와 공유되어 활용되지 못하고, 전용 공간으로 확보되어 버리므로, 설정 이후에 시스템 메모리 사이즈를 고려하여 설정하여야 한다.

 

SWAP 크기 설정

해당 크기가 설정될 경우, 허용 범위를 초과한 컨테이너가, 자동으로 종료되지 않고, Swap 메모리를 활용하여 실행되게 된다. 이에 따라 유휴 컨테이너가 스왑 메모리로 Swap in /Out 되면서 무리한 부하가 발생할 수 있으므로, 0으로 설정한다.

 

설정 후, 재부팅 및 확보된 공간 확인

재부팅 한후, Docker 를 통해 확보된 공간을 확인한다.

Google Login 연동

사전 설정 확인

Java 설치 및 JAVA_HOME 설정 확인

  1. Java 설치 확인, 그렇지 않으면 설치

    https://www.oracle.com/java/technologies/downloads/#jdk21-mac

  2. JAVA_HOME 설정 확인

Unity – Android SDK 설치 확인

Unity 의 Android SDK 가 설치되어 있는지 확인

Android 활성화 확인

  1. File > Build Settings 로 접근
  2. Platform 에서 Android 가 활성화 되어 있는지 확인
  3. 활성화 되어 있지 않으면, Android를 선택하고, 하단 Switch Platform을 클릭 할 것

Player Setting 설정

Player Setting 설정

  1. File > Project Settings > Player 로 접근
  2. Android 탭 선택
  3. Identification 메뉴에서 Package Name 확인 및 입력 (Google Console 에 입력한 Package Name)
  4. Configuration 메뉴에서 Scripting Backend 를 “IL2CPP” 로 선택
  5. Api Compatibility Level : .NET 4.X or .NET Framework 로 선택
  6. ARM64 체크 박스 선택

KeyStore 설정

  1. Player Setting 하단의 “Publishing Settings” 로 이동
  2. Project Keystore 를 기존에 사용하던 (Android Build 에 사용하던) keystore 로 대체

Allow downloads over HTTP 설정

  1. Player Setting 하단의 “Configuration” 로 이동
  2. Allow downloads over HTTP 부분이 Not allowed 로 되어 있는 부분을 Always allowed 로 변경

Google Play Games – Plugin 설치

Plugin 다운로드

https://github.com/playgameservices/play-games-plugin-for-unity/releases/tag/v10.14

설치

  1. 압축 해제 후, 하위 폴더 current-build를 하위의 GooglePlayGamesPlugin-x.yy.zz.unitypackage를 더블클릭 하여 프로젝트에 설치를 진행함
  2. 라이브러리 Import 화면에서 전체 선택 후, Import
  3. “Enable Android Auto-resolution” 팝업 창에서 “Enable” 선택하여 활성화
  4. 만약 해당 창이 뜨지 않을 경우
    1. 빌드 설정이 Android 로 선택되지 않았거나, (설정 변경 수행)
    2. 메뉴를 통해 활성화 시켜줄 수 있음 (Assets > External Dependency Manager > Android Resolver > Force Resolve)

로그인 설정 구성

  1. Windows > Google Play Games > Setup > Android setup으로 접근
  2. Google Play Games 의 Resources Definition 과 Client ID를 입력

Resource Definition

  1. Google Play Console 에서, “게임” 타입의 앱을 생성
  2. Google Console 의 Android 인증 정보를 맵핑 한 후,
  3. 사용자 늘리기 > Play 게임즈 서비스 > 설정 및 관리 > 설정으로 이동하여
  4. 사용자 인증 정보 우측의 리소스 보기 버튼을 통해 해당 정보를 확인할 수 있다.

Client ID

  1. Resource Definition 에서 접근한 설정 창에서,
  2. 사용자 인증 정보 > Android 에서 등록된 앱을 클릭하여 접근하면,
  3. 중간에 *apps.googleusercontent.com 형태로 이루어진 클라이언트 아이디를 확인할 수 있다.

 

프로젝트 정보 적용

위 정보를 입력하고, Setup 버튼을 누르면, 해당 정보가 정상적이라면, 인증이 정상적으로 수행되었다는 알림 창을 확인할 수 있다.

혹시나 에러가 난다면, Client ID 앞뒤로 빈칸이 없는지 확인

 

기타

만약, 해당 인증 정보의 앱 상태가, 테스트 상태 라면, 필히, 테스터에 로그인 테스트를 수행하는 계정의 아이디 정보를 등록해 줘야 한다.

로그인 정보는 Google Play Console 에서,

사용자 늘리기 > Play 게임즈 서비스 > 설정 및 관리 > 테스터 메뉴에서, 테스터를 추가할 수 있다.

 

로그인 화면 구성

Login Canvas 추가

  1. 관련 기능 화면 배치를 위한 Login Canvas 를 추가함
  2. Login Button 추가
  3. Log Message 를 출력할 Edit 추가

Login Manager Script 구현

Start 부분

// 디버그 로그 활성화 여부

PlayGamesPlatform.DebugLogEnabled = true;

// 모듈 활성화

PlayGamesPlatform.Activate();

// 로그인 수행

PlayGamesPlatform.Instance.Authenticate((SignInStatus status) => {

    if (status == SignInStatus.Success) {

string name = PlayGamesPlatform.Instance.GetUserDisplayName();

string id = PlayGamesPlatform.Instance.GetUserId();

string imageUrl = PlayGamesPlatform.Instance.GetuserImageUrl();

}

    else { … 로그인 실패 … }

});

 

Too many connection 에러 처리 방법

문제의 원인

MySQL(MariaDB) 의 기본 Connection 개수 설정을 초과하여 연결을 요청하여 발생함

현재 상태 확인

Max connections 확인

max_connections : 연결을 지속할 수 있는 최대 연결 제한량

mysql> show variables like ‘%max_connection%’;

 

Wait timeout 확인

wait_timeout : 다음 쿼리를 실행할 때 까지 해당 설정으로 지정된 시간 동안 연결을 끊지 않고 대기함

mysql> show variables like ‘wait_timeout’;

 

수정 방법

my.cnf 파일 수정을 통한 수정 방법

/etc/my.cnf 파일에 다음 구문을 추가함

 

[mysqld]

max_connections = 1024
wait_timeout = 60

 

mysql 접속을 통한 설정

mysql> set global max_connections = 1024;

mysql> set wait_timeout = 60;

 

Docker 상의 수정

Docker – Container를 통해 구동 중이라면,

  1. /etc/my.cnf 파일을 외부 volume 으로 추가한 후,
  2. 해당 파일의 권한을 필히 755로 설정해 주어야 함, 그렇지 않으면, mysql이 해당 설정을 무시하게 됨.

/etc/my.cnf 기본 설정 파일

 

# For advice on how to change settings please see
# http://dev.mysql.com/doc/refman/9.0/en/server-configuration-defaults.html

[mysqld]
#
# Remove leading # and set to the amount of RAM for the most important data
# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.
# innodb_buffer_pool_size = 128M
#ee
# Remove leading # to turn on a very important data integrity option: logging
# changes to the binary log between backups.
# log_bin
#
# Remove leading # to set options mainly useful for reporting servers.
# The server defaults are faster for transactions and fast SELECTs.
# Adjust sizes as needed, experiment to find the optimal values.
# join_buffer_size = 128M
# sort_buffer_size = 2M
# read_rnd_buffer_size = 2M

host-cache-size=0
skip-name-resolve
datadir=/var/lib/mysql
socket=/var/run/mysqld/mysqld.sock
secure-file-priv=/var/lib/mysql-files
user=mysql

pid-file=/var/run/mysqld/mysqld.pid
[client]

socket=/var/run/mysqld/mysqld.sock

!includedir /etc/mysql/conf.d/