일정표
| 시간 | 할 일 | 비고 |
| 08:00~10:00 | 코드카타, 디자인 패턴 | |
| 10:00~13:00 | 개인 프로젝트 | |
| 14:00~18:00 | 2시 스크럼, 팀프로젝트 | |
| 19:00~21:00 | 저녁 스크럼, 팀 프로젝트 | |
| 21:00~22:00 | 운동 | |
| 22:00~23:30 | 개인 프로젝트 |
*오늘의 코드카타*
문제. 체육복
점심시간에 도둑이 들어, 일부 학생이 체육복을 도난당했습니다. 다행히 여벌 체육복이 있는 학생이 이들에게 체육복을 빌려주려 합니다. 학생들의 번호는 체격 순으로 매겨져 있어, 바로 앞번호의 학생이나 바로 뒷번호의 학생에게만 체육복을 빌려줄 수 있습니다. 예를 들어, 4번 학생은 3번 학생이나 5번 학생에게만 체육복을 빌려줄 수 있습니다. 체육복이 없으면 수업을 들을 수 없기 때문에 체육복을 적절히 빌려 최대한 많은 학생이 체육수업을 들어야 합니다.
전체 학생의 수 n, 체육복을 도난당한 학생들의 번호가 담긴 배열 lost, 여벌의 체육복을 가져온 학생들의 번호가 담긴 배열 reserve가 매개변수로 주어질 때, 체육수업을 들을 수 있는 학생의 최댓값을 return 하도록 solution 함수를 작성해주세요.
제한사항
전체 학생의 수는 2명 이상 30명 이하입니다.
체육복을 도난당한 학생의 수는 1명 이상 n명 이하이고 중복되는 번호는 없습니다.
여벌의 체육복을 가져온 학생의 수는 1명 이상 n명 이하이고 중복되는 번호는 없습니다.
여벌 체육복이 있는 학생만 다른 학생에게 체육복을 빌려줄 수 있습니다.
여벌 체육복을 가져온 학생이 체육복을 도난당했을 수 있습니다. 이때 이 학생은 체육복을 하나만 도난당했다고 가정하며, 남은 체육복이 하나이기에 다른 학생에게는 체육복을 빌려줄 수 없습니다.
#include <vector>
using namespace std;
int solution(int n, vector<int> lost, vector<int> reserve) {
vector<int> suits(n+2, 1);
for (int l : lost)
{
suits[l]--;
}
for (int r : reserve)
{
suits[r]++;
}
for (int i = 1; i <= n; i++)
{
if (suits[i] == 0)
{
if (suits[i-1] == 2)
{
suits[i - 1]--;
suits[i]++;
}
else if (suits[i + 1] == 2)
{
suits[i + 1]--;
suits[i]++;
}
}
}
int answer = 0;
for (int i = 1; i <= n; i++)
{
if (suits[i] >= 1)
{
answer++;
}
}
return answer;
}
*디자인 패턴*
주제 : Case 2 : 고성능 액션 게임을 위한 프레임 단위 액션 버퍼링 & 콤보 시스템
여러 명작 게임들의 '손맛(입력 반응성)'의 비밀을 알아보자. 플레이어가 전 프레임의 공격 애니메이션이 끝나기도 전에 미리 누른 버튼을 기억해 두었다가, 다음 프레임에 부드럽게 연계기를 이어 나가게 만드는 핵심 아키텍처를 알아보는 시간이다(선입력 버퍼링과 콤보).
1. C++ 개념 및 코드
C++에서 입력 버퍼링과 콤보 시스템은 '커맨드 패턴'과 '유한 상태 기계(FSM)'의 결합으로 구현된다.
입력 이벤트가 발생했을 때 즉시 플레이어의 행동 함수를 실행하는 대신, 입력 자체를 '커맨드 객체(명령어)'로 캡슐화하여 큐(Queue)에 적재한다. 그리고 캐릭터의 현재 상태를 관리하는 FSM이 매 프레임 이 큐를 확인하여, 현재 행동을 취할 수 있는 상태(예: Idle)일 때 최전방의 명령을 꺼내어 실행하는 구조이다.
이때 큐에 담긴 명령은 영원히 유지되는 것이 아니라, 격투/액션 게임의 특성에 맞게 '밀리초(ms) 단위의 유효 시간(TTL: Time To Live)'이 지나면 자동으로 소멸하는 버퍼링 메커니즘을 가진다.
#include <iostream>
#include <queue>
#include <memory>
// 1. 입력 명령의 종류 정의
enum class EInputOrder { LightAttack, HeavyAttack, Dodge };
// 2. 명령어(Command) 구조체: 유효 시간(TTL) 개념 포함
struct ActionCommand {
EInputOrder Order;
float TimeToLive; // 이 명령이 버퍼 안에서 살아있을 시간 (초 단위)
};
// 3. 캐릭터의 가상 FSM 상태
enum class ECharacterState { Idle, Attacking, Dodging };
// 4. 순수 C++ 입력 버퍼링 시스템
class InputBufferQueue {
private:
std::queue<ActionCommand> Buffer;
const float MAX_BUFFER_TIME = 0.3f; // 300ms 선입력 유효 시간
public:
// 사용자가 버튼을 누르면 버퍼 큐에 적재
void PushCommand(EInputOrder NewOrder) {
Buffer.push({ NewOrder, MAX_BUFFER_TIME });
std::cout << "[입력 수집] 새로운 명령 버퍼 진입 (유효시간 300ms)\n";
}
// 매 프레임 유효시간을 차감하고, 만료된 명령은 버림
void UpdateBuffer(float DeltaTime, ECharacterState CurrentState) {
// 1. 오래된 선입력 만료 처리
if (!Buffer.empty()) {
Buffer.front().TimeToLive -= DeltaTime;
if (Buffer.front().TimeToLive <= 0.0f) {
Buffer.pop();
std::cout << "[버퍼 만료] 너무 오래된 선입력이 소멸했습니다.\n";
}
}
// 2. 캐릭터가 행동 가능한 상태(Idle)일 때 버퍼의 명령을 실행
if (CurrentState == ECharacterState::Idle && !Buffer.empty()) {
ActionCommand ExecutingCmd = Buffer.front();
Buffer.pop(); // 큐에서 해제
std::cout << "[명령 실행] 버퍼링된 명령을 꺼내어 실행합니다: ";
switch (ExecutingCmd.Order) {
case EInputOrder::LightAttack: std::cout << "약공격 발동\n"; break;
case EInputOrder::HeavyAttack: std::cout << "강공격 발동\n"; break;
case EInputOrder::Dodge: std::cout << "회피 발동\n"; break;
}
}
}
};
2. 언리얼 C++ 적용
언리얼 엔진 5(UE5) 환경에서 상업용 수준의 액션 게임으로 끌어올리려면 다음과 같은 엔진 고유 시스템과의 융합 및 최적화가 필수적이다.
가비지 컬렉션(GC) 및 구조체 최적화: 초당 수십 번 쌓이고 사라지는 입력 명령들을 매번 new나 UObject 포인터로 동적 할당하면 언리얼 GC에 엄청난 과부하(GC Spike)가 발생하게 된다. 실무에서는 무조건 값 타입(Value Type)인 USTRUCT 기반 고정형 배열(TArray)을 커맨드 큐로 사용하여 가상 메모리 단에서 렉을 원천 차단한다.
향상된 입력(Enhanced Input) 연계: UE5의 향상된 입력 시스템은 자체적으로 입력의 지속성이나 압력을 측정하므로, C++ 버퍼 컴포넌트는 오직 "어떤 UInputAction이 언제 들어왔는가"의 메타데이터만 래핑하여 큐에 쌓는 역할을 전담해야 한다.
애니메이션 프레임과의 동기화 (AnimNotifyState): 액션 게임에서 콤보 시스템의 성패는 애니메이션과의 완벽한 결합에 있다. 1타 공격 모션이 실행되는 도중, 아무 때나 다음 공격이 나간다면 모션이 괴상하게 찢어지게 된다. 따라서 프로그래머는 UAnimNotifyState를 커스텀 구현하여 애니메이션 타임라인 상에 "선입력을 허용하는 구간(Buffer Window)"과 "실제 연계 콤보가 발동되는 구간(Combo Execution Window)"을 프레임 단위로 기획자가 제어할 수 있도록 판을 깔아주는 역할을 한다.
3. 언리얼 C++ 실무 코드
에디터에서 애니메이션 에셋을 보며 마우스 드래그로 콤보/선입력 타이밍을 밀리초 단위로 제어할 수 있는, 극도로 디커플링된 상업용 액션 버퍼 시스템을 구현해보자.
(사례 : ActionBufferComponent.h, cpp)
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "GameplayTagContainer.h"
#include "ActionBufferComponent.generated.h"
// 1. 가볍고 명확한 명령 구분을 위해 언리얼 고유의 GameplayTag 시스템 활용
// 예: Action.Command.LightAttack, Action.Command.Dodge 등
USTRUCT(BlueprintType)
struct FBufferedCommand
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "ActionBuffer")
FGameplayTag CommandTag;
// 명령이 적재된 런타임 게임 시간 (GetWorld()->GetTimeSeconds())
UPROPERTY(BlueprintReadOnly, Category = "ActionBuffer")
float Timestamp = 0.0f;
FBufferedCommand() {}
FBufferedCommand(FGameplayTag InTag, float InTime) : CommandTag(InTag), Timestamp(InTime) {}
};
// 2. 캐릭터의 대기, 공격, 회피 등을 엄격하게 관리할 FSM 구조체
UENUM(BlueprintType)
enum class ECharacterActionState : uint8
{
Idle,
Attacking,
Dodging,
Stunned
};
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class UNREALDESIGNPATTERN_API UActionBufferComponent : public UActorComponent
{
GENERATED_BODY()
public:
UActionBufferComponent();
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
// 사용자가 버튼을 누르면 캐릭터 본체(Enhanced Input)에서 호출할 창구
void BufferInputOrder(FGameplayTag InputTag);
// 애니메이션 노티파이가 열어주고 닫아줄 상태 제어 함수들
void SetBufferWindowOpen(bool bIsOpen) { bIsBufferWindowOpen = bIsOpen; }
void SetComboExecutionWindowOpen(bool bIsOpen);
// 현재 FSM 상태 제어
void SetActionState(ECharacterActionState NewState) { CurrentActionState = NewState; }
ECharacterActionState GetActionState() const { return CurrentActionState; }
// 현재 콤보 진행도를 추적할 변수 (C++는 콤보가 몇 타째인지만 기억하고 모션 분기는 컴포넌트가 제어)
UPROPERTY(BlueprintReadWrite, Category = "ActionBuffer|Combo")
int32 CurrentComboCount = 0;
private:
// 선입력 유효 기간 제한 (300ms = 0.3초)
const float COMMAND_TTL = 0.3f;
// GC 부하가 전혀 없는 순수 값 타입 고정배열 커맨드 큐 [최적화 핵심]
TArray<FBufferedCommand> CommandQueue;
ECharacterActionState CurrentActionState = ECharacterActionState::Idle;
bool bIsBufferWindowOpen = false;
bool bIsComboExecutionWindowOpen = false;
// 만료된 선입력들을 청소하는 내부 함수
void CleanExpiredCommands();
// 버퍼링된 명령 중 조건에 맞는 액션이 있다면 즉시 꺼내어 캐릭터에게 실행 명령을 내림
void TryConsumeBufferedAction();
};
#include "ActionBufferComponent.h"
#include "GameFramework/Character.h"
UActionBufferComponent::UActionBufferComponent()
{
PrimaryComponentTick.bCanEverTick = true;
}
void UActionBufferComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// 1. 매 프레임 너무 오래되어 상해버린 선입력 명령 자동 파기
CleanExpiredCommands();
// 2. 캐릭터가 Idle 상태이거나 콤보 연계 창이 열려있다면 선입력 소비 시도
if (CurrentActionState == ECharacterActionState::Idle || bIsComboExecutionWindowOpen)
{
TryConsumeBufferedAction();
}
}
void UActionBufferComponent::BufferInputOrder(FGameplayTag InputTag)
{
// 캐릭터가 스턴 상태라면 버퍼링 자체를 막아 조작 불가능하게 처리
if (CurrentActionState == ECharacterActionState::Stunned) return;
// 기획상 공격 도중에만 선입력 버퍼를 채우고 싶다면 조건문 추가 가능
// 여기서는 언제든 입력 시 300ms 동안은 버퍼에 기억되도록 구현
float CurrentTime = GetWorld()->GetTimeSeconds();
CommandQueue.Add(FBufferedCommand(InputTag, CurrentTime));
}
void UActionBufferComponent::CleanExpiredCommands()
{
if (CommandQueue.Num() == 0) return;
float CurrentTime = GetWorld()->GetTimeSeconds();
// 역순 순회를 통한 안전하고 빠른 만료 원소 삭제 (RemoveAtSwap 최적화)
for (int32 i = CommandQueue.Num() - 1; i >= 0; --i)
{
if (CurrentTime - CommandQueue[i].Timestamp > COMMAND_TTL)
{
CommandQueue.RemoveAtSwap(i);
}
}
}
void UActionBufferComponent::SetComboExecutionWindowOpen(bool bIsOpen)
{
bIsComboExecutionWindowOpen = bIsOpen;
// 애니메이션이 흐르다가 콤보 창이 딱 열리는 '그 프레임'에 선입력된 공격이 있다면 즉시 터트림
if (bIsComboExecutionWindowOpen)
{
TryConsumeBufferedAction();
}
}
void UActionBufferComponent::TryConsumeBufferedAction()
{
if (CommandQueue.Num() == 0) return;
// 가장 먼저 들어온 명령(최전방) 확인
FBufferedCommand NextCmd = CommandQueue[0];
ACharacter* OwnerCharacter = Cast<ACharacter>(GetOwner());
if (!OwnerCharacter) return;
// [커맨드 + FSM의 융합]
// 캐릭터 상태와 명령 종류에 따른 최종 분기 처리 규칙 적용
if (NextCmd.CommandTag.MatchesTagExact(FGameplayTag::RequestGameplayTag(TEXT("Action.Command.Dodge"))))
{
// 회피는 공격 콤보 창과 상관없이 무조건 최우선으로 버퍼에서 꺼내어 즉시 실행 (캔슬 구르기)
CommandQueue.RemoveAt(0);
CurrentActionState = ECharacterActionState::Dodging;
bIsComboExecutionWindowOpen = false; // 콤보 창 강제 폐쇄
// 캐릭터 본체의 블루프린트 이벤트나 C++ 회피 함수 구동 위임
// OwnerCharacter->PlayDodgeMontage();
return;
}
if (bIsComboExecutionWindowOpen && NextCmd.CommandTag.MatchesTagExact(FGameplayTag::RequestGameplayTag(TEXT("Action.Command.LightAttack"))))
{
// 콤보 타이밍이 열려있고 다음 명령이 약공격이라면 콤보 카운트를 올리고 큐에서 소비
CommandQueue.RemoveAt(0);
CurrentComboCount++; // 1타 -> 2타 -> 3타 빌드업
bIsComboExecutionWindowOpen = false; // 연계가 시작되었으므로 중복 발동 차단하기 위해 닫음
CurrentActionState = ECharacterActionState::Attacking;
// 인터페이스나 전용 함수를 통해 캐릭터에게 알맞은 공격 애니메이션(몽타주) 재생 명령 하달
// OwnerCharacter->PlayAttackComboMontage(CurrentComboCount);
GE_ENGINE_LOG(LogTemp, Log, TEXT("[Combo Link] %d 타 연계 공격 몽타주가 부드럽게 발동합니다!"), CurrentComboCount);
}
}
이제 기획자와 애니메이터가 에디터에서 공격 모션을 보며 "이 타격 직후 15프레임 동안만 다음 선입력을 허용하고 연계하기"를 지정할 수 있는 타임라인 플러그인을 구현해보자.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotifyState.h"
#include "ActionBufferComponent.h"
#include "ANS_ComboWindow.generated.h"
/**
* 공격 애니메이션 몽타주 내부에 드래그로 배치할 콤보 타이밍 노티파이 상태 클래스
*/
UCLASS()
class UNREALDESIGNPATTERN_API UANS_ComboWindow : public UAnimNotifyState
{
GENERATED_BODY()
public:
// 노티파이 바(Bar)가 시작되는 시점에 C++ 컴포넌트의 창구를 열어줍니다.
virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration, const FAnimNotifyEventReference& EventReference) override
{
if (MeshComp && MeshComp->GetOwner())
{
if (auto* BufferComp = MeshComp->GetOwner()->FindComponentByClass<UActionBufferComponent>())
{
BufferComp->SetComboExecutionWindowOpen(true);
BufferComp->SetBufferWindowOpen(true);
}
}
}
// 노티파이 바가 완전히 끝나는 시점까지 플레이어가 연계 버튼을 안 누르면 창구를 닫고 콤보를 끊습니다.
virtual void NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override
{
if (MeshComp && MeshComp->GetOwner())
{
if (auto* BufferComp = MeshComp->GetOwner()->FindComponentByClass<UActionBufferComponent>())
{
BufferComp->SetComboExecutionWindowOpen(false);
BufferComp->SetBufferWindowOpen(false);
// 이 노티파이 구간이 끝날 때까지 공격 연계가 안 일어났다면,
// 공격이 완전히 끝나 후딜레이(Recovery)로 돌아갔음을 의미하므로 콤보 카운트 리셋 및 Idle 복귀
if (BufferComp->GetActionState() == ECharacterActionState::Attacking)
{
BufferComp->CurrentComboCount = 0;
BufferComp->SetActionState(ECharacterActionState::Idle);
}
}
}
}
};
<아키텍처의 핵심 디테일 포인트>
밀리초 단위 유효성 검증 (CleanExpiredCommands): 플레이어가 보스의 광역 공격을 보고 당황해서 약공격 버튼을 난타하다가 마지막에 회피(구르기)를 눌렀다고 가정해 보자. 만약 선입력 큐의 유효 시간 제한이 없다면, 보스의 공격이 끝나고 경직이 풀리는 순간 옛날에 눌렀던 약공격들이 순서대로 다 발동되다가 맞아 죽는 '조작감 대참사'가 일어난다. 이 코드의 COMMAND_TTL 제어 덕분에 딱 최근 300ms 이내에 내린 명확한 의사결정만 인정된다.
RemoveAtSwap이 아닌 RemoveAt(0)의 이유: 일반적인 오브젝트 풀이나 정렬이 상관없는 격자 바구니에서는 성능을 위해 RemoveAtSwap을 사용하는 것이 좋지만, 입력 버퍼링은 무조건 '먼저 누른 버튼이 먼저 나가야 하는 선입선출(FIFO) 구조'를 지켜야 하므로 인덱스 순서 유지를 위해 RemoveAt(0)을 사용하는 것이다. 대개 버퍼에 쌓이는 인풋은 많아야 2~3개 이므로 인덱스 밀림 오버헤드는 무시해도 될 만큼 미미하다.
4. 추가 내용 : 기획자가 에디터에서 해야 하는 작업
1인 개발이 목표라면 결국 프로그래머 + 기획자라는 1인 2역의 모자를 갈아쓰는 타이밍을 아는 것이 효율적이고 정확한 작업을 위한 무기가 된다.
프로그래머로서 C++ 핵심 뼈대(UActionBufferComponent, UANS_ComboWindow)를 빌드업해 두었다면, "기획자/애니메이터"의 역할로 언리얼 에디터 내부에서 오직 마우스 드래그와 세팅만으로 게임의 '손맛'을 튜닝하는 작업을 진행하게 된다.
<Developer가 에디터에서 해야 하는 5단계 작업>
미리 짜놓은 데이터 주도형 시스템 덕분에, C++ 코드를 단 한 줄도 건드리지 않고 아래의 에디터 작업만으로 타격감과 콤보 체인을 완성하게 된다.
[1단계: 커맨드 태그 등록 (프로젝트 세팅)]
가장 먼저 어떤 버튼 조작(커맨드)이 존재할지 이름표를 붙여야 한다.
작업: 편집(Edit) -> 프로젝트 세팅(Project Settings) -> Gameplay Tags 작성.
행동: 새 태그 추가 버튼을 눌러 C++ 시스템과 약속한 태그들을 등록한다.
예: Action.Command.LightAttack (약공격)
예: Action.Command.HeavyAttack (강공격)
예: Action.Command.Dodge (회피)
[2단계: 향상된 입력(Enhanced Input) 자산 바인딩]
마우스 클릭이나 키보드 키를 방금 만든 태그와 매핑하는 과정.
작업: 콘텐츠 브라우저에서 우클릭하여 입력(Input) -> 입력 액션(Input Action)을 생성한다 (IA_LightAttack).
행동: 플레이어의 입력 매핑 컨텍스트(IMC_Default) 에셋을 열고, IA_LightAttack에 마우스 좌클릭(Left Mouse Button)을 매핑한다.
이후 캐릭터 블루프린트에서 이 입력이 들어올 때 C++ 컴포넌트의 BufferInputOrder(Action.Command.LightAttack) 함수로 태그만 토스해 주도록 노드 하나만 연결해 둔다
[3단계: 애니메이션 몽타주(Anim Montage) 섹션 분할]
공격 애니메이션 에셋들을 연속해서 매끄럽게 재생할 수 있도록 편집하는 핵심 단계.
작업: 공격 애니메이션 시퀀스들을 모아 우클릭 후 애니메이션 몽타주 생성을 한다.
행동: 몽타주 에디터의 타임라인 영역에서 우클릭하여 섹션(Section)을 나눈다.
Combo1 섹션: 1타 휘두르기 모션 배치
Combo2 섹션: 2타 횡베기 모션 배치
Combo3 섹션: 3타 내려찍기 모션 배치
핵심 타임라인 설정: 각 콤보 섹션 뒤에는 버튼을 누르지 않았을 때 자연스럽게 기본 대기 상태로 돌아가는 후딜레이 모션(End1, End2) 섹션을 이어서 배치하고 섹션 링크를 끊어준다.
[4단계: 타임라인 작업 - ANS_ComboWindow 배치]
액션 게임의 '손맛'과 '반응성'을 밀리초 단위로 제어하는 콤보 시스템의 렌더링 심장부 작업이다.
작업: 몽타주 에디터의 Notifies 트랙을 확인한다.
행동: 트랙 빈 곳에서 우클릭 후 노티파이 상태 추가(Add Anim Notify State) -> ANS_ComboWindow를 선택한다.
Notify Begin (바의 시작점): 무기를 휘두르고 난 직후, "이 타이밍부터 다음 공격 버튼 선입력을 허용하겠다"는 시점에 놓는다.
Notify End (바의 끝점): 캐릭터의 공격 공격 모션이 완전히 끝나 손을 거두는 시점에 놓는다. 이때까지 사용자가 다음 버튼을 안 누르면 콤보 카운트가 초기화되며 부드럽게 대기(Idle) 모션으로 돌아간다.
마우스 드래그 튜닝: 타임라인에 생성된 바(Bar)의 양 끝을 마우스로 잡고 앞뒤로 늘리거나 줄이며 정확한 프레임 구간을 지정한다.
[5단계: 캐릭터 블루프린트에서 컴포넌트 장착 및 스탯 조정]
마지막으로 내 분신이 될 캐릭터에게 이 모든 초고속 버퍼 기능을 탑재해 준다.
작업: 캐릭터 블루프린트(BP_MyCharacter)를 연다.
행동: 좌상단 컴포넌트 창에서 컴포넌트 추가를 눌러 C++로 작성해 둔 ActionBufferComponent를 부착한다.
디테일 패널 튜닝: 컴포넌트를 클릭하면 우측 디테일 패널에 프로그래머 모드에서 설계해 둔 변수들이 노출된다. 개발 기획자 모드로서 "선입력 유효 시간(COMMAND_TTL)" 수치를 0.3(300ms) 혹은 0.2로 고쳐가며 손가락 감각에 가장 완벽하게 들어맞는 최적의 반응 속도를 찾아낸다.
5. 요약 : 1인 개발 역할 분담
구조가 완성되면개발 루틴은 다음과 같이 완전히 분리되어 효율적으로 진행되는지 잘 살펴보아야 한다.
[프로그래머(C++ 할 때)]: 입력이 씹히지 않게 구조체 배열 큐에 메모리를 안전하게 밀어 넣고 만료 시간을 계산하는 물리적 파이프라인 개설.
[기획자(에디터 켤 때)]: 마우스 드래그로 타임라인 바를 조절하며 '1타 치고 요 타이밍에 눌러야 2타가 나가는구나!'를 직접 플레이해 보며 튜닝.
*팀 프로젝트*
1. 시작하기에 앞서 개념 공부
내 로컬 브랜치인 feat/UI에 Pull Origin을 하려는데 갑자기 fail to push 에러가 발생하면서 git lfs 관련 에러 메시지가 출력되며 pull이 되지 않는 현상이 발생하였다.무거운 에셋인 uasset을 보내려다가 그런 것인데, 해결 방법을 알아보자.
git lfs logs last 명령어로 로그를 분석해보자.
로그를 분석해보니 다음과 같은 로그가 발견되었다.
" Fatal error: Server error ... /verify from HTTP 504"
HTTP 504 (Gateway Timeout): 웹 서버나 프록시 서버가 상위 서버로부터 제시간에 응답을 받지 못했을 때 발생하는 '네트워크 타임아웃' 오류인데, 컴퓨터는 파일을 잘 보냈는데, GitHub LFS 서버 내부에서 데이터를 검증(verify)하는 과정에서 시간이 너무 오래 걸려 서버가 뻗어버린 상태다.
/verify 단계의 트랩: Git LFS는 대용량 파일을 업로드한 직후, 파일을 확인하는 검증(Verify) 절차를 거치는데, 바로 이 마지막 확인 도장을 찍어주는 GitHub 서버가 응답을 안 해준 것이다.
또한, 환경 설정(Environment)부분의 로그에서도 몇가지 체크해야 할 점이 보였다.
ConcurrentTransfers=8 (동시 전송 개수 8개) : 현재 대용량 파일을 동시에 8개씩 밀어 넣도록 세팅되어 있음. 파일 하나하나의 용량이 너무 크면, 8개를 한 번에 검증하다가 GitHub 서버가 부하를 견디지 못하고 504 타임아웃을 뱉었을 가능성이 높다.
Client IP addresses: 25.1.11.93 ... (하마치 VPN 감지) : IP 목록 중에 25.x.x.x 대역이 보이는데, 이건 하마치(Hamachi) 가 켜져 있다는 증거. 팀원들과 멀티플레이 테스트를 하려고 켜둔 가상 네트워크(VPN) 환경이 활성화되어 있으면, 대용량 데이터를 외부 GitHub 서버로 보낼 때 네트워크 라우팅이 꼬이거나 대역폭 제한 때문에 타임아웃(504)이 유발될 확률이 대단히 높다.
해결책 : 우선 하마치를 완전히 끄고, git 프롬프트에서 전송 검증 패스, 그리고 잠금 검증 패스까지 임시로 설정하였다. 해당 방법은 다른 팀원 프로젝트에 영향을 주지 않고 컴퓨터 내부의 .git/config 로컬 설정 파일만 바꾸는 것이다.
git config lfs.verify false // 서버 무결성 검사 패스
git config lfs.locksverify false // 푸시 전 서버의 파일 잠금 상태 조회 패스
글로벌 검사 패스, 로컬 폴더 검사 패스까지 설정하고 다시 시도해본다.
git config lfs."로컬 저장소 주소".locksverify false
git config lfs.locksverify false
git config --global lfs.locksverify false
git config --global lfs.verify false
git config --list | findstr lfs // lfs 세팅 확인하기
그럼에도 push origin이 되지 않았음. 로그를 한번 더 출력해 보니,
objects/batch 단계의 병목 발견: 커밋한 언리얼 에셋 파일들 목록(Batch)이 너무 많거나 개별 파일 용량이 너무 커서, 깃허브의 서버가 내부 LFS 창고 서버로 데이터를 넘기다가 서버 과부하로 통신망이 끊어지며 뻗어버린 상태임.
따라서 동시 전송 개수를 1로 제한해서 통로를 좁히고, Git LFS 대용량 자산만 먼저 따로 업로드 해 두는 방법을 동시에 사용.
git config lfs.concurrenttransfers 1
git lfs push origin 내_브랜치_이름 --all
사실 새로 작업한 내용이 그리 용량이 크지 않았다. 기껏해야 100kb 정도. 그런데도 왜 LFS 에러가 나면서 푸시가 막힌걸까?
그냥 lfs 서버가 맛이 간거같다.
그래서 다음 명령어로 원격 서버에 그냥 밀어 넣기로 했다.
git push origin 내_브랜치_이름 --no-verify
작업을 종료한 후에는 다음과 같은 명령어로 다시 원상복구 시켜놓았다.
git config --unset lfs.locksverify
git config --unset lfs.verify
git config --unset lfs.concurrenttransfers
git config --unset lfs.lfs주소.locksverify
git config --unset lfs.lfs주소.verify
git config --global --unset lfs.locksverify
git config --global --unset lfs.verify
2. 추가 작업한 내용들
<추가 작업 리스트>
우선 너무 바빠서 TIL 작성은 보류
*개인 프로젝트 및 개인 공부*
1. 작업용 소스코드를 .txt파일로 자동변환해주는 UnrealSourceExporter.exe 파일을 x64, Release 형태로 빌드하여 GDrive에 자동 연동시켜놓았다.
*오늘의 총평*
할일이 많았다....