[Project design] 프로그램 생산과 작성(코딩) - 함수 작성법


Study/Project design  2020. 12. 15. 19:10

안녕하세요. 명월입니다.


이 글은 프로그램 생산과 작성(코딩) - 함수 작성법에 대한 글입니다.


실제 프로그램 코딩은 공정에서는 보통 생산이라던가 작성이라는 표현을 많이 사용합니다. 그냥 단어의 표현일 뿐이고 실제는 우리가 아는 프로그램 코딩이 됩니다.

실무에서 혹은 공정을 설정해서 프로그램 작성을 한다고 해서 크게 달라지는 건 없습니다. 앞에 설계도가 작성되었으면 그대로 작성하는 것 뿐입니다.

여기서는 각자의 스타일이 있고 작성법이 있기 때문에 딱히 제가 쓸 말은 없지만 그래도 공정이기 때문에 제 경험에 의한 프로그램 작성법을 설명할까 합니다.


일반적인 워터폴 공정으로 프로젝트를 운영했으면 기본 설계와 상세 설계 단계에서 이미 프로그램 작성에 대한 설계가 다 놔왔으니 그대로 작성만 하면 됩니다.

따로 여기서 이야기 할 만한 내용은 없겠네요.

우리가 애자일 공정이던가 제가 자주 사용하는 짬뽕(설계는 애자일, 테스트는 워터폴)공정으로는 저만의 작성법이 있습니다.

먼저 애자일 공정에서 프로그램 설계서를 작성하는 경우도 있습니다만, 보통은 Jira나 Redmine 툴을 이용해서 스크럼 주기를 설정해서 아이템(Ticket)별로 작성하는 것이 전부입니다.

그래서 상세 설계 단계에서 최대한 추상화와 인터페이스를 만듭니다. 그래도 막상 프로그램을 작성하게 되면 그 때 예기치 못했던 공통 부품과 추상화가 필요한 클래스, 함수가 있습니다.

이때 우리가 공통 부품을 작성하는 방법에 대해 설명해 볼까 합니다.


이 공통 부품을 만들 때 저는 항상 피보나치 수열 알고리즘을 생각합니다.보통 피보나치 알고리즘은 재귀함수를 사용해서 만들게 됩니다.

using System;
// 실행 클래스
class Program
{
  // 피보나치 수열 값 계산
  private static int Fibonachi(int index)
  {
    // 0 이하는 에러
    if(index <= 0)
    {
      throw new IndexOutOfRangeException();
    }
    // 1번째와 2번째는 값이 1이다.
    if(index == 1 || index == 2)
    {
      return 1;
    }
    // f(x-2) + f(x-1)
    return Fibonachi(index - 2) + Fibonachi(index - 1);
  }
  // 실행 함수
  static void Main(string[] args)
  {
    // 1부터 10까지 피보나치 수열 표현
    for(int i = 1; i < 10; i++)
    {
      Console.Write(Fibonachi(i) + " ");
    }
    // 콘솔 출력
    Console.WriteLine();
    Console.WriteLine("Press any key...");
    Console.ReadKey();
  }
}

사실 재귀 함수를 자주 사용하는 건 프로그램 성능상 좋지는 않습니다. 굳이 for문으로 작성할 수 있는 것을 재귀 함수를 만들어서 작성할 필요는 없습니다.

그러나 함수를 이해하는 데는 재귀 함수만큼 좋은 예시가 없기 때문에 작성해 보았습니다.


우리가 함수를 작성하는 데 있어서 저도 대학 시절, 신입 시절에 많이 실수하던 부분이기도 합니다만 그냥 소스가 길어지니깐 보기 편하게 나누는 용도로만 생각한 적이 있습니다.

즉, 그냥 소스를 위에서 아래로 읽는 부분에 있어서 한 함수에 모든 소스를 나열하기 불편하니 그냥 함수로 쪼개는 역할(?)로만 사용한 것입니다.

그런식으로 함수를 접근하게 되면 위 피보나치 재귀 함수는 이해하기 힘들어 집니다.

Fibonachi 함수에 10이란 값을 넣었을 경우, Fibonachi(9) + Fibonachi(8)를 계산하고 Fibonachi(8) + Fibonachi(7) + Fibonachi(7) + Fibonachi(6) 머리 속에서 이런 형태로 계산이 되기 때문에 머리가 복잡해 집니다.


우리가 중고등학교 수학 시간에 배운 함수 방정식이 있습니다. f(x) = 2x + 1같은 식입니다.

그 때, 우리가 f(1) + f(2) = 3 + 5 = 8이라는 것을 알고 있습니다. 이처럼 함수를 순서로 생각하지 말고 파리미터 input과 결과의 output만 생각하는 것입니다.

