코드 리뷰 시스템 Crucible Programming

Crucible은 Jira로 유명한 Atlassian에서 만든 코드 리뷰 시스템 입니다.
http://www.atlassian.com/software/crucible/

Code Collaborator에 비해 기능은 적지만 가격이 저렴합니다.

간단하게 리뷰를 올리는 프로세스를 보여드리겠습니다.

리뷰를 올리는 방식은 Post-Review가 있고 Pre-Review가 있습니다.
  • Post-Review : 저장소에 Commit 된 Changelist 를 기반으로 리뷰를 진행
  • Pre-Review : 소스를 Commit 하기전에 리뷰를 올려서 진행
먼저 Post-Review 프로세스 입니다.

대쉬보드에서 Review 탭을 클릭하면 "Create New Review"가 보입니다. 클릭.

Browse Changesets를 클릭

본인이 Commit한 Changelist가 보입니다. 적절한 항목을 선택하고 "Edit Details"를 클릭.
이렇게 바로 Changelist가 나오게 하려면 Crucible의 계정과 소스저장소의 계정이 동일해야 합니다.


리뷰어를 지정하고 "Start Review"를 클릭합니다.


여기서 부터는 Reviewer의 시선입니다. 변경된 파일들이 좌측에 트리구조로 보이는데, 하나씩 클릭해서 보면 됩니다.
이것은 없던 소스가 삽입된 경우.

이것은 기존 소스가 변경된 경우 입니다.

라인을 클릭하면 코멘트를 달 수 있습니다.

반드시 고쳐야하는 심각한 오류는 Defect를 체크하고 심각도, 분류 항목을 선택해 줍니다.

"Complete"를 클릭해서 Reviewee에게 보냅니다.

Reviewee는 코멘트를 확인하고 수정할것 수정한 후 "Summarize"를 클릭하여 리뷰를 정리합니다.


"Close Review"를 클릭하면 리뷰가 종료됩니다.

다음은 Pre-Review 입니다. Pre-Review를 받기 위해서는 소스저장소에서 patch를 만들어야 합니다. 퍼포스를 예로 들면 아래와 같이 명령줄에 입력하면 현재 pending list를 patch.txt로 만들어 줍니다.

p4 diff -dcu > patch.txt

Post-Review에서 Browse Changesets 선택했던 화면에서 Pre-Commit 선택하고 미리 만들어둔 patch.txt를 업로드 하면 됩니다.

이후는 Post-Review와 같습니다.

그들이 말하지않는 23가지 등 서평

★★★★★ (5개)

나쁜 사마리아인과 비슷한 이야기로 신자유주의의 허상에 대해 이야기 하는 책입니다. 나쁜 사마리아인보다 쉽게 설명되어 있어서 이해하기 좋았습니다. 읽는 재미도 있고 여러모로 추천할만한 책.


★★★★ (4개)
일본 스님이 쓰신 책인데, 잡념이 많을때, 쉽게 화가 날때, 이럴때 스스로를 어떻게 컨트롤 해야 할 것인가를 알려주는 책입니다. 무의식적으로 혹은 수동적으로 여러가지 행동을 동시에 하지 말고(음악 들으면서, 책 보면서, 옆사람 통화 엿들으면서... ), 하나의 행동에 집중해서 느끼고 생각하고 행동하라는 내용이 와닿았습니다.

★★ (2개)
저에겐 너무 어려웠습니다. 중간까지 읽다가 포기. 한국사람에게는 너무 생소한 미국역사상 정치적/사회적인 사건들을 별 설명없이 인용하는 경우가 많습니다. 미국 정치, 역사에 대한 지식이 좀 있어야 이해를 할듯합니다.

★★★★★ (5개)
예수의 생애를 좀 더 객관적인 시각에서 바라보아 재해석을 시도하는 책입니다. 사건에 대한 설명과 의견, 그것을 뒷받침하는 설명들이 상당히 논리적이라 많은 동감을 했고 또한 감동도 했습니다. 너무 재미있게 읽어서 김규항씨의 책들을 더 찾아보게 되는 계기가 되었습니다.

