상세 컨텐츠

본문 제목

알고리즘 문제 풀이: 좋은 코드를 작성하기 위한 고민들

문제풀이

by Gravekper 2021. 9. 28. 09:52

본문

이 문서에서는 알고리즘 문제 풀이 과정에서 좋은 코드를 작성하기 위한 지향점들을 제시합니다. 그리고 문제 풀이 코드에 적용할 수 있는 코드 관리 규칙들을 소개합니다.

기업에서 코드를 작성할 때에는 코딩 컨벤션을 적용할 때가 많습니다. 이런 코딩 컨벤션은 규모가 큰 소스 코드를 효율적으로 관리하기 위한 규칙들입니다. 알고리즘 문제를 푸는 코드는 문제를 풀고 나면 다른 사람들에게 보여주지 않고 버리기 때문에 추구하는 가치가 조금 다릅니다. 이 문서에서는 그런 가치들이 어떻게 다른지를 중심으로 고민합니다.

지향점들

가독성

내가 디버깅하기 위해

정확한 코드를 짜는 것이 무엇보다 중요합니다. 하지만 작성한 코드가 매번 틀리지 않을 수는 없습니다. 정답을 맞추는 코드를 운 좋게 한 번에 완성한 경우가 아니라면 내가 작성한 코드를 다시 읽고 틀린 부분을 고쳐야 합니다. 읽기 힘든 코드를 작성했다면 그만큼 자신을 괴롭히는 과정이 됩니다. 가능하면 과거의 자신과 싸우지 않고 코드를 읽으면 좋겠습니다. 그래서 내가 다시 읽을 수 있는 코드를 작성하는 것이 중요합니다.

프로그래밍 대회에서 좋은 성적을 내거나 코딩 테스트에서 높은 점수를 받는 모든 분들의 코드가 읽기 좋게 짜여 있는 것은 아닙니다. 실제로 BOJ나 Codeforces에서 상위 유저들의 코드를 열람해 보면 도저히 구조를 파악할 수 없게 되어있는 경우가 많습니다. 그런 암호문같은 코드를 작성하는 분들도 대개 디버깅할 때에는 자신의 코드를 읽을 수 있습니다.

남에게 보여주기 위해

팀 대회를 준비하거나 여러 사람이 함께 공부한다면 다른 사람에게 코드를 설명해야 하는 일이 생기기도 합니다. 예를 들어 ICPC와 같은 팀 대회에서는 팀원들에게 자신의 코드를 보여주는 일이 자주 있습니다. 그럴 때 코드의 구조가 팀원을 혼란스럽게 하지 않으면 좋을 것입니다. 그럴 때에는 함께 작업하는 사람들과 컨벤션을 맞춰 놓는 것도 고려할 만 합니다.

코딩 테스트들 중에는 문제를 푼 뒤에 '지원자님, 이 코드는 왜 이렇게 짜셨어요?'같은 질문을 하는 경우도 있습니다. 자신이 작성하는 코드가 어떤 기능을 하는지 잘 파악하고 있으면 이런 질문은 큰 문제없이 대답할 수 있을 겁니다. 하지만 그 부분이 남에게 보이기 부끄러운 코드는 아니면 좋겠습니다.

작성 속도

가능한 빠르게 작성하고 테스트해서 제출해야 다음 문제를 해결할 수 있습니다. 그래서 코드 글자 수가 짧고 타이핑하기 편한 구현방법을 선택할 때가 많습니다. 이 때 다른 지향점들을 희생하는 경우가 있습니다. 상황에 따라 충돌하는 가치들 사이에서 선택이 필요합니다.

휘발성

새로운 코드를 작성할 때에는 표준 라이브러리만 사용해 거의 모든 부분을 새로 작성해야 합니다. 작성한 코드를 제출하고 '맞았습니다'를 받으면 코드는 버려집니다.

그래서 클래스나 연산자 재정의처럼 구현하는 데에 오래 걸리는 편의 기능을 적용하기 어렵습니다. 사용하는 언어의 표준 라이브러리에 특정 기능 지원이 미흡하다면 새로 구현하기 까다로운 경우가 있습니다.

이런 문제를 개선하기 위해 미리 작성된 코드를 프로그램에 포함하는 경우가 있습니다. 사전 작성된 코드 사용이 가능한지는 플랫폼이나 대회마다 규칙이 다르기 때문에 미리 확인하는 것이 좋습니다.

라스트 스퍼트

거의 완성된 코드에서 문제점을 한두 개 찾아 그 부분만 잘 작동하도록 고쳐 제출할 때가 많이 있습니다. 어떨 때에는 한 문제에서 그런 수정을 세 번 이상 하게 되는 경우도 있습니다. 당연히 그런 과정을 반복할 때마다 코드의 구조는 무너지고 읽기 어려워집니다.

