Post

클린 코드(Clean Code)란? - 읽기 쉽고 유지보수하기 좋은 코드 작성법

클린 코드(Clean Code)란?

📌 학습 목표

  • 클린 코드의 개념과 중요성 이해
  • 읽기 쉬운 코드 작성을 위한 원칙들 습득
  • 게임 개발에서의 클린 코드 적용 방법 학습
  • 리팩토링을 통한 코드 품질 개선 기법 파악

📌 정의

클린 코드(Clean Code)읽기 쉽고, 이해하기 쉽고, 수정하기 쉬운 코드입니다. 로버트 마틴(Uncle Bob)이 제시한 개념으로, 단순히 동작하는 코드를 넘어서 다른 개발자가 쉽게 이해하고 유지보수할 수 있는 코드를 의미합니다.

📝 개념 정리

  • 가독성(Readability): 코드를 읽는 사람이 쉽게 이해할 수 있음
  • 단순성(Simplicity): 복잡하지 않고 직관적임
  • 의도 표현(Intent): 코드가 무엇을 하는지 명확히 드러남
  • 중복 제거(DRY): Don’t Repeat Yourself - 중복 코드 최소화
  • 책임 분리: 각 함수와 클래스가 명확한 책임을 가짐

🔑 클린 코드의 핵심 원칙

1. 의미 있는 이름 사용

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
// 나쁜 예 - 의미를 알 수 없는 이름
int d; // 경과 시간?
List<int> lst; // 무엇의 리스트?
void calc(); // 무엇을 계산?

// 좋은 예 - 의도가 분명한 이름
int elapsedTimeInSeconds;
List<Player> activePlayers;
void CalculatePlayerScore();

// 게임 개발 예시
// 나쁜 예
public class GM
{
    public void u() { } // 업데이트?
    public void r() { } // 렌더링?
    int hp; // 체력?
    int mp; // 마나?
}

// 좋은 예
public class GameManager
{
    public void UpdateGameLogic() { }
    public void RenderFrame() { }
    int currentHealth;
    int currentMana;
}

2. 함수는 작고 단일 책임을 가져야 함

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
// 나쁜 예 - 너무 많은 일을 하는 함수
public void ProcessPlayer()
{
    // 입력 처리
    if (Input.GetKey(KeyCode.W)) player.position += Vector3.forward;
    if (Input.GetKey(KeyCode.S)) player.position -= Vector3.forward;

    // 체력 처리
    if (player.health <= 0)
    {
        player.isDead = true;
        ShowGameOverScreen();
    }

    // 애니메이션 처리
    if (player.isMoving) animator.Play("run");
    else animator.Play("idle");

    // 사운드 처리
    if (player.isAttacking) audioSource.PlayOneShot(attackSound);
}

// 좋은 예 - 책임을 분리한 함수들
public void UpdatePlayer()
{
    HandlePlayerInput();
    UpdatePlayerHealth();
    UpdatePlayerAnimation();
    UpdatePlayerAudio();
}

private void HandlePlayerInput()
{
    var input = GetPlayerInput();
    player.Move(input);
}

private void UpdatePlayerHealth()
{
    if (player.IsDead())
    {
        HandlePlayerDeath();
    }
}

private void UpdatePlayerAnimation()
{
    var animationState = player.IsMoving ? "run" : "idle";
    animator.Play(animationState);
}

private void UpdatePlayerAudio()
{
    if (player.IsAttacking)
    {
        audioManager.PlayAttackSound();
    }
}

3. 주석보다는 코드로 설명

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
// 나쁜 예 - 주석에 의존
public void Attack(Enemy target)
{
    // 크리티컬 확률 계산
    float critChance = (luck * 0.1f) + (level * 0.02f);
    bool isCritical = Random.Range(0f, 1f) < critChance;

    // 기본 데미지 계산
    int baseDamage = strength * weaponDamage;

    // 크리티컬이면 데미지 2배
    if (isCritical)
        baseDamage *= 2;

    target.TakeDamage(baseDamage);
}