★★★★ (4개)
김규항씨의 블로그에 올렸거나 신문 혹은 잡지에 기고했던 글들을 모아서 발행한 책입니다. 동감가는 이야기도 많고 재미도 있습니다만 너무 짧은 글들의 모음이라 몰입은 잘 안되더군요.

Protocol Buffers를 패킷으로 활용해 보자 Programming

Protocol Buffers(이하 PB)를 패킷으로 활용해 보기위한 예제입니다.

패킷으로서 활용하려면 패킷이 뭉쳐서 올수 있으므로  메세지의 길이를 알아낼수 있다거나 끝을 표시해주는 기능이 필요합니다. 하지만 아래 링크의 글을 보시면,
http://code.google.com/apis/protocolbuffers/docs/techniques.html
하나의 스트림에 다수개의 메세지를 쓸때, 메세지의 끝이나 시작을 추적하는 것은 직접 구현해야 합니다. PB에는 메세지의 끝을 알아내는 기능이 없습니다. 이것을 해결하기 위한 가장 쉬운 방법은 메세지를 비트에 쓰기전에 그것의 사이즈를 쓰는것 입니다. 나중에 읽어낼때는 사이즈를 먼저 읽고 그 사이즈 만큼의 메세지의 본체를 읽어내면 됩니다.

헤더를 자체적으로 만들어서 사용하라고 권고합니다. 그래서 아래와 같이 헤더를 먼저 쓰고 그 다음에 메세지를 쓰는 방식으로 구현했습니다.


메세지가 미완성인지 체크해주는 기능은 있으므로 잘려서 오는 것은 걱정없습니다.
bool isCompleted = message.ParseFromCodedStream(&stream);
먼저 simplemessage.proto 파일 입니다.
package simple;

enum MessageType {
    LOGIN = 0;
    CHAT = 1;
    MOVE = 2;
}

message Login {
    required int32 id = 1;
    required string name = 2;
}

message Chat {
    required string name = 1;
    required int32 dst_id = 2;
    required string message = 3;
}

message Move {
    required int32 id = 1;
 
    message Position {
        required float x = 1;
        required float y = 2;
    }
    repeated Position track = 2;
}

이전 포스트 참조해서 h/cc파일을 생성합니다. 아래는 생성된 파일을 이용한 소스 파일 입니다.
#include <string>
#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
#include <google/protobuf/text_format.h>
#include "simplemessage.pb.h"

using namespace std;
using namespace google;

// 메세지 사이에 끼워넣을 헤더 구조체
struct MessageHeader
{
   protobuf::uint32 size;
  simple::MessageType type;
};
const int MessageHeaderSize = sizeof(MessageHeader);

class PacketHandler
{
public:
   void Handle(const simple::Login& message) const
   {
    PrintMessage(message);
   }
  void Handle(const simple::Chat& message) const
   {
     PrintMessage(message);
   }
  void Handle(const simple::Move& message) const
   {
      PrintMessage(message);
  }

protected:
   // 메세지의 내용을 텍스트 형식으로 출력
  void PrintMessage(const protobuf::Message& message) const
   {
      string textFormatStr;
     protobuf::TextFormat::PrintToString(message, &textFormatStr);
     printf("%s\n", textFormatStr.c_str());
  }
};

// 패킷을 파싱해서 그것을 인자로 적절한 메서드를 호출해줌
void PacketProcess(protobuf::io::CodedInputStream& input_stream, const PacketHandler& handler)
{
  MessageHeader messageHeader;
   // 헤더를 읽어냄
  while( input_stream.ReadRaw(&messageHeader, MessageHeaderSize) )
   {
// 직접 억세스 할수 있는 버퍼 포인터와 남은 길이를 알아냄
    const void* payload_ptr = NULL;
     int remainSize = 0;
    input_stream.GetDirectBufferPointer(&payload_ptr, &remainSize);
    if (remainSize < (signed)messageHeader.size)
       break;

     // 메세지 본체를 읽어내기 위한 스트림을 생성
    protobuf::io::ArrayInputStream payload_array_stream(payload_ptr, messageHeader.size);
     protobuf::io::CodedInputStream payload_input_stream(&payload_array_stream);

     // 메세지 본체 사이즈 만큼 포인터 전진
     input_stream.Skip(messageHeader.size);

// 메세지 종류별로 역직렬화해서 적절한 메서드를 호출해줌
     switch(messageHeader.type)
      {
     case simple::LOGIN:
       {
          simple::Login message;
         if (false == message.ParseFromCodedStream(&payload_input_stream))
             break;
         handler.Handle(message);
        }
      break;
     case simple::CHAT:
        {
          simple::Chat message;
          if (false == message.ParseFromCodedStream(&payload_input_stream))
            break;
          handler.Handle(message);
       }
        break;
     case simple::MOVE:
       {
          simple::Move message;
         if (false == message.ParseFromCodedStream(&payload_input_stream))
            break;
         handler.Handle(message);
       }
        break;
   }
   }
}

