Ruby on rails 소개

rails_1

Rails는 Ruby로 작성된 MVC모델을 기반 프레임워크입니다.   쉽고 빠르게 웹 어플리케이션을 구축 할 수 있도록 도와줍니다.

Rails와 비슷한 역할을 하는 프레임워크로는 java spring, python django, php code igniter 등이 있습니다.

Rails의 장점은 Ruby 자체가 가진 유연성이라고 볼 수 있습니다. Rails 자체도 Ruby로 이루어졌지만, Rails application도 ruby로 작성됩니다.

Ruby의 모든 것은 객체이며 동적 타입을 통한 코드 유연성을 기반으로 한 손쉬운 회귀 테스트 등 장점이 무궁무진합니다.

제가 느꼈던 Rails의 장점을 나열해보겠습니다.

  • Ruby로 프로그램을 작성한다.
    • 이는 생각보다 큰 장점입니다. 루비 언어 자체가 생산성이 좋은 언어입니다.
  • Ruby는 메소드 접미사로 !나, ?를 통해 역할의 의미를 함축해 포함하기도 하며, 메타 프로그래밍에도 능수 능란한 언어입니다.
  • MVC모델의 완성도가 높다.
    • 모델명에 맞추어, 동일한 이름의 View, Control만 구현하면 무리 없이 동작합니다.
    • 또한 Rake를 통해 MVC 모델의 각 구성요소를 쉽게 build할 수 있고, scaffold 옵션을 통해 MVC 구성 요소를 한번에 구축해주기도 합니다.
  • 라이브러리 연동이 편하다. (리눅스에서는)
    • 안타깝게도 윈도우에서는 미동작하는 gem이 좀 많습니다.
    • 만약 리눅스 환경이라면, 웹 서비스에 까지 필요한 대부분의 기능은 잘 동작한다고 보셔도 무방합니다.
    • 리눅스 환경의 단점으로 일컬어 지는 라이브러리 버전간 호환성 문제는 gemfile에서 특정 버전까지 명시함으로써 우회 가능합니다.

제 개인적으로는 윈도우 환경에서 서비스까지 하기엔 적절치 않습니다만, 리눅스 서버 환경에서 관리한다는 전제하에선 생산성 최고의 언어가 아닌가 생각합니다.

단점으로 일컬어 지는 속도 문제는, ruby는 느리지만 rails는 빠르다 라는 말도 있듯이, 충분히 php에 근접한 속도를 낼 수 있습니다. 이에 대해선 링크로 대신하겠습니다.

http://www.comentum.com/ruby-on-rails-vs-php-comparison.html

http://stackoverflow.com/questions/2529852/why-do-people-say-that-ruby-is-slow

새로 시작할 웹 개발용 프레임워크를 고민 중이시라면, Rails 어떠신가요?

빌드 업 속도를 Rails(철도길)를 달리듯 빠르게 도와줄 겁니다.

rails_2

Ruby on Rails 공식 페이지 RAILS 시작하기 REST란?

Welcome to Ruby!

Welcome to ruby! from 성훈 김

루비를 시작하시는 모든분들께 짧게 읽고 넘어가실 수 있는 슬라이드를 작성해보았습니다.

동적 타입 기반의 스크립트 언어를 아직 해보지 않으셨다면, 파이썬이나 루비! 기왕이면 루비를 추천해드립니다.

짧게 5분정도 소요되니 읽고 가셔요~

TDD에게서 자유로워 지자

2000년대 중후반은 모두가 유닛 테스트에 미쳤다. 아니 TDD에 미쳤다.

Test Driven Development에 대한 서적이 넘쳐났으며, 모두가 TDD를 통해 구원 받을거라는 희망찬 상상에 들떠 있었다.

이 붐을 주도했던 개발자중 한명인 DHH (rails를 만든 이)도 이 흐름에 동참했었다.  그를 비롯한 많은 이의 주장은 테스트 우선 신앙 (Test first fundamentalism)라 불릴 만큼 테스트를 바탕으로 코드를 작성하면 그 퀄리티가 비약적으로 상승 할 것이라는 의견이었다.

여지껏 내가 해온 테스트는 다음과 같았다.

