Services → Blog → Portfolio → About → Contact →
← 블로그 목록

좋은 프로그램의 정석: 독자가 이해하고 신뢰할 수 있는 코드를 만드는 법

좋은 프로그램의 기준과 읽기 쉬운 코드 작성법, 실제 개발 현장에서 검증된 5가지 원칙을 구체적인 사례와 함께 설명합니다.

좋은 프로그램의 정석: 독자가 이해하고 신뢰할 수 있는 코드를 만드는 법

좋은 프로그램의 정석: 독자가 이해하고 신뢰할 수 있는 코드를 만드는 법

제가 20여 년 전 처음 PC 게임 개발 회사에 발을 들였을 때, 초창기에는 자사에서 개발한 엔진의 버그를 찾고 수정하는 역할을 맡았습니다. 당시 코드는 변수 이름만으로는 기능이 이해되지 않는 수준이었고, 한 줄짜리 주석 하나를 해독하는 데 10분 이상 걸려 결국 선배에게 구현 의도 자체를 다시 설명 들어야 했던 적도 있었습니다. 이때 깨달은 것이 있었습니다.

좋은 프로그램이라는 것은 개발자가 아닌 다른 사람이 코드를 읽을 때, 기능이 이해되기 쉬워야 한다는 것입니다.

좋은 프로그램은 시스템 아키텍처, 알고리즘 선택, 한 줄의 함수 이름까지 다른 개발자가 읽었을 때 무리 없이 이해할 수 있도록 설계되어야 합니다. 하지만 대부분의 초심자들은 “성능만 좋으면 되는데 왜 이렇게까지 신경 써야 하지?”라며 이 점을 간과하곤 합니다.

이 글에서는 실제 개발 현장에서 적용 가능한 5개의 원칙을 통해 “좋은 프로그램”이란 무엇인지, 어떻게 만들어야 하는지를 구체적으로 살펴보겠습니다.


읽기 쉽고 이해하기 쉬워야 한다: 코드 독해는 필수

당신은 소프트웨어 개발자가 아니라 문서 작성자입니다.

이 지점은 매우 중요한데, 많은 개발자가 자신이 개발한 코드를 “완벽히 이해한다”며 주석도 생략하고 변수명을 a, b로 지어요. 하지만 몇 주 후 돌아왔을 때 자신의 코드도 이해하기 어려울 정도입니다. 더 중요한 점은, 상사, 동료 개발자, 또는 유지보수자가 자신의 코드를 읽을 때 그 프로그램이 “좋은 프로그램”으로 평가받느냐가 정해집니다.

실제 사례: 의미 없는 변수명이 야기한 재앙

2015년, 제가 속해 있던 팀은 온라인 게임 서버의 트래픽 처리를 담당하는 코드를 리팩토링해야 했습니다. 당시 코드는 다음과 같습니다:

public string Process(string a, int b, List<DataEntry> d) {
    if (b < 10)
        return a;

    foreach (var item in d) {
        if (item.x > 0.5f)
            a += (item.y * b).ToString();
    }
    return a;
}

문제점:

  1. 변수명 a, b, d: 누구도 a가 어떤 역할인지 알 수 없습니다.
  2. 리턴 값의 복잡성: 한 줄의 코드에 Process라는 함수가 a, b, d 세 가지 파라미터를 조작하고 있음에도 불구하고, 기능 설명이 없습니다.
  3. 의미 없는 조건문: if (b < 10)이 어떤 의미를 갖는지 이해하지 못하면 코드 수정도 힘들었습니다.

결과:

