Skip to main content

Ionic Framework이 무엇입니까?

Ionic Framework는 (HTML, CSS, 자바스크립트와 같은) 웹 기술을 이용한 고성능, 고품질 모바일 및 데스크톱 앱을 구축하기 위한 오픈 소스 UI 툴킷입니다.

Ionic Framework는 프론트엔드 사용자 경험이나 앱의 UI 상호 작용 (컨트롤, 상호작용, 제스쳐, 애니메이션)에 중점을 둡니다. 배우기 쉽고, Angular와 같은 다른 라이브러리와 쉽게 잘 통합되거나, 간단한 스크립트를 사용하고 프론트 프레임워크 없이 단독형으로 사용될 수도 있습니다.

현재 Ionic Frameworksms Angular와 React와 공식적으로 연계되며, Vue는 개발중입니다. Ionic Framework를 본격적으로 시작하기 전에 더 배워보고 싶은 분들을 위해, 기본을 살펴보는 비디오를 만들었습니다.

목표

크로스 플랫폼

단 한 개의 코드 베이스로 네이티브 iOS, Android, 데스크톱, 프로그래시브 웹 앱과 같은 멀티 플랫폼에 걸쳐서 작동하는 앱을 만들고 배포해보세요. 한 번의 작성으로 여러 곳에서 실행해보세요.

웹 표준 기반

Ionic Framework는 사용자 컨트롤과 쉐도우 DOM 등 현대 웹 API를 사용하는 HTML, CSS와 JavaScript 같은, 가장 믿을 수 있고 표준화된 웹 기술로 만들어졌습니다. 이 탓에, Ionic 컴포넌트는 안정적인 API를 갖고 있으며, 불안정한 단일 플랫폼 공급자가 아닙니다.

아름다운 디자인

깔끔하고 단순하며 기능적입니다. Ionic Framework는 모든 플랫폼을 막론하고 결과물을 아름답게 표시하고 작동하도록 디자인되었습ㅈ니다. 미리 디자인된 컴포넌트, 타이포그래피, 대화형 패러다임, 멋진 (확장 가능한) 기본 테마와 함께 시작해보세요.

단순함

Ionic Framework는 즐겁게 Ionic 앱을 만들고, 배우기 쉬우며, 웹 개발 기술을 가진 누구든지 접근할 수 있도록 복잡하지 않게 만들어졌습니다.

라이센스

Ionic Framework는 허용 가능한 MIT 라이센스 하에서 배포되는 무료 오픈 소스 프로젝트입니다. 이는 개인과 상용 프로젝트에 무료로 쓰일 수 있다는 것입니다. MIT는 jQuery, Ruby on Rails와 같은 유명한 프로젝트에도 똑같이 쓰인 라이센스입니다.

이 문서 내용(ionic-docs 저장소에 있는)은 Apache 2 라이센스로 보호됩니다.

Ionic CLI

공식적인 Ionic CLI 또는 명령줄 인터페이스는 빠르게 Ionic 앱을 구축하고 Ionic 개발자에게 유용한 수많은 명령을 제공합니다. Ionic의 설치와 업데이트 뿐만 아니라, CLI에는 내장 개발 서버, 빌드 및 디버깅 도구 등을 포함합니다. Ionic Appflow 사용자라면, CLI는 클라우드 빌드와 배포까지 수행하고, 여러분의 계정을 관리하는데도 쓰일 수 있습니다.

Framework 호환성

과거 버전의 Ionic은 Angular와 깊이 묶여 있었지만,  V4 프레임워크는 Angular와 같은 최신 JavaScript 프레임워크와 결합된 단독 웹 컴포넌트 라이브러리로 작동하도록 재구축되었습니다. Ionic은 비록 완전한 웹 컴포넌트 지원을 위해 shim이 필요하지만, 성공적으로 React와 Vue 같은 대부분의 프론트엔드 프레임워크에 쓰일 수 있습니다.

JavaScript

Ionic 4의 주요 목표 중 하나는 컴포넌트를 제공하기 위한 단일 프레임워크에 대한 강한 의존을 제거하는 것이었습니다. 즉, 코어 컴포넌트가 웹 페이지에서 스크립트 태그만으로 단독 작동이 가능하다는 것입니다. 프레임워크와 작동하는 것이 대규모 팀과 대규모 앱에서 최적일 수 있지만, WordPress와 같은 단일 페이지 내용에서도 단독 라이브러리로 Ionic을 사용할 수 있다는 것입니다.

