Post

좋은 코드를 작성하기 위한 원칙

좋은 코드를 작성하는 것의 필요성과, 일반적으로 좋은 코드를 작성하기 위한 주요 원칙들을 알아본다.

좋은 코드를 작성하는 것의 필요성

당장의 구현을 위해 빠르게 코드를 작성하는 것에만 급급하면 기술 부채가 감당 불가능한 수준으로 불어나 추후 유지보수에 문제가 생길 수 있다. 따라서 개발 프로젝트를 진행할 때 가급적이면 처음부터 가독성 좋고 유지보수가 용이한 좋은 코드를 작성하는 것은 두말할 필요 없이 중요하다.

알고리즘 문제해결(PS, Problem Solving)이나 프로그래밍 대회(CP, Competitive Programming)의 경우 보통 문제 해결에 사용한 코드를 해당 문제풀이나 대회가 끝나고 나면 재사용할 일이 없고, 특히 CP의 경우 시간제한이 있기 때문에 좋은 코드를 작성하는 것보다 빠른 구현이 더 중요하지 않느냐는 얘기도 있다. 이 질문에 답하기 위해서는 본인이 무엇을 위해 PS/CP를 하고 어떤 방향을 추구하는지 생각해 볼 필요가 있다.

개인적으로 생각하기에 PS/CP를 통해 배울 수 있는 점들은 다음과 같다.

  • 주어진 실행시간 제한과 메모리 제한 등의 조건 내에서 문제를 해결하는 과정에서 다양한 알고리즘과 자료구조를 사용해 보고 익힐 수 있으며, 이를 통해 실제 프로젝트를 진행할 때도 특정 상황에서 어떤 알고리즘과 자료구조를 사용하면 좋을지 감을 익힐 수 있음
  • 코드를 작성하고 제출하고 나면 즉각적으로 정답/오답 여부와 실행시간, 메모리 사용량에 대한 객관적인 피드백을 받을 수 있으므로, 놓치는 부분 없이 정확한 코드를 빠르고 능숙하게 작성하는 연습을 할 수 있음
  • 다른 고수들이 작성한 코드를 보며 자신이 작성한 코드와 비교해보고 보완점을 찾을 수 있음
  • 실제 개발 프로젝트에 비하면 작은 규모의, 비슷한 기능을 하는 코드를 반복적으로 작성하므로, (특히 혼자 PS를 연습하는 경우) 마감 시한 등에 얽메이지 않고 디테일에 신경쓰면서 간결하고 좋은 코드를 작성하는 연습을 할 수 있음

PS/CP를 단순히 취미로만 즐기는 경우도 물론 있을 수 있지만, 나처럼 PS/CP를 간접적으로 프로그래밍 실력을 기르기 위해 하는 경우라면 마지막의 ‘좋은 코드를 작성하는 연습’ 또한 앞선 3개 못지않게 큰 이점이다. 좋은 코드를 작성하는 것도 처음부터 자연스럽게 되는 게 아니라 반복적인 연습을 통해 꾸준히 숙달해야 하기 때문이다. 또한 복잡하고 읽기 어려운 코드는 디버깅이 어렵고 본인도 오히려 한 번에 정확하게 작성하기 쉽지 않으므로, 비효율적인 디버깅에 시간을 뺏기다 보면 막상 그리 빠르게 구현하지도 못하는 경우도 많다. PS/CP는 물론 현업과 큰 차이가 있지만, 그렇다고 해서 좋은 코드를 작성하는 것을 아예 신경쓰지 않고 당장의 구현에 급급하는 것은 위와 같은 이유로 주객전도이므로 개인적으로는 PS/CP에서도 간결하고 효율적인 코드를 작성하려고 노력하는 편이다.

좋은 코드를 작성하기 위한 원칙

대회에서 작성하는 코드든 실무에서 작성하는 코드든 좋은 코드라고 할 만한 조건은 크게 다르지 않다. 이 글에서는 일반적으로 좋은 코드를 작성하기 위한 주요 원칙들을 다룬다. 다만 PS/CP에서 빠른 구현을 위해 실무에 비하면 상대적으로 타협하는 부분이 있을 수 있는데, 이런 경우는 글 내에 별도로 언급하겠다.

간결한 코드 작성

