티스토리 툴바


News2009/02/18 18:23

SPRINGSOURCE CASE STUDY


GRAILS를 통해 개발을 단순화하고 개발에 가속을 붙인 WIRED.COM


Wired.com은 Wired 매거진의 온라인 부분으로 기술이 어떻게 세상을 변화시키고 있는지에 대한 가장 최신의 기사를 다룬다. Wired.com은 오프라인으로 출력되어 배포되는 매거진의 기사들을 단순히 온라인으로 제공하는것이 아니라 전 세계 1200만 독자들에게 둘도 없는 온라인으로서의 경험을 가져다 준다. Wired.com은 Conde Nast Publications의 Conde Nast Digital에 속해 있다.


도전


최근 Wired.com은 시장에서 새로운 기술을 가진 제품을 독자들이 찾아보고, 검색하고, 비교하는 Product Reviews라는 새로운 독립적인 섹션을 열었다. Product Reviews는 다른 Wired.com의 부분보다 더 인터랙티브한 경험을 제공해야 하기에 웹사이트 개발팀은 사용자에게 노출되는 사이트(User-facing Site[1]) 그리고 편집자가 동적으로 관리할 수 있는 도구에 관련된 어플리케이션을 개발하는데 있어 유연한 접근방법(a flexible approach to building the related application)을 필요로 했다.


Wired.com의 웹사이트는 출판되는 매거진에 맞춰서 종종 빡빡한 마감시간을 마주하게 되므로 개발주기를 빠르게 갖는 것이 Wired.com에 있어서는 매우 중요한 요소이다. 예를 들어, 매거진이 어떤 사이트에서 지정된 URL이나 매거진 사이트의 특징을 다루고 있다면, 그 웹페이지나 그 사이트의 기능은 출판되는 시점에서 반드시 동작하고 있어야 한다는 것이다.


Wired.com에 있어 어플리케이션의 성능은 또 다른 중요한 고려사항이다. Wired.com은 매일매일 수 백 만 건의 대량 트래픽을 감당해야 한다. "성능은 우리의 최대 고려사항중 하나이다." 라고 Wired.com의 기술담당 Paul Fisher는 말한다. "예를 들어 Ruby on Rails와 같은 쾌속 개발 프레임워크의 가장 큰 단점 중 하나는 성능을 충분히 고려하지 않는다는 점이다."


해결책


Wired.com의 주요 웹사이트가 Spring MVC 어플리케이션이지만 Product Reviews를 개발하기 위해 좀 더 쾌속으로 또 유연하게 개발할 수 있는 Grails를 선택하였다. Grails는 진보한 혁신적인 웹 어플리케이션 프레임워크이며 Groovy에 기반을 두고 있다. 또한 Spring, Hibernate, Sitemesh과 같은 오픈소스 기술 위에서 탄생했다. Groovy는 Java Virtual Machine에서 동작하는 동적언어이며 Java와 유사하며 유연한 문법을 제공하기에 수 시간 안에 개발자가 쉽게 Groovy의 문법을 익힐 수 있다.

"우리 Wired.com의 어플리케이션은 Spring, Hibernate, Sitemesh를 사용합니다. Grails가 기반으로 하는 기술과 정확하게 일치합니다."라고  Fisher는 말한다. "우리는 우리가 사용하는 기술들에 대해 익히 알고 있으며 신뢰를 하기에 Grails의 기반이 안정적이다라고 자신있게 말할 수 있다."

Wired.com은 Java Content Repository(JCR)[2]과 같은 Spring이나 Spring 트랜잭션 지원을 통해 그리고 그런 기능성을 Grails로 쉽게 끌어오기도 하며, Spring Module 프로젝트의 한 부분인 Spring-JCR 연동을 레버리징하여 사이트의 기능들을 효율적으로 사용한다(Wired.com even utilizes functionality via Spring such as Java Content Repository (JCR) and Spring transactional support, and easily pulls that functionality into Grails, leveraging the Spring-JCR integration that is part of the Spring Modules project). "우리에게 Grails를 Spring과 함께 사용할 수 있다는 사실은 정말 강력한 점이다."라고 Fisher는 말한다. "우리는 우리가 해왔던 Spring과 Java의 경험들 그리고 이미 존재하는 코드 베이스를 가지고 Grails에서 어떤 성능저하 없이 레비리징 할 수 있다. 또한 Grails 커뮤니티로부터 다양한 플러그인을 활용 할 수 있다는 이득도 있다."


장점


Spring은 wired.com에 다음과 같은 비즈니스 결과를 가져왔다.


쾌속 프로젝트 올리기

