사용자 삽입 이미지

처음 해 보는 밤 배경 그림입니다.
풀 배경 처리하는 데 엄청난 공을 들였었다는... 근데 다시 하라고 하면 못 할 것 같네요.
(어뜨케 했는지 까먹음. ㅎㅎㅎ)

그 외에, 밤을 배경으로 하기 때문에 원화에 그림자 더하는 데 한 시간은 사용했습니다.
원화는 원래 제가 그리던 그 색으로, 즉 대낮에 사방에서 빛이 비치는 조건하의 그림이거든요.
전문용어로 앰비언트 라이트라고 하던가요? 주변광? 하여튼 그림자가 하나도 없고, 적당한 밝기의 빛이 모든 방향에서 비치는 그런 상태. 거기에 그림자를 더하기 위해 전체적으로 밝기를 낮추는 쉐도우 레이어하고 빛이 비치는 방향에 대한 그림자(그리고 하이라이트) 레이어를 추가했습니다. 그러니까, 그림자 처리에는 총 두 개의 레이어가 사용됐군요. 아니, 세 개던가?(배경 자체도 원화하고 같은 방식으로 했기 때문에...)

근데, 그 두 그림자가 겹치면서 그림자에 얼룩이 져 버렸습니다. 아후... 이건 어떻게 고치기도 힘드네요.

트랙백 주소 :: http://wbstory.codns.com/~wbstory/blog/trackback/90

댓글을 달아 주세요

사용자 삽입 이미지

아아... 여기는 미쿡이므로... 블로그 포스팅 날짜하고 하루 정도 차이날 수 있습니다.
원래 앞에 포스팅한 그림을 그린 다음에 바로 이어서 그린 건데, 시간이 너무 오래 걸려서 자정을 넘겨서 포스팅하게 되네요.
그나저나, 눈동자 그리는 건 실패한건가... T.T

트랙백 주소 :: http://wbstory.codns.com/~wbstory/blog/trackback/89

댓글을 달아 주세요

사용자 삽입 이미지

정말 오래간만에 타블렛 펜을 잡아봤습니다.
어쩐지 후련하기도 하고... 좋네요 ^^;

트랙백 주소 :: http://wbstory.codns.com/~wbstory/blog/trackback/88

댓글을 달아 주세요

포케님 블로그에 긴 댓글을 쓰면서 우리나라의 정치가 왜 이모양인지 곰곰이 생각해 보았습니다.
마침 여기가 미국이고, 미국은 정말 정치를 잘하죠. 부시 대통령이 이라크 때려부순 건 잘한 일로 보이지 않지만 미국 설문조사에서 '최악의 대통령'으로 나온 걸 보니 미국 사람들도 잘못한 줄 아는가 봅니다.

우리는 어려서부터 공자, 맹자, 부처에 대한 이야기를 듣고 삽니다. 한자 공부 시간에도 국어 공부 시간에도 심지어 도덕 수업에서도 배우지요. 소위 '군자'라고 불리는 이런 사람들은 정치를 싫어했고 초야에 묻혀 지냈습니다. 누군가가 와서 벼슬을 준다고 하면 더럽다면서 피하고, 조언을 구한다고 하면 말을 빙빙 돌려서 조롱만 하고 쫓아버렸지요. 이게 우리가 배운 '군자'의 모습입니다.

미국에서는 어떨까요? 일단 미국은 기독교가 국교인 나라입니다. 기독교 하면 떠오르는 것은 '성경'이지요. 성경에서 신약성경은 예수의 이야기로 되어 있는데, 이 예수라는 분은 초야에 묻혀 살지 않았습니다. 오히려 죽을 줄 알면서도 적진 한복판으로 뛰어 들었죠. 공자 맹자 하는 소위 '자'자 돌림들이 수양이 깊어지면 깊어질수록 하늘 꼭대기로 올라가서 제자들하고만 알콩달콩(?)하는 것과는 정반대입니다.

우리는 '더러운 정치판'을 보고 슬슬 피합니다. 자신은 군자이고 따라서 더러운 시궁창에서 살지 않겠다는 잠재의식이 깔려 있거든요. 하지만 미국인들은 '더러운 정치판'한가운데에 뛰어들어 과감히 삽을 듭니다. 그들은 이렇게 생각합니다. '내가 저 시궁창을 메우면 나 한 몸만 더러워지겠지만, 내가 저 시궁창을 메우지 않으면 내 뒤의 열 사람이 더러워진다'.

여기서 제가 만화 하나를 소개해 드리겠습니다. 네이버 목요 웹툰, '호랭총각'입니다. 강호진 작가님이 연재중인 만화지요. 여기서 '호랭총각 37편 - 선비의 혼(17)'을 보면 이런 대사가 나옵니다.

'많이 배우고 많이 갖춘 사람이 베풀지를 않으면, 배운 것 적고 없이 사는 숱한 어려운 백성들은 누가 돌보겠느냔 말이다.'
깊이 생각해 볼 문제입니다.

트랙백 주소 :: http://wbstory.codns.com/~wbstory/blog/trackback/87

댓글을 달아 주세요

  1. BlogIcon 포케 2008/04/24 20:05  댓글주소  수정/삭제  댓글쓰기

    댓글 남겨주신데 대한 댓글에도 남겼습니다만,
    김현식님의 의견과 제 의견이 상반될 것은 전혀 없는 것 같습니다.
    오히려 제 의견에 살을 붙일 수 있어서 좋았다고 생각합니다.
    다만, 견해가 조금 다른 것 뿐이고 의미가 와전된 경향도 있었던 것 같습니다.
    가끔 이런 저런 의견을 들려주시면 좋겠네요.
    의견을 교류하면서 종합하면 긍정적인 여론이 조성되는데 한 몫 거들 수 있지 않겠나 생각합니다.

