icon안동민 개발노트

기본 디버깅 기법


 언리얼 엔진의 C++ 프로젝트에서 효과적인 디버깅은 개발 과정의 핵심입니다.

 이 절에서는 기본적인 디버깅 기법과 도구 사용법을 살펴보겠습니다.

Visual Studio 디버거와 언리얼 엔진 에디터 통합

 Visual Studio와 언리얼 엔진 에디터를 함께 사용하면 강력한 디버깅 환경을 구축할 수 있습니다.

  1. 언리얼 프로젝트를 Visual Studio에서 열기
  2. 디버그 모드로 에디터 실행: Debug > Start Debugging 또는 F5 키

브레이크포인트 설정 및 활용

 브레이크포인트를 사용하여 코드 실행을 특정 지점에서 멈출 수 있습니다.

void AMyActor::PerformComplexCalculation()
{
    // 브레이크포인트 설정
    int32 Result = 0;
    for (int32 i = 0; i < 100; ++i)
    {
        Result += i; // 여기에 브레이크포인트 설정
    }
    UE_LOG(LogTemp, Log, TEXT("Result: %d"), Result);
}

 브레이크포인트에 조건을 추가할 수도 있습니다.

  • 우클릭 > 조건 > i == 50을 입력하면 i가 50일 때만 멈춤

워치 윈도우 사용법

 워치 윈도우를 사용하여 변수 값을 모니터링할 수 있습니다.

  1. 디버그 중 Debug > Windows > Watch > Watch 1 열기
  2. 변수나 표현식 입력 (예 : Result, this->GetActorLocation())

콜 스택 분석

 콜 스택을 통해 현재 실행 지점까지의 함수 호출 경로를 확인할 수 있습니다.

  1. 브레이크포인트에서 멈췄을 때 Debug > Windows > Call Stack 열기
  2. 각 함수 호출 단계 확인 및 필요시 해당 프레임으로 이동

언리얼 엔진의 로깅 시스템 활용

 언리얼 엔진의 내장 로깅 시스템을 사용하여 중요 정보를 출력할 수 있습니다.

// 로그 카테고리 정의
DEFINE_LOG_CATEGORY_STATIC(MyLogCategory, Log, All);
 
void AMyActor::SomeFunction()
{
    UE_LOG(MyLogCategory, Log, TEXT("SomeFunction called"));
    UE_LOG(MyLogCategory, Warning, TEXT("Warning: %s"), *SomeString);
    UE_LOG(MyLogCategory, Error, TEXT("Error occurred: %d"), ErrorCode);
}

인게임 디버그 드로잉

 시각적 디버깅을 위해 인게임에서 직접 디버그 정보를 그릴 수 있습니다.

void AMyActor::DebugDrawLocation()
{
    FVector Location = GetActorLocation();
    DrawDebugSphere(GetWorld(), Location, 50.0f, 12, FColor::Red, false, 5.0f);
    DrawDebugString(GetWorld(), Location + FVector(0, 0, 100), GetName(), nullptr, FColor::White, 0.0f);
}

메모리 프로파일링 기초

 언리얼 엔진의 내장 메모리 프로파일러를 사용하여 메모리 사용량을 분석할 수 있습니다.

  1. 에디터에서 Window > Developer Tools > Memory Profiler 열기
  2. 'Take Snapshot' 버튼 클릭하여 현재 메모리 상태 캡처
  3. 메모리 사용량이 많은 객체나 자산 식별

멀티스레드 코드 디버깅 전략

 멀티스레드 코드 디버깅은 복잡할 수 있지만, 다음 전략을 사용할 수 있습니다.

  1. 스레드별 브레이크포인트 설정
  2. 병렬 스택 뷰 사용 : Debug > Windows > Parallel Stacks
  3. 크리티컬 섹션에 로그 추가
FCriticalSection CriticalSection;
 
void AMyActor::ThreadSafeFunction()
{
    FScopeLock Lock(&CriticalSection);
    UE_LOG(LogTemp, Log, TEXT("Entered critical section"));
    // 스레드 안전 코드
    UE_LOG(LogTemp, Log, TEXT("Exiting critical section"));
}

네트워크 게임에서의 디버깅 특수성

 네트워크 게임 디버깅을 위한 특별한 고려사항

  1. 서버와 클라이언트 동시 디버깅 : 여러 인스턴스 실행
  2. 네트워크 역할별 로그 추가