Angular

Angular는 Ionic을 쓸만하게 만든 존재였습니다. 코어 컴포넌트가 단독 웹 컴포넌트 라이브러리로 작동하도록 쓰였지만, @ionic/angular 패키지는 Angular 생태계와의 통합을 용이하게 만듭니다. @ionic/angular는 Ionic 2/3에서 넘어온 Angular 개발자들이 기대하는 기능을 제공하며, Angular 라우터와 같은 코어 Angular 라이브러리와 통합합니다.

React

Ionic은 널리 쓰이고 있는 React 라이브러리에 대한 공식 지원을 제공합니다. Ionic React는 React 개발자들이 iOS, Android, 데스크톱을 타겟으로 하는 앱을 만들 수 있는 기존의 웹 기술을 사용할 수 있게 합니다. @ionic/react와 함께, 여러분은 네이티브 React 컴포넌트를 쓰는 기분으로 모든 코어 ionic 컴포넌트를 사용할 수 있습니다.

미래 지원

다른 프레임워크에 대한 지원도 곧 릴리즈될 것으로 기대됩니다. 현재 Vue에 대한 공식 바인딩이 개발되고 있지만, 일부 컴포넌트는 해당 프레임워크에서 즉시 작동 가능합니다.

Ionic Framework V4+

Ionic 프레임워크 V4는 성능, 호환성과 전체적인 확장성에 중점을 둔, 기저 기술과 프로젝트 가능성의 중요한 진보입니다. V4가 Angular와 @ionic/angular 패키지로 여전히 깊이 결합되긴 하지만, 이제는 프레임워크에 구애받지 않는데, 다시 말해 어떤 다른 JavaScript 프레임워크든지 (Vue, React, Preact 등), 또는 프레임워크가 없든지 작동한다는 것입니다.

웹 표준으로 이동하면서, V4는 Ionic 코어가 프레임워크 의존적인 모델에서 벗어나 현대 브라우저에서 지원되는 표준 컴포넌트 모델에 의존할 수 있게 됩니다. 더 빠른 로드 시간, 더 나은 성능, 적은 코드양을 보여줍니다.

Ionic Appflow

Ionic 앱의 생애주기 관리를 지원하기 위해, Ionic Appflow라고 하는, 개발앱을 위한, 오픈 소스 프레임워크와 별도의 상용 앱 플랫폼을 제공합니다.

Ionic Appflow는 개발자와 팀이 중앙화된 대시보드에서 네이티브 앱을 컴파일하고 라이브 코드 업데이트를 배포하는데 도움을 줍니다. 워크플로우 자동화, 단일 로그인(SSO)과 연결된 서비스와 통합에 대한 접근 같은 고급 기능을 위해 유료 업그레이드 옵션을 사용할 수 있습니다.

Appflow에는 Ionic 계정이 필요하며, 일부 기능을 가지고 놀고 싶은 사람들을 위해 무료 “스타터” 플랜도 있습니다.

생태계

Ionic Framework는 코어팀에 의해 풀타임으로 활발하게 개발되고 유지되고 있으며, 성장과 도입을 촉진하는 전세계 개발자와 기여자 커뮤니티에 의해 생태계가 조성되고 있습니다. 개발자와 크고 작은 회사는 Ionic을 사용하여 어디서나 실행되는 놀라운 앱을 빌드하고 제공합니다.

커뮤니티 가입

전세계 200여 개 나라에 수백만의 Ionic 개발자가 있습니다. 가입하는 방법은 다음과 같습니다.

  • 포럼: 질문하고 아이디어를 나누기 좋은 곳입니다.
  • 슬랙: 개발자들이 만나서 실시간으로 대화를 나누는 활기찬 곳입니다.
  • 트위터: 업데이트를 올리고 Ionic 커뮤니티의 내용을 공유하는 곳입니다.
  • GitHub: 버그를 보고하고, 새 기능을 요청하고, 이슈를 만드는 곳입니다. 풀 리퀘스트도 환영합니다!
  • 글 작성하기: 기술 블록그를 쓰고 Ionic 커뮤니티에서 여러분의 이야기를 나누세요.
갑자계산

C#으로 60갑자(간지干支) 계산식을 세워보자

간지 (干支) : 십간(十干)과 십이지(十二支)를 합쳐서 60주기로 하는 수사입니다.

