행복한 아빠

[Grails1.0 사용자 가이드] 6. 웹 계층 - 1 본문

Grails

[Grails1.0 사용자 가이드] 6. 웹 계층 - 1

행복한아빠 2008. 3. 4. 18:13


6. 웹 계층 - 1

  • 컨트롤러


6.1 컨트롤러
컨트롤러는 요청(request)을 처리하고 응답을 생성하거나 준비하는데 그 범위는 request에 한정된다. 달리 말하면 각 요청(request)별로 새로운 인스턴스가 생성된다. 컨트롤러는 응답을 생성하거나 뷰(view)로 응답처리를 위임(delegate)한다. 컨트롤러를 생성하기 위해 단순히 Controller로 끝나는 이름의 클래스를 생성하며 이는 grails-app/controllers 디렉토리에 있다.

기본 URL 매핑은 컨트롤러 이름의 첫 번째 부분을 URI로 매핑하고 컨트롤러에 정의된 각 액션(action)들은 컨트롤러 이름으로 된 URI안에 매핑한다.


6.1.1 컨트롤러와 액션 이해하기

컨트롤러 생성
컨트롤러는 create-controller 명령어로 생성한다. 예들 들어 Grails 프로젝트의 루트에서 다음과 같이 실행해 보자.

grails create-controller book

이 명령어는 grails-app/controllers/BookController.groovy에 컨트롤러를 생성하는 결과를 낳는다.

class BookController { ... }

BookController는 기본으로 애플리케이션 루트를 기준으로 /book URI로 매핑된다.

사용자 삽입 이미지
create-controller 명령어는 단지 편리함을 위한 것이다. 각자가 좋아하는 텍스트 편집기나 IDE를 이용해서 쉽게 컨트롤러를 만들 수 있다.


액션 만들기
컨트롤러는 코드의 블럭이 지정된 다수의 속성을 가질 수 있다(스크립트에서는 코드도 속성으로 취급한다). 이러한 각 속성들은 URI로 매핑된다.

class BookController {
    def list = {
        // 컨트롤러 로직 수행
        // 모델 생성
        return model
    }
}

이 예제는 속성의 이름이 list이므로 기본으로 /book/list URI로 매핑한다.


기본 액션
컨트롤러는 그 컨트롤러의 루트 URI로 매핑하는 기본 URI 개념을 가지고 있다. 기본으로 이 예제의 기본 URI는 /book 이다. 기본 URI는 다음의 규칙을 이용하여 액션을 인식한다.

  • 오직 하나의 액션만 존재할 경우 컨트롤러의 기본 URI는 그 액션으로 매핑한다.
  • index 액션을 정의할 경우 이 액션이 /book URI에 액션을 지정하지 않은 상황의 요청을 처리하는 액션이 된다.
  • 다르게 defaultAction 속성으로 명시적으로 설정할 수 있다.
def defaultAction = "list"


6.1.2 컨트롤러와 범위(Scope)

유효한 scope들
Scope는 본질적으로 변수를 저장할 수 있는 객체와 같은 해시이다. 다음 scope들이 컨트롤러에서 유효하다.

  • servletContext: 애플리케이션 scope으로도 알려져 있으며 이 scope로 전체 웹 애플리케이션 사이로 상태를 공유할 수 있다. servletContext는 javax.servlet.ServletContext의 인스턴스이다.
  • session: 이 세션은 상태를 사용자와 연관하며 클라이언트와 세션을 연결하기 위해 보통 쿠키(cookies)를 사용한다. 이 세션 객체는 HttpSession의 인스턴스이다.
  • request: request 객체는 오직 현재의 요청에 대한 객체만 저장한다. 이 request 객체는 HttpServletRequest의 인스턴스이다.
  • params: 들어오는 요청(CGI) 파라메터의 변경가능한(mutable) 맵이다.
  • flash: 아래를 보라.


Scope 접근
위의 변수 이름들로 Scope들을 접근할 수 있는데 HttpServletRequest와 같은 Servlet API에서 제공하는 클래스라도 Groovy의 배열 인덱스 연산을 이용할 수 있다.

