금요일, 8월 9

최근 웹 개발환경 이해 - 진보된 웹 환경 공부 #3 MVC on Client-side & advanced



개요

 지난 글 진보된 웹 환경 공부 #2 에서는 REST를 쉽게 구현해 줄 수 있는 RESTlet Framework를 사용해 HelloWorld 서비스를 구현해 봤습니다. REST를 통해 서버를 구현하면서 가장 많이 바뀐 면은 Client-side 입니다.

 지난번에 보았듯이, REST를 구현한 서버는 URI를 통해 호출된 서버자원에 매칭되는 작업을 할 시에 ( 비지니스 로직, 디비, 이미지나 html에 대한 리소스 전송 ) 이를 클라이언트에 전송해주게 되는데, 결과를 JSP나 PHP등의 서버사이트스크립트 페이지를 통해 데이터가 넣어진 형태로 전송해주지 않고, JSON과 같은 데이터 형으로 보내주게 됩니다. 

 이와 같은 구현은, 전에 말씀드렸듯이 서버를 개발하는 사람이 stand-alone하게 개발할 수 있는 여건을 마련해줍니다. 내가 서버를 구현하는데 반환해줄 데이터를 View에 어떻게 넣어줄지 등에 대한 고민을 할 필요가 전혀 없어졌기 때문입니다. 하지만 동시에 Interface 설계가 중요해졌습니다. 

 클라이언트 측면에서는 전송받은 데이터를 View에 넣어줄 필요가 생겼기 때문에 javascript를 통해 서버로부터 데이터를 받아오고 받아온 데이터를 View에 넣어주는 라이브러리들이 등장하게 됩니다.

 이번 글에서는, REST가 구현된 RESTful 서버 구현의 마지막 단계인 클라이언트 측면에서 서버의 자원 호출을 통해 받은 데이터(JSON)을 통해 View를 구성 방법을 다루고자 합니다. 

또한, Javascript 라이브러리인 Backbone.js(MVC패턴), knockout.js(MVVM패턴) 두 가지를 동시에 다뤄서 MVC 패턴과 MVVM 의 차이점 (MVVM이 MVC 보다 뭐가 나은가?)에 대해서도 다루고자 합니다. 


본문

먼저 지난 번의 Helloworld 예제를 조금 수정해서 그럴듯한 사용자 정보가 나오는 REST 서비스를 만듭니다.

public class HelloWorldRESTResource extends ServerResource {
@Get("json")
    public 
    Representation HelloWorld(Representation entity) 
    {
JsonRepresentation response = null;
        try
        {
        LinkedHashMap<String, Object> list = new LinkedHashMap<String, Object>();
            list.put("user_name", "Hanbum Bak");
            list.put("email", "HanbumBak@whatyougonnado.com");
            response = new JsonRepresentation(list);
        } catch(Exception ex) {
        
        }
return response;
    }

}

이제 아래 주소로 접속하면 
http://localhost:8182/helloworld

요런 JSON이 날라옵니다 .
{"user_name":"Hanbum Bak","email":"HanbumBak@whatyougonnado.com"}


OK! 이제 JSON을 받아와 View를 만들어 볼 차례입니다.
연장자 우대해서 MVC 패턴을 사용한 Backbone을 먼저 사용해볼까요?

아! 먼저 RESTlet에서 일반 파일들을 서빙하도록 서비스를 등록해야 하는데, 이 글에서 다룰 범주는 아니므로 간단히 소스만 공개하고 넘어가겠습니다.


========== 파일서빙용 서버리소스 ============
public class StaticPathWebRoutable implements IRestletRoutable {
    
    @Override
    public Router getRestlet(Context context) {
    final String WEB_ROOT = 
"file:///"+
System.getProperty("user.dir") + 
System.getProperty("file.separator") +
"resources" +
System.getProperty("file.separator") +
"web";
    
        Router router = new Router(context);
        router.attach("", new Directory(context, WEB_ROOT));
        context.setClientDispatcher(new Client(context, Protocol.FILE));
        return router;
    }

    /**
     * Set the base path for the Topology
     */
    @Override
    public String basePath() {
        return "/";
    }

}

=========== 서비스 등록용 인터페이스 ==============