읽기 좋은 코드를 위한 3가지 원칙

  1. 함수 이름은 그 함수가 하는 일

    • Bad: Process()CalculateScoreByItemList()
    • Bad: ChangeConfig()UpdateGameDifficultyToHardMode()
  2. 주석은 부가적인 설명을 위한 것

    • 좋지 않은 주석:
      // 해당 조건에 따라 값을 변경한다.
      if (condition) { ... }
    • 좋은 주석:
      // 인벤토리 아이템의 소모도율이 30% 이상이라면 즉각 삭제하지 않고, 대신 보관함으로 이동한다.
      if (item.UsageRate > 0.3f) {
          Inventory.MoveToStorage(item);
      }
  3. 함수 하나의 역할은 단일화

    • 한 함수에 여러 가지 작업을 담지 말라.
      // Bad: 한 번의 호출로 로그인, 유저 데이터 로드, 초기화까지 모든 것을 처리
      public void InitializeUser(string userId) {
          Login(userId); // 로그인
          LoadProfile(); // 유저 프로필 불러오기
          SetInitialInventory(); // 초기 인벤토리 설정
      }
    • 분리된 함수로 역할 정의:
      public void LoginUser(string userId) { ... }
      public void LoadProfileData() { ... }
      public void InitializePlayerInventory() { ... }

시니어 개발자 팁:

코드를 읽는 사람은 자신이 직접 만들지 않았다. 동료 개발자를 생각하세요. 그들은 시간을 절약해야 합니다. 상사나 클라이언트를 생각하세요. 그들은 “이 코드가 왜 이런식으로 동작하는지” 설명해야 할 수도 있습니다. 미래의 당신을 생각하세요. 6개월 후의 당신은 현재의 당신을 이해할 수 없습니다.


좋은 코딩 습관: 코드의 불일치가 혼란의 원천이다

2017년, 저와 팀이 개발한 RPG 게임의 아이템 시스템은 초기에 읽기 쉬운 코드로 시작했지만, 시간이 지남에 따라 규칙이 무너졌습니다.

결과: 유지보수에 50% 이상의 시간이 소요되었으며, 새로운 개발자가 합류하였을 때 1주일 이상의 교육이 필요했습니다.