리눅스 사용자들은 쉘스크립트를 만들고 실행하는 것에 많이 익숙하실 겁니다. 그도 그럴것이, 쉘스크립트를 사용하지 않으면 엄청난 양의 타이핑을 감내해야 하기 때문이지요. 리눅스의 많은 부분이 쉘스크립트로 이루어져 있고, OS의 중요한 부분을 차지하고 있는 만큼 강력한 기능을 갖추고 있습니다.
윈도우에서도 쉘스크립트를 사용할 수 있습니다. 리눅스의 bash 쉘 같은 강력함은 없지만 기본적인 매크로를 작성하고 실행할 능력은 됩니다. 하지만 윈도우 사용자는 마우스로 아이콘을 클릭하는 동작에만 익숙하기 때문에 윈도우에서도 스크립트를 짤 수 있다는 사실을 모르지요.

제가 소개해 드릴 팁은 "여러 프로그램 한 번에 실행시키기" 일명 매크로입니다.
저는 컴퓨터를 켜자마자 하는 작업이 몇 있습니다. 일단 웹브라우저를 실행시키고, 메일 클라이언트를 켜고, IDE를 실행시키고, 탐색기를 열어 특정 폴더에 놓아 둡니다(서브버전 때문입니다). 저 각각의 작업은 최소한 한 번의 클릭이 소요됩니다. 그러니까, 다 합치면 네 번의 클릭을 해야 하네요. 이 일련의 작업을 한 번의 클릭으로 할 수 있다면 좋겠죠? 그걸 도와 주는 것이 바로 윈도우의 start 유틸리티와 배치 파일입니다.

배치 파일은 bat라는 확장명을 가지고 있습니다. 안에 들어가는 건 일반 텍스트이지만 실행이 가능하고, 실행하면 명령 프롬프트에 해당 문서파일의 내용을 직접 타이핑한 것과 같은 효과를 주게 됩니다.

우선, 메모장을 엽니다. 그냥 빈 상태로 열어만 두세요.

그리고, 아래 그림에서 보이는 것과 같이, 실행하고자 하는 아이콘의 등록 정보 창을 엽니다.
사용자 삽입 이미지

빠른 실행 아이콘이든, 바탕 화면 아이콘이든, 시작 버튼에 등록된 프로그램이든 상관없습니다. 저 '등록 정보'(Properties) 는 마우스 오른쪽 버튼을 누르면 나오는 것은 잘 아시죠?
(제가 지금 미국이라서 영문 윈도우 화면으로 설명드리고 있습니다. 양해바랍니다)

사용자 삽입 이미지

이게 '등록 정보' 창입니다. 필요한 것은 'Target' 부분입니다. 한글로 뭐라고 쓰여 있었는지 잘 기억이 안 나네요. 혹시 'Start in:' 부분이 'Target'과 다르신 분은 'Start in:' 도 눈여겨 보셔야 합니다. 대부분, 거의 대부분 'Start in:'과 'Target'은 맨 끝만 빼고 똑같습니다. 그러니까 'Start in:' 이 'Target'의 앞부분과 같다면 신경 쓰실 필요가 없습니다.(거의 90%는 같지 않을까 싶네요.)

'Target'을 메모장에 복사합니다.

"C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\IDE\devenv.exe"

다음, 이걸 다음과 같이 고칩니다.

start /D"C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\IDE\" /B devenv.exe

start 명령은 리눅스 쉘스크립트의 & 과 같습니다. 그러니까, 작업을 백그라운드로 실행시키게 해 주는 유틸리티입니다. 저걸 사용하지 않으면 한 프로그램이 실행되고, 그 프로그램이 종료돼야 다음 프로그램이 실행됩니다. 아마 이런 식의 동작은 대부분의 사용자가 원하는 동작이 아니겠죠? 어쨌든 윈도우에서는 여러 창을 동시에 띄워 작업하는 것이 일반적이니까요.

start 명령의 매뉴얼을 보면 다음과 같이 쓰여 있습니다.

START ["title"] [/Dpath] [/I] [/MIN] [/MAX] [/SEPARATE | /SHARED]
      [/LOW | /NORMAL | /HIGH | /REALTIME | /ABOVENORMAL | /BELOWNORMAL]
      [/WAIT] [/B] [command/program]
      [parameters]

    "title"     Title to display in  window title bar.
    path        Starting directory
    B           Start application without creating a new window. The
                application has ^C handling ignored. Unless the application
                enables ^C processing, ^Break is the only way to interrupt
                the application
    I           The new environment will be the original environment passed
                to the cmd.exe and not the current environment.
    MIN         Start window minimized
    MAX         Start window maximized
    SEPARATE    Start 16-bit Windows program in separate memory space
    SHARED      Start 16-bit Windows program in shared memory space
    LOW         Start application in the IDLE priority class
    NORMAL      Start application in the NORMAL priority class
    HIGH        Start application in the HIGH priority class
    REALTIME    Start application in the REALTIME priority class
    ABOVENORMAL Start application in the ABOVENORMAL priority class
    BELOWNORMAL Start application in the BELOWNORMAL priority class
    WAIT        Start application and wait for it to terminate
    command/program
                If it is an internal cmd command or a batch file then
                the command processor is run with the /K switch to cmd.exe.
                This means that the window will remain after the command
                has been run.

                If it is not an internal cmd command or batch file then
                it is a program and will run as either a windowed application
                or a console application.

    parameters  These are the parameters passed to the command/program


If Command Extensions are enabled, external command invocation
through the command line or the START command changes as follows:

non-executable files may be invoked through their file association just
    by typing the name of the file as a command.  (e.g.  WORD.DOC would
    launch the application associated with the .DOC file extension).
    See the ASSOC and FTYPE commands for how to create these
    associations from within a command script.

When executing an application that is a 32-bit GUI application, CMD.EXE
    does not wait for the application to terminate before returning to
    the command prompt.  This new behavior does NOT occur if executing
    within a command script.

When executing a command line whose first token is the string "CMD "
    without an extension or path qualifier, then "CMD" is replaced with
    the value of the COMSPEC variable.  This prevents picking up CMD.EXE
    from the current directory.