public class StaticWebRoutable implements IRestletRoutable {
    
/*
* classloader path = $(ProjectHome)/bin
*/
    @Override
    public Router getRestlet(Context context) {
        Router router = new Router(context);
        router.attach("", new Directory(context, "clap://classloader/web/"));
        context.setClientDispatcher(new Client(context, Protocol.CLAP));
        return router;
    }

    /**
     * Set the base path for the Topology
     */
    @Override
    public String basePath() {
        return "/";
    }
}

===== Router에 서비스 등록 =============
public class RootRouter extends Application {
protected List<IRestletRoutable> restlets;
@Override
    public Restlet createInboundRoot() {
Context context = getContext();
        Router baseRouter = new Router(context);
        baseRouter.setDefaultMatchingMode(Template.MODE_STARTS_WITH);
        
        /* TODO 
         * 나중에 모듈 로드 방식으로 고도화, 지금은 일단 하드코딩 
         */
        
        restlets = new ArrayList<IRestletRoutable>();
        restlets.add(new HelloWorldWebRoutable());
        restlets.add(new StaticPathWebRoutable());
        
        // attach 
        for (IRestletRoutable rr : restlets) {
            baseRouter.attach(rr.basePath(), rr.getRestlet(context));
        }
        
        return baseRouter;
    }
}


=== 그리고 HelloWorld 용 Routable도 추가합니다. =============

package com.swmaestro.sdn.HelloWorld;

import org.restlet.Context;
import org.restlet.routing.Router;

import com.swmaestro.sdn.core.IRestletRoutable;


public class HelloWorldWebRoutable implements IRestletRoutable {

@Override
    public Router getRestlet(Context context) {
        Router router = new Router(context);
        router.attach("/helloworld", HelloWorldRESTResource.class);
        return router;
    }

    /**
     * Set the base path for the Topology
     */
    @Override
    public String basePath() {
        return "/sm";
    }
}


여기까지 됐으면 본격적으로 Client 측면에서 아래의 목표 출력물을
Backbone과 Knockout 으로 각기 구현해보겠습니다.

목표 출력물
user-name : Hanbum Bak
email : HanbumBak@whatyougonnado.com


=== Backbone ================================

<필요한 라이브러리>
"https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"
"https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore-min.js"
"https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.0.0/backbone-min.js"
</필요한 라이브러리>

HelloBackboneModel = Backbone.Model.extend({
url:"/sm/helloworld",
default: {
user_name: 'test',
email: 'test'
}
});

HelloBackboneView = Backbone.View.extend({
initialize: function(){
this.model.fetch({
success: function(data) {
HelloBackboneView.render();
}
});
},
render: function(){
var temp = "user-name : " + this.model.get("user_name");
temp += "<br>email : " + this.model.get("email");
        this.$el.html(temp);
    }
});

var HelloBackboneModel = new HelloBackboneModel();  
var HelloBackboneView = 

new HelloBackboneView({ model:HelloBackboneModel, el: $("#HelloBackbone") });


=== Knockout ================================

<필요한 라이브러리>
"http://cdnjs.cloudflare.com/ajax/libs/knockout/2.3.0/knockout-min.js"
"https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"
</필요한 라이브러리>


<p>userName: <strong data-bind="text: userName"></strong></p>
<p>email: <strong data-bind="text: email"></strong></p>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/knockout/2.3.0/knockout-min.js"></script>

<script>
// This is a simple *viewmodel* - JavaScript that defines the data and behavior of your UI
function AppViewModel() {
var self = this;
this.userName = ko.observable();
    this.email = ko.observable();
    
    this.update = function() { 
    $.getJSON("/sm/helloworld", function(data) {
        self.userName(data.user_name);
        self.email(data.email);
    });
    };    
}