class BookController {
    def find = {
        def findBy = params["findBy"]
        def appContext = request["foo"]
        def loggedUser = session["logged_user"]
    }
}

또한 디레퍼런스(de-reference: 프로그래밍 언어에서 포인터가 가리키는 번지에 수납된 데이터에 접근하기-naver) 연산을 이용하여 이 값들을 접근할 수 있는데 문법을 명확하게 해준다.

class BookController {
    def find = {
        def findBy = params.findBy
        def appContext = request.foo
        def loggedUser = session.logged_user
    }
}

이것은 Grails가 서로 다른 scope를 일관되게 접근하는 한 가지 방법이다.


Flash scope 사용하기
Grails는 현재 요청과 바로 다음 요청에서만 유효한 속성(attribute)을 임시로 저장하기 위한 flash scope 개념을 지원한다. 그뒤로는 이 속성은 제거된다. 이것은 리다이렉션(redirection) 전에 메시지를 직접 설정할 경우 유용하다. 예를 들면

def delete = {
    def b = Book.get( params.id )
    if (!b) {
        flash.message = "User not found for id ${params.id}"
        redirect(action:list)
    }
    ... // 나머지 코드
}


6.1.3 모델과 뷰

모델 반환
모델은 본질적으로 뷰가 렌더링할 때 사용하는 맵 객체이다. 이 맵의 키들은 뷰에서 접근하는 변수 이름으로 변환된다. 모델을 반환하는 방법은 몇 가지 있는데 첫 번째 방법은 맵 인스턴스를 명시적으로 반환하는 것이다.

def show = {
    [ book : Book.get( params.id ) ]
}

명시적인 모델 반환이 없을 겨우 컨트롤러의 속성을 사용하며 따라서 다음과 같이 작성할 수 있다.

class BookController {
    List books
    List authors
    def list = {
        books = Book.list()
        authors = Author.list()
    }
}

사용자 삽입 이미지
이것은 컨트롤러가 견본(prototype) 범위라는 사실 때문에 가능하다. 다시 말해 매 요청마다 새로운 컨트롤러를 생성한다. 그렇지 않으면 위와 같은 코드는 쓰레드에 대해 안전(thread safe)하지 않을 것이다.

위의 예에서 books와 authors 속성은 뷰에서 사용할 수 있다.

좀 더 진보한 접근방법은 Spring의 ModelAndView 클래스의 인스턴스를 반환하는 것이다.

import org.springframework.web.servlet.ModelAndView
...
def index = {
    def favoriteBooks = … // index 페이지를 위해 좋아하는 책목록을 얻어온다.
    // 이것을 보여주기 위해 list 뷰로 forward한다.
    return new ModelAndView("/book/list", [ bookList : favoriteBooks ])
}


뷰 선택
위의 두 예제에는 어떤 로 렌더링할지 지정하는 코드가 어디에도 없다. 그러면 Grails는 어떤 뷰를 고를지 어떻게 알까? 답은 약속(convention)에 있다. 다음 액션에서 보면

class BookController {
    def show = {
        [ book : Book.get( params.id ) ]
    }
}

Grails는 자동으로 grails-app/views/book/show.gsp를 뷰로 찾는다. (실제는 Grails는 JSP를 먼저 찾는다. 마찬가지로 Grails JSP도 사용할 수 있다.)

다른 뷰로 렌더링하길 원할 경우 render 메소드가 도와준다.

def show = {
    def map = [ book : Book.get( params.id ) ]
    render(view:"display", model:map)
}

이 경우 Grails는 grails-app/views/book/display.gsp 위치의 뷰로 렌더링을 시도할 것이다. Grails는 자동으로 grails-app/views 디렉토리의 book 폴더 위치에서 뷰 위치를 제한하는 것을 주의하라. 이것이 Grails의 약속(convention)이다. 그러나 어떤 공유 뷰를 가지고 있을 경우 대신 이것을 접근할 필요가 있다.

def show = {
    def map = [ book : Book.get( params.id ) ]
    render(view:"/shared/display", model:map)
}

이 경우 Grails는 grails-app/views/shared/display.gsp 위치의 뷰로 렌더링을 시도할 것이다.


