Welcome to F#(6) - 비교본능.

F# 2009. 4. 23. 09:27 Posted by 알 수 없는 사용자
-지난시간에 이어서.
잘들 지내셨는지요. 어느새 수요일이 다 지나가는 군요. 오늘은 어제에 비해서 일이 잘 되셨나요? 그렇습니다. 사람은 자연스럽게 비교를 하려는 본능비스무리 한게 있는거 같습니다. 여친과 다른 사람들을 비교하고, 자기 자식이랑 친구아들을 비교하고, 심지어는 별 찌질한것까지도 비교하면서 그 우위에서 오는 만족감을 느끼려는게 사람이 아닐까요.

하지만, 비교는 좋은 이해의 틀이기도 합니다. 뭔가가 틀리다는 건, 다른 것들과 부딪히면서 비로소 틀린점들이 눈에 들어오기 시작하고 형체를 갖추기 시작합니다. 이번 포스트에서는 지난 포스트에서 봤었던 F#예제와 같은 일을 하는 C#예제를 통해서 차이점을 좀 찾아보고자 합니다. ........늘 느끼는 거지만, 참 서두를 번지르르하게 써놓고 시작을 하네요.-_-



- C#코드부터 보자!

넵. 보여드리겠습니다. 보여드려야죠~. 다만, 아래의 코드는 제가 짠 코드일뿐 모범적인 코드라고는 볼 수 없다는 걸 생각하고 봐주시기 바랍니다.


class Demo
    {
        private string destDir;
        private List<STRING> finalFiles;

        public Demo(string destDir)
        {
            this.destDir = destDir;
            finalFiles = new List<STRING>();
        }

        private List<STRING> GetSthAll(Func<STRING, string[]> GetSth, string path)
        {
            return GetSth(path).ToList();
        }

        public void PrintAllFilesCount(string ext)
        {
            GetAllFiles(destDir);
            List<STRING[]> filesWithExt = finalFiles.Select(file => file.Split('.')).ToList();
            int countOfExt = filesWithExt.Where(file => (file.Length == 2) && (file[1] == ext)).Count();

            Console.WriteLine(countOfExt);
        }

        private void GetAllFiles(string destDir)
        {
            finalFiles.AddRange(GetSthAll(Directory.GetFiles, destDir));
            foreach (string currentDir in GetSthAll(Directory.GetDirectories, destDir))
            {
                GetAllFiles(currentDir);
            }
        }
    }

그리고 위 코드는 아래처럼 실행할 수 있습니다.


class Program
    {
        static void Main(string[] args)
        {
            Demo demo = new Demo(@"c:\ATI");
            demo.PrintAllFilesCount("txt");
        }
    }

위의 코드는 이전 포스트의 코드와 같은 결과를 내는 코드입니다. F#에서는 중간결과를 따로 저장하지 않고 |>를 통해서 바로바로 다음함수로 연결했었기 때문에 중간값의 상태저장에 대한 고려가 필요없었습니다. 그 때문에 제귀호출 과정의 데이터를 어디에 저장해야 할지 역시 고려하지 않아도 되었습니다. 즉, 데이터가 어디에서 어디로 가는지에 대한 데이터의 흐름이 추상화 되어 있었던 것이죠. 위의 코드를 보시면, 명령형언어에서 오는 한계점이 분명히 존재하지만, 특별히 복잡해보인다거나 코드가 길어졌다거나 하는 모습은 볼 수 없습니다. 왜그럴까요?

바로 LINQ때문입니다. 함수형언어의 장점을 녹인 LINQ덕분에 데이터추출과정이 추상화되어서 코드가 큰 차이없이 작성될 수 있었습니다. 최근 추세는 명령형언어와 함수형언어가 조금씩 융화되어 가는 것 같습니다. C#에도 점점 함수형언어의 장점과 동적언어의 장점이 녹아들어가고 있습니다. 물론 C#의 설계자인
Anders Hejlsberg는 static한 언어가 가지는 강점을 여전히 높게 평가하고 있습니다만, 필요하다면 점점 융화되어 가는거겠죠. 그럼, 어디한번 계급장떼고(?)...아니아니 LINQ떼고 한번 해보죠!


- 계급장떼고 붙여봐!
넵, 계급장테고 붙여보겠습니다. LINQ없이 C#2.0의 익명메서드 기능을 이용해서 동일한 코드를 작성해 보겠습니다.