// Activates knockout.js
var temp = new AppViewModel();
ko.applyBindings(temp);
temp.update();
</script>


 MVC를 구현한 Backbone과 MVVM을 구현한 Knockout의 가장 큰 차이점은 눈에 보이다싶이 Data를 View에 Binding하는 형태입니다. Backbone 예제에서는 직접 html 을 입력해줬는데 실제로 Backbone에서 제공하는 Template기능을 사용하더라도 소스 코드만 다를뿐 기본적인 동작원리는 똑같이 html을 입력해주는 형태입니다.

 눈에 보이지 않는 차이점은 Observable로 표현되는 Observer에 따른 View의 갱신 방법인데... Backbone예제에는 이를 넣지 않았는데... 이유는 이 소스를 보면 기분이 아주 안좋아지기 때문입니다. (-_-; )

 MVC 에서는 View가 정말 아무 기능이 없습니다.
 그냥 model 을 사용자에게 보여주기 위한 수단일 뿐이죠.
 하지만 대부분의 언어(??)가 View를 통해 Event를 전달받을 수 있었습니다.
 제가 MVC를 이해한 순간부터 사람들이 왜 이 패턴을 사용하는지 의아했던 이유입니다.

 우리는 View와의 상호작용을 통해 Data를 편집합니다.
 만약 우리가 View의 Data를 수정했다면 처리 로직은 어떻게 되는 걸까요?

 MVC 에서는
 Controller가 입력장치의 Data를 받아 -> Model을 갱신하고 -> View에 반영

 MVVM 에서는
 View가 Data를 갱신하고 -> 데이터로 Model을 갱신할지를 프로그래밍.


 MVC패턴은 View에서 Event를 처리할 수 있게 된 이후부터는 비효율적인 패턴이며, View의 역할은 단순히 Model을 사용자에게 보여주는 것에서 사용자와의 상호작용을 처리하고 이를 통해 모델을 갱신할지 여부를 결정할 수 있는 선까지 확장되었습니다. 그리고 MVVM은 이와같이 바뀐 패러다임을 잘 적용했을 뿐 아니라, 이를 통해 View와 Model의 의존성을 보다 느슨하게 만든 GUI 구현 패턴입니다 


  

마치며

MVC is nothing. or everything.

http://tomyrhymond.wordpress.com/2011/09/16/mvc-mvp-and-mvvm/



MVC is nothing.
MVC 패턴은 예전에 제가 다룬 글에서 이야기했듯, 1970년대의 OS에서나 사용했던 GUI 구현 패턴이죠. 아무것도 아닙니다. 구시대의 잔재일 뿐이죠.

or everything.
하지만 동시에 MVC는 model 2로 발전하면서 Struts와 같은 Framework를 통해 개발자들에게 웹의 시대를 열어줬습니다. 발전된 MVC model 2 패턴은 Client와 Server 개발을 분리하고 Html 등을 담당하는 퍼블리셔 혹은 디자이너와 개발자에게 훌륭한 개발 환경을 제공해줬습니다.


 객체지향에서 100번을 강조해도 지나침이 없는 중요한 키워드는 바로 De-Coupling입니다. 이것은 쉽게 이야기해서, Server-Client 와 같이 긴밀하게 연결될 수 밖에 없다고 생각되는 경우에도 변경과 수정이 상호 간에 미치는 영향을 최소화 하는 것 입니다.

 이를 지켜야 하는 이유는 OOP에서 말하는 "단일 책임의 법칙" (SRP : Single Responsibility Principle)을 준수함을 통해 OOP의 장점인 "개발의 병렬화" 그리고 "재활용성" 을 얻을 수 있기 때문입니다. 보다 나은 "의존성 없는 협업체계"는 MVP를 거쳐 MVVM으로 발전되었습니다. MVVM으로 오면서 MVC model 2에서 해결할 수 없었던 부분들이 해결되어 ( GUI의 갱신 등에 대한... ) 전보다 더 원활한 협업이 가능할 것으로 기대됩니다.


-fin




댓글 1개:

  1. mvc패턴을 대체할 어떠한 것을 찾다가 이 포스팅이 검색되어서 들렀습니다.

    mvc패턴대로는 도저히 프로젝트를 진행할 엄두가 나지 않아 스스로 해결해보려하던게 view에 직접 input을 넣고 그것으로 컨트롤러같은 역할 자를 통해서 다시 처리된 데이터를 받아서 해결을 하려고 하던차였습니다. mvp나 mvvm이란 호칭이 있었군요. 좀더 알아봐야겠습니다. 좋은글 감사합니다.

    답글삭제