독립적으로 동작할 수 있는 클래스에 대한 유닛테스트.

  • 화이트 박스 테스트로써의 유닛 테스트. (나는 크게 선호하진 않았지만, 가끔 진행했고 유닛테스트를 실천한 초반에 특히 많이 진행했다.)
  • 블랙박스 테스트로써의 유닛 테스트. (블랙박스 테스트를 난 매우 선호했는데, 외부에서 사용하는 메소드야 말로 기대치대로 동작하는지 검증되어야 한다고 믿고 있고, 그 믿음에 대해선 변함이 없다.)

외부 프로세스 내지는, 외부 입력을 시뮬레이션 해서 진행하는 컴포넌트 테스트

  • input이 패킷이 주가 되는 서버 프로그램의 경우에는, 시뮬레이터 내지는 테스트 클라이언트라 불리우는 어플리케이션을 통한 테스트.

외부 입력을 통한 부하 테스트

  • 프로파일링을 겸한, 임계치 측정 및 검증.
  • 부하 발생시의 안정성에 대한 검증.

위 테스트중 유닛테스트는 일일 빌드를 통해 검증 컴포넌트 테스트는 업무시간 도중에. 부하테스트는 퇴근하고 난 이후부터 익일까지.

이런식으로 시간을 나누어 관리했다.

헌데 이렇다보니, 컨텐츠 개발하고 버그에 쫓기고 하다보면 막상 테스트를 들여다보고 개선하고, 테스트 코드를 추가하는 일이 만만치 않았다. 당연히 개발 후 테스트 코드 추가보다 더 오래 걸리는, 테스트를 추가하며 기능을 검증하는 일. 즉 테스트 주도 개발과는 거리가 멀게 개발해오고 있었고, 심적으로 매우 불편했다. 꼭 안생겨도 될 버그를, 시간을 조금만 더 쓰면 커버리지를 갖출 수 있는 상황을, 테스트 주도 개발을 외면하면서 맞이한게 아닐까 하는 생각이었다.

이런 생각을 이사님과 토의한적이 있었고, 이사님은 비용이 많이 드는 테스트에 대한 부정적인 의견을 피력하셨다.

“테스트를 작성하기 위한 비용이 너무 크다면, 테스트 코드를 작성하는 것마저 낭비라고 생각한다.”

테스트 코드가 주는 이득을 많이 체감했던지라 나는 당시 크게 공감하진 못했다. 특히나 이 말을 하신 이사님은 테스트 코드 추가를 위해 많은 비용을 감내하시고, 여러 객체의 mocking과 단위 테스트, 시뮬레이트 코드를 위해 시간을 많이 쏟으신 분이셔서 더더욱 놀랐었다.

헌데 DHH의 고백을 읽고나서 다시금 떠올려본 이사님의 말은 나에게 큰 공감을 가져다 주었다.

가장 크게 와닿은 단락은 아래와 같다.

두번째 단계는, unit 부터 system 사이의 테스팅 스펙트럼의 균형을 잡는 것이다.

그렇다! TDD는 유닛 테스트의 중요성만을 지나치게 강조해왔다. 유닛테스트를 통해 해당 클래스의 단독 동작은 보장되지만, 여러개의 클래스가 mocking 없이 묶였을 때의, 다양한 입력과 다양한 상태(state)에서의 출력(동작)은 보장하지 않는다.

실제로 이 부분을 커버하기 위해 나는 컴포넌트 테스트라 불리우는 (시스템 테스트라는 용어가 있는지 이제 알았다. 나도 앞으로 이렇게 써야겠다.) 시스템 테스트를 해오긴했으나, 유닛 테스트를 위한 시간 투자를 덜한다는 찜찜한 마음은 버릴 수 없었다.

하지만 이제 편해졌다. 너무나 당연한 얘기지만 시간은 돈이다. 테스트를 작성하기 위한 시간도 돈이고, 테스트를 유지하기 위한 비용도 돈이다. mocking을 위해, 혼자 일때만 잘 동작할지도 모르는 녀석을 위한 시간낭비가 될 수도 있는 유닛 테스트를 위해 강박을 가질 필요까진 없다.

물론 DHH의 이야기도 테스트의 중요성을, 매우 강조한다. 하지만 테스트 “주도” 개발은 하지 않고 있다는 것을 고백했다. 테스트 “주도” 개발이 얼마나 느리고, 비효율적이며, 그 성과가 미약했음을 고백한 것이라고 볼 수 있다.

이런 얘기가 다른 사람도 아닌 DHH에게서 나온 것은 너무나도 반갑다.