"Grails는 단순하고 명확하고 직관적인 개발 워크플로우와 프로세스를 제공하기에 새로운 개발자가 프로젝트에 쉽고 빠르게 참여할 수 있도록 한다."라고 Fisher는 말한다. "Java나 Grails에 대한 경험이 없는 사람이라도 Grails를 빨리 배울 수 있고, 몇일 지나지 않아 개발에 속도를 높여 매우 생산적이었다. Grails는 웹 개발과 같은 일에 초보인 개발자든 Java에 능숙한 개발자든 모두에게 매우 유용했다." Fisher는 "Grails는 어떤 다른 개발 옵션보다도 단순하며 상당히 적은 코드만을 필요로 하고, 그 점이 개발자들이 정확히 해당 코드가 무엇을 의도하는지 이해할 수 있게 한다. 이 단순함이 wired.com이 이전의 백엔드와 프론트엔드 개발자 사이에 공통으로 작성되는 작업에 대한 과정을 쉽게 하나의 형태로 디자인 하는 것을 가능하게 만들었다(streamline a process)."라고 말한다.


빠른 프로젝트 전달

Grails의 Scaffolding과 영속성 기능을 통해 미리 제공되는 몇몇 기본 기능을 가지고 매우 쉽게 Product Reviews를 개발을 진행 할 수 있다. 또한 작성해야 할 코드의 양도 매우 적다. 사실 Fisher는 첫 관리 도구를 단 몇 일에 걸쳐 개발하였다. 굳어진 Wired.com의 사이트를 빠르게 살아있는 사이트로 바꾸기 위한 가능성은 Grails를 사용하는 계획에서 시작한다. "Grails의 가장 큰 장점 중 하나는 개발 속도다" 라고 Fisher는 말한다." Grails에서 어떤 기능도 몇 단계만 거치면 만들 수 있다. 우리 개발자들은 훨씬 빨리 일을 마칠 수 있었기에 Grails로 개발하는 것이 더 행복하다고 했다.


일관된 관리와 지원

"관리적인 측면에서 Grails는 디버깅을 단순화하고 디버깅하는 시간을 줄여준다." Fisher가 설명하길 "Grails 어플리케이션응 일반적으로 적은 코드를 필요로하고 이것은 어플리케이션을 지원히고 고치는데 더 쉬워진다는 것이며 시간 또한 놀랍도록 절약된다는 것이다. 예를 들어, 어떤 문제나 어떤 어플리케이션에 관한 지원에 대한 해결책을 찾는다고 하자. 이 때 18개의 파일에 걸쳐 300여 줄 의 코드속에서 찾는것 보다 50 여 줄의 코드로 된 한 두 개 파일 만을 가지고 디버깅 하는 것은 훨씬 간단해진다."


더 나은 어플리케이션 기능성

Grails는 Wired.com의 개발자들이 Product Reviews 어플리케이션의 컴포넌트들을 빠르게 개발하고 전달하는 것을 가능하게 했다. 이를 통해 개발팀은 여분의 시간을 새로운 릴리즈의 기능을 개발하는데 더 쏟을 수 있었다. 반복적으로, Product Reviews 섹션은 최초 릴리즈에 비교해 볼 때 사이트의 특징과 기능면에서 눈부신 성장을 이뤄 왔다.


보다 나은 성능의 어플리케이션

Fisher는 "어플리케이션의 성능문제는 큰 고려사항이다."라고 설명한다. "Grails가 기반으로하는 기술들에 대한 안정감으로부터 우리는 플랫폼에서 이끌어 낼 수 있는 성능에 대한 믿음 있다. 우리가 Grails로부터 보아온 성능은 Ruby on Rails나 Django와 같은 다른 쾌속 개발 프레임워크에 비해 매우 고무적이다. 내 경험상 Grails는 훨씬 빨리 동작하며, 보다 나은 성능을 위해 그 기능을 Java로 가져와서 언제든지 최적화 할 수 있다."


좋은 품질의 어플리케이션

Grails는 Product Reveiws 어플리케이션 테스트를 훨씬 쉽게 했다. 그 결과 개발자들은 어플리케이션의 품질을 높이기 위한 어떠한 테스트도 기운나게(?) 작성하였다(encouraged to write tests for everything). 


비즈니스 목표에 집중