응답 렌더링
어떤 경우 컨트롤러에서 직접 응답에 텍스트 일부나 코드를 렌더링하는 것(전형적인 예로 Ajax 애플리케이션이 있다)이 더 쉬울 수 있다. 이것을 위해 매우 유연한 render 메소드를 사용할 수 있다.

render "Hello World!"

위 코드는 응답에 "Hello World!" 텍스트를 직접 쓴다. 다른 예제도 보자.

// 마크업(markup)을 쓰기 위해.
render {
    for(b in books) {
        div(id:b.id, b.title)
    }
}

// 특정 뷰를 렌더링
render(view:'show')

// collection의 각 아이템에 대한 템플릿을 렌더링
render(template:'book_template', collection:Book.list())

// 인코딩content type으로 텍스트 렌더링
render(text:"<xml>some xml</xml>",contentType:"text/xml",encoding:"UTF-8")


6.1.4 리다이렉트(redirect)와 체이닝(Chaining)

Redirects
모든 컨트롤러에 있는 redirect 메소드를 이용하여 액션들을 리다이렉트할 수 있다.

class OverviewController {
    def login = { }
    def find = {
        if (!session.user)
            redirect(action:login)
        ...
    }
}

내부적으로 redirect 메소드는 HttpServletResponse 객체의 sendRedirect 메소드를 사용한다.

redirect 메소드는 다음의 것들이 올 수 있다.

  • 같은 컨트롤러 클래스의 다른 클로우저(closure)
// 같은 클래스 내의 login 액션을 호출
redirect(action:login)
  • 컨트롤러와 액션 이름
// home 컨트롤러의 index 액션으로 redirect
redirect(controller:'home',action:'index')
  • 애플리케이션 context 패스 상의 리소스를 위한 URI
// 명시적인 URI로 리다이렉트
redirect(uri:"/login.html")
  • 또는 완전한 URL
// URL로 리다이렉트
redirect(url:"http://grails.org")

선택적으로 메소드의 params 아규먼트를 이용하여 한 액션에서 다음 액션으로 파라메터를 전달할 수 있다.

redirect(action:myaction, params:[myparam:"myvalue"])

이 파라메터들은 params 동적 프로퍼티로 만들어져 request 파라메터에서 접근할 수 있다. request 파라메터와 동일한 이름으로 파라메터가 설정되면 request 파라메터를 덮어쓰고 컨트롤러의 파라메터를 사용한다.

params 객체 역시 맵이므로 현재의 request 파라메터를 다음 액션으로 전달할 수 있다.

redirect(action:"next", params:params)


Chaining
액션들을 서로 연결할 수 있다. Chaining은 한 액션에서 다음 액션으로 모델을 유지하도록 한다. 아래 액션에서 first 액션 호출을 예로 들어보자.

class ExampleChainController {
    def first = {
        chain(action:second,model:[one:1])
    }
    def second  = {
        chain(action:third,model:[two:2])
    }
    def third = {
         [three:3]
    }
}

모델의 결과는 다음과 같다.

[one:1, two:2, three:3]

다음 체인의 컨트롤러 액션에서 chainModel 맵을 통해 모델을 접근할 수 있다. 이 동적인 프로퍼티는 chain 메소드로 호출한 액션에서만 존재한다.

class ChainController {
    def nextInChain = {
        def model = chainModel.myModel
        ...
    }
}

redirect 메소드와 같이 chain 메소드로 파라메터를 전달할 수도 있다.

chain(action:"action1", model:[one:1], params:[myparam:"param1"])


6.1.5 컨트롤러 인터셉터(Interceptor)
가끔 request, session 또는 애플리케이션 상태 처리를 가로채는 것이 유용할 때가 있다. 액션 인터셉터로 이것을 이룰 수 있는데 before와 after 두 가지 종류가 있다.

사용자 삽입 이미지
인터셉터가 하나 이상의 컨트롤러에 적용할 것 같으면 Filter 작성이 확실히 낫다. 이것은 각 컨트롤러의 로직을 변경하지 않고 여러 개의 컨트롤러에 적용하거나 URI를 가로질러(cross cut) 적용한다.


