프로그래밍/C#/WPF2015. 6. 16. 17:56

1. 데이터 바인딩 배경

WPF에서 데이터 바인딩이란 XAML로 표현되는 UI 요소와 ViewModel로 표현되는 데이터 사이에 관계를 맺는 기술을 의미한다. 이를 통해 UI 부분과 데이터 부분을 서로 독립적으로 다룰 수 있으며, 이것은 곧 UI 디자이너와 개발자의 역할을 보다 분명하게 정의하고 협업을 효율적으로 할 수 있게 한다.

또한, 굳이 UI 디자이너와 개발자 사이의 협업 관계를 고려하지 않는다 해도 애플리케이션 로직(데이터)과 프레젠테이션(UI) 부분을 분명하게 분리한다는 것은 개발 생산성이나 추후 유지보수성을 고려해보았을 때 매우 바람직한 시도라 할 수 있다. 실제로 이러한 시도의 일환으로 과거부터 존재했던 수 많은 MVC 프레임워크를 일일이 논하지 않아도 많은 개발자들은 이미 애플리케이션 로직과 프레젠테이션 부분을 분리하는데 많은 관심과 노력을 기울이고 있다는 것을 알 수 있다.



2. 데이터 바인딩 구조

WPF 데이터 바인딩을 구성하는 요소는 크게 3가지로 생각해 볼 수 있다. UI 요소를 의미하는 바인딩 대상(보다 정확하게는 UI요소의 DependencyObject 속성), 데이터를 의미하는 바인딩 소스 그리고 이 둘 사이의 관계를 맺어주는 바인딩 개체가 바로 그것이다.

바인딩 개체가 제공하는 바인딩 방식은 4가지가 있다.


첫 번째, OneWay방식은 바인딩 소스에서 바인딩 대상 방향으로만 데이터 바인딩을 제공한다. 예를 들어 TextBox의 Text 속성(바인딩 대상)에 바인딩 된 string 객체(바인딩 소스)가 있다면, string 객체를 수정했을 때 수정 된 값이 TextBox에 반영된다. 그러나 반대로 TextBox의 Text속성이 변경되어도 여기에 바인딩 된 string 객체는 변경되지 않는 않는다.


두 번째, TwoWay는 바인딩 소스와 바인딩 대상 양방향 모두 데이터 바인딩을 제공한다. 즉, OneWay에서 TextBox의 Text 속성을 변경하면 여기에 바인딩 된 string 객체의 값도 함께 변경된다는 것을 의미한다. 대부분의 UI 요소는 기본값으로 OneWay 바인딩을 사용하지만 TextBox의 Text속성, CheckBox의 IsChecked 속성 등은 TwoWay 바인딩을 기본값으로 한다.


세 번째, OneWayToSoruce는 OneWay방식의 반대로 동작한다.

네 번째, 아래 그림에서는 표현되어 있지 않지만 OneTime 방식으로 최초 바인딩 소스 값이 바인딩 대상 속성 값을 초기화 하지만 그 이후는 어떤 변환도 바인딩 대상, 바인딩 소스 모두에 반영되지 않는 방식이다.


위 4가지 데이터 바인딩 방식에서 조금 더 관심을 가져야 하는 부분은 TwoWay 바인딩 방식이다.


TwoWay 바인딩은 바인딩 대상의 속성 값이 변경되면, 해당 변경 내용을 다시 바인딩 소스로 전파하는데 이 과정을 UpdateSourceTrigger라 한다. (OneWayToSource 바인딩도 UpdateSourceTrigger를 정의할 수 있다.)


UpdateSourceTrigger

설명

LostFocus

UI 요소가 포커스를 잃었을 때 바인딩 소스를 업데이트한다.

) TextBox가 포커스를 잃었을 때 TextBoxText 속성 값을 여기에 바인딩 된 string 객체로 전파한다.

PropertyChanged

UI요소의 바인딩 된 속성값이 변경될 때 바인딩 소스를 업데이트한다.

) TextBoxText 속성값이 변경되면 여기에 바인딩 된 string 객체로 전파한다.

Explicit

애플리케이션에서 명시적으로 UpdateSource를 호출할 때

) 사용자가 특정 버튼을 클릭했을 때 UpdateSoruce를 실행해 TextBoxText 속성값을 여기에 바인딩 된 string 객체로 전파한다.



3. 예제

먼저 데이터 바인딩을 사용하지 않을 경우 나타날 수 있는 애플리케이션 코드를 살펴보자.


아래  XAML은 사용자 로그인을 처리하기 위해 아이디와 비밀번호를 입력 받고, 로그인 처리를 실행하기 위한 1개의 버튼을 정의하고 있다. 2개의 TextBox와 1개의 버튼은 x:Name을 통해 고유 식별자를 정의하고 있는데 이것은 XAML의 Code Behind(XAML파일명에 .cs확장자를 더한 클래스 파일이다. 예를 들어 MainWindow.xaml은 MainWindow.xaml.cs라는 Code Behind 파일을 지니게 된다.)에서 해당 UI 요소를 접근하는데 사용된다.

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="300" Width="400">

    <StackPanel>
        <StackPanel.Resources>
            <Style TargetType="StackPanel">
                <Setter Property="Margin" Value="5"/>
            </Style>
            <Style TargetType="Label">
                <Setter Property="Width" Value="80"/>
                <Setter Property="VerticalAlignment" Value="Center"/>
            </Style>
            <Style TargetType="TextBox">
                <Setter Property="Width" Value="120"/>
            </Style>
            <Style TargetType="Button">
                <Setter Property="Width" Value="50"/>
                <Setter Property="Height" Value="25"/>
            </Style>
        </StackPanel.Resources>

        <StackPanel Orientation="Horizontal">
            <Label Content="아이디"/>
            <TextBox x:Name="ID"/>
        </StackPanel>

        <StackPanel Orientation="Horizontal">
            <Label Content="비밀번호"/>
            <TextBox x:Name="Passwd"/>
        </StackPanel>

        <StackPanel Orientation="Horizontal">
            <Button x:Name="LoginBtn" Content="로그인"/>
        </StackPanel>
    </StackPanel>

</Window>

아래 코드는 위 XAML의 Code Behind 내용 이다. x:Name으로 명명된 식별자를 통해 TextBox의 Text 속성의 값을 읽거나 설정할 수 있다. Button 역시 x:Name으로 명명된 식별자를 통해 Click 이벤트 등을 제어할 수 있다.


