오늘날의 자바 웹 개발은 실제 필요보다 극적으로 더욱 복잡하다. 자바 영역의 대부분의 최신 웹 프레임워크는 필요 이상으로 복잡해졌고 DRY(Don't Repeat Yourself) 원칙을 받아들이지 않는다.
Rails, Django 그리고 TurboGears와 같은 동적 프레임워크들은 웹 애플리케이션에 대한 좀 더 현대적인 사고 방식으로 가득차게끔 도왔다. Grails는 이러한 개념에 기초하고 자바 플랫폼에서 웹 애플리케이션을 작성하는 복잡성을 극적으로 줄어준다. 그러나 이것이 다른 것과 차이가 나는 점은 grails는 Spring이나 Hibernate과 같이 이미 확립된 자바기술을 기초로 한다는 것이다.
Grails는 완전한 구성(full stack)의 프레임워크이고 핵심기술과 관련된 plug-in들로 웹 개발의 난제들을 풀수 있도록 한다. Grails에는 다음 것들이 포함되어 있다.
Hibernate를 기반으로 한 Object Relation Mapping(ORM) 계층의 용이한 이용
Grails 좀 공부해볼까해서 User Guide보고 나름 정리했습니다. 처음 시작하고 금방 정리할 줄 알았는데 생각보다 양이 많아 시간이 좀 걸렸습니다.
정리하면서 다시 한 번 느끼지만 진짜 막강 Grails입니다. 최신의 좋다는 기술을 잘 통합하였고 생산력 무지 좋습니다. RoR을 쓰고 싶지만 주로 쓰는 언어가 Java라 군침만 흘리고 있었는데 RoR/Ruby 스크립팅 언어의 유연성과 자바의 엔터프라이즈급 서비스가 만나 환상의 조합을 이루고 있습니다.
1. 소개 Grails의 일반적인 내용을 소개합니다. 2. 시작하기 Grails 설치 및 Hello World 수준의 Getting Start가 있습니다.
6. 웹 계층 - 1 웹 계층에서 요청을 처리하는 컨트롤러에 대해 집중 분석합니다. 6. 웹 계층 - 2 Grails에서 뷰를 표현하는 GSP에 대해 설명합니다. 6. 웹 계층 - 3 Grails의 강력한 태그 라이브러리 사용법과 사용자 정의 태그 라이브러리 작성방법을 설명합니다. 6. 웹 계층 - 4 다양한 방법으로 URL을 컨트롤러의 액션으로 매핑하는 방법을 이야기합니다. 6. 웹 계층 - 5 Spring의 웹 플로우를 Grails에서 사용하는 방법을 다룹니다. 6. 웹 계층 - 6 Grails의 강력한 필터 기능을 설명합니다. 6. 웹 계층 - 7 그 밖의 웹에서 Ajax을 사용하는 방법과 Grails가 자동으로 컨텐츠의 종류를 결정하는 방법에 설명합니다.
Ajax
컨텐트 종류 결정(Content Negotiation)
7. 유효성검사 제약조건을 이용하여 데이터 바인딩 같은 곳에서 사용할 수 있는 편리한 입력 데이터 유효성 검사 방법을 다룹니다.
8. 서비스 계층 핵심 비즈니스 로직을 재사용이 쉽도록 서비스 계층으로 분리하고 이것을 이용하는 방법을 설명합니다. 물론 자바로 작성된 서비스를 이용하는 방법도 설명합니다.
만일 도메인 모델을 자바와 Hibernate 매핑으로 작성하는 것을 선호한다면 간단히 필요한 클래스를 임포트하고 scaffold 속성을 해당 자바 클래스로 설정하여 scaffolding을 사용할 수 있다.
동적 Scaffolding scaffold 속성을 사용할 때 Grails는 이것을 위해 코드 템플릿이나 코드 생성 방법을 사용하지 않는다. 따라서 scaffold 컨트롤러에 scaffold 액션과 함께 당신의 고유 액션들을 추가할 수 있다. 예를 들면 아래 예제에서, changeAuthor는 실제 물리적으로 존재하지 않는 show 액션으로 리다이렉트한다.
class BookController { def scaffold = Book def changeAuthor = { def b = Book.get( params["id"] ) b.author = Author.get( params["author.id"] ) b.save() // redirect to a scaffolded action redirect(action:show) } }
필요하다면 scaffold된 액션을 당신의 고유한 액션으로 재정의(override)할 수 있다.
class BookController { def scaffold = Book // 저자와 책 둘 다 반환하도록 액션을 재정의 def list = { [ "books" : Book.list(), "authors": Author.list() ] } }
이 모든 것이 CRUD 인터페이스를 실행시간에 동적으로 생성하는 "동적 scaffolding"으로 알려진 것이다. Grails는 또한 다음 장에서 논의할 "정적" scaffolding도 지원한다.
생성된 뷰를 수정하기 Grails는 유효성 검사와 제약조건을 적합시킨 지능적인 형태의 뷰를 생성한다. 예를 들면, 간단히 빌더의 constraints를 재정렬하여 뷰에 나타나는 필드 순서를 변경할 수 있다.
만일 GORM(Grails Object Relation Mapping)이 원하는만큼 유연하지 않다면 대안으로 Hibernate를 이용하여 도메인 클래스를 매핑할 수 있다. 이것을 하려면 프로젝트의 grails-app/conf/hibernate 디렉토리의 hibernate.cfg.xml을 생성하고 당신의 도메인 클래스에 대응하는 HBM 매핑 xml 파일을 만든다.
이것으로 Grails 도메인 클래스를 더 다양한 범위의 레가시 시스템에 매핑할 수 있고 보다 유연하게 데이터베이스 스키마 생성을 할 수 있다.
Grails에서는 자바로 도메인 모델을 작성할 수 있거나 Hibernate로 매핑된 이미 존재하는 도메인 모델을 재사용할 수도 있다. 이것을 위해 해야 할 일은 필요한 hibernate.cgf.xml 파일과 관련 매핑 파일을 grails-app/conf/hibernate 디렉토리에 넣는 것이다.
게다가 좋은 소식은 여전히 GORM의 동적 저장과 질의 메소드를 호출할 수 있다는 것이다!
15.1 Hibernate Annotation으로 매핑하기 Grails에서 Hibernate의 자바 5.0 Annotation 지원을 이용하여 도메인 클래스를 매핑할 수 있다. Annotation을 사용 하기 위해서는 DataSource의 configClass를 다음과 같이 설정하여 Grails에게 annotation 설정을 사용한다고 알려줘야 한다.
설정은 이게 끝이다! Annotation을 사용해야 하기 때문에 자바 5.0을 설치하였는지 확인한다. 이제 annotation 클래스를 생성하기 위해 간단히 src/java 디렉토리에 새로운 자바 클래스를 생성하고 EJB 3.0 스팩의 일부분으로 정의된 annotation들을 사용한다. (더 자세한 정보는 Hibernate Annotations Docs를 보라)
package com.books; @Entity public class Book { private Long id; private String title; private String description; private Date date; @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } }
그런 후 이 클래스를 Hibernate sessionFactory에 등록해야 하기 위해 grails-app/conf/hibernate/hibernate.cfg.xml 파일에 다음과 같은 엔트리를 추가한다.
웹 서비스는 웹 애플리케이션에 웹 API를 제공하는 것을 이야기한다. 그리고 웹 서비스는 전형적으로 SOAP이나 REST로 구현한다.
13.1 REST REST는 실제로는 그 자체로 기술은 아니고 아키텍쳐 패턴에 가깝다. RETS는 대단히 단순하며 통신 매개로 평이한 XML이나 JSON을 사용한다. 또한 기반 시스템의 표현과 GET, PUT, POST, DELETE와 같은 HTTP 메소드를 사용하는 URL 패턴과 결합한다.
각 HTTP 메소드는 액션으로 매핑된다. 예를 들면 데이터 조회에는 GET, 데이터 생성에는 PUT, 데이터 업데이트 등에는 POST를 사용한다. 이런 면에서 REST는 CRUD에 잘 들어맞는다.
URL 패턴 Grails로 REST를 구현하기 위한 첫번째 단계는 RESTfull URL 매핑을 제공하는 것이다.
여기서 우리는 컨트롤러에서 RESTful API를 제공하기 위해 HTTP 메소드로 매핑하는 URL 매핑 기능을 사용했다. GET, PUT, POST 그리고 DELETE와 같은 각 HTTP 메소드는 컨트롤러의 고유 액션으로 매핑된다.
XML 마셀링(marshaling) - 조회 컨트롤러 구현체는 GET 메소드를 구현하기 위해 Grails의 XML 마셀링을 사용할 수 있다.
import grails.converters.* class ProductController { def show = { if(params.id && Product.exists(params.id)) { def p = Product.findByName(params.id) render p as XML } else { def all = Product.list() render all as XML } } .. }
여기서 하는 일은 만일 id가 있으면 이름으로 Product을 찾아 반환하고 아니면 모든 Proudct을 반환한다. 이 방법으로 /produts로 간다면 모든 product들을 얻을 수 있으며 아니면 /product/MacBook으로 가면 단지 MacBook만 얻을 수 있다.
XML 마셀링 - 업데이트 PUT이나 POST로 업데이트 하기 위해서 들어오는 XML 패킷을 읽는 능력을 가진 params 객체를 사용할 수 있다. 유입되는 XML 패킷을 보자.
13.3. RSS와 Atom Grails는 직접 FSS나 Atom을 지원하지는 않는다. render 메소드의 XML 기능으로 RSS나 ATOM 피드를 구축할 수 있다. 여하튼 인기있는 ROME 라이브러리를 이용한 RSS와 Atom 빌더를 Grails에서 지원하기 위해 Feeds 플러그인이 있다. 이것의 사용 예제는 아래에서 볼 수 있다.
HTML, 자바스크립트 그리고 URL을 사용할 때 여기서 주입(injection) 공격을 보호하기 위해 간단히 데이터를 escape할 수 있는 codec을 제공한다.
역주) escape 공격: 악의적인 사용자가 의도하지 않은 실행을 가능하도록 입력값으로 코드를 입력하는 것을 말한다. 안전하지 않은 서버일 경우 입력값으로 사용자가 입력한 코드(SQL이나 기타 심각한 파라메터 변경)가 그대로 실행되어 심각한 보안문제를 읽으킬 수 있다. 이것을 처리하는 서버는 사용자가 입력한 값이 코드가 아닌 데이터로 안전하게 처리되도록 사용자 입력을 중 명령어로 인식될 수 있는 부분을 적절히 변환해 준다. 이렇게 명령어로 실행될 수 있는 문자를 escape(탈출) 문자로 변경하는 것을 escape한다고 한다. 적절한 우리말을 찾기가 어렵다.ㅠㅠ 예를 들어 SQL에서 입력값으로 '1 or 1 =1'로 들어오면 escape하지 않고 SQL의 문자중 조건으로 단순히 치환하면 모든 데이터를 접근할 수 있다. 이런 경우는 PreparedStatement를 사용하여 해결할 수 있다.
11.1 공격에 대해 안전하게 보호하기
SQL 주입(injection) GORM 도메인클래스의 근간 기술인 Hibernate는 데이터베이스에 전하는 데이터를 자동으로 escape하므로 이것은 이슈가 되지 않는다. 어쨌거나 검사하지 않은 요청 파라메터를 사용하는 좋지 않은 동적 HQL 코드를 작성할 가능성은 여전히 존재한다. 예를 들어 다음은 HQL 주입 공격에 취약하다.
def vulnerable = { def books = Book.find("from Book as b where b.title ='" + params.title + "'") }
절대 이렇게 하지 말라. 파라메터 전달이 필요하다면 대산 이름에 의한(named) 또는 순서에 의한(positional) 파라메터를 사용하라.
def safe = { def books = Book.find("from Book as b where b.title =?", [params.title]) }
피싱(Phishing) 이것은 당신의 브랜드를 훔치는 사회 이슈로 당신의 고객과 공적인 의사소통이 필요하다. 고객은 진짜 이메일 수신을 어떻게 식별하는지 알 필요가 있다.
XSS - cross-site scripting injection 들어오는 요청이 당신의 애플리케이션으로 부터 기인한 것인지 아니면 다른 사이트에서 온 것인지 검증하는 것은 매우 중요하다. 티켓팅과 플로우 시스템이 이것을 도와줄 수 있다. 그리고 Grails는 기본으로 보안을 포함한 Spring 웹 플로우 지원한다.
또한 모든 뷰로 표현되는 데이터 값이 확실히 알맞게 escaped되는 것도 매우 중요하다. 예를 들어 악의를 가진 사람들이 자바스크립트를나 다른 HTML를 데이터나 다른 사람에게 보여주는 태그에 끼워넣지(inject) 못하도록 HTML이나 XHTML로 렌더링할 때 모든 객체에 대해 encodeAsHTML을 호출해야 한다. Grails는 이 목적을 위해 몇 가지 동적 인코딩 메소드들을 제공하는데 출력에 escape된 포맷을 지원하지 않을 경우 고유의 코덱(codec)을 쉽게 작성할 수 있다.
또한 리다이렉트하기 위한 다음 URL을 결정하기 위해 request 파라메터나 데이터 필드를 사용하는 것을 피해야 한다. 예를 들어 로그인 성공 후 리다이렉트를 결정하기 위해 suuccessURL 파라메터를 사용한다고 할 때 공격자는 당신의 사이트를 이용하여 로그인 절차를 흉내낼 수 있어 로그인 한 사용자는 공격자의 사이트로 돌아갈 수 있다. 암시적으로 그 사이트의 로그인한 계정을 자바스크립트 코드로 이용할 수 있게 된다.
HTML/URL 주입 나중에 페이지 안에 link를 생성하기 위해 데이터에 link와 같은 나쁜 데이터를 넣으면, link를 클릭하는 경우 예상치 못한 작동을 유발하거나 다른 사이트로 리다이렉트 할 수도 있으며 request 파라메터를 바꿀 수도 있다.
Grails가 제공하는 코덱을 이용하여 HTML/URL 주입을 쉽게 처리할 수 있다. 그리고 Grails가 제공하는 모든 태그 라이브러리는 알맞은 곳에서 encodeAsURL을 호출한다. 만일 URL을 생성하는 당신 고유의 태그를 작성한다면 이것을 염두해 둘 필요가 있다.
서비스 거부 이 경우 로드 밸런서나 다른 장치가 보다 유용하다. 그러나 또한 터무니 없는 질의와 관련한 이슈가 있는데 예를 들어 공격자가 만든 link가 결과 세트의 개수를 최대로 해서 서버의 메모리 한계를 넘어 시스템이 느려지거나 다운되는 경우가 있다. 여기에서의 해결책은 항상 request 파라메터 동적 finder나 다른 ORM 질의 메소드에 전달하기 전 검사하여 나쁜 파라메터 값을 제거하는 것이다.
def safeMax = Math.max(params.max?.toInteger(), 100) // 100개 이상이 반환되지 않도록 조치 return Book.list(max:safeMax)
추측이 가능한 ID 많은 애플리케이션들이 URL의 마지막 부분으로 GORM이나 다른 곳에서 객체를 조회할 수 있는 ID를 사용한다. 특히 GORM의 경우 보통 ID가 순차적인 integer이므로 쉽게 추측이 가능하다.
따라서 사용자에게 응답을 보내기 전 사용자가 요청한 ID로 그 객체를 보는 것이 허가되었는지 반드시 확인해야 한다. 이렇게 하지 않는 것을 기본 비밀번호로 "letmein" 등을 쓰는 것 같은 필연적으로 깨지는 "애매한 보안(security through obscurity)"이라 한다.
반드시 모든 보호되지 않은 URL은 어떤 방법으로도 공개적으로 접근할 수 있다고 가정해야 한다.
11.2 문자열 인코딩과 디코딩 Grails는 동적 인코드/디코드 메소드 개념을 지원한다. Grails는 표준 코덱 집합을 포함하고 있다. Grails는 또한 실시간에 인식하는 고유한 코덱을 개발자가 공헌할 수 있도록 간단한 메커니즘을 지원한다.
코덱 클래스 Grails 코덱 클래스는 인코드 클로우저(closure)와 디코드 클로우저 둘 다 포함할 수 있다. Grails 애플리케이션이 시작될 때 Grails 프레임워크는 grails-app/utils 디렉토리에서 코덱들을 동적으로 로드한다.
프레임워크는 약속(convention)에 따라 grails-app/utils 아래에서 이름이 Codec으로 끝나는 클래스를 찾는다. 예를 들면 Grails가 제공하는 표준 코덱 중 하나는 HTMLCodec이다.
코덱이 코드 블럭을 지정한 encode 속성을 가지면 Grails는 동적인 encode 메소드를 생성하고 코덱을 표현하는 이름으로 String 클래스에 그 메소드를 추가한다. 예를 들면 HTMLCodec 클래스는 encode 블럭을 정의하므로 Grails는 그 클로우저를 encodeAsHTML이란 이름으로 String 클래스에 붙일 것이다.
HTMLCodec과 URLCodec 클래스는 또한 decode 블럭을 정의하므로 Grails는 이것들을 decodeHTML과 decodeURL 이름으로 붙일 것이다. Grails 애플리케이션 어디에서든지 동적 코덱 메소드를 호출할 수 있다. 예를 들면, 'description'이라는 속성을 가진 보고서가 있는데 설명은 HTML 문서로 표현되기 위해서 escape이 필요한 특수 문자를 가진 경우를 보자. 이것을 처리하기 위한 한 방법은 GSP에서 동적 인코드 메소드를 아래와 같이 호출하는 것이다.
${report.description.encodeAsHTML()}
디코딩은 value.decodeHTML() 문법으로 실행한다.
표준 코덱들
HTMLCodec 이 코덱은 HTML escaping과 unescaping을 수행한다. 따라서 어떠한 HTML 태그를 생성하지 않고 페이지 레이아웃에 해를 끼치지 않고 HTML 페이지에 값을 안전하게 렌더링할 수 있다. 예를 들면, "Don't you know that 2 > 1?" 값은 > 문자가 태그를 닫기 때문에 HTML 페이지에 안전하게 보여줄 수 없다. 이 값은 특히 다음과 같은 input 필드의 값 속성으로 렌더링할 때 아주 나쁘다. 사용 예제
HTML 인코딩은 apostrohe(소유격부호)와 단따옴표는 다시 인코드하지 않는다는 것에 주의하라. 따라서 apostrohpe(')가 페이지를 망가뜨리지 않도록 애트리뷰트 값에 반드시 쌍따옴표(")를 사용한다.
URLCodec Link나 폼 액션 또는 URL 생성이 필요한 어느 시점에서든지 URL을 만들기 위해 URL 인코딩이 필요하다. 이것은 URL의 의미를 바꾸는 불법적인 문자를 막아준다. 예를 들면 "Apple & Blackberry"는 GET 요청에서 엠퍼센드(&)가 파라메터 파싱에서 둘을 분리하므로 파라메터로 잘 작동하지 않을 것이다. 사용예제
<a href="/mycontroller/find?searchKey=${lastSearch.encodeAsURL()}">Repeat last search</a>
Base64Codec Base64 인코드/디코드 기능을 수행한다. 사용 예제
Your registration code is: ${user.registrationCode.encodeAsBase64()}
JavaScriptCodec 유효한 자바스크립트 문자열을 사용할 수 있도록 문자열 escape를 한다. 사용 예제:
사용자 정의 코덱들 애플리케이션은 그들 고유의 코덱들을 정의할 수 있다. 그리고 Grails는 표준 코덱들과 함께 그것들을 로드할 것이다. 사용자 정의 코덱 클래스는 grails-app/utils/ 디렉토리에 정의하고 이름이 Codec로 끝나야 한다. 코덱은 static encode 블럭과 static decode 블럭 두 개 모두 가질 수 있다. 블럭은 동적 메소드를 호출하는 대상이 되는 객체를 나타내는 하나의 아규먼트를 가진다. 예를 들면
class PigLatinCodec { static encode = { str -> // convert the string to piglatin and return the result } }
위 코덱을 애플리케이션에서 다음과 같이 사용할 수 있다.
${lastName.encodeAsPigLatin()}
11.3 인증(Authentication) 현재 인증을 위한 기본 메커니즘은 없지만 수천가지 다른 방법으로 인증을 구현할 수 있다. 어쨌거나 인터셉터나 필터를 이용한 간단한 인증 메커니즘을 구현하는 것은 진짜 간단한다.
필터로 모든 컨트롤러나 URI 공간을 가로질러 인증을 적용할 수 있다. 예를 들면 grails-app/conf/SecurityFilters.groovy에 새로운 필터 집합을 생성할 수 있다.
11.4 보안 플러그인 만일 간단한 인증을 넘어서 인가, 역할 등의 진보된 기능이 필요하다면 유효한 보안 플러그인 중 하나를 고려해 볼 수 있다.
11.4.1 Acegi Acegi 플러그인은 Spring Acegi 프로젝트를 기반으로 하는데 이것은 모든 인증, 인가 종류의 스키마를 구축하기 위한 유연하고 확장성 있는 프레임워크를 제공한다.
Acegi 플러그인을 사용하면 URI와 역할을 맵핑해야 하며 Acegi 플러그인은 사용자, 인증정보 그리고 request 맵 모델을 위한 기본 도메인 모델을 제공한다. 더 자세한 정보는 wiki의 문서나 여기를 보라.
11.4.2 JSecurity JSecurity는 자바 POJO 지향의 또 다른 보안 프레임워크로 역시 realms, 사용자, 역할 그리고 권한 모델을 위한 기본 도메인 모델을 제공한다. JSecurity를 사용하려면 보안을 원하는 각 컨트롤러가 JsecAuthBase라는 컨트롤러를 상속해야 한다. 그런 후 역할을 설정하는 accessControl 블럭을 제공해야 한다. 다음은 그 예제이다.
class ExampleController extends JsecAuthBase { static accessControl = { // All actions require the 'Observer' role. role(name: 'Observer') // The 'edit' action requires the 'Administrator' role. role(name: 'Administrator', action: 'edit') // Alternatively, several actions can be specified. role(name: 'Administrator', only: [ 'create', 'edit', 'save', 'update' ]) } … }
Grails는 Spring MVC가 지원하는 국제화를 기반으로 뛰어난 국제화(i18n)을 지원한다. Grails로 어떤 뷰에 나타나는 텍스트든 사용자의 지역을 기반으로 커스터마이즈할 수 있다. 자바의 Local 클래스에 관한 javadoc을 인용한다.
Locale 객체는 특정 지리적, 정치적 또는 문화적 지역을 표현한다. 작업을 수행하기 위해 Locale이 필요한 연산자를 지역에 민감하다(locale sensitive)고 부르고 사용자를 위한 정보를 조정(tailor)하기 위해 Locale을 사용한다. 예를 들어 숫자를 보여주는 것은 지역에 민감한 연산자이다. 숫자는 사용자 원래 나라, 지역 또는 문화의 관습/관례에 따라 양식화(formatted)되어야 한다.
Locale은 언어 코드와 나라 코드로 이루어진다. 예를 들여 "en_US"는 US 영어이고 "en_GB"는 영국 영어이다.
10.1 메시지 번들의 이해 이제까지 locale에 대해 알아보았다. Grails에서 Locale의 잇점을 얻기 위해서는 메시지 번들을 생성해야 하는데 이것은 표현하고 싶은 서로 다른 언어를 가지고 있는 한다. Grails에서 메시지 번들은 grails-app/i18n 디렉토리 안에 있으며 단순한 자바 프로퍼티 파일이다.
각 번들은 약속에 의해 messages라는 이름으로 시작하고 locale로 끝난다. Grails는 grails-app/i18n 디렉토리 안에 있는 모든 범위의 언어를 위한 메시지 번들을 내장된 묶음들로 취급한다. 예를 들면
messages.properties messages_de.properties messages_es.properties 기타.
사용자가 특정 로케일을 지정하지 않는한 기본으로 메시지를 messages.properties에서 찾을 것이다. 추가하고자 하는 locale로 끝나는 새로운 프로퍼티 파일을 생성함으로써 간단히 고유한 메시지 번들을 생성할 수 있다. 예를 들어 messages_en_GB.properties는 영국 영어를 위한 것이다. (messages_ko_KR 한국.properties)
10.2 로케일 변경 기본으로 사용자 로케일은 들어오는 요청의 Accept-Language 헤더에서 찾는다. 어쨌거나 request 파라메터로 Grails에게 lang이라는 파라메터를 전달함으로써 간단히 로케일을 변경할 수 있다.
/book/list?lang=es
Grails는 자동으로 사용자 로케일을 바꾸고 이것을 쿠키(cookie)에 저장한다. 따라서 다음 request는 새로운 헤더를 갖게 된다.
10.3 메시지 읽기
뷰에서 메시지 읽기 메시지가 필요한 가장 일반적인 장소는 뷰 내부이다. 뷰에서 메시지를 읽기 위해서는 단지 message 태그를 사용한다.
<g:message code="my.localized.content" />
messages.properties(알맞은 로케일 접미어와 함께)안에 다음과 같은 키를 가지고 있으면 Grails는 메시지를 찾을 것이다.
my.localized.content=Hola, Me llamo John. Hoy es domingo.
어떤 경우는 메시지에 아규먼트를 전달할 필요가 있을지도 모른다. 이것도 message 태그로 가능하다.
자동화된 테스트는 Grails의 핵심 부분으로 Groovy Tests를 이용하여 구현하였다. 따라서 Grails는 낮은 수준의 단위 테스트부터 높은 수준의 기능 테스트까지 쉽게 테스트를 만드는 여러 방법을 제공한다. 이 장은 Grails가 테스팅 관점에서 제공하는 차별화된 능력을 설명한다.
알아둬야 할 첫번째는 모든 create-* 명령어는 실제로 통합 테스트를 자동으로 생성하며 끝난다는 것이다. 예를 들어 아래와 같이 create-controller 명령어를 실행한다고 하자.
grails create-controller simple
Grails는 grails-app/controllers/SimpleController.groovy에 컨트롤러를 생성할 뿐만 아니라 test/integration/SimpleControllerTests.groovy에 통합 테스트도 생성한다. 어쨌거나 테스트 안에 로직을 만드는 것은 Grails가 할 수 없는 일이다! 이것은 개발자에게 남겨둔다.
------------------------------------------------------- Running Unit Tests… Running test FooTests...FAILURE Unit Tests Completed in 464ms … ------------------------------------------------------- Tests failed: 0 errors, 1 failures
test/reports 디렉토리에 위 결과 보고서가 만들어진다. 또한 테스트를 따로 돌리수 있는데 그러기 위해서는 Tests 접미어를 뺀 테스트 이름을 지정하면 된다.
grails test-app SimpleController
게다가 스페이스로 이름을 분리하여 지정하면 여러 개의 테스트들을 실행할 수도 있다.
grails test-app SimpleController BookController
9.1 단위 테스트 단위 테스트는 "구성단위" 수준의 테스트이다. 바꿔 말하면 주위 인프라를 고려하지 않고 개개의 메소드나 코드 블럭을 테스트하는 것을 말한다. Grails에서는 단위테스트와 통합테스트의 차이를 알아야 하는데 왜냐하면 Grails는 단위 테스트에서 통합테스트와 실행시간에 이루어지는 어떤 동적 메소드의 주입(inject)로 하지 않기 때문이다.
class FooController { def text = { render "bar" } def someRedirect = { redirect(action:"bar") } }
이것을 테스트하려면
class FooControllerTests extends GroovyTestCase { void testText() { def fc = new FooController() fc.text() assertEquals "bar", fc.response.contentAsStrng } void testSomeRedirect() { def fc = new FooController() fc.someRedirect() assertEquals "/foo/bar", fc.response.redirectedUrl } }
위의 경우응답은 MockHttpServletResponse의 인스턴스이고 이것으로 응답으로 출력한 내용을 획득하기 위해 contentAsString를 사용하거나 예제의 리다이렉트 URL을 획득하기 위해 redirectedUrl을 사용할 수 있다. 실제 버전과 다르게 Servlet API의 이 가짜(mocked) 버전들은 변경할 수(mutable) 있기에 contextPath 등과 같은 request의 속성을 설정할 수 있다.
통합 테스트동안 액션을 호출할 때 Grails는 인터셉터들을 자동으로 호출하지 않는다. 필요하다면 기능(functional) 테스트를 통해 인터셉터는 따로 테스트해야 한다.
서비스와 함께 컨트롤러 테스트하기 컨트롤러가 서비스를 참조한다면 테스트에서 서비스를 명시적으로 초기화해야 한다.
아래 컨트롤러는 서비스를 사용한다.
class FilmStarsController { def populariryService def update = { // popularitySerivce로 뭔가를 한다. } }
이것을 테스트하려면
class FilmStarsTests extends GroovyTestCase { def popularityService public void testInjectedServiceInController { def fsc = new FilmStartsController() fsc.popularityService = popularityService fsc.update() } }
컨트롤러 명령객체(Command Object) 테스트하기 명령객체 테스트를 위해 request에 파라메터를 제공하고 액션을 파라메터 없이 호출하면 명령객체는 자동으로 작동할 것이다.
명령객체를 사용하는 컨트롤러를 보자
class AuthenticationController { def signup = { Signupform form -> ... } }
Grails는 마술과 같이 signup() 호출을 액션 호출로 여기고 가짜 request 파라메터로 명령객체를 채워준다(populate). 컨트롤러를 테스트하는 동안은 Grails가 제공하는 가짜 request와 함께 params도 변경할 수 있다.
컨트롤러와 render 메소드 테스트하기 액션의 몸체 안의 어느 지점에서도 render 메소드로 뷰를 렌더링할 수 있다. 예를 들어 다음 예제를 보자
def save = { def book = Book(params) if(book.save()) { // handle } else { render(view:"create", model:[book:book]) } }
이 예제에서 액션의 결과 모델을 반환 값으로 사용할 수 없다. 그러나 대신 컨트롤러의 modelAndView 속성에 저장할 수 있다. modelAndView 속성은 Spring MVC의 ModelAndView 클래스의 인스턴스이고 액션의 결과를 테스트하기 위해 이것을 사용할 수 있다.
def bookController = new BookController() bookController.save() def model = bookController.modelAndView.model.book
Request 데이터 흉내내기(Simulating) REST 웹 서비스와 같은 request 데이터가 필요한 액션을 테스트할 경우 Spring MockHttpServletRequest 객체를 사용할 수 있다. 예를 들어 들어오는 request로 데이터바인딩을 수행하는 액션을 보자.
def create = { [book: new Book(params['book'])] }
만일 'book' 파라메터를 XML request로 흉내내려면 다음과 같이 할 수 있다.
void testCreateWithXML() { def controller = new BookController() controller.request.contentType = 'text/xml' controller.request.contents = '''<?xml version="1.0" encoding="ISO-8859-1"?> <book> <title>The Stand</title> … </book> '''.getBytes() // note we need the bytes def model = controller.create() assert model.book assertEquals "The Stand", model.book.title }
JSON request도 동일하다.
void testCreateWithJSON() { def controller = new BookController() controller.request.contentType = "text/json" controller.request.content = '{"id":1,"class":"Book","title":"The Stand"}'.getBytes() def model = controller.create() assert model.book assertEquals "The Stand", model.book.title }
JSON을 사용할 때 바인드하기 위한 타입 이름을 지정하는 class 속성을 잊지 않도록 한다. XML에서는 <book> 노드의 이름을 암시적으로 사용하지만 JSON에서는 JSON 패킷의 일부로 이 속성이 필요하다.
여기에서 우리는 "go" 이벤트를 실행하도록 흐름에 신호를 보냈다. 이것은 "next" 상태로 이동하게 한다. 예제에서 변이 액션은 플로우 범위에 hello 변수를 저장한다. 위에서 보듯 ViewSelection의 model 속성을 검사하여 이 변수의 값을 테스트할 수 있다.
태그 라이브러리 테스트하기 태그 라이브러리 테스트는 실제로 아주 단순하다. 왜냐하면 태그를 메소드로 호출하면 그 결과로 문자열로 반환하기 때문이다. 따라서 다음과 같은 태그 라이브러리가 있다고 하면
class FooTagLib { def bar = { attrs, body -> out << "<p>Hello World!</p>" } def bodyTag = { attrs, body -> out << "<${attrs.name}>" out << body() out << "</${attrs.name}>" } }
테스트는 다음과 같을 것이다.
class FooTagLibTests extends GroovyTestCase { void testBarTag() { assertEquals "<p>Hello World!</p>", new FooTagLib().bar(null,null) } void testBodyTag() { assertEquals "<p>Hello World!</p>", new FooTagLib().bodyTag(name:"p") { "Hello World!" } } }
두번째 예제 testBodyTag에서 태그의 몸체를 반환하는 블럭을 전달하는 것에 주목하라. 이것은 body를 문자열로 표현하기 위해 사용하기 좋다.
GroovyPagesTestCase에서 태그 라이브러리 테스트하기 위와 같이 간단하게 태그 라이브러리를 테스트하는 것에 더해 태그 라이브러리를 테스트 하기 위해 grails.test.GroovyPagesTestCase를 사용할 수도 있다. GroovyPagesTestCase 클래스는 일반 GroovyTestCase 클래스의 하위 클래스이고 GSP 렌더링의 결과를 테스트하기 위한 유틸리티 메소드들을 제공한다.
통합테스트에서만 GroovyPagesTestCase를 사용할 수 있다.
다음과 같은 날짜 포맷팅 태그 라이브러리를 보자.
class FormatTagLib { def dateFormat = { attrs, body -> out << new java.text.SimpleDateFormat(attrs.format) << attrs.date } }
다음과 같이 간단히 테스트할 수 있다.
class FormatTagLibTests extends GroovyPagesTestCase { void testDateFormat() { def template = '<g:dateFormat format="dd-MM-yyyy" date="${myDate}" />' def testDate = … // create the date assertOutputEquals( '01-01-2008', template, [myDate:testDate] ) } }
또한 GroovyPagesTestCase 클래스의 applyTemplate 메소드를 이용하여 GSP 결과를 획득할 수도 있다.
class FormatTagLibTests extends GroovyPagesTestCase { void testDateFormat() { def template = '<g:dateFormat format="dd-MM-yyyy" date="${myDate}" />' def testDate = … // create the date def result = applyTemplate( template, [myDate:testDate] ) assertEquals '01-01-2008', result } }
도메인 클래스 테스트 도메인 클래스 테스트는 보통 GORM API를 사용하는 간단한 문제이다. 하지만 몇 가지 알아둬야 할 것들이 있다. 첫번째로 질의를 테스트하기 위해서는 현재 상태를 데이터베이스에 저장하기 위해 종종 "flush"를 할 필요가 있다. 다음 예를 들어보자.
void testQuery() { def books = [ new Book(title:"The Stand"), new Book(title:"The Shining")] books*.save() assertEquals 2, Book.list().size() }
이 테스트는 실제로는 실패한다. 왜냐하면 save 호출은 호출될 때 실제로 Book 인스턴스를 저장하지 않는다. save 호출은 단지 미래 어느 시점에 이 인스턴스들을 저장해야 한다고 Hibernate에게 알려주는 것이다. 변화를 바로 적용하려면 "flush"를 해야 한다.
void testQuery() { def books = [ new Book(title:"The Stand"), new Book(title:"The Shining")] books*.save(flush:true) assertEquals 2, Book.list().size() }
이 경우 flush 아규먼트를 true로 전달하기에 바로 업데이트되고 바로 후에 질의가 가능한다.
9.3 기능(functional) 테스트 기능테스트는 실제 구동하는 애플리케이션을 테스트하는 것이고 자동화하기가 더 어렵다. Grails는 특별히 기능테스트에 대한 지원은 없다. 그러나 Canno WebTest를 플러그인으로 지원한다.
Web Test를 설치하려면 다음 명령어를 입력한다.
grails install-plugin webtest
그런 후 Web Test와 Grails를 사용하려면 어떻게 해야 하는지 설명한 위키의 레퍼런스를 참조하라.
웹 계층과 함께 Grails는 서비스 계층 개념을 정의한다. Grails 팀은 핵심 애플리케이션 로직이 컨트롤러 안에 포함되지 않는 것을 권고한다. 핵심 로직이 컨트롤러에 있을 경우 재사용과 관심의 분리(separation of concerns)를 증진시킬 수 없다.
Grails의 서비스를 애플리케이션의 대부분의 로직을 넣는 장소로 여기고 컨트롤러에는 리다렉트 등의 요청을 다루는 것만 남겨둔다.
서비스 만들기 명령 프롬프트 안의 프로젝트 루트에서 create-service 명령어를 실행하여 Grails 서비스를 만들 수 있다.
grails create-service simple
이 명령어는 grails-app/services/SimpleService.groovy 위치에 서비스를 생성한다. 서비스의 이름은 약속에 의해 Service로 끝나며 서비스는 평범한 Groovy 클래스이다.
class SimpleService { }
8.1 선언적 트랜잭션 서비스는 보통 도메인 클래스간의 상호작용하는 로직을 가진다. 따라서 종종 큰 오퍼레이션으로 나눠진 저장 기능들을 수행한다. 이러한 서비스의 본질 때문에 트랜잭션이 자주 필요하다. 물론 withTransaction 메소드를 이용하여 프로그램상에서 트랜잭션을 사용할 수 있지만 이것은 반복적이고 Spring이 내재한 트랜잭션 추상화의 힘을 충분히 활용하지 못하는 경우다.
서비스는 트랜잭션 경계를 사용할 수 있으며 이것은 근본적으로 서비스의 모든 메소드가 트랜잭션에 참여할 수 있게(transactional) 만드는 선언적인 방법이다. 기본으로 모든 서비스에 트랜잭션 경계를 만들수 있다. 이것을 쓰지 않으려면 단순히 transactional 속성을 false로 한다.
class CountryService { static transactional = false }
물론 미래에 기본설정이 바뀔 경우 이 속성을 true로 바꿀 수 있다. 또는 서비스가 의도적으로 transactional하다고 명확히 하기 위해 사용할 수 있다.
주의: 선언적인 트랜잭션이 작동하기 위한 유일한 방법은 의존성 주입(dependency injection)이다. new BookService()와 같은 new 연산자를 사용하면 트랜잭션 서비스를 사용할 수 없을 것이다.
그 결과 모든 메소드를 트랜잭션으로 감싸고 메소드중 하나라도 예외를 발생하면 자동으로 롤백한다. 트랜잭션의 전파 수준(propagation level)은 기본으로 PROPAGATION_REQUIRED로 설정된다.
8.2 서비스 범위 기본으로, 서비스 메소드 접근은 동기화되어 있지 않다. 따라서 이런 메소드를 동시에 실행하는 것을 막지를 못한다. 사실, 서비스는 singleton이고 동시에 사용하기 때문에 상태를 서비스에 저장하는 것은 매우 주의를 요하는 것이다. 아니면 결코 상태를 서비스에 저장하지 않는 쉬운(보다 나은) 방법을 취한다.
서비스를 특정 범위에 넣음으로써 이러한 동작을 바꿀 수 있다. 지원하는 범위는 다음과 같다.
prototype: 매번 새로운 서비스를 생성하고 이것은 다른 클래스에 매번 넣어진다(injected).
request: Request마다 새 서비스를 생성한다.
flash: 오직 현재와 다음 request에만을 위해 새 서비스를 생성한다.
flow: 웹 플로우에서 플로우 범위마다 새 서비스를 생성한다.
conversation: 웹 플로우에서 대화 범위마다 서비스가 존재한다. 즉 루트 플로우와 그 하위 플로우에 대해서만..
session: 사용자 세션마다 서비스를 생성한다.
singleton (default): 오직 하나의 서비스 인스턴스만 존재한다.
서비스가 flash, flow 또는 conversation 범위에 있을 경우 이 서비스는 java.io.Serializable을 구현해야 한다. 그리고 오직 웹 플로우 상황에서만 사용할 수 있다.
범위 중 하나를 사용하려면 클래스에 static scope 속성을 위 범위 중 하나의 값으로 설정한다.
static scope = "flow"
8.3 의존성 주입(Dependency Injection)과 서비스
의존성 주입 기본 Grails 서비스의 핵심 중 하나는 Spring 프렘임워크의 의존성 주입 능력의 잇점을 취했다는 것이다. Grails는 약속에 의한 의존성 주입을 지원한다. 바꿔 말하면 서비스의 클래스 이름으로 속성이름을 사용하면 자동으로 서비스를 컨트롤러나 태그 라이브러리 등에 넣어(주입)준다.
예로 BookService라는 서비스가 있다고 치자. 다음과 같이 컨트롤러안에 bookService라는 속성을 정의하면
class BookController { def bookService ... }
이 경우 Spring 컨테이너는 BookService의 인스턴스를 설정된 범위에서 자동으로 BookController에 넣어준다. 모든 의존성 주입은 이름에 의해 이루어진다. Grails는 타입에 의한 의존성 주입은 지원하지 않는다. 또한 다음과 같이 타입도 지정할 수는 있다.
class AuthorService { BookService bookService }
하지만 이것은 개발 모드에서 BookService가 변경되었을 경우 리로딩 시 에러를 발생하는 불리한 점이 있긴하다.
의존성 주입과 서비스 동일한 기법으로 다른 서비스에도 서비스를 주입할 수 있다. BookService가 필요한 AuthorService가 있다고 하자. 다음과 같이 AuthorService를 선언하여 서비스를 사용할 수 있다.
class AuthorService { def bookService }
의존성 주입과 도메인 클래스 도메인 클래스에도 서비스를 주입할 수 있다. 이것은 풍부한(rich) 도메인 모델 개발을 도와준다.
class Book { ... def bookService def buyBook() { bookService.buyBook(this) } }
8.4 자바에서 서비스 사용하기 서비스의 강력한 점 중 하나는 이것이 재사용 가능한 로직으로 포장되었기에 자바 클래스는 물론 다른 클래스에서 그것들을 사용할 수 있다는 것이다. 몇가지 방법으로 자바에서 서비스를 재사용할 수 있다. 가장 간단한 방법은 서비스를 grails-app/services 디렉토리안의 패키지로 옮기는 것이다. 기본 패키지(package 선언이 없는 경우)인 경우 그 심각성 때문에 자바에서 클래스로 임포트(import)할 수 없다. 따라서 아래의 BookService는 자바에서 그대로 사용할 수 없다.
class BookService implements bookstore.BookStore { void buyBook(Book b) { // 로직 } }
자바 쪽에는 오직 인터페이스의 레퍼런스만 가지고 구현 클래스는 없기에 어쩌면 뒤의 기법이 더 깨끗하다. 두 방법 모두 자바에서 사용하는 목적을 위해 컴파일 시에 정적으로 클래스(또는 인터페이스)를 결정한다. 이제 src/java 패키지안에 자바 클래스를 생성하고 Spring에서 사용하는 타입과 bean의 이름을 이용하여 setter를 제공하면 된다.
package bookstore; // note: this is Java class public class BookConsumer { private BookStore store; public void setBookStore(BookStore storeInstance) { this.store = storeInstance; } … }
이것이 되었다면 자바 클래스를 grails-app/conf/spring/resources.xml에 Spring의 빈으로 설정할 수 있다. (더 자세한 정보는 Grails와 Spring의 관련 장을 보라)
유효성 검사의 두번째 단계는 validate나 save를 호출할 때 발생한다. 이것은 개발자가 정의한 constraints를 통해 Grails가 한계값을 검사할 때이다. 예를 들어 저장하는 save 메소드를 실행하기전 기본으로 validate를 호출한다. 따라서 다음과 같이 코드를 작성할 수 있다.
if (user.save()) { return user } else { user.errors.allErrors.each { println it } }
7.3 클라이어트에서의 유효성검사
에러 보여주기 전형적으로 유효성검사 에러가 발생하면 렌더링을 하기 위해 뷰(페이지)가 뒤로 돌아가길 원할 것이다. 또한 에러를 표현하는 방법이 필요할 것이다. Grails는 에러를 다루는 풍부한 태그들을 지원하고 있다. 단순히 목록으로 에러를 표현하고 싶다면 renderErrors를 사용할 수 있다.