발상

60간지를 나열한 자료는 쉽게 인터넷 검색으로 구할 수 있지만, 그걸 계산하는 건 많지 않습니다.

요즘 Java를 열심히 배우고 있는 입장에서, 기존에 하던 C#를 까먹어버리면 곤란한 관계로, C#을 오랜만에 활용하여 실력을 썩히지 않도록 노력해야겠다고 다짐했습니다.

그리하여 타임어택 같은 느낌으로 도전하였습니다.

30분 안에 기능 하나는 완성해야 하지 않겠습니까!

접근

수학적인 기존의 지식을 활용하는 것도 유용해보이지만, 배열의 특성을 최대한 활용해보기로 했습니다.

언제 다시 보더라도 쉽게 이해할 수 있는, 구현이 어렵지 않은 코드를 지향하였습니다.

우선, 간지를 각각 배열로 적어봅시다.

static string[] gap = new string[] { "갑", "을", "병", "정", "무", "기", "경", "신", "임", "계" };
static string[] ja = new string[] { "자", "축", "인", "묘", "진", "사", "오", "미", "신", "유", "술", "해" };

어렵지 않습니다.

구현

연도를 간지로 변환

연도를 간지로 바꿔봅시다. 생각하기 편하게 올해(2017, 정유년)를 기준으로 생각해봅니다.

올해와 목표년도의 차이를 구하고 단순히 각각의 배열 길이로 나누어 나머지를 구하면 됩니다. 그 다음 올해의 간지만큼 더해주면 끝이죠.

하지만 여기서 의외로 연도를 간지로 바꾸는데는 구현에 약간의 애를 먹었는데, 앞으로는 문제 없이 구하는데 거꾸로는 엉뚱한게 나왔던 것입니다.

고민하다가 원인은 음수 처리에 있었다는 걸 알았습니다.

private static string YearToGapja(int i) {
            int cha = i - 2017;
            //기준점을 2017년으로 잡아봅니다.
            int gap_cha = cha % gap.Length;
            int ja_cha = cha % ja.Length;

            gap_cha = (gap_cha + 3) % gap.Length;
            ja_cha = (ja_cha + 9) % ja.Length;

            if (gap_cha < 0) {
                gap_cha = gap.Length + gap_cha;
            }
            if (ja_cha < 0) {
                ja_cha = ja.Length + ja_cha;
            }

            return gap[gap_cha] + ja[ja_cha];
        }

음수 처리로 한 번 더 길이를 더해주면 끝에서부터 음수로 세어간 위치를 구할 수 있습니다. 이렇게 하면 몇 년이고 틀리지 않고 다 구할 수 있죠.

아, 여기서 옥에티가 있군요. 기원전을 지원하는 것입니다. 그냥 해서는 0년이 포함되어버리죠.

private static int MinusToBC(int i) {
    if(i < 0) { //
       return ++i;
    }else if(i == 0) {
       return int.MinValue;
    }
    return i;
}

이렇게 음수에 한해서는 입력값을 실제보다 1 더해주면 됩니다.

0을 입력한 경우를 별도로 구분하기 위해 int.MinValue로 넣어줬습니다. 이는 Int32 범위의 최하 숫자를 가져올 수 있는데, 이걸로 무효한 숫자로 처리하면 크게 유용성에 간섭받지 않고 잘 확인할 수 있습니다.

int.MinValue와 같으면 계산을 포기한다는 구절을 넣는 것으로 충분하죠.

한편, Console.ReadLine()을 통해 입력값을 받는 만큼, 이것이 유효한 숫자인지 판별할 필요가 있습니다.

do {
    Console.Write("희망 서력 년도(기원전=>마이너스) 또는 갑자 입력: ");
    input = Console.ReadLine();
} while ((!int.TryParse(input, out i) && !isGapja(input)) || input == "0");

이를 통해 input이 숫자가 아니거나, 갑자가 아닌 경우 또는 0을 입력한 경우 모두 다시 읽으려고 시도하겠군요.

int.TryParse는 Parse 실패시에 out i 값이 0이라고 합니다.

private static bool isGapja(string input) {
            if (input.Length == 2) {
                if(!(gap.Contains(input[0].ToString()))) {
                    return false;
                }
                if (!(ja.Contains(input[1].ToString()))) {
                    return false;
                }
                return true;
            }
            return false;
        }