로그인 화면 구성이 비교적 간단하고, 입력 값 검증 규칙이 복잡하지 않기 때문에 Code Behind에 UI 요소와 데이터를 처리하는 로직이 뒤섞여 있어도 불편함이 크게 들어나지는 않는다. 그러나 화면을 구성하는 UI 요소의 개수가 증가하고, 입력 값 검증 로직이 복잡해짐에 따라 Code Behind의 복잡도도 크게 증가하게 될 것을 쉽게 예상할 수 있다.

using System.Windows;
using System.Windows.Input;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        private string LoginID { get; set; }
        private string LoginPasswd { get; set; }

        public MainWindow()
        {
            InitializeComponent();
            this.Loaded += OnLoaded;
            this.LoginBtn.Click += LoginButtonClick;
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            Keyboard.Focus(this.ID);
        }

        private void LoginButtonClick(object sender, RoutedEventArgs e)
        {
            LoginID = this.ID.Text;
            LoginPasswd = this.Passwd.Text;
            if (string.IsNullOrEmpty(LoginID))
            {
                MessageBox.Show("아이디를 입력해주세요.");
                Keyboard.Focus(this.ID);
                return;
            }
            if (string.IsNullOrEmpty(LoginPasswd))
            {
                MessageBox.Show("비밀번호를 입력해주세요.");
                Keyboard.Focus(this.Passwd);
                return;
            }
            doLogin();
        }

        private bool doLogin()
        {
            //-- 로그인 처리
            MessageBox.Show(string.Format("아이디={0}, 비밀번호={1}", LoginID, LoginPasswd));
            return true;
        }
    }
}

위 코드를 데이터 바인딩을 적용해 UI 부분과 데이터 부분을 유연하게 분리해 보자.


먼저 변경 된 XAML은 아래와 같다앞서 살펴본 XAML과는 크게 차이가 나지 않지만 TextBoxText 속성에 바인딩 구문이 사용되고 있음을 주목하자.

<Window x:Class="WpfApplication1.LoginWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="LoginWindow" Height="300" Width="400">

    <StackPanel>
        <StackPanel.Resources>
            <Style TargetType="StackPanel">
                <Setter Property="Margin" Value="10"/>
            </Style>
            <Style TargetType="Label">
                <Setter Property="Width" Value="80"/>
                <Setter Property="VerticalAlignment" Value="Center"/>
            </Style>
            <Style TargetType="TextBox">
                <Setter Property="Width" Value="120"/>
            </Style>
            <Style TargetType="Button">
                <Setter Property="Width" Value="50"/>
                <Setter Property="Height" Value="25"/>
            </Style>
        </StackPanel.Resources>

        <StackPanel Orientation="Horizontal">
            <Label Content="아이디"/>
            <TextBox x:Name="ID" Text="{Binding LoginID, UpdateSourceTrigger=PropertyChanged}"/>
        </StackPanel>

        <StackPanel Orientation="Horizontal">
            <Label Content="비밀번호"/>
            <TextBox x:Name="Passwd" Text="{Binding LoginPasswd, UpdateSourceTrigger=PropertyChanged}"/>
        </StackPanel>

        <StackPanel Orientation="Horizontal">
            <Button x:Name="LoginBtn" Content="로그인"/>
        </StackPanel>
    </StackPanel>

</Window>

Code Behinde의 내용은 다음과 같다. this.DataContext에 LoginViewModel을 설정하고 있는 부분이 앞서 살펴본 Code Behinde와 차이가 있음을 알 수 있다. DataContext는 XAML의 UI 요소의 바인딩 구문에 사용될 수 있는 데이터를 가리킨다. 예를 들어 x:Name이 “ID”인 TextBox의 Text는 LoginID라는 항목에 바인딩 되어 있는데 이것은 바로 DataContext로 지정한 LoginViewModel의 LoginID 프로퍼티를 가리키는 것이다. 이와 비슷하게 LoginPasswd는 LoginViewModel의 LoginPasswd 프로퍼티를 가리킨다. 앞에서 알아본 내용에 따르면 TextBox의 Text 속성은 기본값으로 TwoWay 바인딩을 사용하므로 LoginViewModel의 LoginID 값을 변경하면 변경 된 값이 TextBox의 Text속성에 반영이 되거나 또는 사용자가 TextBox에 직접 타이핑해 넣은 값이 LoginViewModel의 LoginID에 저장될 것을 기대할 수 있다.

using System.Windows;
using System.Windows.Input;
using WpfApplication1.ViewModels;

namespace WpfApplication1
{
    public partial class LoginWindow : Window
    {
        public LoginWindow()
        {
            InitializeComponent();

            this.Loaded += OnLoaded;
            this.LoginBtn.Click += LoginButtonClick;
            this.DataContext = new LoginViewModel();
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            Keyboard.Focus(this.ID);
        }

        private void LoginButtonClick(object sender, RoutedEventArgs e)
        {
            var viewModel = this.DataContext as LoginViewModel;
            if (string.IsNullOrEmpty(viewModel.LoginID))
            {
                MessageBox.Show("아이디를 입력해주세요.");
                Keyboard.Focus(this.ID);
                return;
            }
            if (string.IsNullOrEmpty(viewModel.LoginPasswd))
            {
                MessageBox.Show("비밀번호를 입력해주세요.");
                Keyboard.Focus(this.Passwd);
                return;
            }
            doLogin();
        }

        private bool doLogin()
        {
            //-- 로그인 처리
            var viewModel = this.DataContext as LoginViewModel;
            MessageBox.Show(string.Format("아이디={0}, 비밀번호={1}", viewModel.LoginID, viewModel.LoginPasswd));
            return true;
        }
    }
}

실제로 아이디와 비밀번호에 값을 입력하고 로그인 버튼을 클릭하면 아래 그림과 같은 결과를 확인할 수 있다. 즉, 바인딩 대상(UI 요소)의 변경이 바인딩 소스(ViewModel)로 올바르게 전파되고 있는 것을 확인한 것이다. 이러한 변경의 전파는 UpdateSourceTrigger 값으로 PropertyChanged를 사용하고 있기 때문에 TextBox의 Text 속성 값이 변경될 때마다 발생하게 된다.


그럼 반대로 바인딩 소스(ViewModel)의 변경이 바인딩 대상(UI 요소)으로 전파되는지도 확인해보자. 이를 위해 간단하게 로그인 버튼 옆에 자동입력 버튼을 하나 추가한다.


 <StackPanel Orientation="Horizontal">

   <Button x:Name="AutoBtn" Content="자동입력"/>

   <Button x:Name="LoginBtn" Content="로그인" Margin="8,0,0,0"/>

 </StackPanel>


