본문 바로가기
카테고리 없음

[BalancingLibra] Logger 시스템 구현

by mazayong 2026. 1. 1.

전에는 Debug.Log로 로그를 남겼는데 Custom Logger를 구현해 커스텀 로그를 활용하는 방식으로 수정했다. 해당 방식으로 수정한 이유와 방법을 간략히 정리해 보겠다.

 

 

 

1. 로그를 남기는 이유 및 이점

2. 기존 Debug.Log 방식의 문제점

3. Custom Logger 기본 구조

4. Custom Logger 구현 및 수정

5. 작업한 코드 적용

6. 후기

7. 출처

 

 

 

 

 

1. 로그를 남기는 이유 및 이점

우선, 로그를 남기는 이유에 대해 고민해보자. 

로그를 남기는 이유는 히스토리를 기록해서 해당 이벤트의 원인, 과정, 결과를 확인하게 만든다.

근본적으로 다양한 상황에서 발생할 수 있는 에러 및 버그를 명시적으로 확인 및 원인 분석을 용이하게 하기 위함이다.

 로그를 통해 얻을 수 있는 결과는 아래와 같다.

 

1)  로직 시각화

= 유니티 에디터를 통해 결과값만 확인하는 것이 아닌 로직의 과정을 확인해 이벤트에 관련된 로직의 프로세스를 빠르게 파악할 수 있다.

 

2) 기기에 상관없이 버그 및 에러 확인 용이

= 개발환경이 아닌 콘솔, 모바일, 다른 PC 등에서도 로그를 확인해 특정 이벤트를 분석할 수 있다.

 

3) 유니티 라이프사이클 순서 확인 가능

= Start(), Update() 순서 꼬임으로 인해 발생하는 문제(매니저가 초기화되기 전 플레이어가 데이터 요구) 등을 확인 후 해결할 수 있다.

 

4) 게임 이벤트 결과에 대한 원인 파악 용이

= 플레이어 체력이 0이 된 것과 같이 게임 내 발생하는 이벤트 결과에 대한 과정, 원인을 분석할 때 로그를 통해 히스토리를 확인해 해결가능하다.

 

 

 

 

 

2. 기존 Debug.Log 방식의 문제점

기존에는 Debug.Log를 원하는 곳에 작성하고, 이를 배포할 때 빌드에서 지우거나 필요한 것만 남기는 방식이었다. 

이 방식의 문제점에 대해 알아보자.

 

1) CPU 사용량 증가 및 GC 발생

Debug.Log는 해당 로그가 발생한 위치를 출력하기 위해 현재 실행중인 모든 호출 스택(Stack Trace) 정보를 수집한다.

=> 메인 스레드에서 진행되기 때문에 프레임 드랍의 주 원인이 된다.

 

Debug.Log에 문자열 또는 다른 값 타입을 출력할 경우를 살펴보자.

= string은 매번 새로운 문자열 인스턴스를 생성하고, int같은 값 타입은 로그에서 object로 변환되는 박싱(boxing)이 발생하여 힙 메모리를 할당한다.

=> GC가 자주 실행되게 만들어 게임이 순간적으로 멈추는 히컵(Hiccup) 현상을 유발한다.

= 로그는 콘솔 뿐만 아니라 디바이스의 파일 시스템에도 기록되어 I/O 병목이 발생할 수 있다.

 

2) 불필요한 빌드 파일 용량 증가

Debug.Log가 참조하는 문자열이나 객체들은 컴파일러가 사용중이라고 판단하여 빌드에서 제거되지 않아 앱 용량이 증가된다.

 

3) 멀티 스레드 이슈

Unity의 Debug API는 기본적으로 메인 스레드 친화적이다.

=> 멀티 스레드 환경에서 빈번하게 호출하면 Lock 경합이 발생하거나 스레드 안전성 문제가 발생할 수 있다.

 

 

 

 

3. Custom Logger 기본 구조

Custom Logger에 필요한 기본 조건은 아래와 같다.

1) 배포 빌드 시 로그 정리 용이 및 성능 최적화

= [Conditional]을 추가해 컴파일 단계에서 해당 함수의 호출을 제외시켜 오버헤드를 0으로 만든다.

= 게임이 출시될 때 개발용 로그 제외시키기 용이하다.

 

2) 로그 분류

= 기본 로그, 경고성 로그, 에러용 로그를 나눠서 적용할 수 있도록 구현한다.

 

3) Logger을 통한 중앙 집중 관리 용이

= 원하는 지점에 로그를 추가할 때 Logger 내 함수를 적용하도록 한다.

= 수정 및 개선이 필요할 때 일괄적으로 적용되어야 한다.

 

 

 

기본 뼈대는 아래와 같다.

using UnityEngine;
using System.Collections.Generic;
using System.Collections;
using System.Diagnostics;

public static class Logger
{

    [Conditional("DEV_VER")]
    public static void Log(string msg)
    {
        UnityEngine.Debug.LogFormat("{0} {1}", System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), msg);
    }

    [Conditional("DEV_VER")]
    public static void LogWarning(string msg)
    {
        UnityEngine.Debug.LogWarningFormat("{0} {1}", System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), msg);
    }

    public static void LogError(string msg)
    {
        UnityEngine.Debug.LogErrorFormat("{0} {1}", System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), msg);
    }
}