void AMyNetworkedActor::SomeNetworkedFunction()
{
    if (GetNetMode() == NM_Client)
    {
        UE_LOG(LogTemp, Log, TEXT("Client: SomeNetworkedFunction"));
    }
    else if (GetNetMode() == NM_DedicatedServer)
    {
        UE_LOG(LogTemp, Log, TEXT("Server: SomeNetworkedFunction"));
    }
}
  1. 네트워크 프로파일러 사용: Window > Developer Tools > Network Profiler

크래시 덤프 분석

 크래시가 발생했을 때 덤프 파일을 분석하는 방법

  1. 크래시 덤프 파일 찾기 : 보통 프로젝트의 Saved / Crashes 폴더에 위치
  2. Visual Studio에서 덤프 파일 열기
  3. 콜 스택 확인 및 문제의 근원 추적

효율적인 디버깅을 위한 코드 구조화

 디버깅하기 쉬운 코드를 작성하는 팁

  1. 단일 책임 원칙 준수 : 각 함수와 클래스는 하나의 책임만 가지도록 설계
  2. 적절한 추상화 레벨 유지
  3. 명확한 에러 처리 및 로깅 추가

 예시:

UCLASS()
class MYGAME_API UMySubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()
 
public:
    UFUNCTION(BlueprintCallable, Category = "MySubsystem")
    bool PerformComplexOperation(const FString& Input, FString& Output);
 
private:
    bool ValidateInput(const FString& Input);
    bool ProcessData(const FString& Input, FString& IntermediateResult);
    bool FormatOutput(const FString& IntermediateResult, FString& Output);
};
 
bool UMySubsystem::PerformComplexOperation(const FString& Input, FString& Output)
{
    if (!ValidateInput(Input))
    {
        UE_LOG(LogTemp, Error, TEXT("Invalid input: %s"), *Input);
        return false;
    }
 
    FString IntermediateResult;
    if (!ProcessData(Input, IntermediateResult))
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to process data"));
        return false;
    }
 
    if (!FormatOutput(IntermediateResult, Output))
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to format output"));
        return false;
    }
 
    return true;
}

일반적인 C++ 버그 패턴 및 해결 방법

  1. 널 포인터 역참조
  • 해결 : 항상 포인터 유효성 검사
if (MyPointer != nullptr)
{
    MyPointer->SomeFunction();
}
  1. 배열 범위 초과
  • 해결 : 범위 검사 및 컨테이너 사용
TArray<int32> MyArray;
if (MyArray.IsValidIndex(Index))
{
    int32 Value = MyArray[Index];
}
  1. 메모리 누수
  • 해결 : 스마트 포인터 사용
TSharedPtr<UObject> MySharedObject = MakeShared<UObject>();
  1. 경쟁 조건
  • 해결 : 적절한 동기화 메커니즘 사용
FCriticalSection CriticalSection;
{
    FScopeLock Lock(&CriticalSection);
    // 스레드 안전 코드
}

 효과적인 디버깅은 개발 과정의 핵심 부분입니다. Visual Studio 디버거와 언리얼 엔진의 내장 도구를 결합하여 사용하면, 복잡한 문제도 효율적으로 해결할 수 있습니다. 브레이크포인트, 워치 윈도우, 콜 스택 분석 등의 기본 기능을 마스터하고, 언리얼 엔진의 로깅 시스템과 디버그 드로잉 기능을 활용하세요.

 멀티스레드와 네트워크 게임 디버깅은 추가적인 복잡성을 가지므로, 특별한 주의가 필요합니다. 적절한 로깅과 동기화 메커니즘을 사용하여 이러한 복잡한 시나리오를 처리하세요.

 크래시 덤프 분석은 예기치 못한 문제를 해결하는 데 중요한 도구입니다. 정기적으로 크래시 리포트를 검토하고 분석하는 습관을 들이세요.

 마지막으로, 효율적인 디버깅을 위해서는 처음부터 잘 구조화된 코드를 작성하는 것이 중요합니다. 단일 책임 원칙을 준수하고, 적절한 추상화 레벨을 유지하며, 명확한 에러 처리와 로깅을 추가하세요. 이러한 실천을 통해 버그를 예방하고, 발생했을 때 빠르게 식별하고 해결할 수 있습니다.