Before 가로채기
beforeInterceptor는 액션이 실행되기전 진행을 가로챈다. 이것이 false를 반환할 경우 채로채어진 액션을 실행하지 않는다. 아래와 같이 컨트롤러의 모든 액션에 대해 인터셉터를 정의할 수 있다.

def beforeInterceptor = {
    println "Tracing action ${actionUri}"
}

위 예는 컨트롤러 정의의 몸체안에 선언했다. 모든 액션 전에 실행되며 처리를 방해하지 않는다. 어쨌거나 일반적인 사용 예는 인증과 관련된 예이다.

def beforeInterceptor = [action:this.&auth,except:'login']
// 일반 메소드로 정의하여 private로 만든다.
def auth() {
    if(!session.user) {
        redirect(action:'login')
        return false
    }
}
def login = {
     // 로그인 페이지를 보여준다.
}

위 코드는 auth라는 메소드를 정의했다. 메소드를 사용하여 외부로 액션으로 노출되지 않는다(즉 private이다). 그리고 beforeInterceptor는 login을 '제외(except)'한 모든 액션들에 적용하고 'auth' 메소드를 실행하도록 정의한다. 'auth'메소드는 Groovy의 메소드 포인터 문법으로 참조하고 이 메소드 자체는 세션에 사용자가 있을지 검사하고 없을 경우 location 액션으로 리다이렉트하고 false를 반환하여 더 이상 진행되지 않도록 한다.


After 가로채기
액션 실행 후 실행되는 인터셉터를 정의하기 위해서는 afterInterceptor 속성을 사용한다.

def afterInterceptor = { model ->
    println "Tracing action ${actionUri}"
}

after 인터셉터는 결과 모델을 아규먼트로 가지며 따라서 모델이나 응답을 후에 조작할 수 있다.

또한 after 인터셉터는 Spring MVC ModelAndView 객체를 렌더링 전 수정할 수 있다. 이 경우 위의 예제는 다음과 같아질 것이다.

def afterInterceptor = { model, modelAndView ->
    println "Current view is ${modelAndView.viewName}"
    if(model.someVar) modelAndView.viewName = "/mycontroller/someotherview"
    println "View is now ${modelAndView.viewName}"
}

이것은 현재의 액션이 반환하는 모델에 따라 뷰를 바꾸는 것을 가능하게 한다. 가로채어진 액션이 redirect나 render를 호출한 경우 modelAndView 는 null일 수 있다는 것에 주의하라.


가로채기 조건
Rails 사용자는 이 인증 예제와 인터셉터 실행에 '예외(except)' 조건을 어떻게 사용하는지 익숙할 것이다. (Rails에서 인터셉터를 필터라고 부른다. 이 용어는 자바 세계의 서블릿 필터와 충돌한다.)

def beforeInterceptor = [action:this.&auth,except:'login']

이 코드는 특정 액션을 제외한 모든 액션에 대해 인터셉터를 실행한다. 액션의 목록을 아래와 같이 정의할 수도 있다.

def beforeInterceptor = [action:this.&auth,except:['login','regiter']]

지원하는 다른 조건은 'only'이다. 이것은 오직 특정 액션에 대해 인터셉터를 실행한다.

def beforeInterceptor = [action:this.&auth,only:['secure']]


6.1.6 데이터 바인딩
데이터 바인딩은 들어오는 요청 파라메터를 객체의 속성이나 전체 객체 그래프에 "집어넣는(binding)" 행위이다. 데이터 바인딩은 보통 폼 submit으로 발생하는 요청 파라메터으로 필요한 형변환을 다뤄야한다. Groovy나 자바 객체의 속성이 잘 정의되지 않은 경우 항상 문자열이다.

Grails 데이터 바인딩 수행을 위해 Spring 기반의 데이터 바인딩 기능을 사용한다.

요청 데이터를 모델에 바인딩하기
요청 파라메터를 도메인 클래스의 속성으로 바인드하기 위해 두 가지 방법이 있다. 첫번째는 도메인 클래스의 암시적인 생성자를 쓰는 것이다.

def save = {
  def b = new Book(params)
  b.save()
}