그리고 Code Behinde에는 다음과 같은 자동입력 버튼을 클릭했을 때 실행 될 핸들러를 하나 추가한다.


 private void AutoButtonClick(object sender, RoutedEventArgs e)

 {

     var viewModel = this.DataContext as LoginViewModel;

     viewModel.LoginID = "myid2";

     viewModel.LoginPasswd = "mypassword2";

 }


애플리케이션을 다시 빌드하고 실행한다. 그리고 자동입력 버튼을 클릭한 다음 바로 로그인 버튼을 클릭해보자. 그러면 아래 그림과 같이 조금은 이상한 결과를 얻게 된다.



AutoButtonClick 핸들러를 통해 LoginViewModel의 LoginID와 LoginPasswd 프로퍼티의 값을 변경 했으나 변경의 전파가 UI 요소로 올바르게 이루어 지지 않았다는 것을 알 수 있다. (Alert 윈도우를 통해 LoginViewModel의 LoginID와 LoginPasswd 프로퍼티는 값이 올바르게 변경되었다는 것을 확인할 수 있다.)


사실 이것은 ViewModel을 구현하는데 있어서 INotifyPropertyChanged 인터페이스의 필요성을 설명하기 위해 의도한 결과이다. OneWay나 TwoWay 바인딩 방식에서 바인딩 소스(ViewModel)의 변화를 바인딩 대상(UI 요소)으로 올바르게 전파하기 위해서는 INotifyPropertyChanged 인터페이스 구현이 반드시 필요하다.

using System.ComponentModel;

namespace WpfApplication1.ViewModels
{
    public class LoginViewModel : INotifyPropertyChanged
    {
        private string _loginID;
        public string LoginID
        {
            get { return _loginID; }
            set
            {
                _loginID = value;
                OnPropertyUpdate("LoginID");
            }
        }