위에서 등장한 IsGapja라는 함수를 만들어두면 반복문을 돌 때 input의 간지 유효성을 확인하고 실제 갑자를 갖고 있는지 확인할 수 있습니다.

결과 확인

여기까지 하면 결과가 시원하게 나옵니다.

희망 서력 년도(기원전=>마이너스) 또는 갑자 입력: 2017
결과: 정유

희망 서력 년도(기원전=>마이너스) 또는 갑자 입력: 2002
결과: 임오

희망 서력 년도(기원전=>마이너스) 또는 갑자 입력: 1988
결과: 무진

와우. 이렇게 하였더니 목표 30분보다 10분 빠르게 끝났네요.

하지만 15분 냉장고를 부탁해 컷에는 실패했습니다. 다음에는 분발합시다!

간지를 연도로

그 다음으로 간지를 연도로 변환해보겠습니다. 60년마다 돌아오니까 환갑이라고 하죠. 아무래도 여러 번 있을 것이고, 시작 년도와 끝 년도도 중요하겠죠.

유연한 적용을 위해서 일부러 시작 년도와 끝 년도를 매개변수로 받았습니다.

그리고 이제 끝없는 고민을 합니다. 위의 간지 변환 사례를 역으로 방정식을 세우면 그만일까요?

아무래도 머리가 핑핑돌고 문제가 생겼을 때 어떤 방식을 써야할지 수알못에게 두려움이 비칩니다.

이럴 때는 컴퓨터를 믿고 왕도를 걸어보기로 합니다. 사실 목표 간지와 기준 간지의 차이만큼을 알고 있으면 그 +/- 60만 반복하면 결과가 됩니다.

그러나 목표 간지와 기준 간지 차이를 구하기엔, 쉽지 않습니다. 계산식을 더 많이 고민하면 되겠지만요.

수학자가 아닌 프로그래머에겐, 코드가 더 많은 답을 줍니다.

private static string[] EnumGapja() {
            string[] s = new string[60];
            for(int i = 0; i < 60; i++) {
                s[i] = gap[i % gap.Length] + ja[i % ja.Length];
            }
            return s;
        }

이렇게 하면 위 전역변수를 활용하여 60갑자가 뚝딱 완성됩니다.

나머지가 끊임없이 찬조출연하더군요.

private static int[] GapjaToYear(string input, int min, int max) {
            string[] eg = EnumGapja();
            int ifrom_gapja=Array.IndexOf(eg, input);

            //서기 4년은 갑자년.
            //4*60n=갑자
            //4*60n+ifrom_gapja=갑자+ifrom_gapja
            List ilist = new List();
            for (int i = 4 + ifrom_gapja; i < max; i+=60) {
                if(i < min) {
                    continue;
                }
                ilist.Add(i);
            }

            return ilist.ToArray();
        }

이제 한없이 길어지던 코드가 한결 간결해졌습니다.

단순히 60갑자 목록에서 index의 차이만 알게 되는 것으로 추가해야할 년도를 알 수 있습니다.

갑자년이 서기 4년이라고 합니다. 이 갑자년과의 차이 = index라는 걸 쉽게 예측할 수 있으니까요.

min년부터 max년까지 반복문을 돌리면 그만이겠습니다.

희망 서력 년도(기원전=>마이너스) 또는 갑자 입력: 무자
28년 88년 148년 208년 268년 328년 388년 448년 508년 568년 628년 688년 748년 808년 868년 928년 988년 1048년 1108년 1168년 1228년 1288년 1348년 1408년 1468년 1528년 1588년 1648년 1708년 1768년 1828년 1888년 1948년 2008년 2068년 2128년 2188년 2248년 2308년 2368년 2428년 2488년 2548년 2608년 2668년 2728년 2788년 2848년 2908년 2968년 3028년 3088년 3148년 3208년 3268년 3328년 3388년 3448년 3508년 3568년 3628년 3688년 3748년 3808년 3868년 3928년 3988년 4048년 4108년 4168년 4228년 4288년 4348년 4408년 4468년 4528년 4588년 4648년 4708년 4768년 4828년 4888년 4948년 5008년 5068년 5128년 5188년 5248년 5308년 5368년 5428년 5488년 5548년 5608년 5668년 5728년 5788년 5848년 5908년 5968년 6028년 6088년 6148년 6208년 6268년 6328년 6388년 6448년 6508년 6568년 6628년 6688년 6748년 6808년 6868년 6928년 6988년 7048년 7108년 7168년 7228년 7288년 7348년 7408년 7468년 7528년 7588년 7648년 7708년 7768년 7828년 7888년 7948년 8008년 8068년 8128년 8188년 8248년 8308년 8368년 8428년 8488년 8548년 8608년 8668년 8728년 8788년 8848 년 8908년 8968년 9028년 9088년 9148년 9208년 9268년 9328년 9388년 9448년 9508년 9568년 9628년 9688년 9748년 9808년 9868년 9928년 9988년