"불가피하게 작성해야하는 불필요한 코드가 많을때, 우리는 비즈니스 로직에 대한 시각을 쉬이 잃을 수 있다." Fisher의 결론은 아래와 같다. "Grails는 일반적인 자바 기반 웹 어플리케이션에서 작성해야 할 불필요한 코드에 대해서 생각하지 않고 비즈니스 목표면에서 해야할 일을 마치기 위헤 무엇에 노력해야 하는가에 대한 집중 하기을 쉽게 만든다." 클로져와 Groovy로부터 지원되는 동적 기능과 결합하여 '설정보다는 관례'에 대한 Grails의 의지는 불필요한 반복을 제거하는데 성공적이다.


© 2009 SpringSource Inc. All rights reserved.

잘못된 한국어 번역이나 표현은 블로그의 댓글이나 lethee(at)gmail.com으로 보내주시기 바랍니다.


[1] user facing definition - PC Magazine, http://www.pcmag.com/encyclopedia_term/0,2542,t=user+facing&i=55816,00.asp 

[2] InfoQ: Integrating Java Content Repository and Spring, http://www.infoq.com/articles/spring-modules-jcr 


Posted by lethee

TRACKBACK http://grails.tistory.com/trackback/37 관련글 쓰기

  1. 화니의 생각  삭제

    2009/02/18 18:29TRACKBACK FROM lethee's me2DAY

    미천한 한국어 실력과 어줍잖은 영어 실력으로 오늘도 번역 한 건.

댓글을 달아 주세요

  1. mage

    해결책 섹션의 제일 마지막 문장에 오타가 있어요. "활욯" 라고 쓰여있는 부분이요.

    2009/02/19 12:41 [ ADDR : EDIT/ DEL : REPLY ]
  2. 이것은 특정 사이트가 적절하게 당신이 자랑 할게요 출판되었다​​ 흥미로운 세부 사항을 포함한 - 많은 넘치는 완성 온다! 내 배우자와 당신이 사용하는 간단한 일에,이 문서를 현금으로 제공하면 인식과 관련된 현재의 독특한 방식을 좋아했습니다. 다른 사람과 비교하면. ;)

    2011/08/18 00:40 [ ADDR : EDIT/ DEL : REPLY ]

HowTo2009/02/02 06:09

다음과 같은 도메인 클래스가 있으면:
package kr.grails

class Book {
	String name
}
Grails Converter의 간단한 사용례는 다음과 같습니다:
package kr.grails

import grails.converters.*

Book.get(1) as JSON
데이터가 있다가 가정하면 결과는 아마도:
{"id":1, "class":"Book", name:"myBook"}

정말 전지전능합니다. 그런데 항상 id와 class가 포함된다는 것이 좀 밉상 입니다.

하지만 Converter를 만들 수 있습니다. 그래서 Converter를 하나 만들었습니다.


package kr.grails.converters;

import org.codehaus.groovy.grails.web.converters.exceptions.ConverterException;
import org.codehaus.groovy.grails.web.json.JSONException;

public class JSON extends grails.converters.JSON {
	@Override
	protected void property(String key, Object value) throws JSONException,
			ConverterException {
		if( key.compareTo("id") == 0 ||
				key.compareTo("class") == 0) {
			return;
		}
		
		super.property(key, value);
	}
}

사용하는 코드를 다음과 같이 변경합니다.
package kr.grails

import kr.grails.converters.*

Book.get(1) as JSON
이제 달라진 결과는 이렇게:
{ name:"myBook"}
코드를 짜보지는 않았습니다만, XML등 다른 것도 가능할 것이라고 생각됩니다. 그리고 grails.converters.deep.*의 것도 가능하리라 생각합니다.



Posted by 새발

TRACKBACK http://grails.tistory.com/trackback/36 관련글 쓰기

댓글을 달아 주세요

HowTo2009/01/29 14:39

소개

이 글은 [1]의 9장 RestController를 보고 작성한 것입니다. RestController를 상속받는 것만으로 도메인 객체를 Web Service로 노출 시킬 수 있습니다. [1]의 소스는 scaffold의 관례를 그대로 따르도록 돼 있습니다. 여기서는 몇 가지 기능을 수정 하고 정리한 것입니다.

수정 사항

  • 'id'말고 다른 프로퍼티를 key로 사용할 수 있도록 수정 - 도메인 객체별로 다른 프로퍼티를 key로 사용할 수 있도록 수정했습니다.

먼저 UrlMappings 파일에서 다음과 같이 설정합니다.:

class UrlMappings {
    static mappings = {
    	"/book/$restKey?"(controller:"book"){
    		action = [GET:"show", PUT:"create", POST:"update", DELETE:"delete"]
    	}
    	//이 라인이 있어야 /book/sample에 매핑됩니다. 순서가 restful action(/book/$restKey)보다 먼저 나오면 안됩니다.
    	"/book/$action"(controller:"book")
	}
}