// 좋은 예 - 코드 자체가 설명
public void Attack(Enemy target)
{
    bool isCriticalHit = RollForCriticalHit();
    int damage = CalculateBaseDamage();

    if (isCriticalHit)
        damage = ApplyCriticalMultiplier(damage);

    target.TakeDamage(damage);
}

private bool RollForCriticalHit()
{
    float criticalChance = CalculateCriticalChance();
    return Random.Range(0f, 1f) < criticalChance;
}

private float CalculateCriticalChance()
{
    return (luck * LUCK_CRIT_MODIFIER) + (level * LEVEL_CRIT_MODIFIER);
}

private int CalculateBaseDamage()
{
    return strength * currentWeapon.Damage;
}

private int ApplyCriticalMultiplier(int damage)
{
    return damage * CRITICAL_DAMAGE_MULTIPLIER;
}

4. 매직 넘버 제거

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
// 나쁜 예 - 매직 넘버
public void RegenerateHealth()
{
    if (timeSinceLastRegen > 5.0f) // 5초는 무엇?
    {
        health += 10; // 10은 무엇?
        timeSinceLastRegen = 0;
    }
}

public bool CanLevelUp()
{
    return experience >= level * 100; // 100은 무엇?
}

// 좋은 예 - 상수로 의미 부여
private const float HEALTH_REGEN_INTERVAL = 5.0f;
private const int HEALTH_REGEN_AMOUNT = 10;
private const int EXPERIENCE_PER_LEVEL_MULTIPLIER = 100;

public void RegenerateHealth()
{
    if (timeSinceLastRegen > HEALTH_REGEN_INTERVAL)
    {
        health += HEALTH_REGEN_AMOUNT;
        timeSinceLastRegen = 0;
    }
}

public bool CanLevelUp()
{
    int requiredExperience = level * EXPERIENCE_PER_LEVEL_MULTIPLIER;
    return experience >= requiredExperience;
}

🎮 게임 개발에서의 클린 코드 적용

1. 게임 상태 관리

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// 나쁜 예 - 복잡한 상태 처리
public class GameController : MonoBehaviour
{
    public int gameState; // 0=메뉴, 1=플레이, 2=일시정지, 3=게임오버

    void Update()
    {
        if (gameState == 0)
        {
            // 메뉴 로직
            if (Input.GetKeyDown(KeyCode.Space))
                gameState = 1;
        }
        else if (gameState == 1)
        {
            // 게임 플레이 로직
            if (Input.GetKeyDown(KeyCode.Escape))
                gameState = 2;
            if (player.health <= 0)
                gameState = 3;
        }
        else if (gameState == 2)
        {
            // 일시정지 로직
            if (Input.GetKeyDown(KeyCode.Escape))
                gameState = 1;
        }
        // ... 복잡한 조건문 계속
    }
}

// 좋은 예 - 명확한 상태 관리
public enum GameState
{
    MainMenu,
    Playing,
    Paused,
    GameOver
}

public class GameStateManager : MonoBehaviour
{
    public GameState CurrentState { get; private set; }

    void Update()
    {
        switch (CurrentState)
        {
            case GameState.MainMenu:
                HandleMainMenuState();
                break;
            case GameState.Playing:
                HandlePlayingState();
                break;
            case GameState.Paused:
                HandlePausedState();
                break;
            case GameState.GameOver:
                HandleGameOverState();
                break;
        }
    }

    private void HandlePlayingState()
    {
        if (Input.GetKeyDown(KeyCode.Escape))
            ChangeState(GameState.Paused);

        if (player.IsDead())
            ChangeState(GameState.GameOver);
    }

    public void ChangeState(GameState newState)
    {
        ExitCurrentState();
        CurrentState = newState;
        EnterNewState();
    }
}