:)

전체 소스

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Gapja {
    class Program {
        //4 5 6 7 8 9 0 1 2 3
        static string[] gap = new string[] { "갑", "을", "병", "정", "무", "기", "경", "신", "임", "계" };
        static string[] ja = new string[] { "자", "축", "인", "묘", "진", "사", "오", "미", "신", "유", "술", "해" };
        static void Main(string[] args) {
            //2017 = 정유년 = 4/9

            while (true) {
                string input = ""; int i = 0;
                do {
                    Console.Write("희망 서력 년도(기원전=>마이너스) 또는 갑자 입력: ");
                    input = Console.ReadLine();
                } while ((!int.TryParse(input, out i) && !isGapja(input)) || input == "0");
                i = MinusToBC(i);
                if(i != int.MinValue) {
                    Console.WriteLine("결과: {0}", YearToGapja(i));
                } else {
                    var result = GapjaToYear(input, 1, 10000);
                    foreach(var r in result) {
                        Console.Write("{0}년 ",r);
                    }
                }
                Console.WriteLine();
            }
        }

        private static bool isGapja(string input) {
            if (input.Length == 2) {
                if(!(gap.Contains(input[0].ToString()))) {
                    return false;
                }
                if (!(ja.Contains(input[1].ToString()))) {
                    return false;
                }
                return true;
            }
            return false;
        }

        private static int MinusToBC(int i) {
            if(i < 0) { //
                return ++i;
            }else if(i == 0) {
                return int.MinValue;
            }
            return i;
        }

        private static string YearToGapja(int i) {
            int cha = i - 2017;
            //기준점을 2017년으로 잡아봅니다.
            int gap_cha = cha % gap.Length;
            int ja_cha = cha % ja.Length;

            gap_cha = (gap_cha + 3) % gap.Length;
            ja_cha = (ja_cha + 9) % ja.Length;

            if (gap_cha < 0) {
                gap_cha = gap.Length + gap_cha;
            }
            if (ja_cha < 0) {
                ja_cha = ja.Length + ja_cha;
            }

            return gap[gap_cha] + ja[ja_cha];
        }

        private static string[] EnumGapja() {
            string[] s = new string[60];
            for(int i = 0; i < 60; i++) {
                s[i] = gap[i % gap.Length] + ja[i % ja.Length];
            }
            return s;
        }

        private static int[] GapjaToYear(string input, int min, int max) {
            string[] eg = EnumGapja();
            int ifrom_gapja=Array.IndexOf(eg, input);

            //서기 4년은 갑자년.
            //4*60n=갑자
            //4*60n+ifrom_gapja=갑자+ifrom_gapja
            List ilist = new List();
            for (int i = 4 + ifrom_gapja; i < max; i+=60) {
                if(i < min) {
                    continue;
                }
                ilist.Add(i);
            }

            return ilist.ToArray();
        }
    }
}

정규식 레퍼런스

도입

정규식 (正規式, Regular Expression)은 특정한 규칙을 가진 문자열의 집합을 표현하는 데 사용하는 형식 언어입니다. 이걸 잘 써야 당장 코드 라인이 줄겠죠. 지금부터 정규식 사용법을 구체적으로 알아보고, 활용 방안의 예시를 베껴서 기록하고자 합니다.