좋은 코딩 습관의 핵심: 일관성

  1. 변수명 규칙을 단순화하세요.

    • 모든 변수는 PascalCase (ItemName) 또는 camelCase (playerScore) 사용
    • 특정한 접두사를 사용하세요: _ (private 변수), m_ (C#의 멤버 변수)
  2. 함수의 역할은 명확하고 단일화

    • 좋은 함수 예:
      public string GetPlayerNameById(int id) {
          // 데이터베이스에서 유저 아이디를 조회하여 이름을 반환
      }
    • 나쁜 함수 예:
      public void ProcessUser(int id) {
          // 아이디로 유저를 찾고, 이름을 가져오고, 로그인 여부를 확인하고...
      }
  3. 주석은 필요할 때만 사용

    • 필요한 주석:
      // 이 함수는 유저의 위치가 지형에 따라 이동 속도를 계산한다.
      public float CalculateMovementSpeed(Vector3 position) {
          // 지형 데이터는 지면이든 물이든 속도에 영향을 미친다.
      }
    • 불필요한 주석:
      // 변수에 값을 할당한다.
      int value = 10; // 10을 대입

실제 개발 현장에서의 적용 방법

시니어 개발자 팁:

코드의 일관성은 디자인 패턴보다 중요하다. 함수 하나가 10가지 역할을 한다면, 다른 개발자가 그 코드를 변경하는 것을 두려워합니다. “함수 하나의 역할은 한 가지”는 원칙을 지킵니다. 그러면 코드의 예측성이 높아집니다.


효율적인 수행: 최적화의 신화와 실용성

2018년, 저의 팀은 게임 서버의 응답 속도를 최적화해야 했습니다. 당시 문제점은 미래의 개발자가 “이 코드는 왜 이렇게 최적화되었지?” 라고 질문할 때, 설명하기 어려웠습니다.

예를 들어, 해당 코드는 “더 빠른 속도로 알고리즘을 돌려 성능을 개선”했으나, 코드 자체는 읽기 어렵고 유지보수성이 낮았습니다.

// Bad: 최적화의 의도는 있지만 읽기 어려움
public int FastFindMaxValue(List<int> numbers) {
    if (numbers.Count > 10)
        return numbers.Skip(5).Max(); // 생략하여 최적화
    else
        return numbers.Max();
}

문제점:

효율적인 프로그램을 위한 3가지 원칙

  1. 플랫폼에 대한 이해가 없으면 최적화도 없다

    • Windows 프로그램이라면 WinAPI의 특징을 이해해야 합니다.
    • Unity3D라면 GC.Alloc이 발생하지 않도록 관리해야 합니다.
    • Web이면 HTTP/2의 특성을 활용하여 최적화할 수 있습니다.
  2. 알고리즘을 선택하기 이전에 데이터의 특성을 파악하세요.

    • 예제 1: 길찾기 알고리즘
      • PriorityQueue가 필요할 수도 있고, Dijkstra 알고리즘이 필요할 수도 있습니다.
    • 예제 2: 유저 데이터 저장
      • HashTable은 빠른 접근이 필요한 경우에만 사용하고, SortedList나 BinarySearchTree는 정렬이 필요한 데이터에만 사용합니다.
  3. 설익은 최적화는 모든 버그의 원인

    • 나쁜 예:
      // 개발 단계에서 성능 최적화를 강요하여 코드가 복잡해짐
      public List<int> GetPlayerScores() {
          // 데이터베이스에서 10만 건의 유저 데이터를 가져온 후
          var scores = db.GetScores();
          // 불필요한 필터링과 정렬까지 한 번에 처리
          return scores.Where(s => s > 0).OrderByDescending(x => x).Take(10).ToList();
      }
    • 좋은 예:
      // 단일 책임 원칙을 적용하여 코드를 분리함
      public List<int> GetTop10PlayerScores() {
          var scores = db.GetScores();
          return FilterPositiveScores(scores)
                 .OrderByDescending(x => x)
                 .Take(10).ToList();
      }

실제 개발 현장에서 적용 방법


적절한 확장성: 미래를 예측하되 과도한 설계를 피하라

2019년, 저의 팀은 신규 게임 엔진을 개발할 때 “미래의 요구사항을 모두 고려”하여 설계하자고 결심했습니다.

결과:

오해의 예:

좋은 프로그램은 모든 가능성을 고려한 확장성 있는 코드여야 한다.

현실은 어떤 요구사항도 예측할 수 없다는 것입니다. 20년 전, 웹 개발자들은 “모바일은 필요가 없다”고 생각했지만, 오늘날 모바일은 전체 트래픽의 50% 이상을 차지합니다.

적절한 확장성 설계의 원칙

  1. 미래는 예측할 수 없다, 하지만 현재 요구사항은 확실하다.

    • 실제 예: 기획서에 “유저 수 10만명”이 적혀 있으면, 스케일링을 고려하세요.
    • “20만명 이상”은 DB 클러스터링, 로드 밸런싱이 필수적입니다.
    • “모른다고 과도한 설계를 피하세요.” → 과도한 상속, 인터페이스 사용을 피하세요.
  2. Design Pattern을 사용할 때, 과도한 추상은 피하세요.

    • 나쁜 예:
      // 모든 클래스가 abstract로 정의되어 있어, 유연성은 있지만 가독성이 떨어짐
      public abstract class DatabaseHandler
      {
          public abstract List<User> GetUsers();
      }
      
      // MySQL용, Oracle용 등 모든 경우를 고려하여 상속 구조로 설계
      public class MySqlHandler : DatabaseHandler { ... }
    • 좋은 예:
      // 현재 필요한 데이터베이스 핸들러를 직접 구현
      public class DatabaseManager
      {
          private List<User> _users;
      
          public List<User> GetUsers()
          {
              // DB 연결 코드
          }
      }
  3. 확장성을 강조하다가 기능이 애매해지면 안 된다.

    • 나쁜 예:
      // 모든 유저 기능이 포함된 거대한 인터페이스
      public interface IGameUser
      {
          void Login();
          void Logout();
          int BuyItem(int itemID);
          void UpdateProfile();
      }
    • 좋은 예:
      // 기능별로 분리하여 단일책임 원칙을 적용
      public interface IGameUserLogin { void Login(); }
      public interface IGameUserShopping { int BuyItem(int itemID); }

실제 개발 현장에서 적용 방법


모듈화: 적절한 분리 없이는 프로그램은 고립된다

2020년, 저의 팀은 게임 서버에서 모듈화를 도입하여 여러 개발자가 병렬 작업할 수 있도록 했습니다.

문제점:

적절한 모듈화의 원칙

  1. 레이어(Layer) 구조로 분리하세요.

    • 예제: 네트워크 게임에서 레이어 구조
      [Front-end (Client)] ↔ [Game Logic Layer] ↔ [Network Layer] ↔ [Database Layer]
    • 장점: 각 레이어의 변경이 다른 레이어에 영향을 미치지 않습니다.
    • 단점: 레이어가 너무 많으면 기능간 통신이 어려워집니다.
  2. 중간자(Mediator)를 두세요.

    • 예제 1: 윈도우 프로그래밍에서 DC(Device Context)
      // 그래픽을 그리기 위해 DC를 확보하여 그립니다.
      using (Graphics g = e.Graphics)
      {
          // 그래픽 작업을 수행
      }
    • 예제 2: Unity에서 Transform 클래스
      // 모든 오브젝트의 위치를 조절하기 위해 Transform을 사용합니다.
      playerTransform.position = new Vector3(10, 20, 0);
  3. 과도한 결합은 시스템을 복잡하게 만듭니다.

    • 나쁜 예:
      // 게임 로직과 네트워크가 직접 연결되어 있는 경우
      public class GameManager
      {
          private NetworkModule _network;
      
          public void Update()
          {
              // 로직 업데이트
              _network.SendPlayerPosition(player);
          }
      }
    • 좋은 예:
      // GameManager는 네트워크에 의존하지 않고, Event를 통해 통신한다.
      public class GameManager
      {
          public void OnUpdate()
          {
              // 로직 업데이트
              EventManager.Trigger("PlayerPositionUpdated", player);
          }
      }
      
      public class NetworkModule
      {
          public void OnEvent(string eventName)
          {
              if (eventName == "PlayerPositionUpdated")
                  SendPlayerPosition(player);
          }
      }

실제 개발 현장에서 적용 방법


핵심 정리: 좋은 프로그램을 만드는 5가지 비결

  1. 읽기 쉽게 코드를 작성하는 것이 개발자의 의무

    • 변수명, 함수명, 주석까지 코드의 모든 부분이 다른 개발자를 고려하여 작성되어야 합니다.
    • 코드는 문서화의 일환이라는 것을 기억하세요.
  2. 일관성이 없는 코드는 유지보수 비용을 증가시킨다

    • 변수명, 함수명, 코드 스타일이 불일치하면 다른 개발자가 수정할 때 실수를 하게 됩니다.
  3. 효율적인 코드보다 읽기 쉬운 코드를 우선하세요

    • 성능 최적화는 코드 리팩토링 이후에 진행해야 합니다.
  4. 미래를 예측하되 과도한 설계를 피하세요

    • 현재 요구사항에 집중하고, 미래는 주석으로 남기세요.
  5. 적절한 모듈화 없이는 시스템은 고립된다

    • 레이어 구조와 중간자를 통해 기능 간 결합을 최적화하세요.

마치며: 좋은 프로그램의 진정한 의미는 다른 개발자의 편리함

저는 2012년 입사한 초보 개발자였을 때, “프로그램이 잘 돌아만 가면 돼!” 라는 생각을 했습니다.

하지만 10년 넘게 개발을 해온 시니어 개발자가 된 지금은 다릅니다.

좋은 프로그램의 진정한 정의는 “나를 대신하여 코드를 읽고 수정할 개발자가 쉽게 이해할 수 있는 프로그램”입니다.

당신의 코드는

좋은 프로그램의 첫 번째 단서는 “나를 대신하여 코드를 읽을 사람이 있다는 생각”에서 시작합니다.

오늘부터, 당신은 그 사람임을 기억하세요.

← 목록으로