Post

나만의 언리얼 코딩 표준

나만의 언리얼 코딩 표준

이 글을 쓰게된 이유

팀 작업이나 팀 프로젝트를 경험해보고 성공적으로 수행하기 위해서는 모든 팀원이 코드의 일관성을 유지하며 협력할 수 있는 기반이 중요하다고 생각이 들었다.
코딩 표준은 이러한 협업의 핵심 요소같고, 소통을 원활히 해주고 유지보수를 효율적으로 만든다.
내가 할 팀작업에도 사용하기 위해서이다.

출처

이 글은 Epic Games의 Unreal Engine C++ 코딩 표준을 참고하여 작성되었습니다.

에픽게임즈에서 코딩 규칙이 중요한 이유를 알려주는데,
그 중에 유지보수의 중요성, 개발자의 지속성 부족, 가독성의 가치가 와닿았다.

나만의 명명 규칙

  • 일반적으로 파스칼 표기법을 사용하고 스네이크 표기법(언더스코어), 카멜 표기법을 사용하지 않습니다. ex) AttackDamage는 올바르지만, attackDamage또는attack_damage는 올바르지 않습니다.

  • 타입 이름에는 추가적으로 대문자로 이루어진 접두사를 포함하여 변수 이름과 구분합니다. ex) FWeapon

  • 템플릿 클래스에는 접두사 T를 포함합니다.
    1
    
    class TInfo
    
  • UObject에서 상속하는 클래스에는 접두사 U를 포함합니다.
    1
    
    class UWeaponContainer
    
  • AActor에서 상속하는 클래스에는 접두사 A를 포함합니다.
    1
    
    class AWeapon
    
  • SWidget에서 상속하는 클래스에는 접두사 S를 포함합니다.
    1
    
    class SWeaponWidget
    
  • 추상적 인터페이스인 클래스에는 접두사 I를 포함합니다.
    1
    
    class ICombatSystem
    
  • 열거형에는 접두사 E를 포함합니다.
    1
    2
    3
    4
    5
    6
    
    enum class EWeaponType
    {
      EWT_Sword,
      EWT_Spear,
      EWT_Gun
    };
    
  • 부울 변수에는 접두사 b를 포함합니다.
    1
    
    bool bIdle
    
  • 그 외 대부분의 클래스는 접두사 F를 포함합니다. 그러나 일부 서브시스템은 다른 글자를 사용하기도 합니다.
  • Typedef의 경우도 타입에 적합한 접두사를 사용합니다.
    ex) typedef TArray<FWeaponData> FWeaponDatas;

변수, 메서드, 클래스 이름은 다음과 같아야 합니다.

  • 명확함
  • 확실함
  • 내용을 파악할 수 있음 이름의 범위가 넓을수록 올바르고 내용을 파악할 수 있는 이름을 사용해야 합니다. 과도한 약어는 피합니다.

부울을 반환할 모든 함수는 Is,Has,Should를 사용해야합니다.

  1. Is : 상태를 나타낼 때 사용됩니다. 보통 Is는 어떤 조건이 참인지 거짓인지를 묻는 질문 형태로 사용됩니다.
    • IsIdle: 캐릭터가 유휴 상태인지 확인합니다.
    • IsDead: 캐릭터가 죽었는지 확인합니다.
    • IsVisible: 객체가 화면에 보이는지 확인합니다.
  2. Has : 보유 여부를 확인할 때 사용됩니다. 일반적으로 어떤 속성이나 아이템을 갖고 있는지 체크할 때 사용됩니다.
    • HasWeapon: 캐릭터가 무기를 가지고 있는지 확인합니다.
    • HasHealth: 캐릭터가 아직 체력이 있는지 확인합니다.
    • HasKey: 플레이어가 열쇠를 가지고 있는지 확인합니다.
  3. Should : 어떤 조건에 따른 예상되는 행동이나 상태를 나타낼 때 사용됩니다. 보통 조건을 만족하면 어떤 동작을 수행해야 할지를 나타냅니다.
    • ShouldReload: 총알이 부족할 때 리로드를 해야 하는지 확인합니다.
    • ShouldAttack: 공격할 필요가 있는지 확인합니다.
    • ShouldSaveGame: 게임을 저장해야 할지 확인합니다.