new Book(params) 코드에서 데이터 바인딩이 발생한다. 도메인 클래스 생성자에 params 객체를 넘겨주면 Grails는 자동으로 요청 파라메터로 바인딩을 시도하는 것을 알아챈다. 따라서 다음과 같이 들어오는 요청이 있다면

/book/save?title=The%20Stand&author=Stephen%20King

title과 author 요청 파라메터를 자동으로 얻고 도메인 클래스에 설정한다. 이미 존재하는 인스턴스에 데이터 바인딩을 수행할 필요가 있다면 properties 속성을 사용할 수 있다.

def save = {
    def b = Book.get(params.id)
    b.properties = params
    b.save()
}

이것은 암묵적인 생성자를 사용할 때와 정확히 동일한 효과를 낸다.


데이터 바인딩과 관계(Associations)
일-대-일
또는 多-대-일 관계를 가졌을 경우 이 관계를 수정하기 위해 Grails의 데이터 바인딩 기능을 사용할 수도 있다. 예를 들어 다음과 같이 들어오는 요청이 있다고 하자.

/book/save?author.id=20

다음과 같이 데이터 바인딩을 할 때 Grails는 자동으로 요청 파라메터에서 .id 접미어를 인식하고 해당 id를 가진 Author 인스턴스를 찾는다.

def b = new Book(params)


여러 개의 도메인 클래스와 데이터 바인딩하기
params 객체로 여러 개의 도메인 객체의 데이터와 바인딩하는 것이 가능하다.

예로 다음과 같이 들어오는 요청이 있다고 하자

/book/save?book.title=The%20Stand&author.name=Stephen%20King

각 파라메터가 author. 이나 book. 같은 접두어를 갖는다는 점에서 위의 요청과 다르다는 것을 볼 수 있다. 파라메터가 어느 타입에 속하는지 분리하는데 이것을 사용한다. Grails의 params 객체는 다차원 해시와 같아서 바인드하기 위해 파라메터의 일정 부분집합만을 참조할 수 있다.

def b = new Book(params['book'])

book.title 파라메터를 이 레벨 아래만 분리하기 위해 접두어를 어떻게 사용하는지 볼 수 있다. Author 도메인 클래스에도 똑같이 할 수 있다.

def a = new Author(params['author'])


데이터 바인딩과 형 변환 에러
어떤 경우는 데이터 바인딩을 수행할 때 일부 문자열이 일부 목적하는 형으로 변환이 불가능할 때가 있다. 이럴 때 형 변환 에러가 난다. Grails는 Grails 도메인 클래스의 errors 속성 안에 형 변환 에러들을 유지한다. 다음 예제를 보자.

class Book {
    ...
    URL publishURL
}

여기에서 URL을 표현하기 위해 도메인 클래스 Book이 자바의 구체적인 형인 java.net.URL을 사용했다. 이제 다음과 같이 들어오는 요청이 있다고 하자.

/book/save?publisherURL=a-bad-url

이 경우 a-bad-url 문자열을 publishURL 속성에 바인드가 불가능하고 형 불일치 에러가 발생한다. 다음과 같이 이것을 검사할 수 있다.

def b = new Book(params)
if (b.hasErrors()) {
    println "The value ${b.errors.getFieldError('publisherURL').rejectedValue} is not a valid URL!"
}

아직 에러 코드들에 대해 다루지 않았지만(Validation 장에 더 자세히) 형 변환 에러를 위해 grails-app/i18n/messages.properties 파일 안의 에러 메시지를 사용하기를 원할 것이다. 다음과 같이 일반 에러 메시지 핸들러를 사용할 수 있다.

typeMismatch.java.net.URL=The field {0} is not a valid URL

또는 더 구체적으로 지정할 수 있다.

typeMismatch.Book.publisherURL=The publisher URL you specified is not a valid URL


데이터 바인딩에서 보안에 대해 고려할 점
요청 파라메터로 속성을 일괄 수정할 때 클라인언트가 악의적인 데이터를 도메인 클래스에 바인드하고 결국 데이터베이스에 저장하지 못하도록 주의하여야 한다.