정규식 쓰임

    • PHP의 preg_match 함수
      • $subject = "abcdef";
        $pattern = '/^def/';
        preg_match($pattern, $subject, $matches, PREG_OFFSET_CAPTURE, 3);
        print_r($matches);
        
    • C#의 RegEx 클래스
      • class TestRegularExpressionValidation
        {
            static void Main()
            {
                string[] numbers = 
                {
                    "123-555-0190", 
                    "444-234-22450", 
                    "690-555-0178", 
                    "146-893-232",
                    "146-555-0122",
                    "4007-555-0111", 
                    "407-555-0111", 
                    "407-2-5555", 
                };
        
                string sPattern = "^\\d{3}-\\d{3}-\\d{4}$";
        
                foreach (string s in numbers)
                {
                    System.Console.Write("{0,14}", s);
        
                    if (System.Text.RegularExpressions.Regex.IsMatch(s, sPattern))
                    {
                        System.Console.WriteLine(" - valid");
                    }
                    else
                    {
                        System.Console.WriteLine(" - invalid");
                    }
                }
        
                // Keep the console window open in debug mode.
                System.Console.WriteLine("Press any key to exit.");
                System.Console.ReadKey();
            }
        }
        /* Output:
              123-555-0190 - valid
             444-234-22450 - invalid
              690-555-0178 - valid
               146-893-232 - invalid
              146-555-0122 - valid
             4007-555-0111 - invalid
              407-555-0111 - valid
                407-2-5555 - invalid
        */
    • Java의 RegEx 클래스
      • import java.util.regex.Matcher;
        import java.util.regex.Pattern;
        
        public class RegexMatches {
        
           public static void main( String args[] ) {
              // String to be scanned to find the pattern.
              String line = "This order was placed for QT3000! OK?";
              String pattern = "(.*)(\\d+)(.*)";
        
              // Create a Pattern object
              Pattern r = Pattern.compile(pattern);
        
              // Now create matcher object.
              Matcher m = r.matcher(line);
              if (m.find( )) {
                 System.out.println("Found value: " + m.group(0) );
                 System.out.println("Found value: " + m.group(1) );
                 System.out.println("Found value: " + m.group(2) );
              }else {
                 System.out.println("NO MATCH");
              }
           }
        }
  • javascript 리터럴 또는 RegExp 클래스
    • var myRe = /d(b+)d/g;
      var myArray = myRe.exec("cdbbdbsbz");
  • URL Rewrite Rule
    • rewrite ^/blog/sitemap(-+([a-zA-Z0-9_-]+))?\.xml$ "/blog/sitemap$2.xml" last;
      rewrite ^/blog/sitemap(-+([a-zA-Z0-9_-]+))?\.xml$ "/blog/index.php?xml_sitemap=params=$2" last;
      rewrite ^/blog/sitemap(-+([a-zA-Z0-9_-]+))?\.xml\.gz$ "/blog/index.php?xml_sitemap=params=$2;zip=true" last;
      rewrite ^/blog/sitemap(-+([a-zA-Z0-9_-]+))?\.html$ "/blog/index.php?xml_sitemap=params=$2;html=true" last;
      rewrite ^/blog/sitemap(-+([a-zA-Z0-9_-]+))?\.html.gz$ "/blog/index.php?xml_sitemap=params=$2;html=true;zip=true" last;

정규식 레퍼런스

Read More

TickCount 탐구

발단

이 의문이 시작된 것은 단순히 String(또는 C#과 같은 언어에서는 string으로 간소화되기도 하죠, 이하 string)과 StringBuilder, StringBuffer의 성능 차이에 대한 초급적인 언급이었습니다.

string은 실제로는 읽기 전용처럼 쓰인다는 건 초보 빼고는 다 알고 있는 사실입니다. immutable이라고 하던데, 변하지 않는다는 말이죠. 따라서 우리가 자유롭게 +나 concat을 하면 string은 그 때마다 새로운 개체를 반환하게 됩니다.

C++에서는 strcat()으로 문자열을 수정할 때 큰 영역을 미리 할당해두면 속도가 비약적으로 증가한다고 하지만, C#과 Java에서는 문자열의 수정이 어렵기 때문에 다른 방법이 필요하다는 것이고, 그 내용은 MSDN에도 잘 나와 있습니다. Java랑 C#이 판박이인 점은 코드를 복붙해보면 쉽게 알 수 있는 내용이더군요.

효과 검증

그래서 이걸 어떻게 알 수 있을까요. Java 선생님은 문자열을 파바바박 쏟아내는 반복문을 짜고, “어라, 별 차이가 없는데” 하셨지만, 실제로 큰 차이가 발생하는 것은 어느 정도 코드를 통해서도 쉽게 파악할 수 있었습니다.

 

/* string performance in C# */
            string me = "";
            Console.WriteLine("String START");
            var cnt = Environment.TickCount;
            for (int i = 0; i < 100000; i++)
            {
                me += "a";
            }
            Console.WriteLine("Time: "+(Environment.TickCount - cnt).ToString());
            Console.WriteLine("END");
            //Console.WriteLine(me);

            StringBuilder sb = new StringBuilder();
            Console.WriteLine("StringBuilder START");
            cnt = Environment.TickCount;
            for (int i = 0; i < 100000; i++)
            {
                sb.Append("a");
            }
            Console.WriteLine("Time: " + (Environment.TickCount - cnt).ToString());
            Console.WriteLine("END");

C# 코드

/* string performance */
        String me = "";
        System.out.println("String START");
        long cnt = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++)
        {
            me += "a";
        }
        System.out.println("Time: "+(System.currentTimeMillis() - cnt));
        System.out.println("END");

        StringBuffer sb = new StringBuffer();
        System.out.println("StringBuffer START");
        System.out.printf("capacity: %d, length: %d\n", sb.capacity(), sb.length());
        cnt = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++)
        {
            sb.append("a");
        }
        System.out.println("Time: " + (System.currentTimeMillis() - cnt));
        System.out.printf("capacity: %d, length: %d\n", sb.capacity(), sb.length());
        System.out.println("END");