함수 파라미터 이름에 접두사 “In/Out” 추가 방식

구분타입전달 방법설명
Inconst 참조, 값const참조(const Type&)함수가 파라미터의 값을 수정하지 않고 읽기만 할 때 사용.
Out참조, 포인터Type&또는 Type*함수가 파라미터의 값을 수정만 하고, 반환값을 호출자에게 전달할 때 사용.
InOut참조, 포인터Type&또는 Type*함수가 파라미터를 수정하고 수정된 값을 호출자에게 전달할 때 사용.
  • In)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    void CheckPlayerStatus(const APlayerCharacter* Player)
    {
      if (Player->IsDead())
      {
          UE_LOG(LogTemp, Log, TEXT("Player is dead!"));
      }
      else
      {
          UE_LOG(LogTemp, Log, TEXT("Player is alive with Health: %d"), Player->GetHealth());
      }
    }
    
  • Out)
    1
    2
    3
    4
    
    void GetPlayerLocation(FVector& OutLocation)
    {
      OutLocation = GetActorLocation();  // 호출자에게 위치 전달
    }
    
  • InOut)
    1
    2
    3
    4
    
    void ReducePlayerHealth(int32& Health, int32 Damage)
    {
      Health -= Damage;  // 체력 감소
    }
    

최신 C++언어 문법

Override 및 Final

상속받은 클래스에서 함수 재정의를 할 때 override를 꼭 사용하고,
자식 클래스에서 가상 함수를 재정의 할 때 virtual 키워드를 사용합니다. (필자는 이게 가독성을 높인다고 생각이 들기때문이다. 사용하지 않아도 똑같이 가상함수로 적용됨)
상속이나 오버라이딩을 제한하는 클래스 및 함수에는 final을 꼭 사용합니다.

Nullptr

nullptr은 모든 경우 C 스타일 NULL 매크로 대신 사용합니다.

Auto

아래 몇 가지 예외를 제외하면 C++ 코드에서 auto 를 사용해서는 안 됩니다. 초기화하려는 타입은 항상 명시해 주어야 합니다. 즉, 읽는 사람에게 타입이 명확하게 보여야 한다는 뜻입니다.

auto 를 사용 가능한 경우는 다음과 같습니다.

  • 변수에 람다를 바인딩해야 하는 경우. 람다 타입은 코드로 표현할 수 없기 때문입니다.
  • 이터레이터 변수의 경우. 단, 이터레이터 타입이 매우 장황하여 가독성에 악영향을 미치는 경우에 한합니다.
  • 템플릿 코드에서 표현식의 타입을 쉽게 식별할 수 없는 경우. 고급 사용 사례입니다.

범위 기반 For

범위 기반 for 문을 사용할 수 없는 경우 ex) 인덱스가 필요할 때
아닌 경우 코드의 가독성과 유지보수성 향상에 도움이 되므로 사용을 권합니다.

  • 단순 순회나 읽기 전용 처리에는 범위 기반 for 문을 사용하는 것이 좋습니다.
  • 수정이 필요한 경우에도 참조(&)를 이용하면 효율적으로 수정할 수 있습니다.
  • 인덱스나 특정 조건에 따른 처리가 필요하다면 전통적인 for문을 사용하는 것이 더 적합할 수 있습니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    {
      // TArray에 몇 개의 정수값을 저장합니다.
     TArray<int32> Numbers = { 10, 20, 30, 40, 50 };
    
      // 범위 기반 for 문을 사용하여 TArray의 각 요소를 순회하며 출력합니다.
      for (const int32& Num : Numbers)
      {
          // 각 요소를 출력
          UE_LOG(LogTemp, Log, TEXT("Number: %d"), Num);
      }
    }
    