        private string _loginPasswd;
        public string LoginPasswd
        {
            get { return _loginPasswd; }
            set
            {
                _loginPasswd = value;
                OnPropertyUpdate("LoginPasswd");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyUpdate(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

INotifyPropertyChanged 인터페이스는 PropertyChangedEventHandler라는 이벤트 객체를 하나 포함하고 있는데, 이 이벤트 객체를 통해 프로퍼티 값이 변경되었다는 것을 UI 요소에 알리게 된다. 이제 다시 애플리케이션을 빌드하고 실행해보자. 그리고 자동입력 버튼을 클릭하면 아이디와 비밀번호 TextBox에 LoginViewModel을 통해 변경된 값이 반영되는 것을 확인할 수 있다.



참고

https://msdn.microsoft.com/ko-kr/library/ms752347(v=vs.110).aspx



'프로그래밍 > C#/WPF' 카테고리의 다른 글

WPF 데이터 바인딩의 기초 - 2  (0) 2015.06.21
WPF 데이터 바인딩의 기초 - 1  (2) 2015.06.16
Posted by devop

댓글을 달아 주세요

  1. 개발 관련 의뢰가 있습니다. 연락주세요. 010-2590 공팔칠육

    2015.07.06 18:05 신고 [ ADDR : EDIT/ DEL : REPLY ]
  2. WPF 어캐하는거에요??

    2019.05.23 01:59 신고 [ ADDR : EDIT/ DEL : REPLY ]

프로그래밍/기타2015. 6. 12. 15:45

나이브 베이지안 알고리즘에 대해 생각해 볼 일이 생겼다.

일단 대체 이게 어디에 쓰이는 알고리즘인지 알아보자.


구글 검색을 해보니 역시 조대협님의 블로그에서 잘 정리된 정보를 찾을 수 있었다. (역시 보물창고)

http://bcho.tistory.com/1010


한 줄 요약하면, 머신 런닝 분야에서 분류 알고리즘으로서 널리 쓰이고 있으며, 이 알고리즘을 통해 문서 분류기 같은 것을 만들 수 있다. 예를 들어 어떤 메일이 있을 때 이 메일이 스팸이냐 아니냐를 분류하거나, 어떤 뉴스 기사가 있을 때 해당 기사가 경제 기사냐, 스포츠 기사냐를 분류한다.


자세한 수학적 이론과 예제는 위에 조대협님 블로그에서 참고하기로 하고 여기에서는 나이브 베이지안 알고리즘을 적용한 문서 분류기 코드를 작성해 보자.


문제) 다음과 같이 5개의 학습 문서가 존재하고, 분류가 comedy(코메디 영화), action(액션 영화) 두개가 존재한다고 하자. 이제 어떤 문서에 fun, furious, fast 라는 3개의 단어만 있는 문서가 있을 때, 이 문서가 코메디인지 액션 영화인지 분리를 해보자. (문제 예시는 조대협님의 블로그 예시를 그대로 가져옴)


 영화 

 단어

 분류 

 1

 fun, couple, love, love

 Comedy

 2

 fast, furious, shoot

 Action

 3

 couple, fly, fast, fun, fun

 Comedy

 4

 furious, shoot, shoot, fun

 Action

 5

 fly, fast, shoot, love

 Action


자 일단 전체 코드 부터 보자.

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class NaiveBayesianClassification {

    private String[] dataSet;
    private Map<String, Long> classifies = new HashMap<>();
    private Map<String, Map<String, Long>> counter = new HashMap<>();

    public NaiveBayesianClassification(String[] dataSet) {
        if (dataSet == null || dataSet.length == 0)
            throw new IllegalArgumentException("Empty dataSet");
        this.dataSet = dataSet;
    }

    private String getClassify(String input) {
        int divide = input.indexOf('|');
        return divide > - 1 ? input.substring(0, divide) : null;
    }

    private String[] getWords(String input) {
        int divide = input.indexOf('|');
        return divide > -1 ? input.substring(divide+1).split(",") : null;
    }

    public void training() {
        Arrays.stream(dataSet).forEach(data -> {
            String classify = getClassify(data);
            String[] words = getWords(data);
            //-- 분류명과 분류명이 나타난 횟수를 classifies에 저장한다.
            if (classify != null) {
                Long count = classifies.get(classify);
                if (count == null)
                    count = 1L;
                else
                    count++;
                classifies.put(classify, count);
                //-- 각 분류명에 대해 특정 단어가 나타난 횟수를 couner에 저장한다.
                if (words != null) {
                    Arrays.stream(words).forEach(word -> {
                        Map<String, Long> wordCounter = counter.get(classify);
                        if (wordCounter == null) {
                            wordCounter = new HashMap<>();
                            counter.put(classify, wordCounter);
                        }
                        Long wordCount = wordCounter.get(word);
                        if (wordCount == null)
                            wordCount = 1L;
                        else
                            wordCount++;
                        wordCounter.put(word, wordCount);
                    });
                }
            }
        });
    }

    public String judgment(String[] words) {
        Map<String, Double> results = new HashMap<>();
        long classifiesTotalCount = classifies.values().stream().mapToLong(Long::longValue).sum();
        classifies.forEach((classify, count) -> {
            double[] points = Arrays.stream(words).mapToDouble(word -> {
                Map<String, Long> wordCounter = counter.get(classify);
                if (wordCounter == null)
                    return 0.0f;
                Long wordCount = wordCounter.get(word);
                if (wordCount == null)
                    return 0.0f;
                long wordTotalCount = wordCounter.values().stream().mapToLong(Long::longValue).sum();
                return (double)wordCount / wordTotalCount;
            }).toArray();
            double total = (double)classifies.get(classify) / classifiesTotalCount;
            total = Arrays.stream(points).reduce(total, (x, y) -> x * y);
            results.put(classify, total);
        });
        results.entrySet().forEach(entry ->
                System.out.println(String.format("%s : %f", entry.getKey(), entry.getValue())));
        return results.entrySet().stream().max(Map.Entry.comparingByValue(Double::compareTo)).get().getKey();
    }

    public static void main(String[] args) throws Exception {
        //-- 학습 데이터
        String[] dataSet = {
                "Comedy|fun,couple,love,love",
                "Action|fast,furious,shoot",
                "Comedy|couple,fly,fast,fun,fun",
                "Action|furious,shoot,shoot,fun",
                "Action|fly,fast,shoot,love"
        };
        //-- 테스트 데이터
        String[] words = {"fun", "furious", "fast"};

        NaiveBayesianClassification classifier = new NaiveBayesianClassification(dataSet);
        classifier.training();
        String classify = classifier.judgment(words);
        System.out.println(classify);
    }

}

trainning 메소드는 dataSet을 통해 문서 분류기를 학습 시키는 역할을 한다. 먼저 데이터로부터 분류와 단어들을 추출하고, classifies에는 분류명과 해당 분류명의 나타난 횟수를 기록한다. counter는 분류별로 특정 단어가 나타난 횟수를 기록하는데 사용한다.


judgment 메소드는 단어들이 주어졌을 때, 주어진 단어를 통해 해당 문서가 어떤 분류에 속할지 계산한다. 이를 위헤 classifies에 포함된 모든 분류들에 대해 확률값을 계산한 후 그 중 가장 큰 확률값을 지닌 분류를 선택한다.


위 분류기에 의한 결과값은 Action : 0.001803, Comedy : 0.000000 으로 "fun", "furious", "fast" 단어들을 포함하는 문서는 Action 영화으로 분류된다.

Posted by devop

댓글을 달아 주세요

기고문2014. 7. 5. 15:09

본 내용은 월간 마이크로소프트웨어 2014년 7월호에 기고된 내용입니다 :)


--


배달음식 주문 중계 서비스 '철가방'의 핵심 기능중 하나인  실시간 알림(PUSH)을 구현하기 위해 Zookeeper와 Vert.x를 적용한 사례에 대해 이야기합니다.



배달음식 주문 중계 서비스의 정의


배달음식 주문 중계 서비스란, 일상생활에서 여러분들이 많이 사용하고 있는 ‘배달의 XX’, ‘요X요’ 등의 스마트폰 어플리케이션을 떠올려 보면 쉽게 알 수 있습니다. 일반 소비자가 스마트 디바이스(스마트폰, 스마트TV 등) 또는 웹과 같은 인터넷 사용이 가능한 매체를 통해 음식 주문을 발생 시키면, 해당 정보를 배달음식 가맹점에게 전달하여, 음식 주문 거래가 성립케 하는 서비스입니다.


전통적으로, 일반 소비자가 배달음식 가맹점과 가장 쉽게 커뮤니케이션할 수 있는 방법은 전화를 이용하는 것 이였습니다. 하지만 전화를 통한 커뮤니케이션 방식에서 일반 소비자는 배달음식 가맹점 연락처를 미리 알고 있어야 한다는 불편함을, 배달음식 가맹점에는 일반 소비자에게 해당 가맹점을 홍보하기 위한 마케팅 비용의 부담이 발생하게 됩니다.


스마트 디바이스를 활용한 배달음식 주문 중계 서비스는 이처럼 전화를 기반으로 하는 서비스에서 일반 사용자와 배달음식 가맹점 양측 모두가 지니는 불편과 부담을 최소화하기 위한 대체 서비스입니다. 물론 최근 유명한 배달음식 주문 중계 스마트폰 어플리케이션을 운영하는 업체에서 주문 중계 수수료에 대한 논란이 있었던 만큼 시장 상황이 그렇게 녹녹치 만은 않고, 앞으로 해결해야할 과제가 아주 많은 분야이기도 합니다.



배달음식 주문 중계 서비스 구조


배달음식 주문 중계 서비스에 대한 포괄적인 이야기는 여기까지 하고, 주문 중계 서비스를 구축하기 위한 기술적 제반 사항에 대해 이야기 해보도록 하겠습니다.


배달음식 주문 중계 서비스를 구성하는 핵심 티어는 <화면 1>과 같이 일반 사용자와 배달음식 가맹점, 그리고 이 둘을 이어주는 주문 중계 서비스업체로 구분할 수 있습니다. 여기에 부가적인 기능을 수행하기 위해 통신사 및 결제대행업체(VAN)가 참여할 수 있습니다.


<화면 1. 주문 중계 서비스 티어>


각 티어의 고유 기능과 티어들 간의 연결을 위해 <리스트 1>과 같이 다양한 플랫폼과 기술이 개발되고 운영되고 있습니다. 모든 내용을 살펴보기에는 분량이 매우 많기 때문에, 핵심 티어인 일반 사용자와 배달음식 가맹점, 그리고 이 둘 사이를 실시간으로 연결하기 위한 방법에 대해 보다 자세히 알아보도록 하겠습니다.


 

 주문 중계 서비스 업체

배달음식 가맹점 

일반 사용자 

 솔루션

 Java 기반 

Spring, Vert.x 

서버 시스템

 Windows 응용 어플리케이션

iOS

Android

Web 

서버 연동방식 

RemoteObjectCall

REST API

연동방식 제공 

RemoteObjectCall 

REST API 

 실시간 알림

TCP PUSH

TTS

SMS/LMS

알림 방식 제공 

TCP PUSH

TTS

SMS/LMS 

APNS

GCM 

<리스트 1. 티어별 시스템 구성>


가장 먼저 일반 사용자가 관심 있어 하는 지역(현재 위치 또는 미리 등록된 지점 등)에 위치한 배달음식 가맹점 정보를 제공하고, 특정 가맹점으로 주문정보를 발생시키는 기능을 수행하는 어플리케이션이 필요합니다. 일반적으로 안드로이드/iOS 운영체제에서 동작하는 스마트폰 어플리케이션은 그 역할을 매우 훌륭하게 수행해 냅니다. 

물론 스마트폰 어플리케이션에 전국의 배달음식 가맹점 데이터 모두를 담을 수 없고, 또한 실시간으로 변동되는 배달음식 가맹점 상태 정보를 수집하고 제공하기 위해 주문 중계 서비스 업체는 스마트폰 어플리케이션과 연동될 수 있는 서버를 운영해야 합니다. 스마트폰 어플리케이션과 서버는 인터넷을 통해 데이터를 송/수신하는 할 수 있는데, RESTful하고 Stateless한 인터페이스(이하 REST API)를 통해 통신하는 것이 일반적인 선택사항 이라 할 수 있습니다.


일반 사용자는 이러한 스마트폰 어플리케이션을 통해 배달음식 가맹점 정보를 확인하고, 최종적으로 선택한 가맹점에 주문정보를 발생시킵니다. 주문정보는 REST API서버에서 최종적으로 확인되고 검증됩니다. 검증 정보에는 주문 금액, 결제방식 등이 포함되며, 경우에 따라 결제대행업체(VAN)와 연동될 수도 있습니다.


자 이제부터 핵심적인 문제가 시작됩니다.

일반 사용자로부터 발생한 주문정보는 현재 REST API서버에서 확인되었고, 주문정보를 해당 배달음식 가맹점에 전달해야 합니다. 주문정보를 배달음식 가맹점에 전달하는데 걸리는 시간은 실시간에 가까울수록 좋습니다. 주문 후 30분 넘도록 음식이 오지 않을 경우 여러분의 모습을 생각해보면 쉽게 그 이유를 알 수 있습니다.



방법 1. 전화 재 주문 방식


먼저 엔지니어 입장에서 가장 간단한 방법이지만, 서비스 운영의 측면에서는 비용이 가장 많이 드는 방법에 대해 이야기 해보겠습니다.


그것은 바로, 발생된 주문정보를 사람이 확인하고, 해당 배달음식 가맹점에 전화를 해서 주문을 대신해주는 방법입니다. 주문정보가 언제 발생할지 예측할 수 없기 때문에, 해당 업무를 수행하는 사람은 24시간 새로운 주문정보가 있는지 확인해야 합니다. 간혹 주문정보를 잘못 확인하고 엉뚱한 주문을 넣는 바람에 일반 사용자는 시키지도 않은 음식을 받아보거나, 배달 장소가 바뀌어 음식이 오지 않는 경험을 할지도 모릅니다. 


스마트폰에서 스마트하게 주문하는 것은 좋았지만, 이 후 과정은 전혀 스마트하지 않은 이 방법이 아이러니하게도 실제 현장에서는 가장 폭 넓게 사용되고 있기도 합니다. 24시간 주문정보를 모니터링하기 위한 직원을 수십 명(또는 수백 명)씩 배치하기 때문에 엄청난 인건비가 지출되기도 합니다. 근본적으로 전통의 전화를 기반으로 하는 커뮤니케이션 방법에서 크게 나아지지 않은 방법입니다.


하지만, 우리 같은 엔지니어는 별로 할 일이 없습니다. 스마트폰 어플리케이션과 이와 연동되는 REST API서버, 그리고 주문정보를 알려주는 윈도우 하나만 개발하면 모든 게 끝납니다.



방법 2. 배달음식 가맹점 프로그램 사용


사실 방법 1의 주문내용 전달 오류, 고비용이란 치명적인 단점에도 불구하고, 많은 서비스 업체에서 해당 방법을 고수하는 이유는, 전국의 모든 배달음식 가맹점에 실시간으로 주문정보를 전달할 수 있는 표준적인 방법도 없고, 그렇다고 전국의 모든 가맹점에 주문정보 알림 프로그램을 설치하기도 어렵기 때문입니다. 현실에는 주문정보 알림 프로그램의 개발부터 배포, 교육, 지속적 관리 등 해결해야할 문제가 산재해 있지만, 지면을 통해서는 ‘Zookeeper, Vert.x를 활용한 실시간 PUSH 알림 구현’이라는 주제에 맞게 주문정보 알림 프로그램 개발이라는 문제에만 집중해 보겠습니다.


가장 먼저 고민해야 할 것은 바로 주문정보 알림 프로그램 그 자체를 무엇으로 어떻게 만들 것인가? 하는 것입니다. PC환경에 독립적이게 웹 기반으로 만들 수도 있고, Windows 네이티브 어플리케이션으로 만들 수 도 있습니다. 반드시 고려해야할 점은 배달음식 가맹점 현장의 컴퓨팅 환경은 상당히 열악한 조건이 많으며, 사용자의 연령대를 고려해봤을 때 사용하기 어렵지 않아야 하고, 사용자에게 많은 조작을 요구하면 주문 받고 요리하는데 바쁜 가맹점 사장님들이 싫어할게 당연하기 때문에 웬만한 건 다 자동으로 처리 되어야 한다는 것입니다.


그래서 우리는 유행이 좀 지난 기술이긴 하지만 위와 같은 요구사항을 수용하기 위해 FLEX AIR를 기반으로 <화면 2>의 배달음식 가맹점 프로그램을 개발하였습니다. (사실 FLEX AIR Windows 어플리케이션은 지금도 충분히 좋은 대안이라 생각합니다.)



<화면 2. 철가방 가맹점 관리 프로그램>


가맹점 관리 프로그램의 주요 기능은 2가지로 요약할 수 있는데, 첫 번째 기능은 스마트폰 어플리케이션처럼 서버와의 연동을 통해 각종 정보 처리가 가능해야 하는 것이고, 두 번째 기능은 실시간 주문정보 알림을 받을 수 있어야 하는 것입니다.



Spring BlazeDS Integration


스마트폰 어플리케이션이 서버와 REST API통해 연결될 수 있는 것처럼, FLEX AIR 어플리케이션 또한 서버와 REST API를 통해 연결될 수 있습니다. 그러나 여기에는 좀 더 나은 대안이 존재하는데 그것은 바로 Adobe에서 제공하는 BlazeDS란 오픈소스 솔루션을 통해 AMF3(Action Message Format 3) 기반의 RemoteObjectService를 사용하는 것입니다. RemoteObjectService에 대해 간단히 설명하자면, FLEX 어플리케이션에서 서버에 정의된 JAVA Method를 Call할 수 있도록 하는 일종의 RPC(Remote Procedure Call) 서비스입니다. AMF3은 현재 폭넓게 사용되고 있는 JSON이나 XML보다 작은 메모리를 사용하고 훨씬 빠른 객체 직렬화 속도를 보여줍니다. 또한, Spring Source에서 Spring BlaseDS Integration을 제공하기 때문에 기존 Spring 기반 어플리케이션에 쉽게 통합할 수 있기도 합니다.



<화면 3. Spring BlazeDS Integration>

출처 : http://10panther01.blogspot.kr


한 가지 주의 사항은, Google App Engine처럼 Load Balancing 되는 환경에서 BlazeDS를 운영하다 보면 중복되는 Flex 세션이 있다는 오류를 만나게 되는데, 해당 오류를 발생시키는 flex-messaging-core.jarlex.messaging.endpoints.BaseHTTPEndpoint를 패치하고 다시 컴파일하면 문제를 해결할 수 있습니다.

 


PUSH서버


BlazeDS의 RemoteObjectService를 통해 첫 번째 요구사항을 충족시켰다면, 두 번째 요구 조건인 실시간 주문정보 알림을 구현하기 위해 PUSH서버를 개발하게 됩니다. PUSH서버와 가맹점 관리 프로그램은 TCP를 통해 연결됩니다. 주문정보가 발생할 경우 PUSH서버에서는 해당 가맹점의 관리 프로그램 연결을 확인하고, 주문정보를 전송하게 됩니다. 여기까지만 본다면 PUSH서버는 별로 어렵지 않게 구현할 수 있을 것 같습니다.



<화면 4. 시스템 구성도>


그러나 <화면 4>와 같은 시스템 구성을 보면 파악할 수 있듯이 위와 같이 간단한 구조에서는 PUSH서버가 SPOF(Single Point Of Failure)이며, PUSH서버와 연결되는 가맹점 프로그램 개수가 적게는 수백 개에서 많게는 수천/수만 개 까지 확장될 수 있다는 점을 감안해보면 결코 가볍게 넘어갈 수 있는 문제가 아님은 자명해 보입니다.


그래서 PUSH서버의 다중화와 부하 분산, 장애극복을 실현하기 위한 시스템 구성을 다시 생각해 보았습니다.



<화면 5. 개선된 시스템 구성도>


이전 시스템 구성과의 차이점이라면, PUSH서버가 다중화 됨에 따라 로드밸런서(Vert.x 기반)가 추가되었고, PUSH서버의 온라인 상태를 체크하기 위해 Zookeeper가 사용된 것입니다. 또한, PUSH서버와 가맹점 관리 프로그램은 간단한 상태체크 알고리즘을 통해 TCP연결 상태를 확인하고, 연결에 문제가 있을시 가맹점 관리 프로그램은 장애극복 모드로 전환되어 PUSH서버로의 TCP연결을 복구합니다.



Zookeeper


먼저 Zookeeper에 대한 간략한 소개를 하자면, ‘분산 작업을 제어하기 위한 트리 형태의 데이터 저장소’라고 할 수 있습니다. Zookeeper에서 관리 되는 트리의 특정 노드에 데이터를 저장하고 변경할 수 있는데, 특정 노드에 감시자(Watcher)를 등록하면 Callback을 통해 클라이언트에게 노드 변경 여부를 알려줍니다. 노드에는 한 번 저장한 데이터가 영구적으로 유지되는 영구 노드(Permanent Node) 외에도 클라이언트 세션이 유효한 동안만 살아있는 임시 노드(Ephemeral Node)와 저장하는 순서에 따라 자동으로 일련번호가 붙는 순차 노드(Sequence Node)가 있습니다. Zookeeper의 기능은 사실상 이게 전부인데, 여기서 우리가 눈여겨봐야 하는 것은 바로 임시 노드(Ephemeral Node)입니다. 


PUSH서버는 Zookeeper와의 세션을 생성하고, 해당 세션이 유효한 동안에만 유지되는 임시 노드를 등록함으로 PUSH서버의 온라인/오프라인 상태를 Zookeeper를 통해 즉각적으로 파악할 수 있도록 합니다. 즉, 새로운 PUSH서버가 추가되면 Zookeeper에 신규 임시 노드가 등록될 것이고, 기존 PUSH서버에 장애가 생겨 세션이 끊어지면 해당 임시 노드는 Zookeeper에서 제거 될 것입니다. 



<화면 6. Zookeeper 임시노드와 PUSH서버>


가맹점 관리 프로그램은 이제 직접적으로 PUSH서버와 연결되지 않고, 로드밸런서를 통해 현재 온라인 상태의 PUSH서버 중 한 개를 배정받아 커넥션을 연결하게 됩니다. 로드밸런서는 알고리즘에 따라 적당한 PUSH서버를 선택함으로서 PUSH서버의 룩업 기능뿐 아니라 부하 분산 장치로서의 역할을 하게 됩니다.


가맹점 관리 프로그램과 PUSH서버는 일정 주기로 Ping을 주고받는 Healthcheck 기능을 지니고 있는데, Healthcheck 알고리즘을 통해 상태 이상이 감지되면 가맹점 관리 프로그램은 PUSH서버와의 연결을 즉각 종료하고, 처음의 과정으로 돌아가 로드밸런서를 통해 새로운 PUSH서버를 룩업하고 연결을 시도함으로서 장애극복을 실현할 수 있습니다.


PUSH서버로의 트래픽 분산 제어를 위해 Zookeeper가 사용되는 만큼, Zookeeper자체가

중단되면 PUSH서버로의 신규 연결 요청이 모두 마비가 됩니다. 따라서 Zookeeper자체도 최대한 정상 동작을 보장해야 하는데, 이를 위해 여러 대의 Zookeeper 서버를 클러스터로 구성해 고가용성을 확보해야 합니다. 이것을 Zookeeper 앙상블(Ensemble)이라 합니다.


앙상블로 묶인 Zookeeper 인스턴스 중 한 대는 쓰기 명령을 총괄하는 리더 역할을 수행하고, 나머지는 팔로어 역할을 수행하는데, 클라이언트가 전달한 읽기 명령은 현재 연결된 Zookeeper 인스턴스에서 바로 반환되지만, 이에 비해 쓰기 명령은 앙상블 중 리더 역할을 수행하는 Zookeeper 인스턴스로 전달되며, 리더 Zookeeper는 모든 팔로어 Zookeeper에게 해당 쓰기를 수행할 수 있는지 질의하게 됩니다. 만약 팔로어 중 과반수(> n/2)의 팔로어로부터 쓸 수 있다는 응답을 받으면 리더는 팔로어에게 데이터를 쓰도록 지시합니다. 즉, 앙상블의 구성하는 Zookeeper 인스턴스 중 과반수가 살아있다면 데이터 읽기/쓰기를 정상적으로 처리할 수 있습니다. (3대를 사용한다면 1대가 중단되어도 문제가 없습니다.)



Vert.x 기반 로드밸런서


가맹점 관리 프로그램과 PUSH서버, 그리고 Zookeeper까지 완료되었다면 최종적으로 로드밸런서만 준비되면 모든 것이 다 갖춰지게 됩니다. 로드밸런서는 앞서 설명한 대로 PUSH서버의 룩업 기능과 부하 분산 역할을 하게 됩니다. HTTP, TCP, WebSocket 등 다양한 프로토콜을 통해 로드밸런서에 접근할 수 있어야 하며, Zookeeper와 마찬가지로 최대한의 동작을 보장하기 위한 클러스터 구성이 가능해야 합니다. 그리고 이에 대한 해답으로 Vert.x를 선택하게 됩니다.


Vert.x는 요즘 핫한 Node.js처럼 비동기 이벤트 방식의 프로그래밍 모델을 제공하는 플랫폼으로, Javascript뿐만 아니라 Java, Groovy 등 다양한 언어를 지원하고, Node.js보다 효율적으로 멀티코어 시스템을 활용할 수 있다는 장점이 있습니다.



<화면 7. Vert.x 기본 구조>

출처: http://www.javaworld.com/article/2078838/mobile-java/open-source-java-projects-vert-x.html


Vert.x의 기본구조에 대해 설명하자면, 하나의 Vert.x 인스턴스는 다 수의 Verticle을 포함할 수 있으며, Vert.x 인스턴스는 동일한 JVM 머신 또는 네트워크상의 JVM 머신들과 클러스터로 구성될 수 있습니다. 클러스터로 묶인 Vert.x 인스턴스 내의 Verticle들은 분산 이벤트 버스를 통해 메시지를 주고받을 수 있습니다. (Vert.x의 이벤트 버스는 HazelCast 라는 In Memory Data Grid 오픈소스 솔루션을 사용합니다.)


Verticle은 Vert.x에서 하나의 실행 단위(또는 배포단위)로 생각할 수 있는데, 쉽게 말하자면 Main 메소드를 포함하는 하나의 Java 클래스라 할 수 있습니다. 즉, 실제적인 어플리케이션 코드를 담고 있는 것이 바로 Verticle이며, 서로 상호작용 하는 1개 이상의 Verticle들의 조합으로 Vert.x 어플리케이션을 만들 수 있습니다. 각각의 Verticle들은 서로 독립적인 클래스로더를 사용하기 때문에, Verticle 내의 선언된 지역번수는 물론 전역변수, 정적변수 까지 독립적인 상태를 유지하며, Thread 경합 상태를 발생시키지 않습니다. (Single Thread 기반이라 생각하고 코드를 작성해도 됩니다.)


Vert.x 어플리케이션을 개발할 때 주의할 점은, 표준 Verticle내의 코드들은 절대 Thread block을 유발해서는 안 된다는 것입니다. JDBC코드와 같이 Thread block 코드를 어쩔 수 없이 사용해야 하는 상황이 있다면, 표준 Verticle이 아닌, Worker Verticle을 사용해야 합니다. Worker Verticle은 표준 Verticle과 다르게, Event Loop Thread를 점유하지 않으며, Vert.x 인스턴스내의 별도의 Worker Thread Pool에서 동작하게 됩니다. 


이상 Vert.x의 특징을 빠르게 살펴보았는데, 보다 자세한 정보는 Vert.x의 공식 홈페이지에서 제공하는 매뉴얼을 통해 파악하실 수 있습니다. Vert.x를 프로젝트에 도입할 예정인 분들은 반드시 해당 자료를 참조하시기 바랍니다.


그럼 구체적으로 Vert.x를 사용해 개발한 로드밸런서의 구조를 살펴보겠습니다. 로드밴런서는 <화면 8>에서와 같이 5개의 Verticle로 구성되있으며, 이중 1개는 앞서 설명한 Woker Verticle로서 Zookeeper 서버와 통신을 담당하며, 3개의 Verticle은 각각 HTTP, TCP, WebSocket을 통해 클라이언트의 요청을 수신하는 역할을 담당하고 있습니다. 마지막 1개의 Verticle은 실질적은 어플리케이션 로직을 담고 있지는 않지만, 나머지 4개의 Verticle들에 대한 설정정보와 이들을 배포(deploy)시키는 역할을 담당합니다.


<화면 8. 로그 밸런서의 Verticle구성>

 

3개의 Verticle에서 클라이언트의 요청을 수신하면, 이벤트 버스를 통해 Worker Verticle에 해당 내용을 전달하게 되고, Worker Verticle은 해당 내용을 전달받아 처리하고, 그 결과는 다시 최초 요청을 전달한 Verticle에 전달하게 되는 매우 간단한 구조입니다.


<화면 9. 클러스터 구성>


클러스터 기능을 사용하면 <화면 9>와 비슷한 형태로 각 서버에 로드밸런서가 배포될 것입니다. 주목할 점은 로드밸런서를 구성하는 Verticle의 개수를 자유롭게 변경할 수 있다는 것입니다. 일반적으로 Worker Verticle에 부하가 몰리기 때문에 Worker Verticle의 개수를 4개로 늘려놓은 모습니다.



Vert.x 예제 코드


간단하게 로드밸런서를 구성하는 Verticle의 소스코드를 보며, Vert.x 개발 스타일에 대해 알아보겠습니다. 


아래 코드는 SocketAcceptorVerticle의 일부분입니다. 일부분이라고는 하지만 사실상 이 코드가 거의 전부이기도 합니다. 클라이언트가 새롭게 연결 되었을 때 호출되는 connectionHandler를 시작으로 Callback방식을 통해 각 이벤트를 처리하는 비동기 이벤트 방식 프로그래밍 모델임에 주목합니다. 


클라이언트로부터 데이터를 수신할 때 호출되는 dataHandler가 가장 중요한 부분입니다. 수신 된 데이터를 이벤트 버스를 통해 ZKClientWorkerVerticle로 전달하고, 리턴 값을 받으면 클라이언트로 최종 응답합니다. 예제에서는 고정 3바이트를 기준으로 TCP스트림을 나누는 방식을 사용하고 있는데, 가변길이 TCP스트림을 처리할 때는 TCP스트림을 정확한 단위로 나누기 위해 첫 4바이트는 뒤따라오는 페이로드의 길이를 알기 위해 사용되고, 해당 길이만큼 TCP스트림을 읽어 들이는 방법을 사용합니다.

//-- Net Server 설정
server = vertx.createNetServer();
server.connectHandler(new Handler<netsocket>() {
	@Override
	public void handle(final NetSocket sock) {
		logger.info("sock.writeHandlerID["+sock.writeHandlerID()+"] remote host connected: "+sock.remoteAddress());
				
		//-- close handler
		sock.closeHandler(new VoidHandler() {
			@Override
			protected void handle() {
				logger.info("sock.writeHandlerID["+sock.writeHandlerID()+"] remote host disconnected: "+sock.remoteAddress());
			}
		});
		//-- data handler
		sock.dataHandler(RecordParser.newFixed(3, new Handler<buffer>() {
			@Override
			public void handle(final Buffer data) {
				logger.debug("The total body received was "+data.length()+" bytes.");
				logger.info("Lookup request received: "+data.toString());
						
				eb.send(address, data.toString(), new Handler<message<string>>() {
					@Override
					public void handle(Message<string> reply) {
						sock.write(reply.body());
					}
				});
			}
		}));
		//-- exception handler
		sock.exceptionHandler(new Handler<throwable>() {
			@Override
			public void handle(Throwable throwable) {
				if( throwable instanceof IOException ) {
					/* 현재 연결은 원격 호스트에 의해 강제로 끊겼습니다(아마도?) */
				}
				else {
					logger.error("sock.writeHandlerID["+sock.writeHandlerID()+"] Unexpected exception occur: ", throwable);
				}
			}
		});
	}
});

ZKClientWorkerVerticle은 더 심플한데, Zookeeper 앙상블에 연결을 설정하고, 앞서 SocketAcceptorVerticle에서 이벤트 버스로 전송한 데이터를 파싱해 요청을 해석하고 그 결과 값을 다시 처음 해당 데이터를 전송한 SocketAcceptorVerticle로 응답하는 것이 전부입니다.

@Override
public void start(final Future<Void> startedResult) {
	super.start();
	
	address 	= StringUtils.defaultIfEmpty(config.getString("address"), DEFAULT_ZKWCLT_ADDRESS);
	zkhostPort 	= StringUtils.defaultIfEmpty(config.getString("zkhostPort"), DEFAULT_ZK_HOSTPORT);
	zkSessionTimeout = (Integer) ObjectUtils.defaultIfNull(config.getInteger("zkSessionTimeout"), DEFAULT_ZK_SESSION_TIMEOUT);
		
	try {
		zk = new ZooKeeper(zkhostPort, zkSessionTimeout, this);
	} catch (IOException e) {
		zk = null;
		logger.error(e, e);
	}
		
	eb.registerHandler(address, new Handler<Message<String>>() {
		@Override
		public void handle(final Message<String> data) {
			String result = createReponse(data.body());
			data.reply(result);
		}
	},
	new AsyncResultHandler<Void>() {
		@Override
		public void handle(AsyncResult<Void> result) {
			if( !result.succeeded() ) {
				result.cause().printStackTrace();
				startedResult.setFailure(result.cause());
			}
			else {
				startedResult.setResult(null);
			}
			logger.info("Zookeeper client worker "+(result.succeeded()?"OK":"FAILED"));
		}
	});
}

마지막으로 StarterVerticle인데 모듈을 구성하는 각 Verticle을 배포하는 역할을 하고 있습니다. Zookeeper로 연결되어 동기연산을 처리하는 ZKClientWokerVerticle은 Vert.x인스턴스의 Event Loop Thread에서 실행되면 안 되기 때문에 WokerVerticle로 실행하는 것에 주목합니다. HTTPAcceptorVerticle과 WSocketAcceptorVerticle은 따로 설명은 하지 않았지만, 앞서 SocketAcceptorVerticle의 흐름과 크게 다르지 않습니다.

package com.hellowd.s3.lookup;

import org.vertx.java.core.json.JsonObject;
import org.vertx.java.core.logging.Logger;
import org.vertx.java.platform.Verticle;

public class StarterVerticle extends Verticle {
	
	private Logger 		logger;
	private JsonObject 	config;
	
	@Override
	public void start() {
		logger = container.logger();
		config = container.config();
	   
	    logger.info("app config: "+config.toString());
	    
	    container.deployVerticle("com.hellowd.s3.lookup.acceptor.HTTPAcceptorVerticle", 
	    		config.getObject("httpAcceptor"));
	    container.deployVerticle("com.hellowd.s3.lookup.acceptor.SocketAccreptorVerticle", 
	    		config.getObject("sockAcceptor"));
	    container.deployVerticle("com.hellowd.s3.lookup.acceptor.WSocketAcceptorVerticle", 
	    		config.getObject("wsAcceptor"));
	    container.deployWorkerVerticle("com.hellowd.s3.lookup.worker.ZKClientWorkerVerticle", 
	    		config.getObject("zkClientWorker"), 4);
	}

}

마치며

이것으로 ‘철가방’ 시스템을 구성하는 주요 부분에 대해 알아보았습니다. 제한된 지면을 통해 많은 내용을 전달하려다 보니, 설명이 부족한 부분도 많은 것이라 생각합니다. 부하분산, 장애극복과 같은 주제는 실제 서비스를 운영하며 사고가 터지면 욕도 먹어보고, 새벽에 불려나가는 경험을 하면서 내가 먼저 인간답게 살아보고자 하는 마음에 연구에 매진하게 되는 분야인 것 같다는 생각을 했습니다. 이 내용이 비슷한 고민을 하고 있는 많은 분들께 작게나마 도움이 되기를 바랍니다.



* 참고문헌 및 자료

BlazeDS

1. http://livedocs.adobe.com/blazeds/1/blazeds_devguide

2. http://sourceforge.net/adobe/blazeds/wiki/Home

3. http://www.jamesward.com/2007/04/30/ajax-and-flex-data-loading-benchmarks

BlazeDS-Spring

1. https://github.com/spring-projects/spring-flex 

Zookeeper

1. http://zookeeper.apache.org

2. http://helloworld.naver.com/helloworld/583580

Vert.x

1. http://vertx.io

2. http://helloworld.naver.com/helloworld/163784

3. http://bcho.tistory.com/860

Posted by devop

댓글을 달아 주세요