그리고 해당 도메인 객체에서 static restKeyProperty에 key로 사용할 프로퍼티 명을 적어줍니다.

package kr.grails
 
class Book {
	String name
 
	//final은 값을 변경해버리는 실수를 방지하기 위해서 넣은 것입니다. 
	static final restKey="name"
}

원래 /$domain/$id로만 가능했던 것을 Book의 경우에는 /book/$name로도 접근할 수 있도록 수정하였습니다.

  • 결과를 반환하는 것을 Content negotiation이 동작하도록 수정하였습니다.

[1]의 코드의 경우 UrlMappings와 RestController.format()은 다음과 같이 구현 돼 있습니다.

        "/$rest/$domain/$id?"{
            controller = "rest"
            action = [GET:"show", PUT:"create", POST:"update", DELETE:"delete"]
            constraints {
                rest(inList:["rest","json"])
            }
        }

	private format(obj) {
		def restType = (params.rest == "rest")?"XML":"JSON"
		render obj."encodeAs$restType"()
	}

즉 uri에 명시한 포멧에 따라서 결과의 포멧이 결정됩니다. 이 것이 바람직 하지 않아보여서 코드를 수정했습니다.

	protected format(obj) {
		withFormat{
			form { render obj as JSON }
			json { render obj as JSON }
			html { render (contentType:"text/plain",text: obj as JSON) }
		}
	}

요청의 Content Type에 따라 반환하는 결과의 형식이 달라집니다. 'text/html', 'text/json', 'text/plain'으로 요청하면 JSON으로 반환합니다. XML보다 JSON이 여러모로 편리하기에 XML은 빼버렸습니다. XML도 추가하면 쉽게 추가됩니다. 원래는 XML도 가능하게 했었는데 테스트 만들기 귀찮아서 빼버렸습니다.-_-;;;이 경우에 XML보다 JSON이 유용해 보여요.

아 'text/josn'을 json으로 알게해주기 위해 config.groovy파일도 수정합니다.

grails.mime.types = [ ...
                      json: ['text/json'],
                      ...
                    ]
  • scaffold를 사용하듯이 도메인 클래스를 알려줄 수 있도록 했습니다.
package kr.grails
 
class BookController extends RestController{
	//grails의 scaffold 모델과 최대한 비슷하게 만들려고 노력했습니다.
	def restScaffold = Book
 
	//이것은 UrlMappings의 샘플을 위해 만든 것입니다.
	def sample = {
		render "sample"
	}
}

물론 UrlsMappings.groovy에서 '$domain'으로 도메인 클래스을 노출시키는 것은 삭제했습니다. filter 설저등 uri 기반으로 사용하는데 불편하고 제약 사항도 좀 있었습니다.

소스 코드

소스 코드를 올립니다. [1]의 코드보다 좀 더 유연하게 수정했습니다. 이제 grails의 scaffold를 이용해서 어플리케이션을 쉽게 만들 었던 것처럼 restScaffold를 정의함으로써 쉽게 웹 서비스를 만들 수 있습니다.(물론 개선 사항이 많을 것이라고 생각합니다…)

이 글에서 다룬 내용의 코드입니다. 테스트도 들어 있습니다.

RestController

/src/groovy에 위치시킵니다. 이 코드를 controllers 폴더에 넣으면 grails의 DI로 인해 코드가 꼬여버리는 것 같습니다. 일단은 컨트롤러를 상속시켜 버렸으니까요. RestController라는 특별한 Controller에 대한 논쟁도 있었나 봅니다만,,,기각된 것 같습니다.

package kr.grails
 
import static org.apache.commons.lang.StringUtils.*
import org.codehaus.groovy.runtime.InvokerHelper
import org.codehaus.groovy.grails.commons.GrailsDomainClass
import grails.converters.*
import static javax.servlet.http.HttpServletResponse.*
 
//abstract로 선언하는 것이 적합해 보였지만 테스트를 위해서 선언하지 않았습니다.
class RestController {
	protected def restKeyProperty = "id"
 
	def show = {
		def result
		if(params.restKey) {
			result = invoke("read", params.restKey)
		} else {
			if(!params.max) params.max = 10
			result = invoke("list", params)
		}
 
		if(result == null) {
			response.sendError(SC_NOT_FOUND, "${restScaffold.class.name} not found with ${this.restKeyProperty}[${params.restKey}]")
		}else{
			format(result)
		}
	}
 