// 메세지를 출력용 스트림에 쓴다
void WriteMessageToStream(
simple
::MessageType msgType,
const protobuf::Message& message,
protobuf::io::CodedOutputStream& stream)
{
  MessageHeader messageHeader;
   messageHeader.size = message.ByteSize();
  messageHeader.type = msgType;
   stream.WriteRaw(&messageHeader, sizeof(MessageHeader));
  message.SerializeToCodedStream(&stream);
}

int main(int argc, char* argv[])
{
  // 메세지에 값 세팅
   simple::Login login;
  login.set_name("alice");
   login.set_id(1);

  simple::Chat chat;
   chat.set_name("rabbit");
  chat.set_dst_id(2);
   chat.set_message("How are you doing?");

  simple::Move move;
   move.set_id(3);
  simple::Move::Position* pos0 = move.add_track();
   pos0->set_x(10.01f);
  pos0->set_y(10.02f);
  simple::Move::Position* pos1 = move.add_track();
   pos1->set_x(20.01f);
  pos1->set_y(20.02f);
   simple::Move::Position* pos2 = move.add_track();
  pos2->set_x(30.01f);
   pos2->set_y(30.02f);

  // 필요한 버퍼의 사이즈를 알아내서 버퍼를 할당
   int bufSize = 0;
  bufSize += MessageHeaderSize + login.ByteSize();
   bufSize += MessageHeaderSize + chat.ByteSize();
  bufSize += MessageHeaderSize + move.ByteSize();
   protobuf::uint8* outputBuf = new protobuf::uint8[bufSize];

  // 메세지를 출력할 스트림 생성
   protobuf::io::ArrayOutputStream output_array_stream(outputBuf, bufSize);
  protobuf::io::CodedOutputStream output_coded_stream(&output_array_stream);

   // 메세지를 스트림에 쓴다
  WriteMessageToStream(simple::LOGIN, login, output_coded_stream);
   WriteMessageToStream(simple::CHAT, chat, output_coded_stream);
  WriteMessageToStream(simple::MOVE, move, output_coded_stream);

   // 패킷 핸들러와 입력용 스트림 생성
  PacketHandler handler;
   protobuf::io::ArrayInputStream input_array_stream(outputBuf, bufSize);
  protobuf::io::CodedInputStream input_coded_stream(&input_array_stream);

   // 패킷 분석
  PacketProcess(input_coded_stream, handler);

   // 버퍼 해제
  delete [] outputBuf;
   outputBuf = NULL;
  return 0;
}
실행 결과 입니다.



Good-bye Frankfurt 기타

대략 1년반정도의 독일생활을 정리하고 돌아왔습니다. 사실 돌아온건 2달쯤 전인데, 더 늦기전에 글을 쓰지 않으면 아예 잊어버릴것 같아서 지금이라도 올립니다.

Frankfurt 라는 도시에 대한 1년반동안의 감상이랄까 그런 글입니다. 일단 전반적으로 만족스러웠습니다. 처음엔 독일인에 대한 선입견 같은게 있었죠. 무뚝뚝하다거나 불친절, 완벽주의, 2차대전 등등 하지만 대부분은 사실과 달랐습니다. 사람들은 대다수가 친절했고 인종차별같은건 단 한번도 당해본적 없습니다. 나름 국제도시라서 다양한 인종들이 많습니다. 백인이 가장 많고, 터키계가 그다음, 나머지 흑인, 아시아계가 소수있죠. 제 시선에는 특별히 다른 인종들을 배척하지 않고 어울려서 잘 살아가고 있었습니다. 북미쪽에서는 인종때문이라기보다는 영어사용에 미숙하면 무시당하는 경우가 종종있다고 하던데, 여기는 어차피 영어모국어권도 아니니 그런 것도 없습니다. 젊은 사람들은 대부분 영어사용에 익숙하고 나이드신 분들은 거의 못하시더군요. 영어만으로 일상 생활에 지장이 없었습니다. 다만 아기를 유치원에 보낸다거나, 집을 계약한다거나 하는 좀 더 디테일한 상황에서는 독일어가 좀 필요한듯 했습니다. 이런 경우에는 회사 인사팀에서 도움을 주었으므로 또한 문제가 없었습니다.