이 문제는 몇 가지 방법으로 빠져나갈 수 있는데 그 한 방법은 Command Objects를 사용하는 것이다. 여기서 말할 또 다른 방법은 유연한 bindData 메소드를 사용하는 것이다.

bindData는 동일한 데이터 바인딩 능력을 가졌다. 그러나 이것으로는 도메인 객체뿐만 아니라 어떤 객체에도 바인드 할 수 있다.

def sc = new SaveCommand()
bindData(sc, params)

그리고 bindData 메소드는 수정을 원하지 않는 특정 파라메터를 제외할 수 있도록 할 수 있다.

def sc = new SaveCommand()
bindData(sc, params, ['myReadOnlyProp'])


6.1.7 XML과 JSON 응답

XML 결과물을 위해 render 메소드 사용하기
Grails는 XML과 JSON 응답을 만들기 위해 좀 다른 방법을 지원한다. 여기서 말할 첫번째 방법은 render 메소드를 이용하는 것이다.

XML에 마크업을 구성하기 위해 render 메소드에 코드 블럭을 전달할 수 있다.

def list = {
    def results = Book.list()
    render(contentType:"text/xml") {
        books {
            for(b in results) {
                book(title:b.title)
            }
        }
    }
}

위 코드의 결과는 다음과 같을 것이다.

<books>
    <book title="The Stand" />
    <book title="The Shining" />
</books>

마크업 구성에서 이름이 충돌되지 않도록 주의해야 한다. 예를 들어 아래 코드는 에러를 발생한다.
def list = {
    def books = Book.list()    // 여기에서 이름이 충돌
    render(contentType:"text/xml") {
        books {
            for(b in results) {
                book(title:b.title)
            }
        }
    }
}

그 이유는 Groovy가 메소드를 호출하려는데 books 지역 변수가 있어서 그렇다.


JSON 결과물을 위해 render 메소드 사용하기
JSON 결과물을 만들기 위해 역시 render 메소드를 사용할 수 있다.

def list = {
    def results = Book.list()
    render(contentType:"text/json") {
        books {
            for(b in results) {
                book(title:b.title)
            }
        }
    }
}

이 경우 그 결과는 다음과 같을 것이다.

[
    {title:"The Stand"},
    {title:"The Shining"}
]

이름 충돌의 위험은 JSON 구성에도 적용된다.


자동 XML 마셜링(Marshalling)
또한 Grails는 특별한 변환기를 통해 도메인 클래스자동으로 XML로 marshalling하는 것을 지원한다.

컨트롤러에 grails.converters 패키지를 임포트하는 것으로 시작한다.

import grails.converters.*

이제 도메인 클래스를 XML으로 자동 변환하기 위해 다음과 같이 매우 읽기 쉬운 문법을 사용할 수 있다.

render Book.list() as XML

그 결과는 다음과 같을 것이다.

<?xml version="1.0" encoding="ISO-8859-1"?>
<list>
    <book id="1">
        <author>Stephen King</author>
        <title>The Stand</title>
    </book>
    <book id="2">
        <author>Stephen King</author>
        <title>The Shining</title>
    </book>
</list>

변환기를 사용하기 위한 다른 대안은 Grails의 codecs 기능을 사용하는 것이다. codecs 기능은 encodeAsXMLencodeAsJSON 메소드를 지원한다.

def xml = Book.list().encodeAsXML()
render xml

XML marshaling의 더 자세한 정보는 REST 장을 보라.


자동 JSON 마셜링(Marshalling)
동일한 메커니즘
으로 Grails는 또한 자동 JSON marshaling을 지원한다. 단순히 XML을 JSON으로 교체한다.

render Book.list() as JSON

결과는 다음과 같을 것이다.

[
    {"id":1,
     "class":"Book",
     "author":"Stephen King",
     "title":"The Stand"},
    {"id":2,
     "class":"Book",
     "author":"Stephen King",
     "releaseDate":new Date(1194127343161),
     "title":"The Shining"}
 ]

다시 말하지만, 동일한 효과를 내기 위한 또 다른 대안은 encodeAsJSON을 사용하는 것이다.


6.1.8 파일 업로드