= [Contidional]을 추가해 빌드 파일을 만들 때에는 개발용 로그가 제외되도록 설정했다.

= Log, LogWarning, LogError을 통해 각 상황별 로그를 다르게 사용하도록 구현했다.

= 로그는 무조건 Logger클래스 내부의 함수들을 통해 구현하기 위해 static을 적용했다.

 

 

4. Custom Logger 구현 및 수정

3에서 구현한 코드를 수정해보자. 문제는 아래와 같다.

 

1) 컨텍스트를 사용하지 않음.

찾아보니 Debug.Log의 가장 강한 기능 중 하나는 로그를 더블 클릭시 하이어라키의 해당 오브젝트를 핑해주는 기능이다.

현재 작성한 코드는 msg만 받아 실행한다.

=> 해당 커스텀 로거만 사용할 경우, 어떤 오브젝트에서 에러가 났는지 판단하기 어렵다.

 

2) LogFormat에서의 문자열 연산 비용 증가

둘 다 값 타입을 이용할 경우의 문자열로 변환 과정을 비교해 보자.

LogFormat의 경우, 박싱 -> 배열 할당 -> 문자열 생성으로 인해 추가 오버헤드가 생긴다.

String Interpolation은 박싱 -> 문자열 생성으로 인해 LogFormat보다 오버헤드가 적다.

=> 그러므로 LogFormat이 아닌 String Interpolation으로 수정했다.

(++ StringBuilder와 ZString은 런타인 할당을 줄이기 위해 사용되지만, 이미 [Conditional]을 사용했으므로 굳이 사용할 필요가 없다. 로그를 서버로 전송해야 해서 DEV_VER 없이 실제로 로그를 남겨야 하는 경우에는 ZString을 사용해야 한다.)

 

3) LogError은 빌드 파일에서 제외하기 어려움

LogError은 중요한 코드여서 [Conditional]을 붙여서 빌드 파일에서 제외하기 어렵다.

=> 라이브 서비스 중 에러가 발생할 경우, 그때마다 ToString() 함수가 실행되어 CPU 사용량이 증가하게 된다.

 

 

 

 

수정한 코드는 아래와 같다.

using UnityEngine;
using System.Collections.Generic;
using System.Collections;
using System.Diagnostics;

//출시용 빌드를 위해 로그 제거
public static class Logger
{

    [Conditional("DEV_VER")]
    public static void Log(object message, Object context = null)
    {
        if (context != null)
            UnityEngine.Debug.Log($"[{System.DateTime.Now:HH:mm:ss.fff}] {message}", context);
        else
            UnityEngine.Debug.Log($"[{System.DateTime.Now:HH:mm:ss.fff}] {message}");
    }

    [Conditional("DEV_VER")]
    public static void LogWarning(object message, Object context = null)
    {
        if (context != null)
            UnityEngine.Debug.LogWarning($"[{System.DateTime.Now:HH:mm:ss.fff}] {message}", context);
        else
            UnityEngine.Debug.LogWarning($"[{System.DateTime.Now:HH:mm:ss.fff}] {message}");
    }

    public static void LogError(object message, Object context = null)
    {
        string logMsg = $"[{System.DateTime.Now:HH:mm:ss.fff}] {message}";
        
        if (context != null)
            UnityEngine.Debug.LogError(logMsg, context);
        else
            UnityEngine.Debug.LogError(logMsg);
    }
}

= String Interpolation을 LogFormat 대신 사용해서 0000

= Object context를 추가해 로그를 찍을 때 this를 넘기면, 콘솔 클릭 시 로그에 대한 오브젝트를 확인할 수 있다.

= 매개변수 타입을 object로 변경해 이전에 값 타입을 넣었을 때 박싱될 수 있는 가능성을 없앴다.

= 릴리즈 빌드에서는 LogError의 시간 부분을 빼야 한다.

 

 

 

5. 작업한 코드 적용

해당 커스텀 로거를 적용하면 아래와 같다.

 

//원하는 로그 메세지와 함께 로그를 찍을 경우
Logger.LogError("Main Camera not found!", this);

//해당 이벤트의 원인을 로그로 출력할 경우
Logger.Log(reason, this);

 

 

 

 

6. 후기

프로젝트들을 진행하면서 로그 시스템을 정비하고 싶었는데, 드디어 리팩토링을 해내서 뿌듯하다. 

현재는 클라이언트단에서만 로그를 작성하는데, 서버단에서도 로그를 제대로 전달할 수 있도록 수정하도록 프로젝트를 잘 발전시켜야겠다.

 

 

 

7. 출처

https://www.yourselfirst.com/personality?utm_source=google&utm_medium=cpc&utm_campaign=REST_Personality_Type&utm_term=gallup&utm_ad_id=762737621208&utm_adset_id=182733933552&utm_campaign_id=22772755612&utm_adset_name=REST_Personality_Type&utm_campaign_name=REST_Personality_Type&gclid=CjwKCAiAmePKBhAfEiwAU3Ko3NLEDZbNhDnvVwrrqZ2cvu9294RMUTTBKq2U6x68VhsJ7ThjB8odzRoCGtUQAvD_BwE&gbraid=0AAAAA-F0voxvn3BRDCrmC3pNpX0V35ruc&uuid=c55d102c-7cc3-4ecd-af4f-71b7e6b750e5