When executing a command line whose first token does NOT contain an
    extension, then CMD.EXE uses the value of the PATHEXT
    environment variable to determine which extensions to look for
    and in what order.  The default value for the PATHEXT variable
    is:

        .COM;.EXE;.BAT;.CMD

    Notice the syntax is the same as the PATH variable, with
    semicolons separating the different elements.

When searching for an executable, if there is no match on any extension,
then looks to see if the name matches a directory name.  If it does, the
START command launches the Explorer on that path.  If done from the
command line, it is the equivalent to doing a CD /D to that path.

샬라샬라... 영문 윈도우라서 전부 영어로... 뭐 실행 우선순위를 지정한다느니 어쩌구저쩌구 써 있네요. /D 옵션은 시작 디렉토리를 지정하고, /B 옵션은 명령 프롬프트 창을 띄우지 않고 프로그램을 실행한다는 옵션입니다. 즉,

start /D"C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\IDE\" /B devenv.exe

이것이 의미하는 바는 "C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\IDE" 디렉토리로 들어가서 devenv.exe 를 실행하되, 명령 프롬프트 창을 띄우지 말고 하라. 라는 뜻입니다.

계속 이런 식으로 붙여 보죠.

start /D"C:\Program Files\Mozilla Firefox" /B firefox.exe
start /D"C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\IDE\" /B devenv.exe
start /D"C:\Program Files\NUnit 2.4.7\bin\" /B nunit.exe
start /D"C:\Program Files\mozilla" /B thunderbird.exe
start /D"C:\Program Files\Microsoft Office\Office12\" /B OUTLOOK.EXE /recycle
start /D"C:\cygwin\" /B Cygwin.bat
start /D"C:\WINNT\" /B explorer.exe "C:\svn\myprojects"


자, 이제 이걸 저장을 합니다. 바탕 화면에 두시든 '내 문서'에 두시든 '새 폴더'에 두시든 그건 자유입니다. 저는 '내 문서'에 넣었습니다. 넣을 때 my-macro.bat 라고 입력하고 저장합니다. 끝에 .bat는 중요합니다.

그리고, '내 문서' 로 가서, 해당 아이콘을 '빠른 실행'에 끌어다 놓습니다. 왜냐고요? 우리의 목적은 '원클릭 매크로'이니까요. 더블 클릭도 귀찮습니다. 헤헤~

bat 파일의 기본 아이콘은 톱니바퀴가 안에 들어있는 응용 프로그램 창 모양 아이콘()인데, 원하면 바꿀 수 있습니다. '등록 정보' 안에서 '아이콘 변경...'을 찾아보세요.

저걸 클릭한 뒤의 화면은 따로 담지 않습니다. 그냥 순간 윈도우가 요란법석을 떨다가 짜잔 하고 한꺼번에 창을 띄우는 것을 보실 수 있을 겁니다.

참고로, 위의 매뉴얼을 찬찬히 보셨으면 아시겠지만, 실행 파일명에 꼭 실행 가능한 파일을 넣으란 법은 없습니다. .doc 파일을 '실행'하라고 하면 MS-Word가 뜨면서 해당 파일을 읽어들이게 됩니다. 이건 리눅스보다 편하네요. 리눅스는 꼭 '실행 파일명 + 데이터 파일명' 이라고 적어 주어야 하거든요.

실행 우선순위를 세부 조정한다거나 하는 건 좀 오버스러운 감이 있어서 설명을 생략하겠습니다. 하지만 조심할 건 /REALTIME 을 이클립스에 적용한다거나 하면 안됩니다! 무거운 애플리케이션을 실시간 옵션을 주어 실행하면 여러 부작용이 생길 수 있습니다.
이메일 클라이언트에 /HIGH를 넣는 튜닝을 하는 것은 괜찮을 것 같네요.

유용하게 써주셨으면 합니다.


트랙백 주소 :: http://wbstory.codns.com/~wbstory/blog/trackback/86

댓글을 달아 주세요

  1. BlogIcon 포케 2008/04/24 06:55  댓글주소  수정/삭제  댓글쓰기

    그냥 이게 가능하지 않을까 생각만 하고 있었는데 의외로 간단하게 되는군요.
    /HIGH 정도는 유용하게 쓰일 것 같네요.
    좋은 정보 감사합니다.

  2. BlogIcon Canal 2008/05/04 22:46  댓글주소  수정/삭제  댓글쓰기

    윈도 95부터 start 명령을 그냥 도스 콘솔창에서 디렉토리 열기 위해서 start . 만을 사용했지만

    이렇게 여러창을 여는 기능이 되는군요 ^^

역할 사슬 모델(Chain of responsibility)은 보통 윈도우 GUI의 이벤트 처리를 위한 용도로 많이 쓰이는 디자인 패턴입니다.
구현 방법이야 여러 가지가 있겠습니다만, 기본적으로는 어떤 메시지를 지정된 핸들러들에게 전달해서, 처리할 수 있는 핸들러만 메시지를 처리하고 그렇지 못한 핸들러는 메시지를 무시하는 원리로 동작합니다.