“KISS(Keep It Simple, Stupid)”

  • 코드가 짧고 간결할수록 당연히 오타나 단순한 버그가 생길 우려가 줄고, 디버깅도 쉬움
  • 가급적 별도의 주석 없이도 쉽게 해석할 수 있게끔 작성하고, 정말 필요한 경우에만 주석을 달아 세부설명을 추가. 주석에 의존하기보다 코드 구조 자체를 간결하게 유지하는 것이 바람직함.
  • 주석을 작성할 경우에는 명확하고 간결하게 작성
  • 하나의 함수에 전달하는 인수는 3개 이하로 하고, 그보다 많은 인수를 함께 전달해야 한다면 하나의 객체로 묶어서 전달
  • 조건문의 깊이(depth)가 이중, 삼중으로 깊어지면 가독성을 저하하므로, 조건문의 깊이를 늘리는 건 가급적이면 지양해야 함 . ex) 위의 코드보다 보호절 숙어(Guard Clause)를 활용한 아래의 코드가 가독성 면에서 유리함

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    async def verify_token(email: str, token: str, purpose: str):
        user = await user_service.get_user_by_email(email)
      
        if user:
            token = await user_service.get_token(user)
      
            if token :
                if token.purpose == 'reset':
                    return True
        return False
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    async def verify_token(email: str, token: str, purpose: str):
        user = await user_service.get_user_by_email(email)
      
        if not user:
            return False
        
        token = await user_service.get_token(user)
      
        if not token or token.purpose != 'reset':
            return False
        
      return True
    
  • 다만, PS/CP에서는 여기서 더 나아가 코드의 길이를 줄여 빠르게 작성하기 위해 간혹 C/C++의 매크로를 활용하는 편법을 사용하는 경우가 있음. 시간이 촉박한 대회에 한해 종종 사용하면 유용하지만, PS/CP에 한해 먹히는 방법이고 일반적으로 C++에서의 매크로 사용은 지양해야 함.
    ex)

    1
    
    #define FOR(i,n) for(int i=0; i<n; i++)
    

코드 모듈화

“DRY(Don’t Repeat Yourself)”

  • 같은 코드를 반복 사용하는 경우 해당 부분을 함수나 클래스로 분리해 재사용
  • 모듈화를 통해 코드를 적극적으로 재사용하면 가독성이 좋아지고, 추후 코드를 수정할 일이 생겼을 때 해당 함수나 클래스를 한 번만 수정하면 되므로 유지보수가 용이해짐
  • 원칙적으로는 한 함수가 두 가지 이상의 일을 하지 않고 하나의 기능만을 수행하는 것이 이상적임. 다만 PS/CP에서 작성하는 코드는 대개 단순한 기능을 수행하는 작은 규모의 프로그램이므로 재사용에 한계가 있고, 시간이 제한되어 있으므로 실무에서처럼 엄격하게 원칙을 따르긴 어려울 수 있음.

표준 라이브러리 활용

“Don’t reinvent the wheel”

  • 알고리즘이나 자료구조를 공부하는 단계에서는 큐나 스택과 같은 자료구조, 정렬 알고리즘 등을 직접 구현해보며 원리를 이해하는 게 유용하지만, 그게 아니라면 표준 라이브러리를 적극적으로 활용하는 게 좋음
  • 표준 라이브러리는 이미 수없이 많이 사용되고 검증되었으며, 최적화도 잘 되어 있어 직접 다시 구현하는 것보다 효율적임
  • 이미 있는 라이브러리를 가져다 사용하면 되므로 불필요하게 동일한 기능을 하는 코드를 직접 구현하느라 시간을 낭비할 필요가 없고, 협업 시에 작성한 코드를 다른 팀원이 이해하기도 쉬움

일관적이고 명확한 명명법 사용

“Follow standard conventions”

  • 모호하지 않은 변수명과 함수명 사용
  • 보통 사용하는 프로그래밍 언어마다 그에 맞는 명명규약(naming convention)이 있으니, 사용하는 언어의 표준 라이브러리에서 사용하는 명명규약을 익히고 클래스, 함수, 변수 등을 선언할 때 일관적으로 적용
  • 각각의 변수와 함수, 클래스가 어떤 기능을 하는지, 그리고 불린(boolean) 타입이라면 어떤 조건에서 참(True)를 반환하는지가 명확히 드러나도록 명명

모든 자료는 정규화해서 저장

  • 모든 자료는 하나의 일관된 형식으로 정규화하여 처리
  • 같은 자료가 두 가지 이상의 형식을 가지면 문자열 표현이 약간씩 달라지거나, 해시 값이 달라지는 등 잡아내기 어려운 미묘한 버그가 발생할 수 있음
  • 시간대, 문자열 등의 자료를 저장하고 처리할 때는 입력받거나 계산하자마자 UTC, UTF-8 인코딩 등 하나의 표준 형식으로 변환해야 함. 해당 자료를 표현하는 클래스의 생성자에서 처음부터 정규화를 수행하거나, 자료를 입력받는 함수에서 곧바로 정규화를 수행하는 것이 좋음.

코드의 논리와 데이터를 분리

  • 코드의 논리와 상관 없는 데이터는 조건문 안에 직접 넣지 말고 별도의 테이블로 분리
    ex) 위의 코드보다 아래의 코드와 같이 작성하는 것이 바람직함.

    1
    2
    3
    4
    5
    6
    
    string getMonthName(int month){
      if(month == 1) return "January";
      if(month == 2) return "February";
      ...
      if(month == 12) return "December";
    }
    
    1
    2
    3
    4
    5
    
    const string monthName[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"};
    
    string getMonthName(int month){
      return monthName[month-1];
    }
    
This post is licensed under CC BY-NC 4.0 by the author.

Comments powered by Disqus.