프로그래밍 방법으로 파일 업로드 하기
Grails는 Spring의 MultipartHttpServletRequest를 통해 파일 업로드를 지원한다. 파일을 업로드하기 위한 첫번째 단계는 다음과 같이 multipart 폼을 만드는 것이다.

Upload Form: <br />
    <g:form action="upload" method="post" enctype="multipart/form-data">
        <input type="file" name="myFile" />
        <input type="submit" />
    </g:form>

그런 다음에 파일 업로드를 다루기 위해 몇 가지 방법이 있다. 첫번째 방법은 Spring의 MultipartFile 인스턴스를 직접 사용하는 것이다.

def upload = {
    def f = request.getFile('myFile')
    if(!f.empty) {
        f.transferTo( new File('/some/local/dir/myfile.txt') )
        response.sendError(200, 'Done')
    }
    else {
        flash.message = 'file cannot be empty'
        render(view:'uploadForm')
    }
}

이것은 MultipartFile 인터페이스를 통해 다른 곳으로 파일을 이동하거나 InputStream을 얻어 직접 파일을 조작하는 등에 확실히 유용한 방법이다.


데이터 바인딩을 이용한 파일 업로드
데이터 바인딩으로 파일 업로드 수행될 수도 있다. 예를 들어 다음 예제와 같이 Image 도메인 클래스가 있다고 하자.

class Image {
    byte[] myFile
}

이제 image를 생성하고 params 객체를 아래와 같이 전달하면 Grails는 자동으로 파일의 내용을 myFile 속성의 byte로 바인드할 것이다.

def img = new Image(params)

image의 myFile 속성을 String 형으로 바꿈으로써 파일의 내용을 문자열로 설정하는 것도 가능하다.

class Image {
    String myFile
}


6.1.9 명령 객체(Command Objects)
Grails 컨트롤러는 명령 객체(command object) 개념을 지원한다. 명령 객체는 Struts 같은 것의 폼 bean과 비슷하며 도메인 클래스를 수정하기 위해 필요한 파라메터 프로퍼티의 부분집합을 추출(populate)하는 상황에서 유용하다. 또는 사용할 수 있는 도메인 객체 없이 데이터 바인딩유효성 검사 같은 기능이 필요할 때도 유용하다.

명령 객체 선언
명령 객체는 일반적으로 컨트롤러 클래스 정의 바로 아래 같은 소스 파일에 선언한다. 예를 들면

class UserController {
    …
}
class LoginCommand {
    String username
    String password
    static constraints = {
        username(blank:false, minSize:6)
        password(blank:false, minSize:6)
    }
}

위 예제는 도메인 클래스에서 할 수 있는 것과 같이 명령 객체에 constraints를 제공할 수 있는 것을 보여준다.


명령 객체 사용하기
명령 객체를 사용하기 위해서는 선택적으로 컨트롤러 액션에 명령 객체 파라메터를 몇 개든 지정할 수 있다. 파라메터 타입을 반드시 지정해야 하는데 그래야만 Grails가 어떤 객체를 생성하고 파라메터 매핑(populate)하고 유효성 검사를 할 지 알 수 있다.

컨트롤러 액션을 실행하기전 Grails는 자동으로 명령 객체 클래스의 인스턴스를 생성하고, 대응하는 이름을 가진 요청 파라메터로 명령 객체의 속성을 설정하고, 유효성 검증을 실행한다. 예를 들면

class LoginController {
    def login = { LoginCommand cmd ->
        if(cmd.hasErrors()) {
            redirect(action:'loginForm')
        }
        else {
            // 그 외 다른 것
        }
    }
}


명령 객체와 의존주입(Dependency Injection)
명령객체에 dependency injection를 사용할 수 있다. 이것은 명령 객체가 Grails 서비스와 연동할 필요가 있는 사용자 정의 유효성 검사 로직을 가지고 있을 경우 유용하다.

class LoginCommand {
    def loginService
        String username
        String password
        static constraints = {
            username(validator: {
                loginService.canLogin(username, password)
            })
        }
}

이 예제에서, 명령 객체는 Spring ApplicationContext에서 이름으로 주입된(injected) bean과 연동한다.

---
원문: 6. The Web Layer

0 Comments
댓글쓰기 폼