2. 플레이어 컨트롤러 리팩토링

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// 나쁜 예 - 모든 기능이 한 곳에
public class PlayerController : MonoBehaviour
{
    void Update()
    {
        // 이동 처리
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        transform.Translate(new Vector3(h, 0, v) * speed * Time.deltaTime);

        // 점프 처리
        if (Input.GetKeyDown(KeyCode.Space) && isGrounded)
        {
            rb.AddForce(Vector3.up * jumpForce);
            isGrounded = false;
        }

        // 공격 처리
        if (Input.GetMouseButtonDown(0))
        {
            Collider[] enemies = Physics.OverlapSphere(attackPoint.position, attackRange);
            foreach (Collider enemy in enemies)
            {
                if (enemy.CompareTag("Enemy"))
                {
                    enemy.GetComponent<Enemy>().TakeDamage(attackDamage);
                }
            }
        }

        // 애니메이션 처리
        bool isMoving = h != 0 || v != 0;
        animator.SetBool("IsMoving", isMoving);
    }
}

// 좋은 예 - 기능별 분리
public class PlayerController : MonoBehaviour
{
    private PlayerMovement movement;
    private PlayerCombat combat;
    private PlayerAnimation animation;

    void Awake()
    {
        movement = GetComponent<PlayerMovement>();
        combat = GetComponent<PlayerCombat>();
        animation = GetComponent<PlayerAnimation>();
    }

    void Update()
    {
        var input = GetPlayerInput();

        movement.HandleMovement(input);
        combat.HandleCombat(input);
        animation.UpdateAnimations(input);
    }

    private PlayerInput GetPlayerInput()
    {
        return new PlayerInput
        {
            Movement = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")),
            Jump = Input.GetKeyDown(KeyCode.Space),
            Attack = Input.GetMouseButtonDown(0)
        };
    }
}

public class PlayerMovement : MonoBehaviour
{
    public void HandleMovement(PlayerInput input)
    {
        if (input.Movement != Vector2.zero)
            Move(input.Movement);

        if (input.Jump && CanJump())
            Jump();
    }

    private void Move(Vector2 direction)
    {
        var movement = new Vector3(direction.x, 0, direction.y);
        transform.Translate(movement * moveSpeed * Time.deltaTime);
    }

    private bool CanJump()
    {
        return isGrounded;
    }

    private void Jump()
    {
        rigidbody.AddForce(Vector3.up * jumpForce);
        isGrounded = false;
    }
}

3. 아이템 시스템 클린 코드

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// 나쁜 예 - 타입별 하드코딩
public void UseItem(int itemId)
{
    if (itemId == 1) // 체력 포션
    {
        health += 50;
        if (health > maxHealth) health = maxHealth;
    }
    else if (itemId == 2) // 마나 포션
    {
        mana += 30;
        if (mana > maxMana) mana = maxMana;
    }
    else if (itemId == 3) // 공격력 버프
    {
        attackDamage *= 1.5f;
        // 10초 후 원복 타이머 설정...
    }
}

// 좋은 예 - 다형성 활용
public abstract class Item
{
    public string Name { get; protected set; }
    public abstract void Use(Player player);
}

public class HealthPotion : Item
{
    private readonly int healAmount;

    public HealthPotion(int healAmount)
    {
        Name = "Health Potion";
        this.healAmount = healAmount;
    }

    public override void Use(Player player)
    {
        player.Heal(healAmount);
    }
}

public class ManaPotion : Item
{
    private readonly int manaAmount;

    public ManaPotion(int manaAmount)
    {
        Name = "Mana Potion";
        this.manaAmount = manaAmount;
    }

    public override void Use(Player player)
    {
        player.RestoreMana(manaAmount);
    }
}

public class AttackBuff : Item
{
    private readonly float multiplier;
    private readonly float duration;

    public AttackBuff(float multiplier, float duration)
    {
        Name = "Attack Buff";
        this.multiplier = multiplier;
        this.duration = duration;
    }

    public override void Use(Player player)
    {
        player.ApplyAttackBuff(multiplier, duration);
    }
}