테스트 코드나 테스트를 줄이자는 얘기가 아니라는 것은 다들 알 것이다. 유닛 테스트와 시스템 테스트간의 밸런스를 맞추고, 테스트에 대한 투자 대비 효율에 대해 지금보다 심도 있게 고민해보자. 그리고 TDD에선 자유로워 지자!

TDD는 죽었다 - Rails를 만든 DHH의 글

TDD is dead. Long live testing. (DHH) — likelink

Ruby on rails 세팅 for Windows

Rails installer

rails_1

위 링크에서 windows용 installer를 다운 받아 설치합니다. ruby도 함께 설치되고, 환경 변수도 설정해줘 아주 편합니다.

Ruby on rails IDE - Aptana Studio 3

태생이 Eclipse긴하지만, plugin 추가 설치 없이 ruby와 rails 모두를 잘 지원해줍니다.

rails_2

위 두개를 설치 하신 후, 

gem install ruby-debug-ide

명령을 통해 ruby-debug-ide를 설치해주셔야 정상적으로 디버깅이 가능해집니다.

ruby 파일의 경우 run (Ctrl+F11), debug (F11) 명령을 이용하시면 되고, rails의 경우 Run server (Ctrl+Shift+)나, debug server (not assigned hot key) 를 통해 디버깅이 가능합니다.

rails_3

브레이크 포인트 설정은 해당 라인의 좌측 바에서 더블클릭 하시거나, Ctrl+Shift+B로 가능합니다.

이렇게 세팅하시면 기본적인 윈도우 상의 루비 개발 환경은 완료됩니다.

코드 작성과 디버깅을 쉽게하는 예외 처리

이 글은, c++을 기반으로 작성되었지만 예외 처리 기능 (try-catch, try-except 등)을 가진 모든 언어에 적용 되는 내용입니다. 파일에서 데이터를 읽는 코드를 작성해봅시다. c++로 작성해보겠습니다.

int _tmain(int argc, _TCHAR* argv[])
{
    FILE* fp;
    fp = fopen("test.bin", "rb");

    if(fp == NULL)
    {
        // 예외 처리
        return 1;
    }

    char buffer[512] = {0, };
    fread(buffer, 512, 1, fp);
    return 0;
}

대부분 위 코드처럼 작성하게 됩니다. 위 코드에서 누락된 예외처리 발견하신분? 예. 바로 fread 부분입니다. 

항목이 읽히지 않았을 때, fread는 0을 리턴하고, 0을 리턴했을 때에는 로그를 찍는다거나 등의 예외처리를 해주어야 파일을 잘못 읽었는지를 조기에 발견 할 수 있습니다. 

헌데 대부분의 수많은 예제는 첫 예제처럼 fread에 대한 검사는 이루어지지 않습니다. 바로 이 부분이 쉽게 놓치는 첫 감염입니다. 

감염을 조기에 발견하지 못하면, 숙주를 찾기 어려워지고 숙주를 찾지 못하면 전염병처럼 퍼지게 됩니다. 위 코드의 모범적 예외 처리 방식은 아래와 같습니다.

int _tmain(int argc, _TCHAR* argv[])
{
    FILE* fp;
    fp = fopen("test.bin", "rb");

    if(fp == NULL)
    {
        // 예외 처리
        return 1;
    }

    char buffer[512] = {0, };
    int read_count = fread(buffer, 512, 1, fp);
    if(read_count == 0)
    {
        // 예외 처리
        return 2;
    }
    return 0;
}

이렇게 되어야 buffer의 데이터가 유효하게 로직으로 전달되었는지 검사할 수 있습니다. 

혹자는, 컨텐츠 코드에서 방어적으로 데이터가 유효한지 검증해야 되는게 아니냐라고 하실 수도 있습니다. 

하지만, 그 당연하다 여겨지는 전치검사/후치보장은 막상 그렇게 당연하게 여겨지지 않는게 현실입니다. 

심지어 클래스 내부 멤버에 대해선 전치검사 대상으로 여겨지지 않는 경우가 태반입니다. 게다가 작성하는 API 함수 및 프로젝트에서 구현한 함수마다 사용법및 예외 처리 방식을 검토하는 일은 매우 번거롭고 실수가 있을 수 밖에 없죠. 