두번째로 이야기하고 싶은게 자연환경인데요, 제가 사는 동네에도 정말 멋진 공원이 2개나 있었는데, 하나는 조깅도 할수 있고 고기도 구워 먹을수 있는 전형적인 공원이었고 나머지 하나는 숲길이 길게 펼쳐져있고 호수도 있어서 산책하기 아주 좋은 그런 곳이었습니다. 평일에는 아침에 조깅하러 가고 주말에 날씨 좋은날 도시락 싸서 가면 하루가 훌쩍가죠. 주말에 가도 사람들이 그리 많지 않았습니다. 멋진 공원 근처에 사는게 희망사항이었는데, 제가 살던 동네(Enkheim)는 그런면에서 만족스러웠습니다.


대중교통에 대해 이야기 해보면 Frankfurt는 전반적으로 대중교통이 잘 되어 있어서 자가용없이도 생활이 가능했습니다. 대신 가격은 상당히 비싼 편이라 서울이 보통 편도 900원정도 하는 거리면 거기서는 4000원 정도였습니다. 통근하는 직장인들은 월정기권을 끊어서 이용하는 듯 했습니다. 약간 할인 되기는 하지만 그래도 비싸죠. 좋은 점이라면 그닥 붐비지 않는다는 점과 장애인과 유모차에 대한 배려가 많다는 점이었습니다. 유모차를 가지고 어디를 가도 불편하지 않게 이동이 가능했습니다. 단점하나 추가하자면 지하철은 한국에 비해 지저분하고 냄새도 좀 나서 그닥 좋지 않았습니다. 어딜가도 서울 지하철만큼 깨끗한데가 없죠.

빼놓을수 없는 Frankfurt의 장점은 유럽여행 다니기 정말 좋은 위치라는 것이죠. 기차로 3시간이면 스위스, 4시간이면 파리, 공항이야 워낙 유명하고 비행편도 많죠. 비용때문에 자주 다니지는 못해도 기차표나 비행기표 모두 미리 끊으면 상당히 저렴하게 구입할수 있는 표가 있습니다. 몇달전부터 미리 계획을 짜서 움직이면 저렴하게 여행을 다닐수있습니다. 

단점은 음식이 별로라는 점. 전반적으로 짜고 팍팍해서 거의 대부분의 음식이 입에 맞지 않았습니다. 다행히 독일교민을 상대로한 인터넷 쇼핑몰 같은게 있어서 라면, 쌀, 김, 참치 등등 주문해서 집에서는 거의 한식을 먹었죠. 이거 없었으면 어떻게 살았을까 생각만 해도 아찔하네요. 물가도 비쌌습니다. 품목별로 차이가 있기는 하지만 전반적으로 서울보다 1.5배 가량 비싸다는 느낌을 받았습니다. 대표적으로 비싼 것은 대중교통, 학용품류, 집세 였습니다. 은행도 불편한 항목중에 하나네요. 오늘 입금을 해도 돈은 이틀쯤후에 이체된다거나, 주소가 없으면 통장 개설자체가 불가능하다거나, 계좌에 돈이 충분치 않으면 수수료를 매달 떼어간다거나 하는 등의 한국과 다른게 유독 은행 일처리에 많았습니다. 일요일에 모든 가게가 문을 닫아서 쇼핑이 불가능한것은 처음에는 불편했지만 적응이 되니 토요일에 미리미리 다 사놓게 되더군요.

장점, 단점이 있지만 그래도 여러모로 살기 좋은 곳이었고 저와 가족들에게는 좋은 기억으로 남은 Frankfurt 였습니다.