코드 포맷

중괄호

다음 예시와 같이 단일 구문 블록에도 항상 중괄호를 포함시켜 주세요.

1
2
3
4
if(bIdle)
{
    return;
}

If-Else

if 또는 else 문에서 실행할 블록은 중괄호 {}로 반드시 묶어야 합니다. 중괄호를 사용하지 않으면, 코드 작성 시 의도치 않은 오류가 발생할 수 있습니다. 예를 들어, 중괄호 없이 작성된 코드에서 새로운 줄을 추가하면, 그 줄은 if/else 블록 외부에 포함되어 예상치 못한 동작을 초래할 수 있습니다. 이를 방지하려면 항상 중괄호를 사용하세요.

1
2
3
4
5
6
7
8
if(bDead)
{
    PlayDead();
}
else
{
    TakeDamage();
}

if 문에 여러 else ifelse가 포함될 때는 각각의 블록이 동일한 들여쓰기를 갖도록 하여 코드의 구조가 명확하게 보이도록 해야 합니다. 들여쓰기를 일관되게 유지하면, 코드의 흐름을 쉽게 이해할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
if (HP < 10)
{
    UE_LOG(LogCategory, Log, TEXT("Low HP"));
}
else if (HP < 100)
{
    UE_LOG(LogCategory, Log, TEXT("Medium HP"));
}
else
{
    UE_LOG(LogCategory, Log, TEXT("High HP"));
}

탭 및 들여쓰기

1. 실행 블록별 들여쓰기

  • 각 실행 블록은 반드시 들여쓰기를 해야 합니다. 들여쓰기를 통해 코드의 계층 구조를 명확히 나타낼 수 있습니다. 예를 들어, if, else, for, while, switch 등에서의 코드 블록은 실행 블록 안의 코드를 들여쓰기로 구분해야 합니다.

2. 탭 사용

  • 줄 시작 부분의 공백에는 스페이스 대신 탭을 사용합니다. 이 표준은 코드 작성자가 탭 크기를 설정한 만큼 들여쓰기를 하도록 보장합니다. 탭 크기는 4자로 설정합니다. 이는 대부분의 개발 환경에서 기본으로 설정되는 크기이며, 이를 기준으로 코드가 정렬됩니다.

3. 탭과 스페이스 혼용

  • 탭을 기본으로 사용하지만, 때때로 스페이스를 사용할 필요가 있을 수 있습니다. 예를 들어, 탭을 사용했을 때 코드 줄을 맞추기 어려운 경우가 있을 수 있습니다. 이런 경우에는 스페이스를 사용할 수 있습니다.

Switch 문 사용

1. 빈 케이스 제외

  • 빈 케이스(동일한 코드를 갖는 다중 케이스)는 피하는 것이 좋습니다. 각 케이스는 독립적으로 다뤄져야 하며, 중복 코드를 피해야 합니다.

2. 명시적인 ‘break’ 또는 ‘falls through’

  • switch 문에서 다음 케이스로 넘어갈 때는 명시적으로 밝혀줘야 합니다. 예를 들어, break 문을 사용하거나,// falls through와 같은 주석을 달아야 합니다. 이는 코드 흐름을 명확하게 이해할 수 있도록 도와줍니다.

3. 디폴트 케이스 추가 디폴트 케이스는 항상 포함시켜야 합니다. 디폴트 케이스는 새로운 케이스가 추가될 때, 예상하지 못한 값에 대한 처리를 할 수 있게 해 줍니다. 디폴트 케이스 뒤에 break를 추가하여 다른 케이스가 추가되더라도 예상치 못한 동작을 방지할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
switch (EState)
{
    case 1:
        // 실행 코드
        // falls through
    
    case 2:
        // 실행 코드
        break;

    case 3:
        // 실행 코드
        return;  // 조건에 맞는 값을 반환

    case 4:
    case 5:
        // 실행 코드 (case 4와 5가 동일한 처리를 할 때)
        break;

    default:
        // 기본 처리
        break;
}