// 사용
public void UseItem(Item item)
{
    item.Use(this); // 각 아이템이 자신만의 방식으로 동작
}

💡 클린 코드 체크리스트

함수 작성 가이드라인

1
2
3
4
5
6
7
8
9
10
11
12
// ✅ 좋은 함수의 특징
-  가지 일만 한다
- 작다 (20 이내 권장)
- 의미 있는 이름
- 매개변수는 3 이하
- 부작용(Side Effect) 없음

// ❌ 피해야 할 것들
-  함수 (스크롤 필요)
- 애매한 이름 (process, handle, manage )
- 많은 매개변수
- 함수 내에서 다른 객체 변경

클래스 설계 원칙

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ✅ 좋은 클래스
public class Player
{
    // 명확한 책임
    // 적절한 크기
    // 응집도 높음
    // 결합도 낮음
}

// ❌ 나쁜 클래스
public class GameManager
{
    // 너무 많은 책임
    // 모든 게임 로직이 한 곳에
    // 수백 줄의 코드
}

🎯 실무에서 자주 묻는 면접 질문

Q: 클린 코드란 무엇인가요? A: “읽기 쉽고 이해하기 쉬우며 유지보수하기 좋은 코드입니다. 의미 있는 이름, 작은 함수, 단일 책임 등의 원칙을 통해 다른 개발자가 쉽게 이해할 수 있는 코드를 작성하는 것을 의미합니다.”

Q: 클린 코드와 성능 사이의 트레이드오프는? A: “일반적으로 클린 코드가 성능에 큰 영향을 주지 않습니다. 오히려 명확한 구조로 인해 최적화 포인트를 찾기 쉬워집니다. 성능이 정말 중요한 부분에서만 가독성을 조금 희생할 수 있지만, 그 부분은 명확히 문서화해야 합니다.”

Q: 레거시 코드를 클린 코드로 바꾸는 방법은? A: “점진적 리팩토링을 통해 개선합니다. 먼저 테스트 코드를 작성하고, 작은 단위부터 함수명 개선, 중복 제거, 함수 분리 등을 진행합니다. 한 번에 모든 것을 바꾸려 하지 않고 안전한 단위로 나누어 진행합니다.”

Q: 게임 개발에서 클린 코드가 특히 중요한 이유는? A: “게임은 요구사항이 자주 바뀌고, 팀원들이 다양한 부분을 함께 작업하며, 런타임에 복잡한 상호작용이 일어납니다. 클린 코드를 통해 빠른 기능 추가, 버그 수정, 팀 협업이 원활해집니다.”


🔧 리팩토링 실습 예제

Before: 복잡한 게임 로직

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ComplexGameLogic : MonoBehaviour
{
    public void ProcessGame()
    {
        // 100줄이 넘는 복잡한 로직...
        if (player.health <= 0 && !gameOver)
        {
            gameOver = true;
            Time.timeScale = 0;
            gameOverUI.SetActive(true);
            if (score > highScore)
            {
                highScore = score;
                PlayerPrefs.SetInt("HighScore", highScore);
                newRecordUI.SetActive(true);
            }
            // 더 많은 로직...
        }
    }
}

After: 클린하게 분리된 로직

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
public class GameManager : MonoBehaviour
{
    private readonly ScoreManager scoreManager;
    private readonly UIManager uiManager;

    public void Update()
    {
        if (player.IsDead() && !IsGameOver)
        {
            EndGame();
        }
    }

    private void EndGame()
    {
        SetGameOverState();
        HandleScoreRecords();
        ShowGameOverUI();
    }

    private void SetGameOverState()
    {
        IsGameOver = true;
        Time.timeScale = 0;
    }

    private void HandleScoreRecords()
    {
        scoreManager.ProcessFinalScore(currentScore);
    }

    private void ShowGameOverUI()
    {
        uiManager.ShowGameOverScreen(scoreManager.IsNewRecord);
    }
}

🔗 관련 개념


This post is licensed under CC BY 4.0 by the author.