같은 프로젝트내에 리턴값 및 예외 처리 방식이 조금만 다르게 작성 (WIN32 API에서의 DWORD형의 결과값과 COM에서 HRESULT 값이 주로 그런 케이스. 어떤 매크로로 검사해야될지부터 혼동이 옴.) 된 걸로도 충분히 혼란스러운데, 여러개의 서드파티 솔루션까지 사용하는 규모가 큰 프로젝트의 경우 그 피로도가 장난이 아니죠. 

그 대안으로 대부분의 언어에서는 예외 처리 기능을 지원합니다. c++의 경우 try-catch(try-except는 SEH)가 그 역할을 하는데요, boost나 STL의 경우에는 내부적으로 오류를 감지하면 throw로 예외를 던지고 있습니다. 말로만 하면 감이 안오실테니 코드로 보여드리겠습니다.

int _tmain(int argc, _TCHAR* argv[])
{
    try
    {
        FILE* fp;
        fp = fopen("test.bin", "rb");

        if(fp == NULL)
        {
            throw std::exception("file open failed.");
        }

        char buffer[512] = {0, };
        int read_count = fread(buffer, 512, 1, fp);
        if(read_count == 0)
        {
            throw std::exception("file data read failed.");
        }
    }
    catch(const std::exception& e)
    {
        std::cout << e.what() << std::endl;
        return 1;
    }
    return 0;
}

사용자 정의 exception 클래스를 이용할 수도 있지만, 여기선 쉬운 예제를 위해 std::exception 클래스를 통해 예외처리를 했습니다. 사실 이렇게 간단한 예제로는 예외처리가 뭐가 더 낫다는건지 감이 잘 오지 않습니다. 저도 그랬으니까요. 자~ 코드가 복잡해집니다. 다양한 기능이 붙고, 추상화니, 메시지 핸들러니 각종 쌈박한 로직을 구현해두었습니다. 만들다보니 기능이 붙고 붙네요. 외부 라이브러리도 붙었습니다. 정규식은 boost쓰고, 컨테이너는 stl 쓰고, json은 json spirit, xml은 tinyxml 을 붙였군요. 제가 이 글을 쓰고자 했던건 사실 tinyxml 때문이었습니다. tinyxml 물론 잘 만들어진 라이브러리입니다. 저도 아주 잘 쓰고 있고요. 헌데 이 tinyxml에서 문제가 하나 있었습니다.

TiXmlElement* pNickname = pPlayerElem->FirstChildElement("nickname");
if (pNickname  == NULL)
    return false;

std::string nickname = pNickname >GetText();

문제가 된 부분이 보이시나요? 전 몇번이나 코드를 훓어보던 중에야 원인을 찾을 수 있었습니다. 

원인은 pNickname >GetText() 이 함수가 문제였습니다.

위 코드는 nickname을 문자열형으로 읽어내는 코드입니다. 위 코드는 정상적으로 데이터가 포함되어있을 땐(엘키) 문제가 없습니다.   헌데 만약 이렇게 비어있는 값()일 때 읽는다면? 문제가 생겼습니다. 

비어있을 때 문제가 생긴다는 것도, 코드를 자세히 들여다 보고 나서야 확인한 것이지, 덤프를 본 직후에 바로 알아볼 순 없었어요. 

한 줄 한 줄 코드를 검토하며 함수 내부를 들여다 본 순간… 아차 싶었습니다.

const char* TiXmlElement::GetText() const
{
    const TiXmlNode* child = this->FirstChild();
    if ( child ) {
        const TiXmlText* childText = child->ToText();
        if ( childText ) {
            return childText->Value();
        }
    }
    return 0;
}

child->ToText() 함수가는 내부에 포함된 데이터가 비어있을 때 NULL이 반환되고, 이어 함수 최하단에서 return 0; 코드를 타 NULL 포인터가 반환되었습니다. 

사실 이 함수가 반환타입이 const char* 인 만큼 return “”; 으로 빈 문자열을 반환할 줄 알았습니다.  하지만 빈문자열과 child가 없는 등의 Text화 실패에 대한 상황을 구분하기 위해 return 0; 으로 처리하고 있더군요. API를 꼼꼼하게 체크해보지 못한 개발자의 잘못도 있지만 구현상의 애매모호함도 엄연히 존재한다고 봅니다. 

  1. 오류 상황과 데이터가 없음을 모두 return 0; 즉 NULL 포인터로 반환하고 있는 문제
  2. const char*형은 문자열형만 반환될뿐 사실은 pointer의 개념보다는 문자열 첫 주소가 반환된다는 의미로도 인식되어, NULL 포인터가 반환될 거라 인식하기 어려운 문제를 가지고 있는것이죠.