Java코드는 다음과 같은 결과가 나왔습니다.

String START
Time: 7467
END
StringBuffer START
capacity: 16, length: 0
Time: 9
capacity: 294910, length: 200000
END

어마어마한 차이네요. 10만 개의 String을 붙이면서 7초 넘게 소요되었습니다. 한편, C#은 그나마 5초 내외로 되는게 코드 최적화의 힘인가 싶습니다. StringBuffer와 StringBuilder 모두 일의 자리나 심지어 0ms로, 찰나에 가까운 수준이라 극명한 차이를 보여주더군요.

역시 이미 사용된 Heap을 얼마나 유용하게 사용하고, 새로운 메모리 할당을 피하는 것이 중요한지 알게 되는 실험이었습니다.

새로운 의문

흥미로운 점은 Java의 StringBuffer와 달리 C#의 StringBuilder 소요 시간이 0ms라고 출력된 점입니다. 코드 한 줄도 아니고 10만인데 그게 가능한 걸까? 오차는 아닐까? 궁금해졌습니다.

당장, Environment.TickCount를 의심하기 시작했습니다. 사실 TickCount로 코드 속도를 비교하는 시대는 멀티 프로세서 시대의 도래로 종말을 맞았다는 이야기도 있습니다. 코드의 선후 관계는 보존되더라도 코드가 동시에 실행되는 타이밍의 차이는 충분히 있을 수 있다는, 쓰레드 위험이 일상화된 멀티 프로세싱의 시대니까요.

그래서 당장 C# 유저들은 무슨 방법을 쓰는가 알아보았습니다.

  • Environment.TickCount
  • GetTickCount / GetTickCount64
  • DateTime.Now(UtcNow).Ticks
  • Stopwatch
  • static Stopwatch.GetTimestamp

Environment.TickCount

가장 편하게 쓸 수 있는 Environment static class의 멤버 TickCount입니다. 컴퓨터의 UpTime을 보여주는 GetTickCount()와 다를 게 없는 특성으로, 부호 있는 정수(int)인지라, 차를 구할 때 24.9일 이상 켜진 컴퓨터에서는 특별한 조치가 필요합니다. MSDN에서는 Int32.MaxValue와 & 연산을 하여 해결하였습니다. (부호만 제거되겠군요)

GetTickCount64

Vista부터 쓸 수 있는 Win32 API인 GetTickCount64는 부호 없는 long 타입으로 반환합니다. 시스템이 켜진 시간으로부터 출력되는 밀리세컨드 값을 담기에 충분한 용량이죠. 다만, 실제로 놀지 않고 컴퓨터가 일한 시간을 구하려면 QueryUnbiasedInterruptTime을 사용해야 합니다.

DateTime.Now(UtcNow).Ticks

서기 1년 1월 1일 자정부터 100나노세컨드로 지난 시간을 나타냅니다. 윤초 같은 보정은 없다고 합니다.

Stopwatch