이런 과정은 문제를 푸는 과정에서 자연스럽게 발생하고, 잘못된 것이 아닙니다. 하지만 가끔 급하게 수정한 코드가 너무 많이 쌓여서 어떻게 동작할지 예측하기 어려워지기도 합니다. 코드가 얼마나 완성되었는지 파악하고 긴급 수정을 너무 일찍 시작하지 않는 습관을 들이면 그런 문제를 어느 정도 예방할 수 있습니다.

소프트웨어 개발을 위한 코드와 비교하면

위에서 언급한 특성들은 휘발성 없이 오랫동안 유지보수해야 하는 코드들의 특성과 거리가 있습니다. 그래서 이 주제에 대한 논쟁이 발생하기도 하고 가끔은 이런 점들 때문에 알고리즘 공부가 쓸모없다고 주장하는 분들도 있습니다. 알고리즘 문제를 푸는 코드와 소프트웨어를 개발하는 코드는 목적과 지향점이 많이 다르므로 상황에 맞는 코드를 작성해야 합니다.

개발 경험이 많지 않은 상태에서 알고리즘 공부를 시작하고, 언젠가 소프트웨어를 개발할 계획이 있다면 문제 풀이와 소프트웨어 개발을 위한 코드에 어떤 차이점이 있고 왜 그런지 알아두면 좋습니다.

휘발성 코드를 짜는 습관이 들어 있다면 유지보수를 고려하는 쪽으로 습관을 바꾸는 데에 시간이 걸릴 수 있습니다. 저는 주변에서 문제풀이를 위해 프로그래밍 공부를 시작한 이후에 소프트웨어 개발을 시작한 분들을 많이 보았는데 습관을 새로 들이면서 고생하는 분들이 있지만 대개 시간이 지나면 큰 문제없이 적응하게 됩니다.

만약 이미 개발 경험이 많거나 알고리즘 문제 풀이를 오랫동안 공부할 계획이 아니라면 이 주제에 대해 많이 고민할 필요는 없습니다. 평소에 소프트웨어 개발에 사용하던 습관대로 코드를 작성해도 큰 문제는 없습니다. 평소보다 조금 더 느슨한 규칙으로 코드를 작성한다고 생각하면 됩니다.

읽기 좋은 코드를 작성하기 위한 규칙들

아래는 디버깅 과정에서 코드의 구조를 다시 파악하고 변경하거나 다른 사람에게 코드를 보여줄 때 더 쉽게 이해할 수 있게 하기 위해 적용할 수 있는 규칙들입니다. 모든 내용은 '권장 사항'과 '제안' 사이에 있습니다. 여러분이 직접 사용해 보고 마음에 드는 방향으로 선택해 사용하면 됩니다.

변수의 유효 범위(scope) 지정

변수가 정의된 범위를 엄격하게 관리하면 코드 작성과 디버깅 과정에서 실수를 줄일 수 있습니다. 유효 범위를 잘못 지정하면 초기화와 관련된 실수를 많이 하게 됩니다.

전역 변수는 적게 사용하는 것이 좋습니다. 하지만 함수 여러 개가 같은 변수에 여러 번 접근하는 경우에 사용하는 함수마다 같은 인자를 매번 넣어주는 것보다 전역 변수를 쓰는 것이 나을 때가 있습니다. 규모가 큰 프로젝트에서 그런 문제가 발생하면 클래스를 만들어 관리할 수 있습니다. 하지만 문제를 풀면서 클래스를 만드는 건 너무 오래 걸립니다. 그래서 전역 변수를 쓰는 일이 생깁니다.

전역 변수를 사용하면 테스트 케이스가 있는 문제를 처리할 때 초기화 문제가 자주 발생하니 주의해야 합니다. 그럴 때에는 테스트 케이스가 시작될 때마다 모든 전역 변수를 초기화하도록 코드를 작성하면 됩니다.

이름 짓기

변수나 함수 이름에 특별히 엄격한 규칙을 적용할 필요는 없습니다. 사용하는 언어의 예약어나 라이브러리와 겹치지만 않으면 됩니다. 너무 길어서 타이핑하기 어렵거나 한 줄이 지나치게 길어지게 하는 이름은 피합시다.

자신에게 익숙한 패턴을 만들면 디버깅하기 쉬워집니다. 만약 다른 사람에게도 보여줘야 하는 코드라면 자신만 알아볼 수 있는 짧은 이름을 너무 많이 사용하지 않는 것이 좋습니다.

작은 블록 내에서 특정한 목적을 위해 잠깐동안 사용하는 변수를 만들 때에는 이름을 통해 변수가 하는 일을 알 수 있도록 짓는 것이 좋습니다. 예를 들어, 특정 구간 i부터 j까지 원소들의 합을 저장한 변수를 'sum1'이라고 짓는 것보다 'sum_i_j'라고 지으면 그 변수가 어떤 역할을 하는지 나중에 파악하기 쉽습니다.

한 줄을 너무 길게 쓰지 않기