캡슐화

캡슐화는 객체 지향 프로그래밍에서 중요한 개념으로, 데이터(변수)와 그 데이터를 처리하는 메소드(함수)를 하나의 단위로 묶어 외부에서 직접 접근할 수 없도록 보호하는 기법입니다. 이를 통해 데이터의 무결성을 보장하고, 외부에서 직접 데이터를 변경하는 것을 방지하여 안전하고 예측 가능한 코드를 만들 수 있습니다.

Getter는 객체의 private 또는 protected 필드 값을 읽는 메소드이고, Setter는 해당 필드 값을 설정하는 메소드입니다. 외부에서 객체의 상태를 직접 수정하는 대신, gettersetter를 통해 값을 읽고 쓸 수 있게 하여 객체의 상태를 제어합니다.

사용 이유:

  • 데이터 보호: 객체의 필드를 직접 수정하는 것을 방지하여, 그 값이 유효한 범위 내에서만 변경되도록 제어할 수 있습니다.
  • 유효성 검증: 데이터를 설정할 때 유효성 검사를 수행하거나, 값이 변경될 때마다 추가 작업을 수행할 수 있습니다.
  • 유지보수 용이: 필드의 내부 구현을 변경해도 Getter와 Setter 메소드만 수정하면 되므로, 외부 코드에 영향을 미치지 않습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class AMyCharacter : public ACharacter
{
    GENERATED_BODY()

private:
    // 내부 변수 (private)
    int32 Health;

public:
    // Getter: Health 값을 반환
    int32 GetHealth() const
    {
        return Health;
    }

    // Setter: Health 값을 설정
    void SetHealth(int32 NewHealth)
    {
        // 유효성 검사: Health 값이 0 이상이어야 한다
        if (NewHealth >= 0)
        {
            Health = NewHealth;
        }
        else
        {
            UE_LOG(LogTemp, Warning, TEXT("Health cannot be negative"));
        }
    }
};

클래스 정렬 순서 및 묶음