즉, Fibonachi(1)와 Fibonachi(2)는 1이고, Fibonachi(3)는 Fibonachi(2) + Fibonachi(1)로 2가 되는 것이고 Fibonachi(4)는 Fibonachi(3) + Fibonachi(2)로 3이 되고 Fibonachi(5)는 Fibonachi(4) + Fibonachi(3)로 5가 됩니다.

이렇게 생각하게 되면 재귀 함수는 그냥 눈에 확 들어오기 시작합니다.


그럼 피보나치는 워낙 쉬우니 하노이 알고리즘으로 예를 들어보겠습니다.

using System;
// 실행 클래스
class Program
{
  // 개수, 시작, 중간, 종착
  static void Hanoi(int disks, string from, string by, string to)
  {
    // 0 이하는 에러
    if (disks < 1)
    {
      throw new IndexOutOfRangeException();
    }
    // 마지막 한 개는 이동으로 끝난다.
    if (disks == 1)
    {
      // 콘솔 출력
      Console.WriteLine("{0} -> {1}", from, to);
      return;
    }
    // 한개를 빼고 from에서 중간으로 넘긴다.
    Hanoi(disks - 1, from, to, by);
    // 콘솔 출력
    Console.WriteLine("{0} -> {1}", from, to);
    // 중간에 있는 걸 to로 넘긴다.
    Hanoi(disks - 1, by, from, to);
  }

  // 실행 함수
  static void Main(string[] args)
  {
    // 3개의 원반을 a,b,c의 봉으로 넘긴다.
    Hanoi(1, "a", "b", "c");
    // 콘솔 출력
    Console.WriteLine("Press any key...");
    Console.ReadKey();
  }
}

하노이는 그냥 재귀로 디버깅 순서로 쫓으려고 하면 머리 터집니다. 콘솔 출력 전에 재귀 함수 호출이고 콘솔 출력 후에 또 재귀 함수 호출입니다.

그러나 우리는 input과 output만 생각해 봅시다.

단순하게 Hanoi(1)은 a -> c로 이동하고 끝입니다.

Hanoi(2)는 먼저 Hanoi(1)가 실행인데 대상이 a -> b로 이동입니다. 그리고 가장 큰 원반을 a -> c로 이동합니다. 마지막으로 b에 있는 걸 c로 이동합니다.

Hanoi(3)은 마지막 원반 빼고 a에 있는 걸 모두 b로 옮깁니다. 그 중간의 내용은 생각하지 말고 그냥 재귀의 값만 생각해서 a에 있는 걸 모두 b로 옮기는 것입니다. a에 있는 걸 모두 b로 옮기고 나면 그리고 가장 큰 원반을 a에서 c로 옮깁니다.

그리고 마지막으로 b에 있는 원반을 모두 c로 옮기는 것입니다. 이렇게 생각하면 함수가 쉬워집니다.


즉, 우리가 코딩을 할 때, 단순히 함수가 길어지는 것을 생각해서 만드는 것이 아니라 위처럼 함수화 할 수 있는 것으로 공통을 만드는 것입니다.

using System;
// 실행 클래스
class Program
{
  // 추출 함수
  static string Extraction(string data)
  {
    return "";
  }
  // 취득 함수
  static int GetDataBaseResult()
  {
    return 0;
  }
  // 값 재설정 함수
  static string ResetData(string data)
  {
    return "";
  }
  // 데이터 베이스에 입력 함수
  static bool InsertDatabase(string data)
  {
    return true;
  }
  // 실행 함수
  static void Main(string[] args)
  {
    // 값을 추출
    var data = Extraction("");
    // 데이터 베이스로부터 데이터를 취득
    int db = GetDataBaseResult();
    // 값을 재 설정
    data = ResetData(data + db);
    // 데이터 베이스로 입력
    if (InsertDatabase(data))
    {
      // 콘솔 출력
      Console.WriteLine("OK");
    }

    // 콘솔 출력
    Console.WriteLine("Press any key...");
    Console.ReadKey();
  }
}

실무에서 작성하는 Facade 패턴으로 만들어 보았습니다. Main함수에 그냥 모든 로직을 넣는 것이 아니라 행위 별로 함수를 묶고 Main에는 설계도처럼 실행 행위와 결과만 확인하는 것입니다.

즉, Extraction의 함수 내용을 생각하지 말고 Extraction(값)을 넣으면 반드시 무슨 값이 나온다만 생각해서 프로그램을 작성하는 것입니다.


이렇게 하면 무슨 효과가 있냐면 Main함수는 설계도처럼 어떻게 동작하는 지가 소스로 모든게 보이게 됩니다. 즉, 설계도가 필요가 없어지는 것입니다. 또, 함수의 재사용성도 많이 향상되고 크게는 설계도를 작성하지 않아도 프로그램 전체가 보이는 효과도 있습니다.

그래서 개인적인 생각으로 설계도 생략으로 프로그램 코딩을 하려면 역시 디자인 패턴이 필수겠네요..


여기까지 프로그램 생산과 작성(코딩) - 함수 작성법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.