	// updates a domain object
	def update = {
		def domain = invoke("get", params.restKey)
 
		if(domain == null) {
			response.sendError(SC_NOT_FOUND, "${restScaffold.class.name} not found with ${this.restKeyProperty}[${params.restKey}]")
			return
		}
		mapParams()
 
		domain.properties = params
 
		if(!domain.hasErrors() && domain.save()) {
			format(domain)
		} else {
			response.sendError(SC_BAD_REQUEST, "${restScaffold.class.name} could not be saved with ${this.restKeyProperty}[${params.restKey}]")
		}
	}
 
	protected mapParams() {
		withFormat{
			form{
				def input = ""
				request.inputStream.eachLine {
					input += it
				}
 
				// convert input to name/value pairs
				if(input  && input != '') {
					input.tokenize('&').each{
						def nvp = it.tokenize('=');
						params.put(nvp[0],nvp[1]);
					}
				}
			}
			json{
				request.JSON.each{ key, value -> params.put(key, value) }
			}
		}
	}
 
	// create a domain object
	def create = {
		def result
		def domain = InvokerHelper.invokeConstructorOf(restScaffold, null)
 
		mapParams()
		domain.properties = params
 
		if(!domain.hasErrors() && domain.save()) {
			result = domain
		}
		else {
			log.error domain.errors
			result = response.sendError(SC_BAD_REQUEST, "${restScaffold.class.name} could not be created with ${this.restKeyProperty} of ${params.$restKey}")
		}
 
		format(result)
	}
 
	// deletes a domain object
	def delete = {
		def result = invoke("get", params.restKey);
 
		if(result) {
			result.delete()
			format(result)
		} else {
			result = response.sendError(SC_NOT_FOUND, "${restScaffold.class.name} not found with ${this.restKeyProperty}[${params.$restKey}]")
		}
	}
 
	def beforeInterceptor = {
		//detect domain class
		if( !this.metaClass.hasProperty(this, "restScaffold") ) {
			response.sendError(SC_NOT_FOUND, "not found domain class")
			return false
		}
 
		//detect rest key property
		if( restScaffold.metaClass.hasProperty(restScaffold, "restKey") ) {
			this.restKeyProperty = restScaffold.restKey
		}
		true
	}
 
	protected invoke(method, parameters) {
		if( ( method == "read" || method == "get") && this.restKeyProperty != "id") {
			def capitalized = capitalize( this.restKeyProperty )
			InvokerHelper.invokeStaticMethod(restScaffold, "findBy$capitalized", parameters)
		}else {
			InvokerHelper.invokeStaticMethod(restScaffold, method, parameters)
		}
	}
 
	protected format(obj) {
		withFormat{
			form { render obj as JSON }
			json { render obj as JSON }
			html { render (contentType:"text/plain",text: obj as JSON) }
		}
	}
}

사용 방법

윗부분에서 대충 사용방법을 설명했습니다만 혼란 스러울 것 같아서 순서대로 다시 설명합니다.

  • 먼저 Domain 클래스를 다음과 같이 만듭니다.
package kr.grails
 
class Book {
	String name
 
	//final은 값을 변경해버리는 실수를 방지하기 위해서 넣은 것입니다. 
	static final restKey="name"
}
  • Controller도 만들어 줍니다.
package kr.grails
 
class BookController extends RestController{
	//grails의 scaffold 모델과 최대한 비슷하게 만들려고 노력했습니다.
	def restScaffold = Book
 
	//이것은 UrlMappings의 샘플을 위해 만든 것입니다.
	def sample = {
		render "sample"
	}
}

/src/groovy/에 RestController.groovy도 넣어 줍니다.

  • Config.groovy에서 json mime 타입도 설정 해줍니다.
grails.mime.types = [ ...
                      json: ['text/json'],
                      ...
                    ]
  • UrlMappings.groovy도 설정합니다. 그리고 RestClient같은 걸로 테스트 해보면 잘 됩니다. RestClient도 groovy로 작성된 것 같습니다. swingbuilder로 말입니다.
class UrlMappings {
    static mappings = {
    	"/book/$restKey?"(controller:"book"){
    		action = [GET:"show", PUT:"create", POST:"update", DELETE:"delete"]
    	}
    	//이 라인이 있어야 /book/sample에 매핑됩니다. 순서가 restful action(/book/$restKey)보다 먼저 나오면 안됩니다.
    	"/book/$action"(controller:"book")
	}
}
메시지나 에러처리등은 좀 손 보고 있습니다만 아직 여기까지입니다.

참고

1. Beginning Groovy And Grails

Posted by 새발

TRACKBACK http://grails.tistory.com/trackback/35 관련글 쓰기

댓글을 달아 주세요