이 정렬 순서는 필자의 개인적인 가독성 향상을 위한 방식입니다. public -> protected -> private 순으로 작성하는 것을 추천합니다.

  • public 섹션
    1. 생성자는 객체 생성 및 초기화 과정을 한눈에 확인할 수 있도록 클래스의 가장 상단에 배치합니다.
    2. Getter/Setter 메소드는 객체의 상태를 외부에서 쉽게 접근하고 수정할 수 있게 도와주기 때문에, public 섹션의 상위에 배치하는 것이 효과적입니다.
    3. 기능 관련 메소드는 클래스의 핵심 동작을 담당하므로 public 섹션에서 중요한 순서로 배치합니다. 이러한 메소드들은 클래스의 주된 역할을 수행하는 메소드들입니다.
    4. 인터페이스 메소드는 상대적으로 중요도가 낮은 메소드일 수 있지만, 외부와의 상호작용을 담당하므로 기능 관련 메소드 아래에 배치합니다.
    5. RPC 메소드는 네트워크와 관련된 메소드로 중요한 역할을 하지만, 인터페이스 메소드와 유사한 카테고리로 묶을 수 있습니다.
  • protected 섹션
    1. protected 메소드는 클래스 내에서만 접근 가능하며, 상속된 클래스에서만 사용할 수 있습니다. 이 섹션에는 클래스의 핵심 기능에 관계된 메소드가 주로 위치합니다.
    2. protected 섹션은 클래스의 내부 구현에 중요한 메소드들을 배치하는 곳입니다. 이러한 메소드들은 다른 클래스에서는 접근할 수 없지만, 상속받은 클래스에서는 필요한 기능을 구현하는 데 사용됩니다.
  • private 섹션
  1. 변수 (Private 변수)
    • private 섹션에는 객체의 상태를 관리하는 중요한 데이터들이 위치합니다. 이 데이터는 클래스 외부에서 직접 접근하거나 수정할 수 없고, 메서드를 통해 간접적으로 조작됩니다.
    • 예시로, PawnExtComponent, HealthComponent, CameraComponent와 같은 변수들은 객체 상태를 나타내며, 외부에서 수정될 수 없게 private으로 선언됩니다.
  2. 델리게이트 관련 메서드
    • 델리게이트(Delegate)는 특정 이벤트나 상태 변경을 외부에 전달하는 방법입니다. 델리게이트 메서드는 클래스 내부에서 특정 조건이 발생했을 때 다른 객체나 클래스에 알리기 위해 사용됩니다.
    • OnTeamChangedDelegate와 같은 델리게이트는 주로 게임의 특정 이벤트에 반응하거나 UI 업데이트, 상태 변화를 반영하는 데 사용됩니다.
  3. OnRep_ 메서드 (네트워크 복제 관련 메서드)
    • OnRep_ 메서드는 네트워크 복제에서 변수가 변경될 때 호출되는 특별한 메서드입니다. 이 메서드는 변수가 네트워크에서 복제되었을 때 클라이언트 쪽에서 상태를 업데이트하거나 UI/애니메이션 등을 갱신할 때 사용됩니다.
    • OnRep_ReplicatedAcceleration, OnRep_MyTeamID는 각각 복제된 데이터에 대한 처리를 담당하는 메서드로, 이를 통해 네트워크에서 상태가 일관되게 유지됩니다.
  4. 기타 헬퍼 메서드
    • private 섹션에 선언된 기타 헬퍼 메서드들은 특정 기능을 캡슐화하여 외부에서 접근하지 않도록 하며, 객체의 내부적인 작업을 처리합니다.
    • 예를 들어, OnControllerChangedTeam 메서드는 팀 변경 이벤트를 처리하고, 이와 관련된 로직을 내부적으로 관리합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
UCLASS()
class MYGAME_API AMyCharacter : public AActor
{
    GENERATED_BODY()

public:
    // 1. 생성자 (상단에 위치)
    AMyCharacter();

    // 2. 객체 상태 관련 메소드 (Getter/Setter)
    UFUNCTION(BlueprintCallable, Category = "Character")
    FVector GetCharacterLocation() const;

    UFUNCTION(BlueprintCallable, Category = "Character")
    void SetCharacterHealth(float Health);

    // 3. 기능 관련 메소드
    UFUNCTION(BlueprintCallable, Category = "Character")
    void Jump();

    UFUNCTION(BlueprintCallable, Category = "Character")
    void Attack();

    // 4. 인터페이스 메소드
    virtual void OnInteract() override;

    // 5. RPC 메소드
    UFUNCTION(Server, Reliable, WithValidation)
    void ServerMoveCharacter(const FVector& NewLocation);

protected:
    // 보호된 메소드
    virtual void BeginPlay() override;
    virtual void Tick(float DeltaTime) override;

    // 팀 관련 메소드 (예시)
    void SetTeamID(int32 NewTeamID);

private:
    // 1. 개인적인 변수들
    UPROPERTY()
    UMyAbilityComponent* AbilityComponent;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Character", meta = (AllowPrivateAccess = "true"))
    float Health;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Character", meta = (AllowPrivateAccess = "true"))
    FVector CharacterLocation;

private:
    // 델리게이트 관련 메서드 (주로 이벤트 처리)
    UFUNCTION()
    void OnControllerChangedTeam(UObject* TeamAgent, int32 OldTeam, int32 NewTeam);

    // 네트워크 복제 관련 메서드
    UFUNCTION()
    void OnRep_ReplicatedAcceleration();

    UFUNCTION()
    void OnRep_MyTeamID(FGenericTeamId OldTeamID);
};
This post is licensed under CC BY 4.0 by the author.