아래 세 가지 구현을 예로 보여드립니다. 같은 내용을 영문 위키피디아(http://en.wikipedia.org/wiki/Chain-of-responsibility_pattern (새 창으로 열기)) 에도 올려놓았습니다. 한국 위키피디아에는 아쉽게 해당 토픽이 없네요.

추상 클래스 기반 구현

using System;

namespace Chain_of_responsibility
{
    public abstract class Chain
    {
        private Chain _next;

        public Chain Next
        {
            get
            {
                return _next;
            }
            set
            {
                _next = value;
            }
        }

        public void Message(object command)
        {
            if ( Process(command) == false && _next != null )
            {
                _next.Message(command);
            }
        }

        public static Chain operator +(Chain lhs, Chain rhs)
        {
            Chain last = lhs;

            while ( last.Next != null )
            {
                last = last.Next;
            }

            last.Next = rhs;

            return lhs;
        }

        protected abstract bool Process(object command);
    }

    public class StringHandler : Chain
    {
        protected override bool Process(object command)
        {
            if ( command is string )
            {
                Console.WriteLine("StringHandler can handle this message : {0}",(string)command);

                return true;
            }

            return false;
        }
    }

    public class IntegerHandler : Chain
    {
        protected override bool Process(object command)
        {
            if ( command is int )
            {
                Console.WriteLine("IntegerHandler can handle this message : {0}",(int)command);

                return true;
            }

            return false;
        }
    }

    public class NullHandler : Chain
    {
        protected override bool Process(object command)
        {
            if ( command == null )
            {
                Console.WriteLine("NullHandler can handle this message.");

                return true;
            }

            return false;
        }
    }

    public class IntegerBypassHandler : Chain
    {
        protected override bool Process(object command)
        {
            if ( command is int )
            {
                Console.WriteLine("IntegerBypassHandler can handle this message : {0}",(int)command);

                return false; // Always pass to next handler
            }

            return false; // Always pass to next handler
        }
    }

    class TestMain
    {
        static void Main(string[] args)
        {
            Chain chain = new StringHandler();
            chain += new IntegerBypassHandler();
            chain += new IntegerHandler();
            chain += new IntegerHandler(); // Never can reached
            chain += new NullHandler();

            chain.Message("1st string value");
            chain.Message(100);
            chain.Message("2nd string value");
            chain.Message(4.7f); // not handled
            chain.Message(null);
        }
    }
}

추상 클래스 기반으로 구현하는 방법입니다. 하지만 이 방식은 썩 좋지 않은 방식입니다. 일단 클래스 상속을 꼭 해야 되기 때문에 파생 클래스를 핸들러로 등록할 수 없습니다.
그 외에 ArrayList라는 좋은 컬렉션 클래스가 있는데 굳이 링크드리스트를 구현해서 사용하고 있는 것도 눈여겨 보아 주시기 바랍니다.

인터페이스 기반 구현

using System;
using System.Collections;

namespace Chain_of_responsibility
{
    public interface IChain
    {
        bool Process(object command);
    }

    public class Chain
    {
        private ArrayList _list;

        public ArrayList List
        {
            get
            {
                return _list;
            }
        }

        public Chain()
        {
            _list = new ArrayList();
        }

        public void Message(object command)
        {
            foreach ( IChain item in _list )
            {
                bool result = item.Process(command);

                if ( result == true ) break;
            }
        }

        public void Add(IChain handler)
        {
            List.Add(handler);
        }
    }

    public class StringHandler : IChain
    {
        public bool Process(object command)
        {
            if ( command is string )
            {
                Console.WriteLine("StringHandler can handle this message : {0}",(string)command);

                return true;
            }

            return false;
        }
    }

    public class IntegerHandler : IChain
    {
        public bool Process(object command)
        {
            if ( command is int )
            {
                Console.WriteLine("IntegerHandler can handle this message : {0}",(int)command);

                return true;
            }

            return false;
        }
    }

    public class NullHandler : IChain
    {
        public bool Process(object command)
        {
            if ( command == null )
            {
                Console.WriteLine("NullHandler can handle this message.");

                return true;
            }

            return false;
        }
    }

    public class IntegerBypassHandler : IChain
    {
        public bool Process(object command)
        {
            if ( command is int )
            {
                Console.WriteLine("IntegerBypassHandler can handle this message : {0}",(int)command);

                return false; // Always pass to next handler
            }

            return false; // Always pass to next handler
        }
    }

    class TestMain
    {
        static void Main(string[] args)
        {
            Chain chain = new Chain();

            chain.Add(new StringHandler());
            chain.Add(new IntegerBypassHandler());
            chain.Add(new IntegerHandler());
            chain.Add(new IntegerHandler()); // Never can reached
            chain.Add(new NullHandler());

            chain.Message("1st string value");
            chain.Message(100);
            chain.Message("2nd string value");
            chain.Message(4.7f); // not handled
            chain.Message(null);
        }
    }
}

추상 클래스 구현보다 좀 더 유연해졌습니다. 인터페이스를 기반으로 구현했기 때문에 파생 클래스를 핸들러로 등록할 수도 있게 되었습니다. 또한 ArrayList라는 컬렉션을 사용했지요.

델리게이트와 이벤트를 사용한 구현

using System;

namespace Chain_of_responsibility
{
    public class StringHandler
    {
        private static int count;

        public void Process(object command)
        {
            if ( command is string )
            {
                string th;
                count++;

                if ( count % 10 == 1 ) th = "st";
                else if ( count % 10 == 2 ) th = "nd";
                else if ( count % 10 == 3 ) th = "rd";
                else th = "th";

                Console.WriteLine("StringHandler can handle this {0}{1} message : {2}",count,th,(string)command);
            }
        }
    }

    public class StringHandler2
    {
        public void Process(object command)
        {
            if ( command is string )
            {
                Console.WriteLine("StringHandler2 can handle this message : {0}",(string)command);
            }
        }
    }

    public class IntegerHandler
    {
        public void Process(object command)
        {
            if ( command is int )
            {
                Console.WriteLine("IntegerHandler can handle this message : {0}",(int)command);
            }
        }
    }

    class Chain
    {
        public delegate void MessageHandler(object message);
        public static event MessageHandler Message;

        public static void Process(object message)
        {
            Message(message);
        }
    }

    class TestMain
    {
        static void Main(string[] args)
        {
            StringHandler sh1 = new StringHandler();
            StringHandler2 sh2 = new StringHandler2();
            IntegerHandler ih = new IntegerHandler();

            Chain.Message += new Chain.MessageHandler(sh1.Process);
            Chain.Message += new Chain.MessageHandler(sh2.Process);
            Chain.Message += new Chain.MessageHandler(ih.Process);
            Chain.Message += new Chain.MessageHandler(ih.Process); // Can also reached

            Chain.Process("1st string value");
            Chain.Process(100);
            Chain.Process("2nd string value");
            Chain.Process(4.7f); // not handled
        }
    }
}

이 방법이 C#에서 이벤트 처리를 위해 주로 사용하는 방식이라 할 수 있습니다.
인터페이스조차 사용하지 않았지요.
델리게이트는 일종의 함수 포인터와 비슷한 개념인데, 좀 더 객체지향적이고 좀 더 안전한 타입입니다. 이벤트는 델리게이트를 좀 더 편하게 사용하기 위한 C# 언어 차원의 배려이고요. 이벤트 키워드 없이 델리게이트만으로도 위의 코드를 사용할 수 있습니다.


트랙백 주소 :: http://wbstory.codns.com/~wbstory/blog/trackback/85

댓글을 달아 주세요

아하하, 갑자기 글이 쓰고싶어져서 느닷없이 들어왔습니다.
(내 블로근데, 느닷없이 들어왔다는 표현은 좀...)

판타지소설 작가들은 거의 무시하고 있지만, 사실상 '언어'라는 것은 전세계가 다 똑같을 수 없는 것입니다. 굳이 성경책을 비유로 들지 않아도, 지리적으로 고립된 상태로 수 백년 이상 지나게 되면 서로 말이 완전히 달라지게 되죠. 겨우 50년 지났지만, 남북한의 언어 차이는 이미 보이고 있잖아요?

저는 세계관을 설계할 때 최대한 '말이 되는' 세계관을 짜고 싶었기 때문에, 이 언어 문제로 무척 고심했습니다. 결론은 '외국어 배우듯이 배운다' 정도로 나긴 했지만, 이 언어통합을 위한 연구 과정에서 파생된 언어가 있습니다. 그것이 바로 '컴패니온 어' 다른 말로 '엘프어'입니다.

제 세계관에서 엘프는 살아있는 것이라면 그 누구와도, 그 어느 것과도 대화가 가능합니다. 텔레파시 능력을 가진 종족이기 때문에 상대방의 생각을 직접 읽을 수도 있고 자신의 생각을 다른 사람에게 주입할 수도 있지요. 날 때부터 정신방어막이 쳐 있는 드워프들과는 대화가 안되긴 하지만, 그 외의 모든 종족과는 대화가 가능합니다.(드워프들과 대화할 때는 드워프어로 대화합니다)

컴패니온 어는 고차원에서 설명하자면 텔레파시로 구성된 와이어리스 인터넷 비슷한 개념이지만, 그건 잠시 후에 말씀드리겠고, 지금은 컴패니온어 자체에 대한 특성을 말씀드릴게요. 뭐, 일종의 프로토콜 설명서라고 할 수 있습니다.

컴패니온어의 첫 번째 특징은, 문법이 없다는 겁니다. 텔레파시로 생각의 단위를 그대로 보내기 때문에, 컴패니온어에는 읽는 순서만이 정해져 있을 뿐 다른 문법이 일체 없습니다. 실제 컴패니온어의 문법 요소는 로케이터와 시퀀서 두 개 뿐이랍니다. 사실 이조차도 개인간 대화에서는 쓰이지 않습니다.

컴패니온어의 두 번째 특징은, 중의 표현이 없다는 겁니다. 동음이의어라든지 표의어, 의성어 등등 오해의 소지가 있는 표현은 일체 없습니다. 그 누가 읽더라도 동일하게 해석되고, 그 누가 말하더라도 뜻을 온전히 전달할 수 있습니다. 사실 이것 때문에 엘프들이 발전을 멈춘 겁니다. 컴패니온어는 상상력을 너무나 제한하기 때문에 인간들처럼 상상에 대한 훈련이 안 되어 상대적으로 '둔한 머리'를 갖게 됐죠. 아는 건 많은데 응용을 못한다고나 할까요?

컴패니온어의 세 번째 특징은, 따로 배울 필요가 없다는 겁니다. 사실 이건 일부만 사실입니다. 컴패니온어를 대화에만 사용한다면, 따로 배울 필요가 없다는 게 사실이지만, 컴패니온어로 쓰여진 글을 해석한다면, 문자 자체는 배워야 합니다. 엘프의 경우 문자 해석 능력이 아예 유전자에 하드 코딩돼 있어서 날 때부터 글을 읽을 수 있지만(쓰진 못합니다), 타 종족은 그렇지 못하죠. 하지만 텔레파시가 가능한 인간이나 유니토피아인은 문자만 배우면 글도 문제없이 읽을 수 있습니다.
(이론적으로만 그렇습니다. 실제로는 뇌 구조의 차이로 발생하는 의미 훼손이 심합니다)
컴패니온어의 특징상 글을 다른 의미로 해석한다거나 하는 일은 없습니다. 글이 중간에 잘렸다면 또 모르지만요. 즉, 한 번 완전히 읽을 수 있게 되면 그 뒤로는 모든 컴패니온어를 읽을 수 있습니다. 단어를 모른다거나 하는 일이 발생하지 않기 때문입니다. 컴패니온어는 그 글을 쓴 사람의 지식과 느낌을 그대로 담고 있지요.

그리고 놀랍게도, 컴패니온어의 글씨 습득 비법은 '한 번 듣는' 겁니다. 이미 컴패니온어를 읽을 수 있는 사람이 다른 사람에게 특정 문자들을 보여 주면서 한 자 한 자 읽어 주기만 하면 됩니다. '특정 문자'임에 유의하세요. '모든 문자'가 아닙니다.
(물론, 그 '특정 문자'에 대해서는 완전하게 들어야 합니다. 보통, 엘프 이외의 종족은 이 '특정 문자'의 절반도 듣지 못합니다)

컴패니온어는 자기기술형 언어입니다. 그냥 수로만 따지면 컴패니온어의 단어 수는 수백, 수천억 개가 넘으며 증가 속도도 굉장합니다. 하지만 문자의 수는 오히려 드워프어보다도 적습니다. 문자로 쓰인 컴패니온어는 거의 대부분이 로케이터 심볼이고 일부가 시퀀서 심볼입니다. 컴패니온어를 읽는 사람은 로케이터를 읽는 것이고, 그 로케이터 심볼을 떠올리면 두뇌의 특정 영역이 활성화되면서 마치 꿈을 꾸듯 해당 지식이 머릿속에 떠오릅니다. 단, 로케이터만으로는 스틸컷만 전달이 되므로, 시퀀서 심볼을 통해 그 생각의 단편들을 이어 나가게 됩니다. 이것이 컴패니온어의 읽는 방식입니다.

이 언어는 그래서 단점도 함께 내포하고 있습니다. '도 아니면 모'가 그것인데, 글씨가 중간에 훼손되면 다음 시퀀서 심볼까지 모든 글을 읽을 수 없게 됩니다. 특히 시퀀서 심볼이 훼손되면 두 개를 읽을 수 없게 됩니다. 또, 정신병이 있거나 뇌가 손상된 사람도 읽을 수 없습니다.
그리고 에르스시대에는 또 다른 문제가 있습니다. 이 컴패니온어는 습득하려면 한 번 읽어주어야 하고, 읽어 주려면 텔레파시를 사용해야 하고, 텔레파시는 가이아 포스가 있어야 사용할 수 있는데 에르스시대에는 가이아 포스가 거의 없거든요. 그래서 에르스시대에는 그 누구도 글로 쓰인 컴패니온어를 읽고 해석할 수가 없습니다. 이론적으로는요.
(실제로는 역공학적 접근을 사용해서 일부를 해독합니다. 생각을 하고, 그 생각을 컴패니온어로 적고, 시퀀서 심볼을 기준으로 조각낸 다음에, 패턴 매칭을 해 보는 겁니다)

드워프의 경우는 컴패니온어를 결코 습득할 수 없는데, 이는 컴패니온어가 텔레파시를 기반으로 하는 언어이기 때문입니다. 드워프는 날 때부터 정신방어벽이 있어서 자신도 텔레파시를 사용할 수 없거든요. 거창하게 정신방어벽이라고 했지만, 사실은 드워프의 두뇌 구조가 엘프와 좀 다릅니다. 로케이터는 뇌의 특정 부위의 활성 정도를 나타내는 지표인데, 드워프의 뇌 구조가 엘프와 다르니 떠오르는 생각도 엉뚱하게 되는 것이죠. 거의 모든 경우, 드워프가 컴패니온어를 읽으려고 하면 두통만 생기고 아무 것도 떠오르지 않습니다. 사실 이 때문에 엘프와 드워프가 사이가 안 좋은 것일 수도 있습니다. 엘프 입장에서는 대화를 시도하는 거지만 드워프 입장에서는 정신 공격인 셈이니까요. 나중에 엘프가 드워프어를 배워서 대화가 가능해지긴 했지만, 뭐 첫인상이 벌써 그렇게 박혔으니...

인간이나 유니토피아인은 뇌 구조가 엘프와 거의 완전히 똑같습니다. 설정을 읽어 나가다 보면 왜 똑같을 수 밖에 없는지 이해가 되실 겁니다. 아, 유니토피아인의 경우에는 뇌 구조가 엘프에 맞게 패치됐다는 말씀은 드려야겠네요. 그건 설정에 안 써있으니까요.

자, 어쨌든 이렇게 주저리주저리 컴패니온 어에 대한 설명을 드렸습니다.
저는 그럼 이만...




트랙백 주소 :: http://wbstory.codns.com/~wbstory/blog/trackback/84

댓글을 달아 주세요

제 위키, 웨더브레스 이야기 위키. 안 본지 거의 일 년은 넘은 것 같군요. 실제로 몇몇 문서는 1년이 넘게 방치 상태입니다. 제가 그만큼 글쓰기에 신경을 안 썼다는 증거죠.

제가 가장 고심하는 게, 지명과 그 지역의 실제 지도상의 위치, 도시의 크기 등입니다. 물론 역사의 흐름을 설정하는 것이 세계관 설정에 가장 중요한 요소겠지만, 저는 일단 지도가 정확해야 역사를 설정할 수 있다고 믿고 있습니다.

그리고 바로 어제, 그 지도를 고쳐야 할 만큼 커다란 실수를 찾아냈습니다. 바로 '바벨 타워'에 대한 설정입니다.

원래 저는 바벨 타워의 전체 길이를 반지름으로 하는 반구 형태의 돔을 방어막으로 설정하고 있었습니다. 그렇게 하자니, 도시 하나를 감쌀 정도의 반구 크기는 도시 길이의 절반은 되어야 했고, 어떻게 어떻게 찌그러뜨려서 4킬로미터라고 설정을 했었습니다. 그리고 그 상태로 16차 개정판을 완성을 시켰지요.
그런데, 어제 다시 계산을 해 보니, 수신 범위와 방어막 크기가 서로 엄청난 불일치를 보이는 겁니다. 원래 제가 생각하던 것은 수신 범위가 방어막 크기라고 할 작정이었는데, 4킬로미터나 되는 수신탑의 수신 범위, 다시 말해 지평선 거리는 무려 225.9킬로미터! 전파의 굴절을 감안한 거리도 195.7킬로미터나 됩니다. 이게 단일 타워의 최대 수신 거리이므로, 두 개의 타워만 최대수신거리로 잇는다면 거의 700킬로미터가 넘는 방어 범위가 형성됩니다. 아시다시피, 1000킬로미터만 되면 그 크기는 거의 텍사스 주만합니다. 아르타누스 도시의 크기를 생각해 봤을 때, 어이없을 정도로 큰 거죠.

그래서 재계산을 해 봤더니... 230미터가 딱 적당했습니다. 230미터짜리 타워의 지평선 거리는 54.2킬로미터, 전파 수신 거리는 46.9킬로미터입니다. 두 개의 타워가 서로 수신 범위에 있으려면 이 길이의 두 배 정도 되니까 약 100킬로미터 정도 사이를 두고 두 타워가 서 있을 수 있다는 계산이 나옵니다. 이렇게 하면 제가 처음에 설정했던 도시 크기와 타워의 수신 반경이 제대로 맞아떨어집니다. 대신, 아마도, 방어막이라는 설정은 포기해야 할 지도 모르겠네요. 수신 반경 안에서는 인간들의 마법력이 엘프와 대등하기 때문에 그 영역이 보호 영역이 되는 것이다. 뭐 이렇게 설정을 바꾸어야 겠습니다.

음, 어렵네요. 역사를 창조한다는 것은, 판타지 소설을 쓴다는 것은. 톨킨과 C.S 루이스의 상상력에 정말 진정으로 경의를 표하는 바입니다.


트랙백 주소 :: http://wbstory.codns.com/~wbstory/blog/trackback/83

댓글을 달아 주세요

  1. BlogIcon 포케 2008/04/14 01:30  댓글주소  수정/삭제  댓글쓰기

    일부 만화나 애니메이션을 보면 사소한 부분까지 치밀하게 설정하는 경우가 많은데 놀라곤합니다.
    눈에 보이지 않는 부분을 신경쓰기란 솔직히 귀찮기도하고 난해하기도 하고 여러가지로 힘든 일입니다.
    그렇기 때문에 그걸 해내는 분들을 볼 때 경의를 표해 마땅하다고 생각합니다만...

    한편으로 만화 애니메이션에서 주어지는 자유도는 때로 '대충 얼버무리는' 논리가 불가능한 것을 가능하게 하는 상상력의 바탕이 되기도 합니다.
    그게 제가 만화 애니메이션을 좋아하는 이유 중 하나네요.
    과학적으로 불가능하다고 생각하는 것도 대충 얼버무려 "아... 그런가?"하고 수긍하게 만드는 힘도 놀랍다고 생각하고 있습니다.

    업계에서 활동중인 분이 말씀하시길 한국은 위로 올리면 재거나, 따지는 경우가 많은데 일본은 모든지 수긍하고 상품화하려는 시도를 아끼지 않는다고 하더군요.
    그게 일본 만화 애니메이션의 원동력이 되었고 다양성을 추구할 수 있는 환경을 이루게 되었다는 이야기였습니다.

    저는 소설 쪽은 잘 모르겠지만 적어도 만화 애니메이션에서는 그런 생각을 가진 분들을 존경하고 있습니다.
    이론마저 무에서 유를 창조하는 쪽은 정말 재미있으면서 동경하게 되는군요. ^ ^

AOP(Aspect Oriented Programming)이란 것은 아직 좀 생소한 개념입니다. 대충 제가 보기에는 메소드마다 항상 처리해야 하는 어떤 작업을 한 군데서 몰아서 관리하는 개념이 강하더군요.
AspectJ에서 먼저 다룬 개념으로 알고 있었는데, 좀 더 알아본 결과 MS의 COM+에서 먼저 지원하고 있었다고 합니다. 요즘의 닷넷 언어에는 애트리뷰트라는 것을 통해 AOP를 지원하고 있고요.

이 AOP라는 것을 실제로 사용해 본 결과를 말씀드리자면, 디자인 패턴 중 프록시 패턴과 많이 닮아 있었습니다. 처음부터 인터페이스에 맞춰서 코딩을 했다면, AOP미지원 언어라도 프록시 패턴을 사용해 같은 결과를 얻을 수 있을 것이란 결론에 도달했습니다.

프록시 패턴이라는 게 별 게 아닙니다. 프록시가 '대리인'이란 뜻이죠? 이런 식으로 씁니다.

1. 인터페이스를 만든다.
public Interface IProxy
{
    public int DoSomeWork(int a);
}

2. 인터페이스를 구현하는 객체(진짜 객체)를 만든다.
public class RealObject : IProxy
{
    public int DoSomeWork(int a)
    {
        Console.WriteLine("Real method");
        return a;
    }
}

3. 진짜 객체를 '감싸는' 프록시를 만든다.
public class ProxyObject : IProxy
{
    private RealObject real;

    public ProxyObject()
    {
        real = new RealObject();
    }

    public int DoSomeWork(int a)
    {
        // 메소드 수행 전에 하고 싶은 일?
        Console.WriteLine("Before Invoke");
        // 그리고 실제 메소드를 수행.
        int result = real.DoSomeWork(a);
        // 메소드 수행 후에 하고 싶은 일?
        Console.WriteLine("After Invoke");

        return result; // 리턴값을 조작하고 싶다면 여기서...
   }
}

사용할 때는

IProxy prox = new ProxyObject();
이렇게 하면 프록시 객체가 동작하는 것이고,
IProxy prox = new RealObject();
이렇게 하면 실제 객체가 동작하는 것입니다.

이게 프록시 패턴이고, Attribute를 사용하는 것은 다음과 같습니다.
참조 : http://www.trugoods.co.kr/DataFolder/FreelancerPortfolio/AOP%EB%A5%BC%20%ED%96%A5%ED%95%98%EC%97%AC,%20%EB%8B%B7%EB%84%B7%20%ED%94%84%EB%A1%9D%EC%8B%9C.pdf (새 창으로 열기)
using System.Runtime.Remoting;
using System.Runtime.Remoting.Activation;
using System.Runtime.Remoting.Proxies;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Services;


    class MyProxy : RealProxy
    {
        private MarshalByRefObject target;

        public MyProxy(MarshalByRefObject target, Type serverType)
            : base(serverType)
        {
            this.target = target;
        }

        public override IMessage Invoke(IMessage msg)
        {
            IMessage retMsg = null;

            Console.WriteLine("Some pre-processing...");
            if (msg is IConstructionCallMessage)
            {
                Console.WriteLine("Constructor Interrupted.");

                // 객체 생성 메시지 처리
                IConstructionCallMessage ctorMsg = (IConstructionCallMessage)msg;
                RealProxy proxy = RemotingServices.GetRealProxy(target);
                // 실제 객체를 생성한다.
                proxy.InitializeServerObject(ctorMsg);
                // 객체 생성 결과를 ‘만들어’ 반환한다.
                retMsg = EnterpriseServicesHelper.CreateConstructionReturnMessage
                    (ctorMsg, (MarshalByRefObject) this.GetTransparentProxy());
            }
            else if (msg is IMethodCallMessage)
            {
                IMethodCallMessage callMsg = (IMethodCallMessage)msg;
                if ( callMsg.MethodName == "bar" )
                    Console.WriteLine("Method Interrupted.");

                if ( callMsg.MethodName == "add" )
                {
                    Console.WriteLine("Method Interrupted : add");
                    Console.WriteLine("Parameter list :");

                    for (int i = 0; i < callMsg.ArgCount; i++)
                    {
                        Console.WriteLine("{0} | {1}: {2}", i, callMsg.GetArgName(i), callMsg.GetArg(i));
                    }
                    retMsg = RemotingServices.ExecuteMessage(target, callMsg);
                }
                else
                {
                    retMsg = RemotingServices.ExecuteMessage(target, callMsg);
                }
            }
            Console.WriteLine("Some post-processing...");
            return retMsg;
        }
    }

    [AttributeUsage(AttributeTargets.Class)]
    class MyProxyAttribute : ProxyAttribute
    {
        public override MarshalByRefObject CreateInstance(Type serverType)
        {
            MarshalByRefObject target = base.CreateInstance(serverType);
            MyProxy proxy = new MyProxy(target, serverType);
            MarshalByRefObject obj = (MarshalByRefObject)proxy.GetTransparentProxy();
            return obj;
        }
    }

Invoke라는 메소드를 주의해서 보시면 됩니다. 여러 if 문을 사용해서 전달되고 있는 메시지가 생성자에 관련된 것인지, 메소드에 관련된 것인지, 메소드에 관련돼 있다면 메소드의 이름은 무엇인지 등을 판별하여 적절한 루틴을 수행합니다.
add메소드의 경우, 파라메터 리스트까지 뽑죠.

이렇게 애트리뷰트를 생성한 후, 사용은 이렇게 합니다.

    [MyProxy]
    class TargetObject : ContextBoundObject
    {
        public TargetObject(string s)
        {
            Console.WriteLine("constructor invoked !!! " + s);
        }
        public void foo()
        {
            Console.WriteLine("foo() invoked !!!");
        }
        public void bar()
        {
            Console.WriteLine("bar() invoked !!!");
        }
        public int add(int a, int b)
        {
            Console.WriteLine("add() invoked !!!");
            return a + b;
        }
    }

[MyProxy] 라고 적혀 있는 한 줄에 주목해 주세요. 또한 ContextBoundObject 를 상속하고 있음도 유의해서 보아 주세요. 아직은 저 클래스를 상속하지 않으면 AOP가 작동하질 않습니다.
(다시 말해, 이미 상속받아 있는 클래스에는 사용할 수 없습니다 -.-)

    public static void Main()
    {
            TargetObject obj = new TargetObject("test");
            obj.foo();
            obj.bar();

            Console.WriteLine(obj.add(3, 2).ToString());
    }


이렇게 실행시키면, 결과는 :
Some pre-processing...
Constructor Interrupted.
constructor invoked !!! test
Some post-processing...
Some pre-processing...
foo() invoked !!!
Some post-processing...
Some pre-processing...
Method Interrupted.
bar() invoked !!!
Some post-processing...
Some pre-processing...
Method Interrupted : add
Parameter list :
0 | a: 3
1 | b: 2
add() invoked !!!
Some post-processing...
5
계속하려면 아무 키나 누르십시오 . . .

이렇게 됩니다.
프록시 패턴과 별다를 게 없지요.
하지만 장점도 분명히 있습니다. 프록시 패턴으로는 '모든 메소드에 대하여' 라는 동작을 정의하기 어렵습니다. 인터페이스가 항상 완전하게 만들어지는 것도 아니고, 때로는 구상 클래스에 프록시를 적용해야 합니다. 인터페이스를 뽑아내면 못할 것도 없지만 애트리뷰트를 사용한 AOP의 개념을 사용하는 것보다는 어려운 것이 사실입니다.

AOP라는 개념은 잘 쓰면 약이요, 못 쓰면 독인 기술입니다. 여러 메소드에 걸쳐 있는 횡단 관심사를 구현하는 데에는 좋지만, 횡단 관심사가 아닌 것에 AOP를 적용한다면 오히려 로직이 꼬여 버리고, 추적도 어렵게 되는 문제가 있습니다. 결론은 '쓰되 가려쓰라'는 것이죠.
프록시 패턴을 사용하면 추적이 비교적 쉽습니다. AOP처럼 '점프'를 하지 않기 때문이지요.

참, 프록시 패턴과 데코레이터 패턴이 생김새는 비슷하지만 다른 개념입니다. 프록시 패턴은 나중에 '진짜' 클래스로 바꿔치기하더라도 코드가 작동하지만, 데코레이터 패턴은 진짜 클래스로 바꿔치기 했을 때 작동하지 않을 수도 있습니다. 둘의 차이점은 '공통 인터페이스'에 있지요. 데코레이터 패턴은 공통 인터페이스가 없어도 됩니다.

마지막으로, 괜찮은 참고문헌 사이트 주소를 적습니다.
http://www.imaso.co.kr/?doc=bbs/gnuboard_pdf.php&bo_table=article&page=4&wr_id=618&publishdate=20030901 (새 창으로 열기)

트랙백 주소 :: http://wbstory.codns.com/~wbstory/blog/trackback/82

댓글을 달아 주세요