class Demo2
    {
        private string destDir;
        private List<STRING> finalFiles;

        public Demo2(string destDir)
        {
            this.destDir = destDir;
            finalFiles = new List<STRING>();
        }

        private List<STRING> GetSthAll(Func<STRING, string[]> GetSth, string path)
        {
            return GetSth(path).ToList();
        }

        private List<STRING[]> GetSplitedList(List<STRING> data, Func<STRING,STRING[]> splitMethod)
        {
            List<STRING[]> splitedList = new List<STRING[]>();
            foreach (string str in data)
            {
                splitedList.Add(splitMethod(str));
            }
            return splitedList;
        }

        private int GetFilesCountWithExt(List<STRING[]> data, Func<STRING[], bool> filterMethod)
        {
            List<STRING[]> filteredList = new List<STRING[]>();
            foreach (string[] fileStr in data)
            {
                if (filterMethod(fileStr))
                {
                    filteredList.Add(fileStr);
                }
            }
            return filteredList.Count;
        }
        public void PrintAllFilesCount(string ext)
        {
            GetAllFiles(destDir);
            List<STRING[]> filesWithExt = GetSplitedList(finalFiles, delegate(string str) { return str.Split('.'); });
            int countOfExt = GetFilesCountWithExt(filesWithExt, delegate(string[] fileStr) { return (fileStr.Length == 2) && (fileStr[1] == ext); });
            Console.WriteLine(countOfExt);
        }

        private void GetAllFiles(string destDir)
        {
            finalFiles.AddRange(GetSthAll(Directory.GetFiles, destDir));
            foreach (string currentDir in GetSthAll(Directory.GetDirectories, destDir))
            {
                GetAllFiles(currentDir);
            }
        }
    }

어떻습니까? 이젠 차이가 좀 드러나기 시작합니다. 익명메서드를 이용해서 람다를 흉내냈지만, 데이터추출과정을 추상화하진못해서 그 추출과정의 중간값저장이나 추출루프를 직접 제어해줘야 합니다. 이렇게 되면, 문제가 조금씩 복잡해질수록 코딩도 어려워 지겠지요.(Func<>는 .NET 3.5에서 추가된거지만 봐주셈요-_-)

즉, 단순히 LINQ라는 기술을 쓰고 안쓰고정도의 문제가 아니라, 추상화 단계가 낮아지는 것이기 때문에 생각의 복잡도는 높아질 수 밖에 없는 것입니다. 데이터의 흐름을 추상화해주는 함수형언어의 특징이 이런부분에서 굉장한 강점으로 작용하게 됩니다.



- 할말다했냐?
네 별로 대단치않은 예제이지만, 최선을 다해서 한번 비교를 해봤습니다. 물론, 예제가 완전히 적절한건 아니고 크기나 복잡도 역시 높은 수준이 아니라 명확한 비교라고 볼 수는 없지만, 하나의 예제가 될 수 있다고 생각합니다. 이번 포스트에서 함수형언어인 F#의 장점을 못 느끼셨다면 모두 제 책임이나 따뜻한 질타를 주시기 바랍니다. 끝으로 Expert F#의 저자인 Don Syme의 블로그에 올라온 사례하나를 소개하고 마치겠습니다.

-포스트 원문-

The first application was parsing 110GB of log data spread over 11,000 text files in over 300 directories and importing it into a SQL database. The whole application is 90 lines long (including comments!) and finished the task of parsing the source files and importing the data in under 18 hours; that works out to a staggering 10,000 log lines processed per second! Note that I have not optimized the code at all but written the application in the most obvious way. I was truly astonished as I had planned at least a week of work for both coding and running the application.

Ralf Herbrich, Microsoft Research
(http://blogs.msdn.com/dsyme/archive/2006/04/01/566301.aspx)


-제 맘대로 해석-_-;;;-

첫번째 프로그램은 300개가 넘는 디렉토리에 있는 11,000개의 텍스트파일에 저장되어있는 100GB나 되는 로그 데이터를 가져와서 분석해서 SQL데이터베이스에 저장하는 거였습니다. 어플리케이션은 전체 길이가 주석을 포함해서 딱 90줄이었고, 로그파일들을 읽어들여 분석하고 저장하는데 18시간이 채 안걸렸습니다. 1초에 1만줄의 로그를 처리한 셈이죠! 더 중요한건 코드를 최적하려고 하지도 않았고 그냥 제일 명확하게 보이도록 짰습니다. 제가 진짜 놀랐던건, 솔직히 전 그 프로그램 짜고 돌리는데 일주일은 걸릴 줄 알았었거든요.



- 참고자료

1. Expert F#, Don Syme, Adam Granicz, Antonio Cisternino, APRESS

2. http://blogs.msdn.com/dsyme/archive/2006/04/01/566301.aspx