Search

'전체'에 해당되는 글 119건

  1. 2011.04.06 다른 판타지를 내게 보여라. 세 얼간이 (4)
  2. 2011.04.01 DataGrid and Dynamic Column -- Part1 (2)
  3. 2011.03.23 GenericTypeExtension works in .NET4.0
  4. 2011.03.14 WPF UI Virtualization -2, SeeMoreStackPanel 만들기
  5. 2011.03.13 소년탐정 김전일과 성급한 일반화.
  6. 2011.03.01 [책리뷰]보살예수 (5)
  7. 2011.03.01 WPF UI Virtualization -1
  8. 2011.02.01 베가본드
  9. 2011.01.30 모든 것이 산산이 부서지다. (3)
  10. 2011.01.18 김영하의 꿈
  11. 2011.01.18 파이 이야기
  12. 2011.01.09 연구자의 자질. 천재 유교수의 생활 소네트 83번 (2)
  13. 2011.01.09 언제부터 보이지 않는 키워드 for, foreach. (8)
  14. 2010.12.19 낙타의 삶을 벗어나 - 재현이란 무엇인가
  15. 2010.12.18 작은 믿음 (6)
  16. 2010.11.19 사탄의 신부 by 신해철 (2)
  17. 2010.10.03 WPF를 생각한다2 - 좋은 프레임워크의 조건. (2)
  18. 2010.08.17 Not my kinda Scene - Powderfinger
  19. 2010.08.11 인간의 굴레에서 (5)
  20. 2010.08.11 WPF를 생각한다. (6)
  21. 2010.08.04 에덴의 동쪽
  22. 2010.07.15 Canvas supports ScrollViewer.
  23. 2010.07.14 TwoWay Binding of SelectedItem in TreeView
  24. 2010.05.31 Left Shift + Right Shift + 마우스 클릭
  25. 2010.05.30 니코스 카잔차키스의 수난 - 다시 못 박힌 예수 (2)
  26. 2010.05.25 크리스토퍼 이셔우드의 '바가바드 기타'의 서문 중에서
  27. 2010.04.28 Down to My Last - Alter Bridge
  28. 2010.04.27 마태복음 18장 (4)
  29. 2010.03.30 GBlog 3호 나오다~
  30. 2010.03.29 Simple WPF Tip : Is A in bound of B?

다른 판타지를 내게 보여라. 세 얼간이

끄적끄적 2011.04.06 20:50 Posted by 아일레프

'세 얼간이'는 코미디 장르에 어울리지 않는 긴 러닝 타임이 전혀 부담스럽지 않은 영화다. 그런데 이 영화에 대해서 이렇게만 평한다면 내게 불만을 가질 사람들이 꽤 많을 것이다. 현재 이 영화의 네이버 평점은 놀랍게도 9.45이다. 한국 영화, 헐리웃 영화도 아닌 인도영화가 9.45란 평점을 기록한 것은 아마 이례적인 일이라 할 수 있을 것이다. 게다가 리뷰를 보니 감동스러웠다는 의견이 대부분을 차지하고 있는 것으로 보아 이 영화는 코미디 영화가 그렇게 원하는 감동과 재미란 두 마리 토끼를 잡았다고 평할 수도 있을 것이다.

그런데 난 결코 후한 점수를 주지 못하겠다. 일단 너무 단편적인 캐릭터로 일관한다. 주인공 란초는 가히 '신'급이며, 악역을 맡고 있는 캐릭터들은 너무 찌질하며 못났다. 바름과 그릇됨. 선과 악. 좋음과 나쁨. 이렇게 이분법적인 극단의 캐릭터들로 이루어진 영화는 현실을 바르게 보여주지 못한다. 맞다. 동물이 사람과 대화하고, 사람이 마법을 부리는 장면이 꼭 나와야 판타지가 아니다. 이 영화는 판타지다. 현실에선 존재하기 어렵지만, 사람들이 너무나 원하는 장면을 아름답게 보여주는 판타지다.

그런데 판타지란것이 내 맘에 들지 않았던 것은 아니다. 내가 후한 점수를 줄 수 없는 결정적 이유는, 이 영화가 보여주는 판타지가 내가 원하는 판타지가 아님을 종국에 가서 알게 되었기 때문이다.

이 마지막 장면. 판타지로 기억에 남기고 싶은 이 영화는 '성공'이란 단어를 삽입함으로 판타지의 한 가운데에 가혹한 현실을 집어 넣어버렸다. 결국 대전제는 '성공'이었던 것이다. 안타깝게도 이것으로 이 영화에서 말하고자 하는 교육이 결국 성공으로 가는 보다 편하고 좋은 길임을 보여주는 것 밖에 되지 않았다. 좋아, 이것만으로는 괜찮을 수도 있다. 개개인이 판단하는 '성공'이 다를 수 있기 때문이다. 그런데 이 현재를 살고 있는 우리는 자신만의 성공을 스스로 정의하지 못하고 성공을 곧 부와 명예로 인식 하도록 끊임없이 강요받고 있다. 이 흐름은 학교, 회사를 막론하고 종교 단체(망할…), 심지어 서점에서도 바로 목격할 수 있다. 내가 가장 짜증나는 부분은 본래의 목적을 지워버리고 "성공"을 억지로 끼워넣는 만행이다. "고전으로 리딩하라"는 전 세계 0.1%의 부자는 인문 고전을 읽고 있다고 말하며 고전 읽기의 동기부여를 위해 성공을 끌어들인다. 이건 양반이다. 책의 본 메시지를 가려버리는 책 제목과 책 날개를 버젖이 홍보 수단으로 사용하고 있다.(아웃라이어를 보라.) 심지어 지하철의 도인들도 더 이상 "도를 믿으세요?"라고 묻지 않는다. 이젠 "어떻게 성공할 수 있는지 궁금하지 않으세요?"라고 묻는다. 젠장. 한국은 성공에 너무 미쳐있다. 이 영화도 그것에 도달하는 방법론만 차이가 있을 뿐 그 '미침'을 보여주는 사례 중 하나이다. 이 영화가 보여주는 성공은 이 사회가 강요하는 그것과 전혀 다르지 않다. 여기서 난 감독의 상상력의 부재를 탓하지 않을 수 없다.

난 정말 간절히 묻고 싶다. 우리에겐 '성공'이 아닌 다른 그 무언가를 삶의 목적으로 둘 수 있는 상상력이 없는가? 좋아, 이게 없다면 다른 '성공'을 그릴 상상력이 없는가? 젠장, 란초는, 란초는 성공하지 않았으면 더 좋았다. 성공하더라도 다른 '성공' 이면 더 좋을 뻔 했다.

후.. 잠시 숨좀 고르고…

좋다. 현 자본주의 사회의 그 특성상 개개인은 자신도 모르게 '성공'을 자신의 목표로 삼게 된다. 많은 사람들이 이것을 부정하지 않으리라 생각한다. 그런데 이를 위해 '효율성'이란 단어가 점점 더 높은 위치를 차지하게 된다. 그런데 이 한국이란 나라에선 그 효율적인 방법이란 것이 그다지 정의롭게 느껴지지 않음으로 우리가 어렸을 때 교육받아온 도덕관과 충돌하게 되고, 만약 그것에서 비롯된 긴장감을 이기지 못하면 한 개인은 어느새 둘 중 하나를 포기해야 할 지경에 이르게 된다. 이것이 극단으로 치닫으면 부당거래와 같은 상황이 닥칠 수도 있다. 도덕과 성공이란 서로의 가치가 양갈래로 갈라져 있기에 이런 현상은 생각보다 자주 나타날 수 있다.

세상에는 불공평한 일 따위는 얼마든지 있다는 사실 말이다. 선생님도 부모님도 “노력해라, 노력하면 보답받을 거야” 라고 하지만, 말하는 목소리에 힘이 실려 있지 않은 이유는 본인들 삶 주변에서도 비슷한 일이 잔뜩 있기 때문이리라. 그런 것도 모르고 “노력하자, 노력하면 보답받지 못할 일은 없어”라고 진지하게 받아들이며 자라 버리면, 어른이 되고 나서 자기를 차고 월급을 더 많이 받는 남자와 결혼해 버린 옛 애인을 죽여서는 보스턴백에 쑤셔 넣어 내다버리는 전개가 되는 거다. -- 마야베 미유키 "우리 이웃의 범죄" 중

A는 어떤 영화에 깊은 감동을 받아 그 영화의 주인공 처럼 살기로 작정한다. A는 자신의 마음의 소리를 들으며, 머리 속으로 '알이즈 웰'을 외치며 자신의 삶을 개척해 나간다. 하지만 자신이 그 영화의 주인공 만큼의 재능이 없음을 알게되고, 스스로가 가는 이 길이 1등이 아니면 모두 지옥으로 떨어지는 길임을 완벽히 확인한 그 순간, 그는 자신의 목숨을 끊으리라 다짐했다. -- 결코 일어나지 않아야 할 미래의 일.

이런 비극들에 대한 비판이 시스템과 사회로 향하는 것을 막기 위해선가? 사회는 늘상 란초와 같은 예외 인물의 사례를 보편인양 제시하고 이 문제를 오직 개인만의 것으로 수렴시킴으로 문제는 사회가 아니라 당신 자신이라 말한다.

"그건 당신의 잘못입니다. 체제를 탓하지 마세요" - 지그 제글러

천만에, 이것은 나의 잘못만이 아니다. 시스템과 체제를 탓하겠다. 우리에겐 필요 이상의 무거운 짐이 주어져 있다. 내가 원하지도 않은 삶의 형태로 나 스스로를 옥죄여 간것은 오직 내가 나이기 때문만은 아니요, 이 체제와 시스템에도 원인이 있다. 그럼으로 오직 개인의 '란초되기' 그것으로 '성공하기' 란 메시지를 전해오는 이 영화에 난 반대다. 이런 삶은 현실만으로 족하다. 지끔껏 만나지 못했으나 우리 모두가 속으로 꿈꿔온 다른 판타지를 내게 보여라. 다른 목적지에 불을 밝혀라.

신고

DataGrid and Dynamic Column -- Part1

프로그래밍 2011.04.01 15:08 Posted by 아일레프
public class Data
{
    public string Type { get; set; }
    public DateTime DateTime { get; set; }
    public string Detail { get; set; }
}

위 Type 으로 이루어진 데이터의 집합이 다음과 같을 때,

<local:Data Type="A" DateTime="10-01-2009 19:56:01" Detail="활동 1"/>
<local:Data Type="A" DateTime="10-01-2009 21:56:01" Detail="활동 2"/>
<local:Data Type="A" DateTime="10-01-2009 23:56:01" Detail="활동 3"/>
<local:Data Type="A" DateTime="10-02-2009 19:56:01" Detail="활동 1"/>
<local:Data Type="A" DateTime="10-02-2009 21:56:01" Detail="활동 1"/>
<local:Data Type="B" DateTime="10-02-2009 14:56:01" Detail="활동 4"/>
<local:Data Type="C" DateTime="10-03-2009 18:56:01" Detail="활동 2"/>

이 7개의 Data를 아래와 같이 표시해야 한다고 해보자. "Type"이라는 Key로 grouping한 데이터를 날짜 순으로 표시하는 것이 목적이다.

이 경우 역시 DataGrid를 떠올리게된다. 그런데 DataGrid를 이용하기 위해 가장 먼저 생각해야 하는 것은 Column의 설정이다. 보통 DataGrid.Columns에 이를 설정해야 하는데 이 경우에는 쉽지 않다. Column으로 사용되게 되는 날짜 정보를 컴파일 이전 타임에 결정할 수 없기 때문이다. 당장 위 경우, {D, 2009/10/04 07:21, 활동 1} 데이터가 추가되게 되면 "2009 /10/4"라는 Column이 필요하게 된다. 따라서 우리는 Runtime에 어떻게 DataGrid의 Column을 설정할 수 있는가? 라는 질문을 던져야 하고 이에 대한 해답을 얻어야 한다. 또한 Column을 Runtime에 결정하는 것 외에 또 한가지 문제가 남는다. DataGrid의 Column은 ItemSource에 바인딩된 개개의 Item의 Property에 대응되게 되므로 바인딩된 Item객체의 Property를 어떻게 Dynamic하게 만들수 있는가? 라는 질문에 답해야 하는 것이다.

해답은 다양하게 있을 수 있다. 나는 3개의 가능성을 떠올렸고 이 가운데 2개는 동작하는 것을 확인했다.(나머지 하나는 확실하지 않다.) 이번 포스트는 첫번째 해답에 대해 다룬다.

 

DataGrid에 바인딩될 객체 만들기

이를 해결하기 위해 DataBase에서 얻어온 객체를 그대로 사용하는것이 아니라 DataGrid의 Row를 표현할 수 있는 새로운 class를 정의하기로 한다. 그런데 어떻게 Property를 Dynamic하게 만들 수 있을까? 사실 모든 Type의 Property는 컴파일 타임 이전에 결정된다. 그런데 단 하나의 Property는 Runtime에 결정될 수 있다. 바로 index Property "[ ]"가 바로 그것이다. 이를 이용하자.

DataGrid에 표현될 Column들은 "Key"와 "날짜" 요소들이므로 바인딩을 위한 클래스는 다음과 같이 설계될 수 있을 것이다.

public class DataForBinding : INotifyPropertyChanged
{
    private IEnumerable<Data> originalDatas;
    public string Key { get; private set; }
    public string this[string day] //yyyy/MM/dd Format의 String이 Input으로 들어오게 된다.
    {
        get 
        {
            DateTime dateTime;
            if(!DateTime.TryParseExact(day, "yyyy/MM/dd",null, System.Globalization.DateTimeStyles.None, out dateTime))
            {
                throw new ArgumentException();
            }
            StringBuilder stringBuilder = new StringBuilder();
            foreach (Data data in originalDatas.Where(d => d.DateTime.Day == dateTime.Day && d.DateTime.Month == dateTime.Month))
            {
                stringBuilder.AppendLine(string.Format ("{0:00}:{1:00}", (int)data.DateTime.TimeOfDay.TotalHours,data.DateTime.TimeOfDay.Minutes) 
                                            + " - " + data.Detail);
            }
            //19 : 56 – 활동 1
            //21 : 56 – 활동 2
            //23: 56 – 활동 3 과 같은 string을 반환한다.
            return stringBuilder.ToString();
        } 
    }