결과적으로 GetText() 함수의 구현 코드를 보니 NULL 포인터가 반환되는지 검사해주어야 했습니다.

위 코드를 예외처리해봅시다.

TiXmlElement* pNickname = pPlayerElem->FirstChildElement("nickname");
if (pNickname  == NULL)
    return false;

if(pNickname->GetText() == NULL)
    return false;

std::string nickname = pNickname >GetText();

TiXmlElement* pUserID = pPlayerElem->FirstChildElement("user_id");
if (pUserID  == NULL)
    return false;

if(pUserID >GetText() == NULL)
    return false;

std::string user_id= pUserID>GetText();

이렇게 해주어야합니다. GetText를 사용하는 코드가 많으면 많을수록 if문은 늘어납니다. 물론 위의 if문을

if (pUserID  == NULL || pUserID >GetText() == NULL)
    return false;

or 연산으로 수정할수야 있겠지만, 그렇다손쳐도 GetText() 하는 함수마다 체크해주어야 함은 변함이 없습니다.

const char* TiXmlElement::GetText() const
{
    const TiXmlNode* child = this->FirstChild();
    if ( child == NULL) 
    {
        throw std::exception("TiXmlElement::GetText() FirstChild is NULL");
    }

    const TiXmlText* childText = child->ToText();
    if ( childText == NULL) 
    {
        throw std::exception("TiXmlElement::GetText() ToText Failed.");
    }
    return childText->Value();
}

이렇게 코드를 작성했다면 어떨까요?

try
{
    TiXmlElement* pNickname = pPlayerElem->FirstChildElement("nickname");
    if (pNickname  == NULL)
        continue;

    std::string nickname = pNickname >GetText();

    TiXmlElement* pUserID = pPlayerElem->FirstChildElement("user_id");
    if (pUserID  == NULL)
        return false;

    std::string user_id= pUserID>GetText();
}
catch(const std::exception& e)
{
    std::cout << e.what() << std::endl;
    // 일괄적인 예외 핸들링
    return false;
}

만약 try안에서 호출된 다른 함수에서 throw가 존재한다해도 코드의 흐름을 멈추고, 원인을 알아낼 수 있게 됩니다. 

이렇게 되면 예외처리를 일괄적으로 할 수 있고, 코드의 흐름을 중단하는 역할도 맡길 수가 있습니다. 

 보통 외부 라이브러리 함수에서 크리티컬한 상황이 발생했을 경우라거나, 유틸리티 함수등에서의 실패를 리턴값으로 반환하곤 하는데, 이보다 더 강하게 코드의 진행을 중지 시키고 싶을 때 (2차 감염을 막고, 현재 상황의 위험성을 알리기 위해서 주로 이렇게 하죠) 라고 볼 수 있습니다. 

리턴값은 그 함수를 사용하는 코드마다 체크 로직과 핸들링 로직을 구현해주어야 합니다. 

이에 비해 예외 처리는, 적절한 위치에 try-catch로 묶어주기만해도 throw로 던져진 예외를 핸들링 할수가 있게 됩니다. 

물론 모든 코드가 그렇게 예외처리를 하는 것은, 코드의 흐름을 원치 않는 곳에서 중단 시키기 때문에, 상황에 따라 국소적인 코드마다 try-catch 핸들링을 해주어야 하는 부작용도 있긴하지만, 순작용이 훨씬 많은 예외 처리 방식입니다.   심지어 이는 C++에 국한된 것이 아닌, 대다수의 언어에서 지원되는 예외처리 방식이므로 한번 몸에 익혀두시면 두고 두고 활용하시기에 좋습니다. 

C++도 표준 라이브러리들은 대부분 throw로 예외처리를 하기 때문에 try-catch로 예외처리를 할수있고, ruby등의 기타 스크립트 언어들도 내부 함수 예외를 try-catch로 핸들링 할 수 있습니다. 

try-catch 적용시 많이들 어렵게 생각하시는 점은, 어디서 부터 어디까지 try-catch로 감싸도 되는가에 대한 고민이라고 생각하는데요, 이는 boost나 stl등 내부적으로 throw를 사용하고 있는 함수들 부터 적용해보시면 어렵지 않게 적응하실 수 있습니다.

코드의 흐름을 제어하고, 일반화된 오류 처리를 도와주는 예외처리. 적극적으로 써보시면 어떨까요?