한 줄에 너무 많은 글자가 들어가면 내용을 파악하기 어려워집니다. 그럴 때에는 적당한 곳에서 줄을 바꾸면 읽기 쉬워집니다.

if((S[0][x2 + 1] - S[0][x1]) == (x2 - x1 + 1) && (S[1][y2 + 1] - S[1][y1]) == (y2 - y1 + 1)) {
//너무 길다
    
if((S[0][x2 + 1] - S[0][x1]) == (x2 - x1 + 1) && 
   (S[1][y2 + 1] - S[1][y1]) == (y2 - y1 + 1)) {

위 코드처럼 비슷한 조건 두 개를 이어서 비교하는 경우 줄바꿈을 통해 두 조건문을 같은 열에 배치하면 조건문을 관리하기 편리해집니다.

C++은 코드 위에서 줄바꿈이 자유로운 편입니다. Python에서는 줄바꿈을 하려면 앞쪽 줄 끝에 '\'를 넣거나 괄호 안에서 줄바꿈을 해야 합니다.

주석

기억할 자신이 없는 부분에 주석을 작성하면 도움이 될 때가 많습니다. 예를 들면 이런 경우입니다.

  • '어디가 시계방향 풀이에 대한 코드이고 어디가 반시계방향에 대한 코드인가'
  • '이 함수의 수행 시간이 O(log n)이라는 것을 기억해야 하는 경우'

어떤 부분이 기억할 만하고 어떤 부분이 기억하기 어려운지는 문제를 푸는 경험이 쌓이면 좀 더 쉽게 파악하게 됩니다.

일관성

혼자 짜는 코드에 치명적인 일관성 문제가 있더라도 자신은 불편하다고 느끼지 않아서 그렇게 쓰는 것이기 때문에 디버깅에 큰 문제가 생기지는 않습니다.

반대로 다른 사람에게 문제를 푸는 코드를 보여줄 때 코드의 일관성이 떨어지면 읽는 난이도가 올라가고 읽는 사람에게 나쁜 인상을 줄 수 있습니다. 이후에 알고리즘 문제 해결 이외에 프로그래밍을 할 계획이 있다면 일관성 있는 코드를 작성하기 위해 고민할 가치가 있습니다.

오토 인덴트 사용하기

개발 도구의 코드 관리 설정을 사용해 자동으로 코드가 정리되도록 설정하고, 자주 코드를 정리하세요. 예를 들어, 코드를 저장(Ctrl+S)할 때마다 코드가 정리되도록 하고 자주 저장하면 큰 힘을 들이지 않고 코드를 읽기 좋은 모양으로 유지할 수 있습니다.

코드 작성 습관에

이 부분에 설명하는 내용은 조금 맥락이 다르지만 비슷한 문제들을 해결하기 때문에 함께 언급합니다.

자주 빌드하기

문제 풀이 코드를 짤 때에 수십 줄을 작성하는 동안 빌드를 한 번도 누르지 않는 경우가 많이 있습니다. 이렇게 빌드를 오랫동안 미루면 코드 윗쪽에서 만들어진 (주로 타입과 관련된) 문제가 수정되지 않은 채로 눈덩이처럼 커져 나중에 여러 줄을 고쳐야 하는 일이 자주 생깁니다.

이런 문제를 해결하려면 빌드 버튼을 자주 눌러서 컴파일하는 데에 문제가 없는지 자주 확인해야 합니다. 문제 풀이 과정에서 만드는 프로그램은 보통 1초 안에 빌드 가능하기 때문에 시간이 오래 걸리지도 않습니다. 빌드 버튼을 누르지 않더라도 백그라운드에서 컴파일 오류를 확인해주는 IDE를 사용하면 이런 문제를 미리 파악할 수 있습니다. 이런 경우에도 가능하면 컴파일러가 오류를 지적해줄 때마다 바로 고쳐야 문제가 쌓이는 것을 막을 수 있습니다.

기능별로 코드 나누기

프로그램을 작성하는 과정에서 어떤 부분에서 기능 단위로 분리할 수 있는지 파악하면 유리합니다. 코드를 기능별로 나누기 위해 반드시 서로 다른 함수에 구현할 필요는 없습니다. 어디서 어떤 기능이 끝나는지 기억해 두거나 기억하기 어렵다면 간단한 주석으로 표기하면 좋습니다. 디버깅 과정에서 특정 상태에 대해 출력하거나 디버거의 breakpoint로 사용할 지점을 선택할 때에도 도움이 됩니다.

유닛 테스트하기

여러 단계를 거쳐야 하는 문제를 해결할 때에는 가능하면 각 단계를 완성할 때마다 적합한 결과물이 나오는지 확인하는 것이 좋습니다. 여러 가지 장점이 있지만 문제 풀이에서는 모든 코드를 완성한 뒤에 테스트하는 것보다 기억력을 잘 활용할 수 있습니다.

 

관련글 더보기

댓글 영역