    public DataForBinding(IEnumerable<Data> datas)
    {
        if (datas == null)
            throw new ArgumentNullException("datas");
        if (datas.Count() == 0)
            throw new ArgumentException();

        this.originalDatas = datas;
        this.Key = datas.First().Type;
        if (!datas.All(d => d.Type == Key))
        {
            //하나의 Row에 대응되는 Data들은 모두 Type이 동일해야 하므로
            throw new ArgumentException();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

이제 Service로 부터 얻은 Data집합을 DataForBinding 집합으로 변환해야 할 것이다.

IEnumerable<Data> datas = GetData(); //Data Service로 부터 원본 객체를 얻는다.
IEnumerable<DataForBinding> datasForBinding = GetBindingData(datas); //바인딩을 위한 객체로 변환한다.

이 경우 GetBindingData는 다음과 같이 구현될 수 있다.

private IEnumerable<DataForBinding> GetBindingData(IEnumerable<Data> datas)
{
    IEnumerable<IGrouping<string, Data>> groupDatas = datas.GroupBy(data => data.Type);

    foreach (IGrouping<string, Data> group in groupDatas)
    {
        yield return new DataForBinding(group);
    }
}   

DataGrid의 Column 설정하기

바인딩될 Data는 위와 같이 손쉽게 만들 수 있었다. 이제 DataGrid의 Column을 설정하는 일이 남았다. 이 경우 DataGrid의 Column객체를 다음과 같이 설정하면 된다. 이는 Binding객체가 index 프로퍼티에도 적용될 수 있기에 가능한 것이다.

private void SetDataGridColumn(IEnumerable<DataForBinding> datasForBinding, IEnumerable<Data> datas)
{
    dataGrid.ItemsSource = datasForBinding;

    //Add Key Columns
    DataGridTextColumn keyTextColumn = new DataGridTextColumn();
    keyTextColumn.Header = "Key";
    Binding binding = new Binding("Key");
    keyTextColumn.Binding = binding;
    dataGrid.Columns.Add(keyTextColumn);


    //Add Date Columns
    IEnumerable<string> columnStrings = datas.GroupBy(data => data.DateTime.Date).Select(group => group.Key.ToString("yyyy/MM/dd"));

    foreach (string columnString in columnStrings)
    {
        DataGridTextColumn dateTextColumn = new DataGridTextColumn();
        dateTextColumn.Header = columnString;
        Binding bindingForDate = new Binding("[" + columnString + "]");
        dateTextColumn.Binding = bindingForDate;
        dataGrid.Columns.Add(dateTextColumn);
    }
}

 



이 방법의 문제점

그런데 이 방법은 아쉬움이 깊게 남는다. 왜인가? View와 분리되어야 할 논리 클래스에서 DataGrid, DataGridTextColumn과 같은 UI 객체를 그대로 사용하고 있기 때문이다. 이 경우 View와 논리 클래스 사이의 의존도로 인해 View가 변경되었을 때 어쩔 수 없이 논리 클래스도 변경되어야 하는 안타까운 일이 생길 수 있다. 만약 여러분의 논리 클래스에서 UIElement객체를 직접 참조하거나 new 키워드를 통해 생성하는 코드가 있다면 부지런히 다른 방법을 탐구해보아야 한다. Part2에서는 이를 해결한 해답을 보여주기로 한다.

 

신고

GenericTypeExtension works in .NET4.0

프로그래밍 2011.03.23 18:41 Posted by 아일레프

-- I'm sorry for my poor English. --

There are many "GenericTypeExtension" implementations.


http://stackoverflow.com/questions/1706123/how-to-reference-a-generic-type-in-the-datatype-attribute-of-a-hierarchicaldatate

http://blogs.msdn.com/b/mikehillberg/archive/2006/10/06/limitedgenericssupportinxaml.aspx

http://windowsclient.net/blogs/rob_relyea/archive/2009/06/01/xaml-using-generic-types-in-xaml-2009.aspx


All of these don't work in .NET4. Why? These GenericTypeExtensions use IXamlTypeResolver and IXamlTypeResolver.Resolve("generic:List`1") correctly returns "typeof(List<>)" in .NET3.0. However, it can't resolve in .NET 4.0. "IXamlTypeResolver.Resolve("generic:List`1")" just throw an Exception in .NET4.

"Character '`' was unexpected in string 'generic:List`1'.  Invalid XAML type name."

But don't be afraid. There are other useful services NET4.0 provide. If you used IXamlNamespaceResolver and IXamlSchemeaContextProvider, you can get a generic type easily.


xmlns:generic="clr-namespace:System.Collections.Generic;assembly=mscorlib"
XamlNamespaceResolver nameResolver = 
                                serviceProvider.GetService(typeof(IXamlTypeResolver)) as IXamlNamespaceResolver;
IXamlSchemaContextProvider schemeContextProvider = 
                                serviceProvider.GetService(typeof(IXamlSchemaContextProvider)) as IXamlSchemaContextProvider;
XamlTypeName xamlTypeName = new XamlTypeName(nameResolver.GetNamespace("generic"), "List`1");
Type genericType = schemeContextProvider.SchemaContext.GetXamlType(xamlTypeName).UnderlyingType;

If you explore the ServiceProviderContext through Reflector, you can find other services the ServiceProviderContext implement.

ITypeDescriptorContext, IXamlTypeResolver, IUriContext, IAmbientProvider, 
IXamlSchemaContextProvider, IRootObjectProvider, IXamlNamespaceResolver, 
IProvideValueTarget, IXamlNameResolver, IDestinationTypeProvider

 

I attached a complete GenericTypeExtension, please enjoy it. Thanks~!


[ContentProperty("TypeArguments")]
public class GenericTypeExtension : MarkupExtension
{
        
    private Collection<Type> _typeArguments = new Collection<Type>();
    public Collection<Type> TypeArguments
    {
        get { return _typeArguments; }
    }

    // generic:List`1
    private string baseTypeName;
    public string BaseTypeName
    {
        get { return baseTypeName; }
        set { baseTypeName = value; }
    }
    public GenericTypeExtension() { }
    public GenericTypeExtension(string baseTypeName) { this.baseTypeName = baseTypeName; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (string.IsNullOrEmpty(baseTypeName))
            throw new ArgumentNullException("BaseTypeName");
        string[] baseTypeArray = baseTypeName.Split(':');

        if (baseTypeArray.Length != 2)
            throw new ArgumentException("BaseTypeName");

        if (TypeArguments.Count == 0)
            throw new ArgumentException("TypeArguments");

        IXamlNamespaceResolver nameResolver = 
                                        serviceProvider.GetService(typeof(IXamlTypeResolver)) as IXamlNamespaceResolver;
        IXamlSchemaContextProvider schemeContextProvider = 
                                        serviceProvider.GetService(typeof(IXamlSchemaContextProvider)) as IXamlSchemaContextProvider;

        if (nameResolver == null || schemeContextProvider == null)
        {
            IRootObjectProvider rootObjectProvider = serviceProvider.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider;
            if (rootObjectProvider as DependencyObject != null &&  
               !DesignerProperties.GetIsInDesignMode(rootObjectProvider as DependencyObject))
                throw new Exception("This Generic markup extension requires these services");
            else
                return null;
        }

        XamlTypeName xamlTypeName = new XamlTypeName(nameResolver.GetNamespace(baseTypeArray[0]), baseTypeArray[1]);
        Type genericType  = schemeContextProvider.SchemaContext.GetXamlType(xamlTypeName).UnderlyingType;
        Type[] typeArguments = TypeArguments.ToArray();

        return genericType.MakeGenericType(typeArguments);
    }
}


        <local:GenericTypeExtension BaseTypeName="generic:List`1">
            <x:Type TypeName="system:String"/>
        </local:GenericTypeExtension>

신고

가상화 지원 Panel을 구현하기 위해 가장 어려운 부분은 보여지지 않는 영역의 크기를 추정하는 것이라고 말한 바 있다.이 "추정"에 따른 한계에 의해 WPF에 포함된 VirtualizingStackPanel은 바인딩된 개별 Item의 크기가 각각 다르고 총 Item의 수가 적을 때 스크롤바의 Thumb크기가 일정하지 않은 문제가 발생한다.

이를 확인하기 위해 아래의 코드로 실험해 보았다.

public class DataItem
{
    public string Content { get; set; }
    public double Height { get; set; }
}
 
public partial class SeeMoreStackPanelTest : UserControl
{
    public SeeMoreStackPanelTest()
    {
        InitializeComponent();
 
        this.DataContext = GetDataItems(50);
    }
    private IEnumerable<DataItem> GetDataItems(int count)
    {
        Random random = new Random((int)DateTime.Now.Ticks);
        //높이가 20~ 250인 DataItem을 반환한다.
        for (int i = 0; i < count; i++)
        {
            yield return new DataItem { Content = i.ToString(), Height = random.Next(230) + 20};
        }
    }
}

<ListBox ItemsSource="{Binding}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Border Width="100"
                Height="{Binding Height}" BorderThickness="1" BorderBrush="Black" CornerRadius="5"> 
                <StackPanel>
                    <TextBlock Text="{Binding Content}"/>
                    <TextBlock Text="{Binding Height}"/>
                </StackPanel>
            </Border>
        </DataTemplate>
    </ListBox.ItemTemplate>

위 코드로 동작되는 ListBox는 화면에 보이는 ListBoxItem의 크기에 따라 우측 ScrollViewer의 Thumb 크기가 들쑥날쑥 변하게 된다.

또 위 프로그램을 실행해보면 한가지 문제가 더 있음을 발견하게 되는데 그것은 "Smooth Scroll"을 사용할 수 없다는 것이다. ScrollViewer는 항상 ItemContainer의 크기 단위로 Offset이 변하게 되는데 이것을 원하지 않아 ScrollViewer.CanContentScroll을 false로 준다면 당장 VirualizingStackPanel의 가상화 동작을 지원받을 수 없게 된다.

  1. ItemContainer의 크기가 가변적일 때 ScrollViewer의 Thumb가 일정하지 않다.
  2. 가상화 기능과 Smooth Scroll을 동시에 사용할 수 없다.

이 두가지 문제를 해결해 보자. 1번 문제는 ScrollViewer의 Extent 크기를 "추정"에서 얻어오기에 발생하는 문제이므로 기존의 시나리오로는 완벽히 해결할 수 없다. 이럴 땐 돌아가야 한다. Google Reader와 같은 Scroll방식을 사용하면 어떨까?

*GoogleReader의 ScrollViewer의 모습. 한번에 모든 Data를 Query하는 것이 아니라 Scroll Thumb가 가장 하단에 위치했을 때 일부 Data를 추가로 조회해 오고 있다. 살펴보니 GoogleReader는 UI Virtualization과 Data Virtualization을 매우 적절히 활용하고 있음을 확인할 수 있었다. *

GoogleReader와 같은 Scroll방식을 적용하면 얻어진 ItemContainer에 대해선 정확한 사이즈를 얻어낼 수 있으므로 1번의 문제가 해소된다. 2번의 문제는 가상화 Panel을 직접 구현하면 매우 쉽게 해결 할 수 있다. 좋다. 시작해보자.

1단계. 이름 정하기, IScrollInfo 인터페이스 구현하기

고민끝에 이름을 SeeMoreStackPanel로 하기로 했다. Naming은 어렵다. ㅜ.ㅜ 그리고 IScrollInfo 인터페이스를 구현해야 한다.

public double VerticalOffset
{
    get;
    private set;
}
 
public void SetHorizontalOffset(double offset)
{
    if (offset < 0 || viewportSize.Height >= ExtentHeight)
    {
        offset = 0;
    }
    else
    {
        if (offset + viewportSize.Height >= ExtentHeight)
        {
            offset = ExtentHeight - viewportSize.Height;
        }
    }
    VerticalOffset = offset;
 
    ScrollOwner.InvalidateScrollInfo();
    InvalidateMeasure();
}
 
public void SetVerticalOffset(double offset)
{
    throw new NotImplementedException();
}
 
public void LineDown()
{
    SetVerticalOffset(VerticalOffset + 5);
}
 
public void LineUp()
{
    SetVerticalOffset(VerticalOffset - 5);
}
//이것은 IScrollInfo Interface의 일부 구현이다.

LineUp, LineDown이 불렸을 때 VerticalOffset의 차이를 5로 두었다. 이것으로 Smooth Scrolling이 가능해진다. 그 어떤 값을 두어도 상관 없을 것이다. 나의 편의를 위해 Left, Right등의 Horizontal 동작의 기능은 구현하지 않았다.

2단계. ItemContainer들을 생성해 표시해보기

일단 VerticalOffset이 0인 경우 만을 생각하기로 하자. 2단계를 위해 ItemContainer를 만들고, ScrollViewer정보를 업데이트하고 ItemContainer들을 배치해야 한다. 이를 위해 MeasureOverride, ArrangeOverride 메소드를 override해 구현한다.

MeasureOverride메소드를 보자. 사실 가상화 지원 패널의 반은 이 메소드의 구현에 있다고 해도 과언이 아니다. 이 메소드 내에서 우리는 ItemContainer를 생성하고, ScrollViewer의 정보를 업데이트 하고, 더 이상 필요하지 않은 ItemContainer를 삭제해야 한다. 이 중 ItemContainer를 삭제하는 것은 선택사항이므로 당연히(?) 여기선 구현하지 않는다. 아래 코드에서 Assertion을 주목해 읽어주면 되겠다.

protected override Size MeasureOverride(Size availableSize)
{
    if (VerticalOffset != 0)
        throw new InvalidOperationException("2단계에서는 VerticalOffset이 0인 경우만 고려한다.");
 
    ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
    if (itemsControl == null)
    {
        throw new InvalidOperationException("단독으로 사용될 수 없는 Panel입니다.");
    }
    double remainHeight = availableSize.Height;
    UIElementCollection children = this.InternalChildren;
    GeneratorPosition position = new GeneratorPosition(-1,0);
    IItemContainerGenerator generator = ItemContainerGenerator as IItemContainerGenerator;
    using (generator.StartAt(position, GeneratorDirection.Forward, true))
    {
        while (remainHeight > 0)
        {
            bool isRealized;
            UIElement element = generator.GenerateNext(out isRealized) as UIElement;
            if (element == null) break;
            if (isRealized) //새로 생성된 ItemContainer
            {
                Debug.Assert(InternalChildren.Contains(element) == false,
                                         "새로 얻은 element이므로 InternalChildren은 이 element를 포함하지 않는다.");
                this.AddInternalChild(element);
                //이 PrepareItemContainer메소드가 어떤 역할을 하는지 알고 싶다면 이 PrepareItemContainer메소드 호출을 제거하고 
                //프로그램을 실행시키면 된다. 
                //PrepareItemContainer메소드 호출을 제거하면 Measure 호출 후
                // element.DesiredSize 값이 정상적으로 얻어지지 않는다. 
                //추측하건데 PrepareItemContainer에 ItemTemplate으로 VisualTree를 얻어 
                //ListBoxItem의 Child로 설정하는 부분이 있지 않을까 싶다.
                ItemContainerGenerator.PrepareItemContainer(element);
                element.Measure(availableSize);
            }
            else
            {
                Debug.Assert(InternalChildren.Contains(element));
            }
            remainHeight -= element.DesiredSize.Height;
        }
    }
    this.extentSize = availableSize;
    if (remainHeight < 0)
    {
        this.extentSize.Height += remainHeight * -1;
    }
    this.viewportSize = availableSize;
    this.ScrollOwner.InvalidateScrollInfo();
    return availableSize;
}

VerticalOffset이 0인 경우, 즉 ItemsControl이 처음 로드 되었을 때의 경우만 고려했으므로 코딩이 이정도로 끝나게 되었다. 이것으로 ItemsControl이 로드되었을 때 화면내에 보여질 만큼의 ListBoxItem만 생성되게 된다. 생성된 ItemContainer를 배치하는 ArrangeOverride메소드를 확인해보자.

protected override Size ArrangeOverride(Size finalSize)
{
    if (VerticalOffset != 0)
        throw new InvalidOperationException("2단계에서는 VerticalOffset이 0인 경우만 고려한다.");
    ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
    if (itemsControl== null)
    {
        throw new InvalidOperationException("단독으로 사용될 수 없는 Panel입니다.");
    }
    IItemContainerGenerator generator = itemsControl.ItemContainerGenerator as IItemContainerGenerator;
    double topPosition = 0;
    GeneratorPosition position = new GeneratorPosition(-1, 0);
    using (generator.StartAt(position, GeneratorDirection.Forward, true))
    {
        while (topPosition < finalSize.Height)
        {
            bool isRealized;
            UIElement element = generator.GenerateNext(out isRealized) as UIElement;
            if (element == null) return finalSize;
            Debug.Assert(isRealized == false,
                            "MeasureOverride에 의해 이미 생성된 ItemContainer이어야 하므로 이 값이 false이다.");
 
            Debug.Assert(InternalChildren.Contains(element),
                            "InternalChildren은 이 element를 포함하고 있어야 한다.");
            element.Arrange(new Rect(new Point(0, topPosition), element.DesiredSize));
            topPosition += element.RenderSize.Height; 
        }
    }
    return finalSize;
}

좋다. 이제 3단계에서는 VerticalOffset이 변경되었을 때의 기능을 추가할 것이고, 4단계에서는 Scroll Thumb가 ScrollViewer의 마지막에 닿았을 때 추가적으로 ItemContainer를 생성해 표시하는 기능을 추가할 것이다. 추측하건데, 이 정도로도 머리속에 3,4 단계에 대한 그림이 머리 속에 훤하게 그려진 분이 있을 것이다.

3단계. VerticalOffset이 0보다 큰 값일 경우의 동작 구현

이제 VerticalOffset이 0보다 큰 값일 경우의 동작을 구현해 보자. 먼저 MeasureOverride 메소드를 봐야한다. 다시한번 말하지만 이 메소드는 ItemContainer를 생성, 파괴, ScrollViewer 정보를 업데이트하는 역할을 한다. 그런데 우리의 시나리오에 의하면 이 MeasureOverride가 ItemsContainer를 생성하는 경우는 단 3가지 이다. 첫번째 경우는 ItemsControl이 처음으로 로드되었을 경우이며, 두번째는 ScrollEnd 동작이 수행되었을 때, 즉 VerticalOffset + ViwportHeight = ExtentHeight일 경우이다. 그리고 마지막 경우는 바인딩 된 ItemsSource 자체에 변화가 일어났을 경우이다. 여기서 첫번째, 두번째 경우만 고려해보면 MeasureOverride 메소드는 다음과 같이 될 것이다.

protected override Size MeasureOverride(Size availableSize)
{
    ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
    if (itemsControl == null)
    {
        throw new InvalidOperationException("단독으로 사용될 수 없는 Panel입니다.");
    }
    double remainHeight = availableSize.Height;
    UIElementCollection children = this.InternalChildren;
 
    //새 ItemContainer를 생성할 경우는 다음 두가지 경우 이다. 
    //첫번째는 처음 로딩되었을 경우이며
    //두번째는 ScrollToEnd가 동작되었을 경우이다.
    if ((VerticalOffset == 0 && InternalChildren.Count == 0) || VerticalOffset + ViewportHeight == ExtentHeight)
    {   
        CreateItemContainer(ref availableSize, ref remainHeight);
        this.extentSize = availableSize;
        if (remainHeight < 0)
        {
            this.extentSize.Height += remainHeight * -1;
        }
    }
 
    this.viewportSize = availableSize;
    this.ScrollOwner.InvalidateScrollInfo();
    return availableSize;
}

하나의 If문이 추가되었고 ItemContainer를 생성하는 부분은 CreateItemContainer라는 메소드로 리플랙션을 수행했다. 3단계에서 MeasureOverride메소드의 변경은 이것으로 충분하다. 그럼 이제 생성한 ItemContainer들을 배치할 차례이다. ArrangeOverride메소드는 InternalChildren에 추가된 ItemContainer 들을 VerticalOffset을 고려해 배치하면 된다. 이는 정말 너무 간단하다.

TranslateTransform translateTransform = new TranslateTransform();
public SeeMoreStackPanel()
{
   this.RenderTransform = translateTransform;
}
protected override Size ArrangeOverride(Size finalSize)
{
    ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
    if (itemsControl== null)
    {
        throw new InvalidOperationException("단독으로 사용될 수 없는 Panel입니다.");
    }
    double topPosition = 0;
    //InnerContainer에 추가된 Item들을 배치한다. 
    foreach(UIElement element in this.InternalChildren)
    {
        element.Arrange(new Rect(new Point(0, topPosition), element.DesiredSize));
        topPosition += element.RenderSize.Height; 
    }
    translateTransform.Y = VerticalOffset * -1;
    return finalSize;
}

정말 쉽지 않은가? 이것으로 3단계가 끝났다. 이제 남은 것은 ScrollBar가 마지막까지 내려갔을 때 Item을 추가시키기만 하면 된다.

4단계. ScrollToEnd가 동작했을 때, VerticalOffset + ViewportHeight = ExtentHeight 일 경우에 Item을 추가

이것 역시 굉장히 쉽다. CreateItemContainer에 다음을 추가하면 되겠다.

if (VerticalOffset == 0 && InternalChildren.Count == 0)
 
   //처음 ItemsControl이 로드되었을 경우의 동작 코드
   // …
   // ...
 
else //ScrollToEnd 명령이 불렸을 경우이다. 이 때는 5개의 Item을 추가적으로 생성하기로 한다.
 
   Debug.Assert(VerticalOffset + viewportSize.Height == ExtentHeight);
 
   //모든 Item들이 추가되었을 경우
   if (InternalChildren.Count == ItemsControl.GetItemsOwner(this).Items.Count) 
       return;
   
   IItemContainerGenerator generator = ItemContainerGenerator as IItemContainerGenerator;
   GeneratorPosition position = generator.GeneratorPositionFromIndex(InternalChildren.Count);
#if DEBUG
   int count = InternalChildren.Count;
#endif
   using (generator.StartAt(position, GeneratorDirection.Forward, true))
   {
       for (int i = 0; i < 5; i++)
       {
           bool isRealized;
           UIElement element = generator.GenerateNext(out isRealized) as UIElement;
           //더 이상 생성할 Item이 없을 경우
           if (element == null) break;
           if (isRealized) //새로 생성된 ItemContainer
           {
               Debug.Assert(InternalChildren.Contains(element) == false, "새로 얻은 element이므로 InternalChildren은 이 element를 포함하지 않는다.");
               this.AddInternalChild(element);
               ItemContainerGenerator.PrepareItemContainer(element);
               element.Measure(availableSize);
               //ScrollViewer의 ExtentSize를 조절한다.
               this.extentSize.Height += element.DesiredSize.Height;
           }
       }
   }
 
#if DEBUG
   Debug.Assert(ItemsControl.GetItemsOwner(this).Items.Count == InternalChildren.Count ||
               count + 5 == InternalChildren.Count);
#endif
 

마치며

어떤가? 금새 가상화 패널을 하나 만들었다. 물론 이것이 완전한 패널의 기능을 하려면 또 다른 여러 경우를 생각해야 한다. 바인딩 된 ItemsSource가 변경되었을 경우에 대한 코드를 추가해야 하며, HorizontalScrollBar의 기능 또한 추가해야 한다. 그리고 추가된 ItemContainer들의 크기가 변경되었을 때의 경우도 고려해보아야한다. 매우 심심하면 한번 직접 구현해 보시라. :)

 

첨부된 파일은 위 SeeMoreStackPanel을 테스트 하기 위한 프로젝트이다. txt파일로 각 단계의 소스코드를 첨부해놓았다. 


신고

소년탐정 김전일과 성급한 일반화.

끄적끄적 2011.03.13 12:47 Posted by 아일레프

소년 탐정 김전일을 보면 위와 비슷한 상황을 몇번 만날 수 있다. 살인 사건이 일어나고, 여러가지 수수께끼에 의해 그 사건은 미궁으로 빠진다. 그리고 김전일과 또다른 누군가가 이사건의 해결을 두고 경쟁을 펼치게 된다. 이때 먼저 해답을 얻었다며 범인을 지목하는 이는 김전일이 아닌 김전일의 경쟁자이다. 그 김전일의 경쟁자는 자신의 풀이 과정을 조리있게 설명하며 자신이 승리자임을 확인한다. 그리고 승리자인 그가 발표의 장에서 나가면 김전일의 친구 중 한명이 김전일에게 묻는다.

"김전일, 저 사실을 몰랐던 거야?"

김전일은 대답한다.

"아니. 알고 있었어. 하지만 풀리지 않은 수수께끼가 남아있어"

...

...

...

안다는 것, 이해한다는 것이란 무엇일까? 그것은 이 세상에서 존재하는 다양한 현상을 그보다 적은 규칙과 도식으로 일반화하고, 추상화 한다는 것을 뜻할지도 모른다. 하이젠베르크의 '부분과 전체'에서 하이젠베르크의 친구가 다음과 같은 말을 하는데 굉장히 인상적이었다.

"즉 다양성을 일반적인 간단한 것에 귀착시키는 일, 바로 자네가 좋아하는 그리스 인식으로 말하면 '많은 것'을 '하나'에다 소급시키는 일을 우리는 '이해'하였다는 말로 표현하는 것이다." - 볼프강 파울리

추론이라고 번역되는 deduction이란 단어에 공제, 삭제의 의미가 함께 들어있는 것도 주목할 필요가 있겠다.

뉴턴이나 아인슈타인등 과거의 과학자들이 위대한 이유는 이 복잡한 세상을 그 이론의 폐구간 안에서 모든 것을 명료히 설명하는 규칙과 질서를 발견했다는 것일테다. 우린 그러한 과학자는 아니지만 적어도 자신이 경험하고 있는 이세계를 설명하고, 이해하고 싶다는 욕구를 분명히 가지고 있다. 너저분하게 널려있는 어떤 객체들의 특징을 어떻게든 정리해 우리의 언어 내에서 주어와 술어로 표현한 후, 스스로 앎을 얻었다는 확신 속에서 평안한 안식을 얻고 싶어지는 것이다. 이 이해하고 싶다. 앎을 얻고 싶다는 욕구가 강하기 때문일까? 우린 적은 사례들의 표본만을 가지고 전체의 규칙을 일반화하기 쉽상이다. 그런데 그 결론을 얻기까지의 과정 중 가장 큰 영향을 미치는 것이 침착한 이성의 눈으로 바라본 설득력있고 명백한 증거들이 아니라, 의식 혹은 무의식의 저변에 깔려있는 과거 자신만의 어떤 '경험'이라는 사실을 난 부인하기 어렵다. 이 선행된 경험의 힘이 얼마나 오묘하고도 강한지, 그 경험을 뒤엎고도 남을 만한 다른 경험들이 이미 높은 자리를 차지한 선행 경험의 힘을 이겨내기까지는 많은 시간과 배의 충격이 필요하다. 이러한 사실과 누구보다 먼저 빠르게 답을 내리려는 경쟁심리가 우리로 하여금 성급히 "~은 ~이다"라고 말하게 해 성급한 일반화의 오류에 빠지게 한다.

김전일이 훌륭한 이유는 한층 더 깊은 수읽기로 남들이 보지 못한, 수수께끼 이면의 다른 수수께끼를 발견한다는 것과 그 모든 것을 설명할 수 있는 규칙을 발견할 때까지 앎으로 부터 오는 안식의 순간을 끊임없이 지연시킨다는 점에 있다. 우린 이러한 김전일의 자세와 끈기를 배워야 할지도 모르겠다. 가끔씩 섣불리 "~은 ~때문에 ~이다" 라고 말하기 이전에 김전일의 눈빛과 표정을 흉내내본다. 지금 우리에게 필요한 것은 빨리 답을 내리는 것이 아니라 더 깊은 답을 내리는 것이 아닐까. 하고 생각해본다.

신고

[책리뷰]보살예수

즐거운 책 이야기 2011.03.01 16:53 Posted by 아일레프

보살예수 - 10점
길희성 지음/현암사

성서는 성서 그 자체만으로 읽혀서는 안된다. 기독교는 그 외부와 만남을 통해, 그리스 철학, 실존주의, 맑시즘 등 과의 만남을 통해 그 보편성을 넓혀왔다. 그러한 만남이 없이 성서만 우리에게 주어졌다면 그것은 너무 조잡하다. 성서와 계시 만으로 기독교는 결코 세계 종교가 될 수 없었다고 생각한다.ㅡ 길희성, 2011.02.20 새길교회 청년들과의 대화 중에서

외부와의 만남을 통해 기독교의 보편성을 넓힐 수 있다고 믿는 그가 보살예수라는 책을 썻다면 그 목적은 제법 분명해 보인다. 기독교가 불교라는 외부와의 만남을 통해서, 부처, 또는 보살과의 만남을 통해서 기독교의 보편의 영역을 더욱 확보하는 일에 있을 것이다. 아니, 사실 이 말은 길희성 교수님의 생각을 심히 왜곡하는 것이다. 기독교의 보편성을 넓히려는 것이 아니라 기독교와 불교가 가지는 근본적 메시지의 보편성을 넓히는 것이다. 나아가 종교 다원주의자로서의 그는 기독교와 불교가 서로 각자 올라가는 산의 정상이 동일함을 자신과 우리에게 보이려 하는 것이다.

하지만 불교와 기독교는 굉장히 다르다. 태어난 역사적 배경이 다르며 중심 사상도 달라도 너무 다르다. 불교는 만물의 근본의 원인인 일자(一字)의 존재를 상정하지 않기 때문에 무신론 종교라 부를 수 있다면, 기독교는 유일신 종교의 대표주자이다. 기독교가 개인의 영혼의 존재를 굳게 믿고 있다면, 곧 실체론을 주장한다면 불교는 철저히 비실체론이며 유명론을 말한다. 기독는 하나님 나라와 유일신의 존재를 말함으로써 저기 피안(彼岸)의 세계를 항시 바라보지만 불교는 피안과 형이상학에는 도무지 관심이없다. 이렇게 명백히 드러나는 차이를 극복할만한 같음이 두 종교에, 예수와 부처 사이에 존재하는 것일까? 보살예수, 이 책의 부제는 기독교와 불교의 창조적 만남이지만 자칫하면 어색한 만남으로 끝날 수 있는 많은 난관을 가지고 있는 것이다.

길희성교수의 이 책은 기독교인을 대상으로 했기 때문에 불교에 대해 자세하고도 쉽게 설명해주고 있다. 이 불교란 기독교와 같이 하늘에서 벼락으로 떨어진 계시를 바탕으로 만들어진 것이 아니라 인간 부처와 그의 제자들이 치열한 현실과의 싸움을 통해서 얻은 것이기에 기독교보다 더욱 더 명료하고 논리적이다. 그리고 그 싸움이란 철학이 세상과 싸우는 방식과 다르지 않아보인다. 곧 현실의 분명한 문제의식, 그리고 문제에 대한 깊은 통찰, 그리고 그것을 해결하기 위한 새로운 개념과 이론의 생성의 반복에 다름 아닌 것이다.

불교와 불교의 창시자 싯타르타의 목표는 매우 간단했다. 이 세상을 살면서 생기는 고통을 없애는것이 바로 그의 목표이고 불교의 목표이다. 싯타르타는 이 고통의 원인이 "있지도 않은 것을 있다고 생각하고 집착하는 것"에 있다고 보았다. 이 "있지도 않은 것"이란 무엇인가? 바로 세상의 모든 것에 비 의존적인 "자아"의 존재이다. 당시 인도의 사상은 철저한 실체론이라 불릴 수 있었다. "카파 우파니샤드"에서는 다음과 같이 말한다.

자아는 태어나는 것도, 죽는 것도 아니다. 어디에서 오거나 무엇이 되는 것도 아니다. 이것은 태어나지도 않고 영원하며, 원초적이다. 육신이 살해될 때도 이것은 살해되지도 않는다.

불교는 당시 인도를 지배하고 있던 이 사상에 정면으로 대항한다. 육신이 살해될 때도 살해되지 않는 영원하며 원초적인 자아의 존재에 대해 부정한다. 또한 이에 대한 앎으로서 자아에 대한 집착을 벗음으로써 고통에서 벗어날 수 있다고 말한다. 이 실체론에 대항하기 위해 탄생한 이론이 바로 불교의 연기론(緣起論)이다. 연기론이란 '나'란 결코 혼자 존재할 수 밖에 없고 '나'란 다른 것에 의존재서 존재할 수 밖에 없다고 선언한다.

이것이 있기 때문에 저것이 있고, 이것이 생기기 때문에 저것이 생긴다. 이것이 없기 때문에 저것이 없고, 이것이 소멸하기 때문에 저것이 소멸한다.

불교는 모든 것이 스스로 존재하지 않고 "타자"에 의존한다고 말한다. 그럼으로 자신의 존재가 사실 없음을 아는 것, 이 앎을 통해 자신을 변형하고 모든 집착을 끊고 모든 번뇌를 남김없이 극복하는 것을 열반이라 하고 이 열반에 다다른 자를 아라한(阿羅漢)이라 칭했다. 곧 초기 불교의 목표는 개개인이 아라한의 경지에 이르는 것이라 할 수 있겠다.

그런데 이 열반에 대한 개개인의 목표가 강조되자 문제가 생겼다. 열반이란 목표 내에서 "개인"으로서의 깨달음과 해탈만을 중시하게 된다는 것이다. 개인의 열반이라는 목표가 오히려 사람을 더욱 "개인"으로 몰고가는 것이었다. 따라서 이러한 초기 불교는 그 다음 불교, 곧 대승 불교에게 "작은 수레"라는 놀림을 받게 되고 안타깝게도 이것이 곧 이름이 되어 후대 사람들에게 소승불교라 불리게 되었다.

이제 대승불교의 문제제기가 시작되었다. 개개인의 집착을 끊으려 정진하고 수행하는 행위가 중생과 승려를 더욱 구분하게 되고 이것은 부처가 처음 가르쳤던 가르침과 맞지 않는 다는 것이 그들의 문제제기이다.

살아 있는 다른 모든 생명체가 고통 받고 있는데 행복이 가능 한 것일까? 온 세상이 고통으로 울부짖는데당신은 구원받을 수 있겠는가?

이제 불교의 목표가 개개인의 해탈과 열반에서 중생 일반에 대한 구제로 이동하게 된다. 그리고 이러한 목표를 이루기 위해선 다시한번 이론과 개념이 정립되어야 한다. 대승불교의 이론을 정립했다 할 수 있는 나가르주나는 불교의 연기론을 끝까지 확장해 모든 일체의 것은 공(空)하다라고 말했다. 바로 공(空) 사상이 등장한 것이다. 이 공하다 라는 선언은 '무엇이 없다'라는 것이 아니다. 공은 모든 것이 존재한다고도 할 수 없고 비존재한다고 할 수도 없다고 말한다. 비유비무(非有非無) 인 것이고 생즉시공(色卽是空) 공즉시색(空卽是色)인 것이다. 이렇게 나가르주나가 설파한 이유는 일체의 모든 독단적이고 배타적인 이원론을 피하려한 것이다. 곧 생사와 열반을 구분하는 태도를 벗어나 (생사와 열반이 하나다, 번뇌가 곧 보리이며 보리가 곧 번뇌이다.) 사물을 바로 있는 그대로 바로 보는 것에 목표를 두는 것이었다.

이런 공사상을 바탕으로 모든 집착, 심지어 열반에 대한 집착으로부터 벗어나야 한다는것으로부터 불교의 목표가 아라한에서 '보살'로 이동하게 되었다. "보살"이란 새로운 개념이 등장하게 된 것이다. 이 보살이란 개념이 무엇이냐면(실로 놀랍기 그지 없는데) 보살이란 모든 생명체가 열반에 이를 때 까지 자신의 열반을 뒤로 미루는 사람을 의미한다. 보통의 중생이 업과 집착에 의해 환생한다면 보살은 자신의 '원'에 의해 스스로 윤회의 수레바퀴로 뛰어드는 것이다.(놀랍지 아니한가? 상상력이 이 정도는 되어야!) 또한 집착과 업에 의해 사람이 환생한다면 보살은 중생에 대한 사랑으로 중생에 대한 집착이 남아 윤회하는 자라고도 볼 수 있겠다. 달라이라마는 이 보살의 하나의 예인 것이다. 이 보살이 되려면 자비심과 지혜가 동시에 필요하다. 지혜만 있으면 자신의 해탈만을 추구하려 하며, 자비만 있으면 번뇌와 집착을 낳아 고통에 빠질 뿐이다.

저자 길희성교수는 이러한 보살의 특징으로 자유로움을 언급했다. 보살은 생사의 세계로부터 자유로우며 보살은 생사의 세계로 부터 자유로울 뿐 아니라 열반에 대한 집착으로부터도 자유롭다. 또한 보살은 자기 자신으로부터도 자유롭다. 길희성 교수는 예수도 이 보살의 특징에 견주는데 과연 예수 또한 보살의 특징을 모두 가지고 있고 보살의 행동방식으로 예수 또한 살았다고 말한다. 그리고 이것은 충분히 우리가 인정할 만한 사실이다. 보살과 예수 모두 자비를 말했다. 사랑을 말했다. 생사와 열반을 구분하지 않고 만인의 고통을 극복하려 자신을 자유롭게 풀어 놓았다.

하지만 이것으로 는 길희성 교수가 말하고자 하는 바가 충분히 말해지지 않는다. 그는 한발짝 더 걸었다. 보살과 예수의 행동의 같음을 말하는 것을 넘어서서 보살이 보살되게 하는 깨달음과 예수가 예수되게 하는 그것이 동일하다라고 말한 것이다. 보자, 보살이 보살되게 하는 것은 공, 진여(眞如)로써의 공의 깨달음이다. 예수가 예수되게 하는 것은 아빠 하나님의 사랑의 은총이다. 결국 길희성 교수는 이 진여로써의 공의 깨달음과 아빠 하나님의 사랑의 은총이 결국 같음을 말한다. 이것을 말하기 위해 길희성 교수는 기독교인이 인정하는 인격적신으로서의 하나님을 반드시 넘어서야 했다. 이를 위해 그는 공을 바탕으로 하나님을 사유한다.

불교의 공관은 하느님을 대상적 존재로 취급하는 조잡하고 저급한 신관을 가차 없이 파괴하며, 이것은 누구보다도 그리스도인들 스스로가 환영해야 할 일입니다. 무한한 하느님, 절대유로서의 하느님은 결코 유나 사물, 혹은 유한한 인격체 같은 존재자가 아니기 때문입니다. 아무리 뛰어나고 예외적인 존재자라 할 지라도 하느님을 하나의 존재자로 파악하는 한, 우리는 하느님이라는 무한한 실재를 하나의 유한한 존재로 격하하는 것입니다. 공관은 그리스도인들에게 유한한 것을 무한한것으로 숭배하는 우상숭배를 방지해줍니다.

그는 기독교인이 너무 당연하게 생각하는 인격적 하나님으로서의 신관을 초월해야 한다고 말하며 한국 기독교의 보편적인 신관을 비판한다. 만물의 일자로서의 하나님에 인간의 모습을 너무 투영해 기독교의 신관을 유치하게 만들었다고 말하는 것이다. 나아가 기독교인으로서의 그는 이 인격적 신을 말하는 신관을 넘어서 나아가 공관 이후의 신관에 대해서 논해야 한다고 말한다.

하느님을 만물의 존재를 가능하게 하면서도 세계와 떨어져 있기보다는 만물에 내재하면서 만물의 존재를 가능하게 하는 실재로 생각해야 합니다. 구체적 사물의 유한성을 초월하면서도 다양한 사물에 즉해서 함께 움직이는 역동적 신관을 생각해야 합니다.

또한 길희성 교수는 공과 사랑의 하느님이 별개의 실재가 아니라 동일한 실재가 달리 이해되는 것이라 말한다. 나가르주나는 이런 말을 했다.

공의 이치가 있기 때문에 모든 존재가 성립할 수 있다. 만일 공의 이치가 없다면 어떤 존재도 성립하지 않는다.

길희성 교수는 공의 이치로서의 하느님을 바라보는 것이라 할 수 있다. 나아가 공이 사랑의 존재론적 의미라면 사랑은 공의 인격적 언어라 말한다. 공이 곧 하느님의 사랑이며 로고스라고 그는 말한다. 또한 그 공의 이치가 '사랑'을 가지고 있다고 생각하고 그 사랑의 원리가 이 우주의 궁극적 힘이며 존재론적 원리라 말한다. 예수와 보살은 그러한 우주의 힘, 공의 이치, 사랑, 로고스의 육화라고 볼 수 있다는 것이다. 이 작업을 통해 그는 두 종교의 보편성을 확보하고 두 종교의 근본적 메시지의 보편성을 더욱 넓혀나갔고 그것을 위해 자신의 신관을 숨김 없이 말했다.

하지만 아쉬움이 남는다. 공의 이치가 곧 하느님이라 말하는 부분의 설명이 조금 부족하다고 느꼈으며 그로인해 그의 메시지가 내게는 가슴 벅차게 다가오지 않았다. 완전히 이해하지 못하면 감동할 수 없는 것이다. 아마도 내가 가지고 있는 지식과 여러 한계들로 그것을 이해하기 힘들었을 것이다. 그러나 그것을 좀더 설득력있게 이해시켜달라고 외치며이 책에 매달리는 것은 나의 욕심일까? 한가지 더 희망이 남는 다면 이것이 이른 리뷰라는 것에 있다. 이 책은 열개의 장으로 이루어저 있는데 내가 읽은 것은 8장 까지이다. 이제 두장이 더 남아있다. 다행이다.

 

References :

  1. 보살예수
  2. 하룻밤의 지식여행 불교편
  3. 공이란 무엇인가
  4. 그리스도인을 위한 오강남의 불교이야기
신고

WPF UI Virtualization -1

프로그래밍 2011.03.01 14:55 Posted by 아일레프

Virtualization은 Data Virtualization과 UI Virtualization으로 나뉠 수 있다. Data Virtualization이란 필요한 Data만을 데이터 저장소에서 가져와 성능을 향상시키는 방법을 의미하고 UI Virtualization이란 화면에 보여지지 않는 컨트롤 객체를 생성 않는 것으로 속도를 향상시키는 기법을 의미한다. 이 포스트는 UI Virtualization만을 다룬다. 만약 Data Virtualization에 대해 자세히 알고 싶은 분은 놀랍도록 멋진 Data Virtualization in WPF and beyond을 참고하면 되겠다.

UI Virtualization을 좀 더 쉽게 살펴보기 위해 ListBox를 예로 들어보자. 1000개의 int 데이터를 가지는 List<int> 가 ListBox의 ItemsSource에 바인딩 되었다고 가정하면 ListBox의 VisualTree는 다음과 같이 구성될 것이다.

ListBox        
  ScrollViewer      
    ItemsPresenter    
      Panel  
        ListBoxItem
        ListBoxItem
        ListBoxItem
...
...
...

UI 가상화를 사용하지 않는다면 데이터가 1000개 이므로 ListBox가 로드되었을 때 생성될 ListBoxItem들의 숫자는 총 1000개이다. WPF의 UI Virtualization은 "화면에 보여지지 않는 객체를 생성하지 않는 것으로 속도를 향상시키는 기법" 이라고 말했다. 이를 ListBox의 예로 국한하면 화면에 보여지지 않는 불필요한 ListBoxItem을 생성하지 않는 것으로 구체화할 수 있다. ListBox의 예에서 UI Virtualization을 가능하게 하려면 이 ListBoxItem의 생성을 컨트롤 할 수 있어야 한다는 것이다. 그렇다면, ListBoxItem을 생성하게 하는 객체가 무엇인가? 라는 질문을 던져야 한다. 당신은 후보가 다음과 같을 때 어떤 객체가 ListBoxItem을 생성하게 한다고 생각하는가?

  1. ListBox
  2. ItemsPresenter
  3. Panel

 

...

...

...

 

자, 답을 보자. ListBoxItem객체는 ListBox의 GetContainerForItemOverride 메소드를 통해 생성된다. 하지만 위 질문의 답은 ListBox가 아니다. ListBox의 GetContainerForItemOverride메소드는 ListBoxItem을 생성하기 위해 이용당하는 것이다. 잘 살펴보면 GetContainerForItemOverride 메소드를 호출하는 것은 ItemsPresenter내의 ItemContainerGenerator라는 객체이며 이 객체를 이용해 최초에 ListBoxItem을 생성하라는 의지를 발하는 녀석은 "Panel"이다. 만약 StackPanel이 ItemsPanel로 사용되었다면 ItemsSource가 바인딩 된 시점에 다음 코드가 불리게 된다.

public abstract class Panel : FrameworkElement, IAddChild
    internal virtual void GenerateChildren()
    {
        IItemContainerGenerator generator = (IItemContainerGenerator)_itemContainerGenerator; 
        if (generator != null)
        {
            using (generator.StartAt(new GeneratorPosition(-1, 0), GeneratorDirection.Forward))
            { 
                UIElement child;
                while ((child = generator.GenerateNext() as UIElement) != null) 
                { 
                    _uiElementCollection.AddInternal(child);
                    generator.PrepareItemContainer(child); 
                }
            }
        }
} 

StackPanel은 ListBox의 Items가 변경되었을 때 자신의 Panel의 GenerateChildren()메소드를 그대로 사용해 모든 데이터에 대응되는 ItemContainer를 만들어 자신(StackPanel)에 삽입하는 것을 볼 수 있다. 하여간 중요한 것은 ItemsContainer를 생성하게 만드는 객체는 바로 Panel이라는 사실이다. 따라서 UI Virtualization을 구현하려면 바로 이 Panel을 만지작 거려야 한다.

그런데 Panel만을 수정하는 것으로 UI Virtualization을 구현할 수 있을까? 다른 정보가 필요하지 않을까? 가상화 지원 Panel은 일반적인 Panel(Grid, StackPanel, WrapPanel)과 다르게 특정 시점에 화면에 보여야만 하는 컨트롤만을 생성해 자신의 Children에 추가해야한다. 그런데 이것은 Panel 홀로 할 수 없는 일이다. 어떤 것이 추가로 필요할까? 그렇다. 바로 "화면"에 대한 정보, 곧 ScrollViewer정보를 알 수 있어야 하며 나아가 ScrollViewer를 컨트롤 할 수 있어야 한다. UI Virtualization기능은 가상화 지원 Panel과 ScrollViewer를 동시에 필요로 한다. 둘 중 하나라도 만족하지 못하면 UI Virtualization 기능을 구현할 수 없다. UI Virtualization 기능을 위해선 이 ScrollViewer의 역할이 굉장히 중요하다.

이 ScrollViewer를 살펴보자. WPF ScrollViewer Overview 페이지를 보면 Physical Scroll 과 Logical Scroll의 개념이 나온다. Physical Scroll을 사용하면 Pixel, DIU단위의 스크롤링이 가능하다. 반면 Logical Scroll을 사용하면 ScrollViewer의 스크롤 동작을 수행했을 때 pixel단위로 움직이는 것이 아니라 임의의 논리 단위 만큼의 Height 또는 Width만큼 Scroll의 Offset이 이동하게 된다.(ListBox의 동작을 생각해보라!)

일단 Physical Scroll이 어떻게 가능할 지 생각해보자. ScrollViewer는 자신의 가용 크기를 알아야 하며 자식이 요구하는 크기를 알아야 한다. 이는 모두 Measure동작에서 이루어진다. ScrollViewer는 자식에게 Measure(double.PositiveInfinity, double.PositiveInfinity)를 사용해 자식이 요구하는 최대 크기를 알아낼 수 있고 MesureOverride 메소드에서 들어오는 constraint인수로 자신의 가용 크기를 알아 낼 수 있다. 이 때 자식이 요구하는 크기가 ScrollViewer의 Extent Size가 되며, ScrollViewer의 가용 크기, 곧 화면에 노출되는 ScrollViewer의 크기가 Viewport Size가 된다. 따라서 다음이 성립된다.

  • Viewport Size : ExtentSize = Scroll Thumb Size  : ScrollBar Size

그리고 해당 정보를 사용해 VerticalScrollBar, HorizontalScrollBar의 Visual을 결정할 것이다. 그리고 이후 Scroll동작이 일어나면 ㅡ VerticalOffset또는 HorizontalOffset 값이 변경되면 ㅡ ScrollViewer의 Offset을 이용해 Child를 Arrange시키면 되는 것이다.

물론 Logical Scroll은 이와 다르게 동작한다. Logical Scroll을 위해서 ScrollViewer는 자신의 기능(Extent Size 결정, Viewport Size결정, Vertical Offset, Horizontal Offset이 변경되었을 때의 동작 등)을IScrollInfo라는 Interface를 구현하는 객체에게 위임해야 한다. 실제로 Logical Scroll로 동작하는 ScrollViewer는 ScrollBar를 표시하는 일만 하는 것 같다. 대부분의 일들을 ScrollViewer자신이 결정하지 않고 자신의 기능을 위임한 객체에게 맞긴다. UI Virtualization을 위한 Panel은 자식들의 실제 Physical Size를 알 수 없기에 반드시 이 Logical Scroll을 사용해야 한다.

이제 UI Virtualization을 위한 Panel의 동작을 살펴보자. 이 Panel의 동작은 ItemContainer의 생성, 화면에 보여지지 않는 ItemContainer의 파괴(Optional), ScrollViewer의 정보 설정, ItemContainer의 배치로 요약될 수 있다.

  1. ItemContainer 생성
  2. 화면에 보이지 않는 ItemContainer 파괴
  3. ScrollViewer의 정보 설정
  4. ItemContainer의 배치

1,2,3 동작은 Panel의 Measure동작시 이루어진다. Measure 동작시 Panel은 ScrollViewer 정보와 자신의 가용 크기를 바탕으로 화면에 보여져야하는 ItemContainer를 ItemContainerGenerator를 이용해 생성해야 한다. Vertical Orientation의 가상화 지원 StackPanel을 만든다고 한다면 아마 다음과 같은 코드를 사용할 수 있을 것이다.

protected override Size MeasureOverride(Size availableSize)
{
    _viewport = availableSize;
 
    ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
    if (itemsControl.Items.Count == 0)
    {
        return new Size();
    }
 
    double offset;
    GeneratorPosition position = ItemContainerGenerator.
                        GeneratorPositionFromIndex(FindItemIndexFromOffset(VerticalOffset, out offset));
 
    #region 영역의 크기만큼의 ItemContainer을 생성한다.
    using (this.ItemContainerGenerator.StartAt(position, GeneratorDirection.Forward, true))
    {
        double remainHeight = availableSize.Height
 
        while (remainHeight > 0)
        {
            bool isRealized;
            UIElement container = ItemContainerGenerator.GenerateNext(out isRealized) as UIElement;
            if (container != null && isRealized)
            {
                this.AddInternalChild(container);
                ItemContainerGenerator.PrepareItemContainer(container);
            }
            else if (container != null && !isRealized)
            {
                if (!Children.Contains(container))
                {
                    this.AddInternalChild(container);
                }
            }
            else
            {
                break;
            }
            container.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
            remainHeight -= container.DesiredSize.Height;
        }
    }

위 작업이 종료되면 2,3번 동작을 MeasureOverride 메소드내에 포함시켜야 한다. 2번은 Optional할 수 있으므로 지나치더라도 3번 동작을 수행해야하는데 그 중에서도 ScrollViewer의 Extent Size를 결정해야한다. 그런데 이 ItemContainer의 크기가 고정적이라면 이것이 어렵지 않은데 각각의 ItemContainer 크기가 가변적이라면 어쩔 수 없이 "추정"이 들어가야 하므로 굉장히 복잡하고도 어렵다. 따라서 이 "추정"을 가능한 실재와 정확하게 하는 것이 가상화 Panel을 만드는 프로그래머의 주 목표라 할 수 있겠다. "추정"이 들어가는 부분은 이 뿐만이 아니다. 위 코드를 보면 FindItemIndexFromOffset이라는 메소드가 있는데 이 역시 ScrollViewer의 Vertical Offset을 바탕으로 가장 위에 보여질 Item의 Index를 "추정" 해야 한다.

protected override Size MeasureOverride(Size availableSize)
{
       // ...
       // ...
       // ...  
    this.extentSize = GetExtentSize();
    this.ScrollOwner.InvalidateScrollInfo();
    CleanUpItems(firstElement, lastElement);
    return availableSize;
}

현재 회사에서 만들고 있는 가상화 지원 패널은 ItemContainer의 크기를 바인딩 된 Data의 값을 바탕으로 얻을 수있기에 이 부분이 크게 어렵지 않았는데 이런 부가 정보가 없이 정확히 ScrollViewer의 정보를 계산하는 가상화 지원 패널을 만드는 것은 사실상 불가능하다.

이 Measure동작을 완료했다면 4.생성한 ItemContainer들을 배치하는 일이 남았다. 이 일은 ArrangeOverride 메소드가 담당해야 한다.

protected override Size ArrangeOverride(Size finalSize)
{
    double startYlocation =0.0;
    double offset;
    if (Children.Count == 0) return finalSize;
    var index = FindItemIndexFromOffset(VerticalOffset, out offset);
            
    GeneratorPosition position = ItemContainerGenerator.GeneratorPositionFromIndex(index);
            
    using (this.ItemContainerGenerator.StartAt(position, GeneratorDirection.Forward, true))
    {
        while (startYlocation < finalSize.Height)
        {
            bool isRealized;
            UIElement container = ItemContainerGenerator.GenerateNext(out isRealized) as UIElement;
            if (container == null) return finalSize;
            if (!Children.Contains(container))
            {
                throw new InvalidOperationException();
            }
 
            container.Arrange(new Rect(new Point(HorizontalOffset * -1, startYlocation),
                    new Size(finalSize.Width + HorizontalOffset,container.DesiredSize.Height)));
            startYlocation += container.RenderSize.Height;
        }
    }
    return finalSize;
}

여기까지 왔다면 ItemContainer들이 정상적으로 화면에 보이는 것을 확인 할 수 있을 것이다. 하지만 위 Measure와 Arrange코드는 사실 "최소한"의 부분이다. 사실 가장 중요한 부분은 역시 "추정"에 있는데 내가 만든 Panel은 그 동작이 현업의 특수한 상황에 의존적이라 이 코드에 추가하지 않았다. 완전한 코드를 소개하면 좋을 텐데 그러지 못해 조금 아쉽다. 그래서 다음 포스트에는 이 "추정"동작을 완벽 하게 해낼 수 없기에 이를 다른 방식으로 비껴나가는 전략을 취하는 Panel을 소개하기로 한다.

 

 

결론

보기에도 간단해 보이지 않는 이 가상화는 만들어진 소수의 ItemContainer를 바탕으로 보여지지 않는 영역을 예측, 추정해야 하는 것이기에 어려움이 있다. 때로는 "확률"이 쓰일 수도 있고 "평균"에 의지해야 할 수도 있다. 그리고 안타깝게도 이것은 결국 추정이기 때문에 어떤 경우에는 옳았던 구현이 다양한 상황에 의해 어떤 경우에는 틀릴 수 있다. 또한 모든 것을 이해한다고 생각하고 코딩에 뛰어들었지만 이 프로그램 세상은 언제나 그랬듯이 내가 미처 알지 못한 미지의 영역을 비웃으며 소개한다. 물론 그 미지의 영역을 헤집고 나아가는 것이 이 세계를 여행하는 자의 가장 큰 즐거움이라 할 수 있겠지만.

References :

신고

베가본드

즐거운 책 이야기 2011.02.01 22:10 Posted by 아일레프

베가본드란 만화를 보면 '천하제일'에 자신의 길을 정하고 그 길을 거침없이 걸어가는 한사람의 이야기를 볼 수 있다. 주인공 무사시는 천하제일에 가까워지는 자신을 발견하기 위해 평화를 외면하고 전장을 택한다. 그는 늘 이기지만 이겨도 이겨도 개운하지 않다. 뭘까. 왜 그럴까. 게다가 이상하고도 놀라운 것은 싸움에 이긴 그는 흔들리고 싸움에 진 사람은 해방된다는 것이다.

난 이 장면을 보고 온몸에 소름이 돋았다. 그는 패배를 얻음으로 자유를 얻었다. 그러나 무사시는 얻은 승리로 인해 계속 싸워야 하고 그것으로 고통받을 것이다. 그런데 그는 도대체 왜 싸우는 것인가?

"그걸 몰라서 물어? 강해지기 위해 그렇잖아? 최고가 되기 위해 그래서 그렇잖아?"

그런가? 하지만 '강함'이란 것은 과연 존재하는 것인가? 최고라는 것이 과연 실재하는 것일까? 김민기씨는 그의 아름다운 노래 "봉우리"에서 우리가 정말 올라야 할 봉우리는 높은 곳에 있는 것이 아니라 바로 여기에 있다고 한다.

불교는 여기에 그치지 않고 한술 더 떠서 이 질문에 다음과 같이 답한다.

"있지도 않은 것을 있다고 믿는 허황된 지식과 욕망이 고통을 낳는다."

무사시는 이것을 바보같이 왜 모르는 것일까? 이 만화를 보면 많은 사람들이 무사시에게 이것을 알린다. 네가 쫓는 그것은 사실 없다고. 그 길에서 나오라고. 그 또는 다른 누군가가 그린 허황된 꿈과 헛된 관념을 버리고 진짜 삶을 살라고 한다. 사랑하는 사람과 연을 맺고, 아이를 가지고, 가정을 꾸리라 한다. 무사시는 바보같이 왜 이걸 모르는 걸까?

어쩌면 무사시도 그 사실을 알고 있을지 모른다. 하지만 그는 여기서 끝을 볼 수 없다. 저 앎은 무사시에게 성급한 깨달음이다. 무사시는 아직 끝을 보지 못했다. 더 가봐야한다. 세상의 모든 것이 영원하지 않고 실체가 없다고 하더라도 자신과 자신이 가는 그 길은 실체가 있는 그것이라 믿고 끝까지 밀고 나가야한다. 결국 '헛됨'으로 증명될 그 길의 끝이 무엇인지 감이 오더라도 생각하지 말아야한다. 거기에 이 이야기의 아름다움이 있는 것이다.

그 후에 그는 얻을 수 있을 것이다. '모든 것이 헛됨'을. '~이 공하다'는 깨달음은 그 길 끝에 서있는 자만이 얻을 수 있는 선물이며 축복이다. 지금의 무사시에게도, 그리고 지금의 내게도 이른 깨달음이다. 좀 더 가보자.

신고

모든 것이 산산이 부서지다.

즐거운 책 이야기 2011.01.30 01:51 Posted by 아일레프

 
모든 것이 산산이 부서지다 -10점
치누아 아체베 지음, 조규형 옮김/민음사

이 소설을 읽고 나니 기존의 어떤 소설도 주지 못한 혼돈과 슬픔으로 머리 속이 어질 어질 해졌다. 기존에 머리 속에 차분히 정리되었던 물음표들이 다시 고개를 들어 날 괴롭힌다. 우리가 살고 있는 이 세계가 아이러니와 도덕적 딜레마로 가득찬 불투명한 회색지대라는 것을 증언하는 것이 문학의 역할 중 하나라고 한다면 이 소설은 과연 그 역할을 훌륭히 해냈다. 이전에 블로그에 올린 바 있는 소설 "수난"과 한번 비교해보겠다. 작가는 이 '수난'이란 책을 통해 옳음과 그름, 선과 악에 대해 묻고 있지 않는 것 처럼 보인다. 독자는 소설을 읽으며 어떤 인물들이 '선'의 편에 있는지, '악'의 편에 있는지 굉장히 자연스러이 쉽게 구분할 수 있고 그것으로 인물들에 대한 호불호가 완전히 갈리게 된다. 수난의 작가, 니코스 카잔차스키가 물었던 것은 그것이 아니었다. 작가는 우리가 다른 질문에 대답하길 원한다. "넌 지금 이 현실 속에서 어느 편에 있니?" 라고. 끊임 없이 들려오는 이 질문이 가슴을 꽤나 괴롭힌다. 그렇게 괴롭혀진 가슴은 마지막 장을 덮는 즉시 부글 부글 끓어 오르게 되고, 나만은 이 세상에서 선의 편에 서야 한다고 굳은 결심을 하게끔 만든다. 당장 거리로 나가 짱돌을 들어 이 더러운 세상의 시스템에 집어 던지고 싶어지게 하는 것이다. 그런데, '모든 것이 산산이 부서지다'는 달랐다.

사실 이 소설이 뻔할 것이라 생각했다. 아름다운 아프리카 부족의 풍경을 보여주겠지. 자연과 함께 숨쉬며 공존하는 삶, 무엇인가를 파괴함으로써 얻어지는ㅡ 그것에 인한 반대급부로 얻어지는 뒤끝이 찝찝한 행복이 아닌 '뭔가 진짜 행복'이라 부를 수 있는 그런 삶과 풍경을 보여주겠지. 그러나 그 행복은 서방의 하얀 마귀들에게 짖밟히게 될 것이고 그로 인해 확보하게 된 감정으로 그들을 좀 더 미워할 수 있게 되겠지.

그런데 그렇지 않았다. 작가는 그들의 행복한 모습을 주목해 보여주지 않았다. 오히려 소설의 분위기는 서방의 하얀 마귀가 오기 한참 전인데도 어둡고 침침했다. 자신이 힘겹게 얻은 강자의 자리를 빼앗기지 않으려 애쓰는 주인공 오콩코의 조급한 모습은 이 치열한 경쟁 사회에서 각개약진하는 우리들의 모습과 다르지 않았으며 그의 성에 차지 않는 그의 아들 은워예의 모습은 마치 나 자신을 보는듯 측은했다. 그들로서는 설명할 수 없는 자연의 변덕과 예측 할 수 없는 삶의 모습들을 종교와 주술로 풀어나가는 모습들도 내 눈에는 예쁘게 보여지지 않았다. 그들의 삶은 완벽한 것이 아니었던 것이다. 유토피아의 아주 작은 일부라도 발견하고픈 내 욕심과 기대감은 전혀 충족되지 못했다. 쌍둥이가 태어난 것은 대지의 여신에 대한 모독이므로 내다 버려야하는 사회, 다른 부족의 죗값으로 얻었던 이케메푸나, 어느새 오콩코의 아들이 되어주었으며 은워예의 형이 되어주었던 이케메푸나를 죽이라 명하는 숲과 동굴의 신이 지배하는 사회. 그리고 그 이케메푸나를 자신의 손으로 죽여버리는 주인공 오콩코. 내가 이것을 어떻게 예쁘게 봐줄 수 있단 말인가? 당혹스러웠다. 이건 내가 기대했던 풍경이 아니었던 것이다. 우리가 살고 있는 이 사회처럼 그 곳에도 역시나 전통란 단어의 긍정적 이미지로 덮어버릴 수 없는 사회적 모순들이 있었던 것이다. 이 미친 세상을 살아가는 우리들이 당연스럽게 변화와 '진보'라는 단어를 갈망하듯이 그들도 그랬을 것이다. 그곳은 에덴이 아니었다. 인간이 자리를 차지한 땅에 에덴이 자리잡기란 참으로 어렵다. 그리고 침략자들은 이런 균열을 반가워 하며 그들에게 살짝 문을 열어 더 나은 세계를 맛보여준다.

백인 선교사가 그들의 종교를 들고 왔을때 몇몇 사람들은 새로운 세계를 반기며 그 종교를 맞이 했다. 고통을 참으며 아이를 낳았지만 쌍둥이 이기에 버릴 수 밖에 없었던 가련한 여인, '오수'라 불리는 하층민들이 백인의 종교에 흡수되었고, ... 주인공 오콩코의 아들도 부족의 신을 버렸다. 그리고 부족의 지도자 중 한 사람도 그 길을 택하고 말았다. 부족은 처음에 그 종교와 공존을 선택했지만 결국은 깨지고야 말 부대낌이었다. 그들은 몰랐다. 백인이 믿는 신에서 탄생된 종교들은 무한한 사랑의 메시지를 가지고 있는 한편 배타적이고 폭력적인 메시지도 함께 가지고 있다는것을. 들어올 때는 사랑의 얼굴을 보여주지만 어느새 자리를 잡으면 반대편 얼굴을 보인다. 그리고 그 때는 늦었다. 백인의 종교를 뒤에서 조종하는 것이 그들의 신이 아닌 열강이란 것을 알아차린 그 때는 이미 늦었다.

아무일이 없었다는 듯 이전으로 돌아갈 수는 없다. 드넓은 하늘을 날아본 독수리는 노동 없이 먹이가 주어진다 하더라도 새장 안에서 결코 행복할 수 없다. 그들도 마찬가지다. 선교사들이 보여준 밝음을 본 이상 이전으로 돌아갈 수는 없다. 한번 생긴 균열은 그 범위를 계속 넓혀가고 인간의 힘으로는 그것을 메우기 어렵다.

카잔차스키의 "수난"에서 난 이 질문을 끄집어 냈다. "질서의 수호인가 사랑과 정의인가" 그때는 대답하기 쉬웠다. "수난"에서 혼돈의 여지는 없다. "질서의 수호"란 편에 있는 사람들은 하나 같이 악하고 비열하다. 카잔차스키는 질서의 편에 서있는 자들의 상황을 배려심 있게 쓰지 않았다. 이 책은 어떤가? "전통이냐 진보냐"라는 질문을 끄집어 낼 수도 있을 것이다. (전통 대신 보수란 말이 더 적당할 것도 같지만 한국에서 저 '보수'란 단어에 원치않게 추가된 부정적 어감을 생각해 볼때 적당하지 못해 전통을 선택했다.) 둘다 빛이 있고 어둠이 있다. "전통"이라고 말하기에는 그들의 무지로 인해 희생당하는 쌍둥이 들이 마음에 걸리며 "진보"라 말하기엔 그들에게 진보의 메시지를 가르쳐주는 교회 뒤의 하얀 마귀들이 걸린다.

이러지도 못하고 저러지도 못한다. 다만 이 아프리카의 땅에 존재하는 개인으로서 슬퍼하고 운명의 신을 저주하는 그들의 마음을 헤아릴 수 밖에. 부족의 전통안에서 강자로 우뚝 서고 싶었던 오콩코는 그의 꿈을 펼칠 무대 자체가 사라지자 자살을 선택할 수 밖에 없었다. 그의 죽음과 그들을 함께 묶어 두었던 매듭에 백인이 쑤신 칼집이 들어가자 그들의 모든것이 부서지는 광경을 보며 난 옳고 그름을 따지는 질문을 접을 수 밖에 없었다. "전통이냐 진보냐" 이런 질문을 던지고 답하는 것은 사치이다. 다만 애도할 수 밖에. 그들을 이렇게 패배하게 만들었던 이 가혹한 운명의 땅과 시간에 깊은 애도를 표하며 눈을 감는 것 외에는 할 수 있는 것이 없었다.

이 글을 쓴 치누아 아체베는 기독교인이라 한다. 그가 기독교인의 교리를 받아들였다면 그는 나이지리아 기존의 신앙을 버린 것이라 볼 수도 있다. 그는 이 소설을 쓰면서 어떤 마음이었을까. 자신의 땅에 지워진 이 가혹한 역사의 한 부분을 이렇게 가감없이 보여준 그는 얼마나 많은 눈물을 흘렸을까. 이 소설은 다른 무엇도 아닌 그 눈물을 재료로 쓰여진 책인 것이다.

그 눈물의 결실에 깊이 감사한다. 당신 덕에 난 가슴 저미는 이야기를 알게되었다. 고맙다. 하지만 당신의 이 다음 책 "더 이상 평안은 없다"는 읽지 않을 것이다. 가슴 저미는 아픔의 감정은 이것으로 족하다. 더는 알고 싶지 않다.

신고

김영하의 꿈

가상의 이야기 2011.01.18 23:13 Posted by 아일레프

잘 지내시지요? 전 당신이 들려주는 목소리로 늘 하루를 마감합니다. 적고 나니 굉장히 낮 뜨거운 문장이군요. 사연은 이래요. 언젠가 2주일간 원인 없는 불면증에 시달렸어요.(원인이 없다는 것에 참 기가 막혀요.) 생전 처음 수면제를 먹어봤어요. 하지만 별로 효험이 없더군요. 그렇게 힘들게 하루를 보내다 아이폰을 사게 되었고 그 안에 불면증 치료제가 있다는 것을 알게되었지요. Itunes U에 여럿 존재하는 영어 강의가 그것이었어요. 예, 원인 없는 불면증 정도는 이해 할 수 없는 말을 집중해 들으면 치유되는 법이지요. 그 후 잠을 있다는 자신감이 생겼고 불면증은 멀리로 갔지요. 그리고 마침내 이해할 수 있는 말을 들으며 잠을 잘 수 있게 되었답니다. 당신의 팟케스트로 당신이  책 읽어 주는 소리를 들으며 스스르 잠들게 있게 된거에요.(물론, 에피소드 하나를 끝까지 들은 적은 없습니다. 늘 잠이 에피소드의 마지막을 알리는 음악 소리보다 먼저 찾아오거든요) 이렇게 당신의 목소리는 내 하루의 끝자리에서 잠의 시작을 준비하는 고요한 음성이 되어주었답니다.

 

그런데 삶이 쉽나요? 당신의 목소리로도, Itunes U의 영어강의도 치료해줄 수 없는 원인 있는 불면증이  찾아왔습니다.  우리는 지금껏 만나지 못한 풍경에 취했고 매일 매일 새로운 곳으로 향하는 문을 열었어요. 그것은 아름다웠어요. 그런데 우리는 풍경에서 빠져나와 문을 닫아버렸어요. . 물론 사건이 있었지요. 거기에 대해서는 말하고 싶지 않네요. 물론 아쉬움이 남지만 그에게 문을 다시 열어보자며 매달리진 않을거에요. 남자란 매달리는 여자에게 자신의 닫힌 문을 허락하지 않는다는 사실을 알고 있거든요. 어머니에게 늘상 들어서 알아요.

 

"남자에게는 '전부'주면 안된단다. 종자들은 모든 것을 가졌다 싶으면 냉큼 도망가 버리거든. 지혜로와야 된다. 조금 사랑하고 현명해야된다."

 

그것이 정답이라고 믿어요. 그렇다면 상황에서 현명해져야할 필요가 있죠. 그와 나의 관계는 이미 닫힌 문이 되었고 우린 문의 열쇠를  강바닥에 던져버렸어요. 물론 찾을 수도 있겠죠. 혹시 몰라요. 강물에 발을 담그자 마자 열쇠가 밑에 있을지도. 그런데 현명한 여자에요. 작은 확률에 마음을 담아 잠시 뒤에 찾아올 절망을 짊어지는 실수는 하지 않을거에요. 또한 그가 어느날 마음을 돌려 강에서 열쇠를 힘겹게 찾아 나를 앞으로 이끈다고 해도 기분좋게 거절할거에요. 지나간 것은 돌릴 없거든요. 풍경은 이미 지나갔어요.

 

그는 기억 속에서 점점 쪼그라들어 바람 빠진 풍선처럼 추해졌지만 풍경의 기억만은 선명해지독한 불면증을 남겼고,  불면증은 당신에게로 깊이 다가가게 했어요. 당신의 팟케스트 하나의 에피소드도 전부 들을 수 없었던 제가 에피소드 3개를 듣고도 잠이 오지 않아 멀뚱 멀뚱 천장을 보는 경험을 하게 된거에요. 어쩌면 불면은 다행인지도 몰라요. 당신의 팟케스트가 정말 훌륭하다는 것을 알게 해 주었거든요. 내 몸의 머리 부터 끝까지 불어오는 찬바람에 가슴이 저며와도 귀만은 당신의 목소리를 놓치지 않으려 애쓰는 내 모습에 실소가 터져 나온것이 한두번이 아니랍니다. 그러나 당신의 팟케스트를 듣는 것보다 난 자고 싶어요. 난 잠을 사랑해요. 잠이 있어야 내일 삶을 시작할 수 있고 잠이 있어야 꿈을 꿀 수 있거든요. 그리고 운이 좋으면 꿈속에서 풍경을 다시 수도 있지요.

 

어제는 9시에 집에 왔어요. 친구들과 마음껏 수다를 떨며 술을 거하게 먹었던 장소에 정신을 두고 왔기에 난 잠들 수 있을거라며 기뻐했고, 그 기쁨이 헛되지 않게 잠이 들었답니다. 그런데, 삶이 그리 쉽나요? 난 꿈을 꾸었어요. 아나콘다가 호랑이에게 시비를 걸어 아나콘다에게 유리한 아마존의 강가에서 싸움이 벌어지게 되었지요. 아나콘다는 기세 등등하게 호랑이의 몸을 졸라 죽이려 했지만 호랑이는 날렵하게 몸을 돌려 아나콘다의 목을 물었고, 그 영향으로 난 잠에서 깼습니다. 새벽 두시더군요. 그리고 다시 불면이 찾아왔습니다. 당신과 만날 시간이 온거에요.

 

레이몬드 카버의 이야기는… 오, 훌륭했습니다. 이적의 목소리도 멋졌지요. 그 뿐인가요? 로알드 달의 그 웃기는 이야기.  지금 생각해도 웃음이 나요. 하지만 가장 압권은 역시 밀로라드 파비치의 "카자르 사전"이었습니다. 그 흥미로운 이야기며, 마지막 그 아름다운 음악이라니. 난 용기를 얻어 다시 잠을 청했어요. 역시나 잠이 오지 않더군요. 그래서, 쓰기 시작했어요. 잠을 자기위해 쓰기 시작했어요. 머리 속에 있는 생각이며 감정이며 마구 마구 토해 내며 펜을 휘갈겼어요. 글이란 것이 이렇게 고통을 잊기 위한 도구가 될 수 있다는 것을 알게 되었습니다. 또한 철저한 나만을 위한 글쓰기가 이런 것이란 것을 알았습니다. 그렇게 몇 번 토해내고 나니 잠이 오더군요. 그리고 놀라운 일이 벌어졌습니다. 당신이 내 삶에 관여하기 시작한 것 입니다.

 

어느 날 고향에 갔어요. 집의 인테리어가 많이 변했더군요. 사방이 온통 책이었어요. 이게 뭐냐고 어머니에게 물었어요.

 

"이게 다 뭐에요?"

"이번에 친구들을 초대해 파티를 하려고 인테리어를 했단다."

"참 비싼 인테리어군요, 우리 집에 책 읽는 사람은 전혀 없잖아요. 왜 이런 짓을 했어요?"

"나도 별 생각이 없었는데, 김영하란 작가가 이벤트를 하길래 냉큼 신청했어. 그 사람이 자기 서재의 모든 것들을 2주일간 빌려준다고 했단다."

"공짜로요?"

"아니, 400만원."

"400만원이요? 말도 안돼! 그게 무슨 이벤트야! 이런 사기꾼 같으니!"

"그러지 마라. 그 사람 참 좋은 사람이더라."

 

어머니는 그렇게 말해놓고선 방에서 나갔어요. 난 멍하니 방을 살폈죠. 난 쌓여진 책들의 기에 눌렸지만 침착히 책장에 다가가 책 하나를 꺼냈어요. "돈키호테 3"이었어요. 돈키호테는 2 까지 있는 줄 알았는데 3이 있더군요. 그런데 책을 펼치니 사진 한 장이 펄럭 거리며 바닥에 떨어졌어요. 그 사진을 주워  살펴보니 그와 당신이 어깨 동무를 채로 그곳에 있더군요. 당신과 그는 아는 사이였던 건가요?  . 기가 막힐 일이에요. 당신과 그는 한패였어요. 그와 어떤 작당을 한거죠? 그와 당신은 새로운 풍경안에서 기뻐하는 나를 보며 실소를 입에 품었겠군요.  훌륭한 계획이에요. 풍경을 가져감으로써 불면증을 선사하고 이제 불면증으로 당신에게 묶여있게 하다니. 책장 앞에서 오랜 시간을 울어야 했어요. 그리고 시간으로 돌아왔어요.


... 


이제 알겠나요? 당신은 내 삶에 개입했어요. 난 당신이 내게 들려준 카자르 사전의 두번째 이야기를 기억해요. 꿈은 자리에 남아 균열을 선사합니다. 난 당신의 꿈으로 시작되어 나의 삶을 지나쳐가는 균열을 당신에게로 기꺼이 이끌 것입니다. 그리고 그것을 멈추지 않을 겁니다. 두렵지 않나요?

 

미안해요. 당신을 두렵게 만들 생각은 없어요. 꿈의 감정 처럼 당신을 원망하는 것도 아니에요. 사실 당신에게 부탁이 있어요. 다시 이야기를 만들어줘요. 당신과 그가 그린 시나리오는 끝이 나빴지만 과정에서 보여지는 풍경은 정말 훌륭했어요. 다시 한번 이야기를 만들어줘요. 그것보다 아름다운 광경을 내게 보여줘요. 이전의 그가 힘들다면 다른 배우도 괜찮아요. 사실 그는 중요치 않아요. 풍경이면 족해요. 이번에 당신은 있어요. 이전에는 당신과 둘만이라서 실패했던거에요. 이번엔 나도 함께에요. 그리고 잘할거에요. 헛된 실수는 하지 않을거에요. 우리 같이 한번 만들어봐요.  당신의 이야기의 끝이 이번과 같이 새드엔딩이라도 괜찮아. 다시 불면증에 걸릴 것이고 불면증은 다시 당신에게로 이끌것이니까.  




-- 처음 글이 머리 속에서 나왔을 그녀는 "그와의 이야기를 다시 만들어내요, 헤어진 사람이 다시 만나고 다시 사랑을 시작하는 진부한 이야기를 써줘요" 김영하에게 청하지만 지금은 풍경이면 족하다고 한다. 결말이 현실에 가깝기에  마음에 든다.

저작자 표시
신고

파이 이야기

즐거운 책 이야기 2011.01.18 22:59 Posted by 아일레프

어젠 11시에 누웠다. 평소 취침시간보다 약간 이른 시간이었지만 월요일의 묘한 기운이 날 몹시 피곤하게 했기 때문에 어서 잠에 들고 싶었다. 잠을 좀 더 빨리 부르려고 4분의 1쯤 본 "파이 이야기"책을 꺼내 읽었다. 엄청난 실수였다. 주인공이 호랑이와 함께 표류하기 전에 책 넘기는 짓을 멈췄어야했다. 몸은 피곤해 졸려 죽겠는데 이야기의 흡입력이 너무 강해 20분만 더보자, 10분만 더보자 하다가 결국 마지막을 본 뒤에야 잠들 수 있었다. 소설의 참맛은 역시 서사다.

이 소설의 '1'을 여기 둔다. (다시 읽으니 코 끝이 찡하다.)

 

1

아픔을 겪은 후 난 슬프고 우울했다.

공부와, 마음을 다해 꾸준히 행한 종교 의례 덕분에 차츰 삶을 되찾았다. 나는 남들이 이상한 종교의식이라고 여길 만한 예배를 계속 올려왔다. 고등학교에서 일 년을 보낸 후, 토론토 대학에 진학해서 두 가지를 공부했다. 전공은 종교학과 동물학. 종교학 졸업논문 주제는, 사페드 출신으로 16세기의 위대한 카발라 사상가였던 아이삭 루리아의 우주론과 관련된 내용이었다. 동물학 논문은 세발가락나무늘보의 갑상선에 대한 기능적인 분석이었다. 연구 대상으로 나무늘보를 선택한 것은 이 동물의 차분하고 조용하고 내성적인 태도가 갈가리 찢긴 내 자신을 위로해주어서였다.

나무늘보는 발가락이 둘인 종과 셋인 종이 있다. 뒷발은 모두 발가락이 셋이므로, 앞발에 따라 종을 나눈다. 어느 여름, 무더운 브라질 밀림에서 발가락 셋인 나무늘보를 연구하는 행운을 누렸다. 나무늘보는 대단히 흥미로운 생물이다. 유일한 습관이 게으름 피우기다. 하루 평균 스무 시간씩 자거나 휴식한다. 우리 연구팀은 세발가락나무늘보의 수면습관을 실험했다. 초저녁에 잠든 다섯 마리의 머리 위에 물이 담긴 빨간 플라스틱 접시를 올려두었다. 다음날 아침에 가보니, 접시는 그대로 있고 물에는 벌레가 들끓었다. 나무늘보는 해질 무렵에 가장 분주하다. '분주'하다고는 하지만, 좀 그렇다는 것이지 아주 바쁘다는 뜻은 아니다. 이 동물은 나뭇가지에 거꾸로 매달려서, 시속 400미터로 움직인다. 땅에서는 시속 250미터로 나무에 기어오른다. 이것도 다급할 때의 속도다. 다급한 치타보다 440배 느린 속도다. 급한 일이 없으면 한 시간에 4, 5미터 정도만 움직이는 동물이 바로 나무늘보다.

세발가락나무늘보는 외부에 많이 알려지지 않았다. 동물학자 비브는 보통의 둔감함을 2점, 극도의 예민함을 10점으로 나누고 나무늘보의 미각, 촉각, 시각, 청각에 2점을 주었고, 후각에는 3점을 주었다. 숲에서 잠든 세발가락나무늘보는 두세 차례 쿡쿡 찌르면 깨어난다. 졸린 눈으로 사방을 둘러보지만 찌른 사람은 알아보지 못한다. 모든 걸 흐릿하게 보는 나무늘보가 왜 그렇게 둘러보는지는 불확실하다. 청력의 경우, 안 들린다기보다는 소리에 관심이 없다. 비브는 자거나 음식을 먹는 나무늘보 옆에서 총을 쏘아도 별 반응이 없다고 보고했다. 후각이 약간 낫긴 하지만 과대평가해선 안된다. 나무늘보가 코를 킁킁거려 썩은 가지를 피할 수 있다고 하지만 동물학자 벌록은 나무늘보가 썩은 가지에 매달렸다가 땅에 떨어지는 경우가 '많다'고 보고한 바 있다.

그런 동물이 어떻게 생존하는지 궁금할 것이다.

놈들은 너무 느린 덕분에 목숨을 부지한다. 잠과 게으름 덕분에 재규어와 스라소니, 큰수리, 아나콘다에게 먹히지 않는다. 나무늘보의 털에는 건기에 갈색 식물이, 우기에는 초록색 식물이 서식한다. 그래서 나무늘보는 주변의 이끼나 나뭇잎과 뒤섞여, 흰개미나 다람쥐의 둥지나 나무의 일부로 보인다.

세발가락나무늘보는 환경과 완벽한 조화를 이루어 평화로운 초식 동물로 살아간다. 터를러는 "나무늘보의 입에는 언제나 맘씨 좋은 미소가 걸려 있다"고 했다. 내 눈으로 직접 그 미소를 본 적이 있다. 난 동물에게 인간의 특징과 감정을 투사하기를 좋아하지는 않지만, 브라질 밀림에서 고개를 들다가 쉬고 있는 나무늘보를 볼 때면, 물구나무서서 명상하는 요가 수행자나 기도에 몰두한 은자 앞에 있는 듯한 기분이었다. 내 과학적인 접근법으로는 닿을 수 없는 상상력 넘치는 삶을 사는 현자 앞에 있는 느낌이랄까.

가끔 내 전공은 뒤섞여버렸다. 종교학을 전공하는 친구들ㅡ 위가 어디인지 모르고, 엉터리 같은 이성의 노예가 되어 갈피를 못 잡는 불가지론자들 ㅡ 을 보면 세발가락나무늘보가 떠올랐다. 생명의 기적을 보여주는 세발가락나무늘보를 보면 신이 떠올랐다.

과학도들과는 아무 마찰도 없었다. 그들은 다정하고, 무신론자이며, 열심히 공부하며, 술고래들이다. 과학에 대해 생각하지 않을 때는 섹스와 체스, 야구만 머리에 있는 친구들이다.

내 입으로 이렇게 말해도 좋을지 모르지만, 나는 아주 우수한 학생이었다. 세인트 미카엘 칼리지 시절 사 년 내내 우등생이었다. 동물학과에서 받을 수 있는 상은 모두 받았다. 종교학과에서 상을 못 받은 것은 학과에서 주는 상이 없었기 때문이다.(하긴 종교학 부문에서 어찌 인간이 상을 줄 수 있을까). 토론토 대학 학부생이 받을 수 있는 최고의 상은 주지사상이다. 캐나다에서 잘나가는 명사 중에는 그 주지사상 수상자들이 많다. 체격이 다부지고 지나치게 활달한 기질을 가진 그 백인 남학생만 아니었다면, 내가 상을 거머쥐었을 것이다.

그때 받은 모멸감에 지금도 자존심이 상한다. 살면서 고통을 많이 겪으면, 더해가는 아픔은 참기 힘들기도 하지만 사소해지기도 한다. 내 인생은 유럽 그림에 나오는 해골과 비슷하다. 옆에는 늘 씩 웃는 해골이 있어, 야망의 아둔함을 일깨워준다. 나는 그것을 보며 중얼거린다. '사람을 잘못 골랐어. 넌 삶을 믿지 않을지 몰라도 난 죽음을 안 믿거든. 저리 가!' 해골은 낄낄대면서 가까이 다가오지만, 난 놀라지 않는다. 죽음은 생물학적인 필요 때문에 삶에 꼭 달라붙는 것이 아니다 ㅡ 시기심 때문에 달라붙는다. 삶이 워낙 아름다워서 죽음은 삶과 사랑에 빠졌다. 죽음은 시샘 많고 강박적인 사랑을 거머쥔다.

하지만 삶은 망각 위로 가볍게 뛰어오르고, 중요하지 않은 한두 가지를 놓친다. 우울은 구름의 그림자를 지나칠 뿐이고. 그 백인 남학생은 '로즈장학위원회'에서 장학금을 받았다. 난 그를 좋아한다. 그가 옥스퍼드에서 풍요로운 경험을 누리길. 부의 여신인 라크시미가 어느 날 내게도 행운을 듬뿍 내려준다면, 옥스퍼드는 다섯번째로 가고 싶은 도시다. 그보다는 먼저 메카, 바라나시, 예루살렘, 파리에 가보고 싶다.

직장생활에 대해서는 별로 할 말이 없다. 그저 넥타이가 올가미고, 거꾸로이긴 해도 조심하지 않으면 목이 졸릴 거라는 것밖에.

캐나다를 사랑한다. 인도의 열기와 음식, 벽을 타고 오르는 도마뱀, 은막 위에서 펼쳐지는 뮤지컬, 거리를 거니는 소, 까악대는 까마귀 떼, 크리켓에 대한 이야기까지 그립지만, 캐나다를 사랑한다. 캐나다는 너무 추워 정신을 차리기 힘든 대단한 곳이고, 헤어스타일이 제멋대로인 선량하고 지적인 사람들이 사는 곳이다. 어쨌거나 폰디체리에는 내 마음이 젖어들 만한 게 없다.

리처드 파커는 쭉 나와 함께 있었다. 그를 잊어본 적이 없다. 보고 싶다고 해야 할까? 그렇다. 보고 싶다. 지금도 꿈에서 그를 본다. 주로 악몽이지만, 사랑이 얼룩진 악몽이다. 사람의 묘한 심리다. 어떻게 그렇게 불쑥 날 버릴 수 있었는지 지금도 이해가 안 된다. 작별인사도 없이, 한 번 돌아보지도 않고 어떻게 그렇게 훌쩍 가버렸을까? 도끼로 쪼개는 것처럼 가슴이 아프다.

멕시코 병원의 의료진은 믿지 못할 정도로 내게 친절했다. 환자들도 마찬가지였다. 암 환자나 교통사고 환자들이 내 소문을 듣고는 휠체어를 밀며 모여들었다. 보호자들까지 모여들었다. 영어를 하는 사람이 없고 나는 스페인어를 못 하는데도. 그들은 미소를 보내고, 손을 잡고, 머릴를 쓰다듬어주었다. 내 침대에 음식과 옷가리를 놓아주기도 했다. 그들 때문에 난 참지 못하고 웃음과 울음을 터뜨렸다.

이틀쯤 지나자 설 수 있었다. 어지럽고 메스껍고 힘이 없었짐나, 두어 발 뗄 수 있었다. 빈혈에다, 나트륨 수치가 너무 높고 칼슘 수치는 너무 낮다는 혈액검사 결과가 나왔다. 몸에 액체가 고여서 다리가 퉁퉁 부었다. 몸에 코끼리 다리를 붙여놓은 것 같았다. 소변은 샛노란 색이었다가 점차 갈색으로 변했다. 일주일쯤 지나자 정상적으로 걸을 수 있고, 끈까지는 못 묶어도 구드를 신을 수는 있었다. 어깨와 등에 상처가 남아 있었지만 피부는 나았다.

처음 수도를 틀자 요란한 소리를 내며 물살이 쏟아지는 데 놀라, 몸이 흐물흐물해져 털썩 주저앉았다. 간호사의 품에서 기절해버렸다.

캐나다에서 처음 인도 식당에 갔을 때 나는 손으로 밥을 먹었다. 웨이터가 못마땅하게 바라보면서 "지금 막 배에서 내렸나보군요?"라고 했다. 나는 허옇게 질렸다. 조금 전까지도 음식을 음미하는 미뢰였던 손가락이, 웨이터의 눈길에 더러운 게 되어버렸다. 나쁜 짓을 하다 들킨 죄인처럼 손가락을 얼어 붙었다. 감히 손가락을 쭉쭉 빨 수가 없었다. 난 죄지은 듯 냅킨에 손을 닦았다. 웨이터는 그런 말이 내게 얼마나 상처를 주는지 몰랐다. 살에 못을 치는 것 같았다. 나이프와 포크를 집어 들었다. 그런 도구는 써본 적이 없었다. 손이 떨렸다. 삼바가 맛이 없어졌다.

 

ㅡ 와 이 정도 길이의 글을 옮겨 쓴 적은 처음이다. ㅡ 1에 해당하는 이 글은 나무늘보 이야기로 쿡쿡 웃게 만들더니(나무늘보 대박) 여기 저기 왔다 갔다하는 이야기로 날 혼란스럽게 했다. 갑자기 "리차드 파커"란 이름이 나오더니 병원으로 공간이 옮겨진다. 이야기를 다 읽은 나는 이제야 이 첫 이야기를 읽고 그의 마음을 헤아린다. 리차드 파커는 나빳다.

모니터 안에서 인도와 한국이 축구 경기를 하고 있다. 주인공 파텔도 저 인도 사람과 같은 생김새일까? 경기는 지금 3:1이다. 이대로 끝났으면 좋겠다.

신고

오랜만에 시간이 비는 일요일이었다. 편한 마음으로 '천재 유교수의 생활'이라는 만화책을 보았는데, 와. 소네트 83번 일화가 멋졌다. 저 위 그림에 보이는 교수와 어리버리해 보이는 학생의 일화가 담겨져 있었는데, 저 학생은 이해가 너무 느리다. 남들이 다 알고 넘어가는 걸 끙끙 붙잡고 있다. 저 학생이 자주 하는 말은 "죄송합니다. 모르겠습니다."이다. 저 교수는 이 학생을 참 많이도 혼내는데 사실은 마음 깊이 아끼고 있었다. 그 이유는 저 학생이 머리가 좋아서가 아니다. 자질을 가지고 있기 때문이다.

"넌 연구가로서 무엇보다 중요한 자질을 한가지 가지고 있다. 그건 자신이 진심으로 납득하지 않는한 결코 다음 단계로 넘어가지 않는 것이다." 아. 이 얼마나 아름다운 문장인가.

이와 함께 예전에 보았던 "카이스트"란 드라마의 일화가 생각났다. 보셨던 분은 알겠지만 극 중에 '만수'란 캐릭터가 나온다. 이 사람은 명색이 카이스트 대학원생인데 절대 그렇게 보이지 않는다. 대학원생이 허구헌날 대학생인 이민우에게 질문하고 도움을 청하고 폐를 끼친다. 어느날 민우가 하도 갑갑해 교수님에게 "만수형은 도대체 어떻게 대학원에 들어간거죠?"라고 묻는다. 그러니 교수님이 다음과 같이 말했다.(정확히 기억 나지는 않는다.)

"실험 과목이 있었어. 만수도 그 과목을 듣고 있었는데 대부분의 학생이 하루면 끝나는 실험을 저 만수란 녀석은 1~2주가 되서 겨우 겨우 끝내는거야. 어려운 실험도 아니었는데 말이야. 진로를 잘못 선택한것은 아닐까 하고 안타까워했지. 그런데 말이야, 만수는그 과목에서 어떤 실험과 과제도 성공해내지 못한 적이 없었어. 시간이란 가치로 그를 따진다면 그는 낙제지. 하지만 그는 낙제생이 아니었어. 그는 결코 포기하지 않는 덕목을 가지고 있었던 거야. 그래서 그는 대학원생이 되었지."

허구헌날 "좀 더 빨리, 남보다 먼저"란 구호가 항상 머릿 속에 있는 내게 시간을 무시해버림으로써 그것에 결코 패배하지 않는 저 두명은... 머랄까... 멋지다.

신고

동료가 무엇인가를 붙잡고 있길래 보았더니 발상이 기발한 흥미로운 코드가 있었다. 정확히 기억이 나진 않지만 다음과 같았다.

   1: List<object> listA = new List<object>();
   2: listContainer.Where(l=>
   3:      {
   4:           listA.AddRange(l);
   5:           return true;
   6:      });
   7: //listContainer의 type은 List<<List<object>>이다.

그렇다. 그는 List<> 객체가 ForEach라는 메소드를 가지고 있는 것을 몰랐던 것이다. 그가 겪었던 문제는 Where를 ForEach로 바꾸는 것으로 간단히 해결되었다. 하지만 질문은 계속 되어야한다. 왜 동료가 작성했던 저 코드로는 원하는 결과를 얻을 수 없었던 것일까? 이 글을 읽고 있는 당신, 더 이상 읽지 말고 여기서 잠깐 생각해보시라!

대부분 정답을 바로 머리에 떠올렸을것이다. 부끄럽지만 난 2번 디버깅을 한 후에야 원인을 알았다.(어처구니 없게도 내 첫번째 추측은 C#컴파일러가 똑똑해 저 코드를 의미없다고 판단해 삭제시켜 컴파일 했으리라는 생각이었다.) 정답을 보자. Where<>는 IEnumerable<>을 반환하고 IEnumerable<>은 IEnumerator<>를 반환하는데 이 IEnumerator<>의 MoveNext()와 Current{}가 불린 적이 없기 때문에 Where절이 실행되지 않았던 것이다. 에잇! 뭐라는 거야! 열거자를 반환했는데 열거자가 열거되지 않아 Where()내의 lambda문이 실행되지 않았다는 것이다.(What Does the Yield Keyword Really Generate?글을 보면 깊은 곳까지 자세히 알 수 있다.) 즉 위 코드는 다음과 같이 쓰면 실행된다.

   1: List<object> listA = new List<object>(); 
   2: var results = listContainer.Where(l=> 
   3:      { 
   4:           listA.AddRange(l); 
   5:           return true; 
   6:      }); 
   7: foreach(var result in results); 

물론 다음과 같이 쓸 수도 있다.

   1: List<object> listA = new List<object>(); 
   2: listContainer.Where(l=> 
   3:      { 
   4:           listA.AddRange(l); 
   5:           return true; 
   6:      }).ToList();
   7:  

그런데 동료가 굳이 foreach를 사용하지 않고 Where메소드를 사용하려했던 이유는 IEnumerable<> 조합 + 확장 메소드 + lambda 조합으로 코드를 작성할때 손이 훨씬 덜가기 때문이리라.(추측임) 변수이름 치고 . 찍고 Where절 누르고 이러면 인텔리센스로 스피디 하게 훅훅 지나간다. 그리고 코드에 ()=> 이런거 보이면 나 왠지 코딩 좀 하는 것 같아 보인다. 여기에 맛들이면 언젠가 부터 코드에 foreach, for가 사라지기 시작한다. IEnumerable<>에 없는 ForEach, For는 자주 사용하는 Library dll의 필수 아이템이다.

   1: public static void ForEach<T>(this IEnumerable<T> list, Action<T> action)
   2: {
   3:     foreach (var item in list)
   4:     {
   5:         action(item);
   6:     }
   7: }

그런데 이게 좋은 습관은 아닌 것 같다. 일단 ForEach 확장 메소드를 쓰면 그 자리에 없었던 함수 호출의 수가 집합의 크기만큼 생겨버린다. 게다가 Action<T> delegate를 통한 호출이라 Inlining을 통한 최적화도 쉬울 것 같지 않다. 뭐... 사실 여기서 생긴 성능 감소는 알고도 억지로 눈감는 수많은 문제들에 비하면 너무 사소해 그냥 지나가도 양심에 부담감이 없지만 break 와 return 키워드들을 쓸 수 없다는 것이 큰 아픔이다.(ForEach내의 return문의 의미는 return이 아니라 continue이기에) 아니나 다를까 최근에 ForEach와 lambda문으로 조합된 코드를 foreach로 변경해야 할 일이 있었다. 코드를 처음 만들 때는 필요 없었는데 나중에 break가 필요했기 때문이다. 이 후에는 ForEach보다는 foreach를 사용하는데 사실 여전히 ForEach 타이핑의 편리함이 못내 그립다.

결론 : foreach, for를 사랑해주세요.

잡담 : 그런데 빌드시 C# 컴파일러는 코드 삭제를 통한 최적화는 하지 않는건가?

   1: List<object> listA = new List<object>(); 
   2: var results = listContainer.Where(l=> 
   3:      { 
   4:           listA.AddRange(l); 
   5:           return true; 
   6:      }); 
   7: foreach(var result in results); 
   1: List<object> listA = new List<object>(); 
   2: listContainer.Where(l=> 
   3:      { 
   4:           listA.AddRange(l); 
   5:           return true; 
   6:      }).ToList();
   7:  

위 코드들 전부 최적화 옵션주고 컴파일 했을 때 허공으로 사라질 것이라 생각했는데 IL코드도 정상적으로 나오고 실행결과도 이전과 동일했다. 뭐, 열거자는 "열거"라는 의미가 있기에 저 코드를 의미없는 것이라 생각하지 않는구나! 라고 결론을 내리고 다음 코드로도 실험해보았는데

   1: for(int i=0;i<10;i++){}

이것도 정상적으로 IL 코드에 추가되더라. 그래서 C# 컴파일러는 코드 제거를 통한 최적화는 하지 않는다는 결론을 내렸다.(최적화 옵션을 주고 빌드했을 때 바뀐점은 첫번째 예의 코드내의 foreach 키워드가 while로 변한것이었다. 여기서도 foreach는 사랑받지 못한다. ㅜ.ㅜ). 아 그리고 .NET 프로그램은 2번 최적화 기회를 가지는데 IL 코드로 변환될 때가 첫번째요, IL코드가 Native코드로 변환되는 JIT 컴파일 타임이 두번째이다. 그런데 Language Compile Time보다 JIT Compile Time에 더 많은 최적화가 이뤄진다고 한다.(좀 의아하다.) 마지막 'for'문의 예는 IL 코드에 추가되긴 했지만 JIT Compile Time에 IL 코드 삭제를 통한 최적화가 이루어졌을 수도 있기 때문에 실제로 실행되었는지 여부는 알 수 없다.(이걸 알 수 있는 방법은 없나?)

--- 있었다. JIT 컴파일러가 생성한 기계어 확인하는 법을 보면 확인 할 수 있다. 정책임님 감사해요~^^

references : Release IS NOT Debug: 64bit Optimizations and C# Method Inlining in Release Build Call Stacks, What Does the Yield Keyword Really Generate?

신고
TAG forEach

재현이란 무엇인가 - 10점
채운 지음/그린비

좋은 하나를 소개하려합니다. 그린비 출판사의 개념어 총서 시리즈 1권인 "재현이란 무엇인가" 책입니다. 책에서 말하는 사고를 많이 접해본 사람에게는 그저 평범한 책으로 다가올 있겠지만 제게는 적지 않은 충격을 주었습니다. 이전에 니체의 "짜라투스트라는 이렇게 말했다" 읽었을 때도 이와 비슷한 떨림을 느꼇지만워낙 책이 어려워 떨림이 무엇에서 시작되는지 함부로 말하기 어려웠는데 책은 쉽다는 미덕을 갖추고 있어 지금 내게 맞는 책이 되어주었습니다

, '재현'이란 무엇일까요? 그것은 '재현적 사고' 무엇인가를 말하는 것이기도 합니다. 그것이 뭔지 말하기 전에 1 전쯤 회사 사장님과 나눈 대화의 일부를 소개합니다.

 

" 여자친구를 사귀지 않지?"

"글쎄요, ... 아마도 제가 원하는 사람이 나타나지 않아서 그런것 같아요."

"네가 원하는 사람이 어떤 사람인데?"

"그다지 이쁘지는 않아도 되지만 , 피부가 깨끗했으면 좋겠고,  웃는 모습이 이뻣으면 좋겠고, 자기 주관이 뚜렸했으면 좋겠고, 너무 현실적이지 않았으면 좋겠고... 주절 주절"

"그래? 그런데 그런 사람이 세상에 있을까? 네가 그리는 그런 사람이 세상에 있을까?"

"영화나 소설에서 보면 일생 한명은 만날 있다던데요?"

"그건 영화니까. 소설이니까. 네가 마음 속에 그린 이른바 이상형이란 어쩌면 실재하지 않는 '로보트'인지도 몰라. 마음 속에 네가 원하는 사람의 모습을 하나 하나 붙여 로보트를 만들고 로보트를 사랑하는거지. 그런데 로보트란 결국 자신이 그린 것이고, 그것으로 시작된 사랑은 타인에게로 깊숙히 뻗어나가기 어려워. 타인에게로 향해야할 사랑이 자신에게로 치우치는 거지. 분명한 것은 로보트와 일치하는 사람은 없다는 거야. 로보트와 닮은 사람을 만났다 해도 멀지 않아 네가 그린 형에서 벗어난 모습을 찾아낼 것이고 그것이 마음에 짐을 수도 있지."

"..."

"때문에 중요한 것은 마음 속에 이미 자리잡은 '이상형' 모습을 하나 하나 지워나가는 거라 생각해. 그러면 많은 사람들을 사랑할 있을거야."

 

좋은 조언이죠? 재현적 사고란 사장님게서 꾸짖은 생각의 모습입니다. 사랑할 사람의 상을 미리 마음에 그려놓고 그와 똑같은 사람을 만나기 위해 최선을 다하는 . 넓게 보면 마음 속에 어떤 완벽한 상을 그려놓고 그것 대로 살아가려하는 . 이것이 재현적 사고입니다. 이것이 문제가 되냐면 하나 하나 소중한 삶의 방향을 '이래야 한다. 이러면 안된다'라고 성급히 규정 짖고 삶의 옳고 그름, 선과 악의 경계선을 긋 때문입니다. 문제되는 사실은 '무엇을 재현한다.?'라는 문장에서 목적어인 '무엇' 자기 자신이 정한 것이 아니라 그것이 어쩌면 사회와 종교 또는 미디어로 부터 받은 억압과 교육의 찌꺼기 있다는 것입니다. 한번 생각해보세요. 당신이 꿈꾸는 사랑의 모습은 온전히 자신이 만든 것입니까? 혹시 아름 다운 영화나 소설이나 종교의 경전을 보고 "저게 바른 사랑이야, 사랑은 저래야해."라고 생각한 것은 아닌지요? 더욱 문제되는 것은 영화, 소설, 종교의 경전이 말하는 아름다운 사랑의 모습이 현실의 자기 모습으로 이어지기 어렵다는 것입니다. 그럼으로 하지 않아도 의심을 하게 것이고 혹은 그것으로 죄의식을 느낄 지도 모릅니다. 속으로 말하겠죠. "이건 내가 꿈꾸는(사실 남이 그려줬을지도 모르는) 삶의 모습이 아니야." " 사랑은 이러지? 사랑은 온유하고 시기하고 질투하지 않는 것이라 했는데 이러지?" 이런 식의 사고는 삶을 자유롭게 하지 못하는 장애물이 됩니다. 낙타의 삶이죠

 

"인내력 있는 정신은 이와 같은 모든 무거운 짐을 짊어지고, 짐을 싣고 사막을 달리는 낙타처럼 정신의 사막을 달린다. - 짜라투스트라는 이렇게 말했다 중에서"

 

한가지. 재현적 사고는 다른 이를 자신의 잣대로 평가하게 합니다. 자신의 마음 속에 그려놓은 완성된 삶의 잣대로 자신을 포함한 개인 개인을 줄세우는 것이죠. 그후엔 자기보다 뒤에 있는 사람을 바라보며 우월감을 느낄 것이고 자신보다 앞서있는 사람을 보면 열등감을 느낄 것입니다. 마음 속에서 그려놓은 완성된 삶의 모습을 한번 지워볼까요? 줄세워진 사람들은 모두 뿔뿔히 흩어져 우주의 별과 같이 개인 개인으로서 아름답지 않습니까

 

지금 제겐 어울리지도 않는 말들을 너무 많이 했네요. 애교로 봐주셨으면 합니다. 이제 이상 나름대로의 감상을 말하는 것은 그만두고 좋은 책의 머릿말의 일부를 여기 옮깁니다.

 

 

. 머릿말_ 디렉현,

인어공주의 비극

어린 시절에 한때나마 인어공주의 비극에 눈물을 흘린 적이 있던가... 모르겠다. 가지 분명한 , 도무지 그녀의 사랑을 납득할 수가 없었다는 사실이다. 인어공주의 비극은 왕자가 그를 선택하지 않았다는 사실에서 비롯되지 않는다. 말이야 바른 말이지, 왕자가 잘못했나. 인어공주는 사랑의 힘을 삶의 힘으로 전환시키지 않았으며(인어공주가 능동적으로 선택할 있는 유일한 행위가 '죽거나 죽이거나'라니!), 자신의 사랑을 '행위' 책임지지 않았으며(언어를 스스로 포기해 놓고 대체 생면부지의 왕자와 어떻게 소통을 한단 말인가! 설마 미모로?), 자신이 만든 사랑의 상을 현실에 투사해 놓고는 망상 속에서 허우적거렸다(내가 인간이 되면 왕자님도 사랑하지 않을까? 맙소사!) 됐지만, 인어공주의 비극은 스스로가 자초한 거다. 그게 사랑의 본질이든 뭐든, 아무튼 허무한 비극에 도무지 공감할 없었다.

미야자키 하야오의 '벼랑위의 포뇨' 인어공주의 비극을 완벽하게 전복한다. '물고기' 포뇨는 아버지의 명령을 어기고 세상 밖으로 나온다. 포뇨의 인간-되기는 마법이나 사랑의 망상에서 비롯된 것이 아니라 세상 밖으로의 모험에서 시작된 . 파도에 휩쓸려 포뇨는 정신을 잃은 상태에서 소스케에게 발견된다.

소스케는 속에서 파닥거리는 포뇨를 구해 주고, 포뇨는 병을 깨다가 손을 베인 소스케의 상처를 햝아 그의 상처를 치유해 준다. 이보다 가슴 찡한 만남이 있을까. 서로에게 선물이 되는 만남. 함께 밥을 먹고 잠을 자고 수몰된 세계를 항해하고, 어두운