Google Protocol Buffers에서 Reflection 사용법 Programming

Protocol Buffers(이하 PB)에 대한 설명은 이전 포스트를 참고해주세요.
http://javawork.egloos.com/2720889

PB에서 Reflection을 사용하면 정의된 필드/값에 하나씩 접근할수 있습니다. 이런 방식으로 PB의 소스를 수정하지 않고 여러 기능을 추가할수 있습니다. 예를 들면 제가 하려고 하는, 직렬화된 PB 버퍼를 JSON 형식으로 변환하기, 같은 기능이죠.

아래 코드는 PB에서 제공하는 text format과 같은 형식의 문자열을 출력하는 예제입니다. .proto 파일은 이전 포스트에서 사용한 데이터를 그대로 사용했습니다.

#include <fstream>
#include <string>
#include "addressbook.pb.h"
#include <google/protobuf/descriptor.h>

using namespace std;
using namespace google;

void PrintGroupField(const protobuf::Message& message, const protobuf::FieldDescriptor* descriptor);
void PrintField(const protobuf::Message& message, const protobuf::FieldDescriptor* descriptor);
void IteratingWithDescriptor(const protobuf::Message& root_message);

void PrintGroupField(const protobuf::Message& message, const protobuf::FieldDescriptor* descriptor)
{
const protobuf::Reflection* reflection = message.GetReflection();
int repeated_fieldCount = reflection->FieldSize(message, descriptor);
const protobuf::Descriptor* child_descriptor = descriptor->message_type();
for(int j=0;j<repeated_fieldCount;++j)
{
printf("%s {\n", descriptor->name().c_str());
const protobuf::Message& child_message = reflection->GetRepeatedMessage(message, descriptor, j);
const protobuf::Reflection* child_reflection = child_message.GetReflection();
int child_fieldCount = child_descriptor->field_count();
for(int k=0;k<child_fieldCount;++k)
{
const protobuf::FieldDescriptor* curChildFd = child_descriptor->field(k);
printf(" ");
PrintField(child_message, curChildFd);
}
printf("}\n");
}
}

void PrintField(const protobuf::Message& message, const protobuf::FieldDescriptor* descriptor)
{
const protobuf::Reflection* reflection = message.GetReflection();
switch(descriptor->type())
{
case protobuf::FieldDescriptor::TYPE_INT32:
{
printf("%s : %d", descriptor->name().c_str(), reflection->GetInt32(message, descriptor));
break;
}
case protobuf::FieldDescriptor::TYPE_STRING:
{
printf("%s : %s", descriptor->name().c_str(), reflection->GetString(message, descriptor).c_str());
break;
}
case protobuf::FieldDescriptor::TYPE_ENUM:
{
const protobuf::EnumValueDescriptor* enumValue = reflection->GetEnum(message, descriptor);
printf("%s : %s", descriptor->name().c_str(), enumValue->name().c_str());
break;
}
case protobuf::FieldDescriptor::TYPE_MESSAGE:
{
PrintGroupField(message, descriptor);
break;
}
}
printf("\n");
}

void IteratingWithDescriptor(const protobuf::Message& root_message)
{
const protobuf::Descriptor* root_descriptor = root_message.GetDescriptor();
int root_fieldCount = root_descriptor->field_count();
for(int i=0;i<root_fieldCount;++i)
{
const protobuf::FieldDescriptor* curFd = root_descriptor->field(i);
PrintField(root_message, curFd);
}
}

int main(int argc, char* argv[])
{
const char* test_filename = "person.txt";
fstream ifs(test_filename, ios::in | ios::binary);
tutorial::Person person_message;
person_message.ParseFromIstream(&ifs);
ifs.close();
IteratingWithDescriptor(person_message);
return 0;
}
예외처리도 안되어 있고, 소스 자체가 person 데이터 형식의 출력에 치우쳐있어서 다른 형식으로 직렬화된 버퍼는 바르게 출력하지 못할수 있습니다. Reflection의 사용법을 알려드리기 위한 예제이니 감안하고 봐주시기 바랍니다.

출력값은 아래와 같습니다.





1 2 3 4 5 6 7 8 9 10 다음