Stopwatch는 new 키워드 없이 인스턴스화가 가능한 Static 메소드 StartNew()를 통해 시간을 재기 시작하여 Stop()을 호출한 다음, ElapsedMilliseconds와 같은 속성으로 경과 시간을 측정할 수 있습니다.

  • IsHighResolution 속성에서 현재 컴퓨터의 하드웨어가 별도의 고해상도 성능 카운터 기능이 있는지 알 수 있는데, 현세대 컴퓨터는 대체로 True를 반환하는 것 같습니다.
  • Frequency 속성은 타이머의 틱 주기를 반환합니다.
    long frequency = Stopwatch.Frequency;
        Console.WriteLine("  Timer frequency in ticks per second = {0}",
            frequency);
        long nanosecPerTick = (1000L*1000L*1000L) / frequency;
        Console.WriteLine("  Timer is accurate within {0} nanoseconds", 
            nanosecPerTick);
    • 이와 같은 코드에서 Stopwatch 타이머의 정확도가 나타납니다.
        Timer frequency in ticks per second = 2343750
        Timer is accurate within 426 nanoseconds

static Stopwatch.GetTimestamp

Stopwatch의 인스턴스화 후 계산하는 시간을 아까워한 일부 유저는 GetTimestamp static 메소드의 성능이 더 좋지 않을까 생각하더군요.

  • 고해상도 성능 카운터가 활성화된 상태에선, 현재값을 출력합니다.
  • 일반 시스템 시계를 쓰는 경우, DateTime.Ticks와 동일한 값을 출력합니다.

벤치마킹

Repeating measurement 3 times in loop of 10,000,000:

Measured: GetTickCount64() [ms]: 228
Measured: Environment.TickCount [ms]: 50
Measured: DateTime.UtcNow.Ticks [ms]: 83
Measured: Stopwatch: .ElapsedMilliseconds [ms]: 856
Measured: static Stopwatch.GetTimestamp [ms]: 476
Measured: Stopwatch+conversion to DateTime [ms]:  811

Measured: GetTickCount64() [ms]: 219
Measured: Environment.TickCount [ms]: 50
Measured: DateTime.UtcNow.Ticks [ms]: 84
Measured: Stopwatch: .ElapsedMilliseconds [ms]: 847
Measured: static Stopwatch.GetTimestamp [ms]: 465
Measured: Stopwatch+conversion to DateTime [ms]:  799

Measured: GetTickCount64() [ms]: 235
Measured: Environment.TickCount [ms]: 51
Measured: DateTime.UtcNow.Ticks [ms]: 84
Measured: Stopwatch: .ElapsedMilliseconds [ms]: 851
Measured: static Stopwatch.GetTimestamp [ms]: 470
Measured: Stopwatch+conversion to DateTime [ms]:  791

Compare that with DateTime.Now.Ticks [ms]: 1150

General Stopwatch information:
- Using high-resolution performance counter for Stopwatch class.
- Stopwatch accuracy- ticks per microsecond (1000 ms): 2.3
 (Max. tick resolution normally is 100 nanoseconds, this is 10 ticks/microsecond.)
- Approximated capacity (maxtime) of TickCount [dd:hh:mm:ss] 25:20:31:23

Done.

단일 스레드만 쓰도록 프로세스 설정한 다음, 스레드 우선 순위를 높이고, Stopwatch를 활용하여 1000만 번 각각 호출하는데 걸린 시간입니다.

TickCount가 공통적으로 빠르지 않냐 싶은데, Stopwatch는 ElapsedMilliseconds 표시를 위해 추가로 계산하는 시간도 고려해야 할 듯 합니다.
개인적으로는 Start() Stop()을 반복하는 게 더 의미 있는 행동이 아닌가 싶기도 한데 말이죠.

 

벤치마킹 소스 코드 참고: https://stackoverflow.com/questions/243351/environment-tickcount-vs-datetime-now

결론

GetTickCount는 혼돈과 혼돈입니다. 설마 20여 일 동안 컴퓨터를 켜고 있을 멍청이가 있으리라 생각했을까요. DWORD로 부호 없애봐야 40여 일이군요.

쓰라는 거 써서 큰 이상이 생길 것 같진 않습니다. 하지만, 무엇이 오래 걸리는지 메소드와 클래스별 특징을 잘 알고 있다면, 오래 걸리는 작업을 최대한 회피하거나 미루는 방식으로 퍼포먼스를 더 향상시킬 수 있겠지요.

앞으로도 궁금한 점이 있으면 직접 실험해야겠다 마음 먹게 되는 주제였습니다.