Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 27장 동적 언어 지원

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.



27. 동적 언어 지원


27.1 소개

스프링 2.0부터 스프링에서 동적 언어(JRuby 등)를 사용해서 클래스와 객체를 정의할 수 있도록 지원하고 있다. 그래서 지원하는 동적 언어로 클래스 다수를 작성해서 투명하게 스프링 컨테이너를 인스턴스화하고 객체를 의존성 주입하도록 구성할 수 있다.

현재 지원하는 동적 언어

  • JRuby 0.9 / 1.0
  • Groovy 1.0 / 1.5
  • BeanShell 2.0

바로 사용할 수 있는 동적 언어의 완전한 예제는 Section 27.4, “시나리오”에 나와 있다.

Note: 스프링 2.5에서는 위에 명시한 버전만 지원한다. 특히 JRuby 1.1(상당히 많은 API 변경이 있었다)은 현재 지원하지 않는다.

왜 일부 언어만 지원하는가?
지원하는 언어는 a) 자바 엔터프라이즈 커뮤니티에서 많은 인기를 가진 언어이면서 b) 스프링 2.0 개발 기간 동안 다른 언어에 대한 요청도 없었고 c) 스프링 개발자가 가장 익숙한 언어이기 때문에 선택했다. 그래도 언어는 계속 추가할 예정이다. <자신이 좋아하는 동적 언어를 추가하고 싶다면> 언제든 스프링의 JIRA에 이슈를 올릴 수 있다.(또는 직접 구현하거나)


27.2 첫 예제

이번 장에서 동적 언어 지원을 자세히 설명하는데 자세히 살펴보기 전에 동적 언어로 정의한 빈의 예제를 가볍게 살펴보자. 처음 작성할 빈의 동적 언어는 Grooby이다.(이 예제는 스프링 테스트 슈트에서 가져왔으므로 다른 지원 언어의 예제를 보고 싶다면 소스코드를 참고해라.)

아래 예제에서 Grooby 빈으로 구현할 Messenger 인터페이스를 보자. 이 인터페이스는 일반적인 자바로 정의되어 있다. Messenger에 참조로 주입할 의존 객체는 Groovy 스크립트로 구현되었다는 것을 알지 못한다.

package org.springframework.scripting;

public interface Messenger {
  String getMessage();
}

Messenger 인터페이스에 의존성을 가진 클래스 정의가 다음에 나와 있다.

package org.springframework.scripting;

public class DefaultBookingService implements BookingService {
  private Messenger messenger;

  public void setMessenger(Messenger messenger) {
    this.messenger = messenger;
  }

  public void processBooking() {
    // 주입된 Messenger 객체를 사용한다...
  }
}

다음은 Groovy로 작성한 Messenger 인터페이스의 구현체이다.

// from the file 'Messenger.groovy'
package org.springframework.scripting.groovy;

// 구현할 Messenger 인터페이스(Java로 작성된)를 임포트한다
import org.springframework.scripting.Messenger

// Groovy로 구현을 정의한다
class GroovyMessenger implements Messenger {
  String message
}

마지막으로 DefaultBookingService 클래스의 인스턴스에 Groovy로 정의된 Messenger 구현체를 주입할 때 영향을 주는 빈 정의가 다음에 나와 있다.

Note
동적 언어로 작성한(dynamic-language-backed) 빈을 정의하는 커스텀 동적 언어 태그를 사용하려면 스프링 XML 설정파일 상단에 XML 스키마를 선언해야 하고 IoC 컨테이너로 스프링 ApplicationContext 구현체를 사용하도록 해야 한다. 동적 언어로 작성한 빈을 일반적인 BeanFactory 구현체와 사용하는 것도 지원하지만, 이 동작을 하는 스프링 내부의 시스템을 관리해야 한다. 스키마에 기반을 둔 구성에 대한 자세한 내용은 Appendix C, XML Schema-based configuration를 참고해라.


주입된 Messenger 인스턴스가 Messenger 인스턴스이므로 bookingService 빈(DefaultBookingService)은 private messenger 멤버 변수를 평소처럼 사용할 수 있다. 딱히 특별할 것도 없이 그냥 일반적인 Java와 Groovy다.

이상적으로는 위의 XML 스니펫만으로 이해할 수 있어야 하지만 이해하지 못하더라도 크게 걱정할 필요는 없다. 왜 위와 같은 설정을 사용했는지에 대한 자세한 내용은 이어서 나올 것이다.

27.3 동적 언어로 작성된 빈 정의

이번 장에서 스프링이 관리하는 빈을 지원하는 동적언어로 정의하는 방법을 설명한다.

이번 장에서 동적 언어의 문법을 설명하지는 않는다. 예를 들어 애플리케이션에서 특정 클래스를 Groovy로 작성하려고 하면 이미 Groovy를 알고 있다고 전제한다. 동적 언어에 대해서 자세히 알고 싶다면 이번 장 마지막의 Section 27.6, “추가 자료”를 참고해라.

27.3.1 공통 개념

동적 언어에 기반을 둔 빈을 사용하는 과정은 다음과 같다.

  1. 동적 언어 소스코드의 테스트를 작성한다.(당연히)
  2. 그 다음 동적 언어 소스 코드 자체를 작성한다 :)
  3. XML 설정에 적절한 <lang:language/> 요소를 사용해서 동적 언어에 기반을 둔 빈을 정의한다. (물론 스프링 API를 사용해서 프로그래밍적으로 이러한 빈을 정의할 수도 있다. - 이번 장에서는 이러한 방식의 고급 설정을 다루지 않으므로 어떻게 정의하는지 알고 싶다면 소스 코드를 참고해야 하지만) 이는 반복되는 과정이다. 동적 언어 소스 파일마다 최소 하나의 빈 정의가 필요할 것이다.(물론 여러 빈 정의가 같은 동적 언어 소스 파일을 참조할 수 있지만)

처음 두 과정(동적 언어 소스파일을 테스트하고 작성)은 이 장의 범위를 넘어간다. 사용하는 동적 언어의 명세와 레퍼런스 문서를 참고해서 동적 언어 소스를 작성해라. 스프링의 동적 언어 지원은 동적 언어 소스파일의 내용을 이해한다는 (약간의) 가정을 하고 있으므로 이번 장을 계속 읽으려면 동적 언어의 문서를 먼저 보아야 할 것이다.

27.3.1.1 <lang:language/> 요소

마지막 단계는 동적 언어에 기반을 둬서 사용할 빈 정의를 정의하는 것이다.(일반적인 JavaBean 설정과 다르지 않다.) 하지만 컨테이너가 인스턴스화하고 구성할 클래스의 정규화된 클래스 명을 지정하는 대신 동적 언어에 기반을 둔 빈을 정의할 때는 <lang:language/> 요소를 사용한다.

지원하는 각 언어는 각각 <lang:language/> 요소를 가진다.

  • <lang:jruby/> (JRuby)
  • <lang:groovy/> (Groovy)
  • <lang:bsh/> (BeanShell)

설정에 사용할 수 있는 속성과 자식 요소는 빈을 정의한 언어에 따라 다르다. (언어에 특화된 부분은 아래에 나와있다.)

XML 스키마
이번 장의 모든 설정 예제는 스프링 2.0에서 새로 추가된 XML 스키마를 사용한다. XML 스키마의 사용하기 전에 예전 방식으로 스프링 XML 파일을 DTD 기반으로 유효성 검사하는 것이 가능하지만 이렇게 하면 요소의 편리함을 얻지 못할 것이다. XML 스키마 기반의 밸리데이션이 필요없는 예전 방식의 설정 예제는 스프링 테스트 슈트를 참고해라.(꽤 복잡하고 사용하는 스프링 구현이 모두 나와 있다.)


27.3.1.2 갱신 가능한(Refreshable) 빈

스프링의 동적 언어 지원에서 가장 강력한 장점(유일한 장점은 아니다.) 중 하나는 '갱신 가능한 빈' 기능이다.

갱신 가능한 빈은 동적 언어로 작성한 빈에 약간의 설정을 추가한 것이다. 동적 언어로 작성한 빈은 사용하는 소스파일 리소스의 변경사항을 감시할 수 있어서 소스파일이 변경되었을 때 재로드할 수 있다.(예를 들어 개발자가 파일시스템에서 파일을 수정하고 저장했을 때)

이를 통해 개발자는 애플리케이션에 다수의 동적 언어 소스파일을 포함해서 배포하고 스프링 컨테이너가 동적 언어 소스파일로 빈을 생성하도록 구성하고(사용하는 메커니즘은 이번 장에서 설명한다.) 나중에 요구사항 변경이 있거나 다른 외부요소가 영향을 주거나 동적 언어 소스파일을 변경했을 때 변경사항을 빈에 적용할 수 있다. 운영하는 애플리케이션을 종료할 필요가 없다.(웹 애플리케이션인 경우에는 재배포할 필요가 없다) 이렇게 수정된 동적 언어로 작성된 빈은 변경된 동적 언어 소스파일에서 새로운 상태와 로직을 가져와서 적용할 것이다.

Note
이 기능은 기본적으로는 비활성화되어 있다.


갱신 가능한 빈을 사용하기가 얼마나 쉬운지 예제를 살펴보자. 갱신 가능한 빈 기능을 활성화하려면 빈 정의에서 <lang:language/> 요소에 딱 하나의 속성을 추가하면 된다. 이번 장 앞부분의 예제를 다시 사용해서 갱신 가능한 빈을 사용하는 스프링 XML 구성이 다음에 나와 있다.

<beans>
  
  <lang:groovy id="messenger"
      refresh-check-delay="5000" <!-- 5초마다 갱신을 확인하도록 설정한다 -->
      script-source="classpath:Messenger.groovy">
    <lang:property name="message" value="I Can Do The Frug" />
  </lang:groovy>

  <bean id="bookingService" class="x.y.DefaultBookingService">
    <property name="messenger" ref="messenger" />
  </bean>
</beans>

개발자가 해야 하는 것은 이것이 전부다. 'messenger' 빈 정의의 'refresh-check-delay' 속성은 사용하는 동적 언어 소스파일의 변경사항으로 빈을 갱신할 밀리 초를 정의한다. 'refresh-check-delay' 속성에 음수를 할당하면 갱신 동작을 끌 수 있다. 기본적으로 갱신 기능은 비활성화되어 있다는 점을 유념해라. 갱신기능이 필요 없다면 속성을 정의하지 않으면 된다.

다음 애플리케이션을 실행하면 갱신 가능한(refreshable) 빈 기능을 사용할 수 있다. 다음 코드에서 '실행을 멈추도록 하는' 불필요한 부분은 크게 신경 쓰지 마라. 다음 코드에서 개발자가 동적 언어 소스파일을 열어서 수정하는 동안 프로그램의 실행이 멈춰서 프로그램이 다시 실행될 때 동적 언어를 사용한 빈이 갱신되도록 System.in.read() 호출이 이곳에만 존재한다.

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;

public final class Boot {
  public static void main(final String[] args) throws Exception {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
    Messenger messenger = (Messenger) ctx.getBean("messenger");
    System.out.println(messenger.getMessage());
    // 소스파일을 열어서 수정하는 동안 실행을 멈춘다...
    System.in.read();
    System.out.println(messenger.getMessage());
  }
}

이 예제의 목적에 따라 Messenger 구현체의 getMessage() 메서드에 대한 모든 호출은 메시지를 따옴표로 감싸야 한다고 가정해 보자. 프로그램의 실행이 멈춰있을 때 Messenger.groovy 소스파일을 아래와 같이 수정했다.

package org.springframework.scripting

class GroovyMessenger implements Messenger {
  private String message = "Bingo"

  public String getMessage() {
    // 메시지를 따옴표로 감싸도록 구현체를 변경한다
    return "'" + this.message + "'"
  }

  public void setMessage(String message) {
    this.message = message
  }
}

프로그램 실행했을 때 입력을 멈추기 전에 출력은 I Can Do The Frug가 될 것이다. 소스파일을 변경하고 저장한 후에는 프로그램이 다시 실행되고 동적 언어로 작성한 Messenger 구현체에서 getMessage() 메서드를 호출하면 'I Can Do The Frug'가 될 것이다. (따옴표가 추가되었다.)

'refresh-check-delay' 값의 범위내에서 변경이 되더라도 스크립트의 변경이 리프레시를 발생시키는 것이 아니라는 점을 이해하는 것이 중요하다. 동적 언어로 작성한 빈의 메서드를 호출할 때까지 스크립트의 변경이 실제 '적용되지' 않는다는 점을 이해하는 것도 마찬가지로 중요하다. 동적 언어로 작성한 빈에서 메서드 호출이 일어났을 때만 사용하는 스크립트 소스가 변경되었는지 확인한다. 스크립트 갱신과 관련한 모든 예외는 호출코드로 전파하는 fatal 예외가 된다.

앞에서 설명한 갱신 가능한 빈의 동작은 <lang:inline-script/> 요소 표기로 정의한 동적언어 소스 파일에는 적용되지 않는다. (Section 27.3.1.3, “인라인 동적 언어 소스 파일” 참고) 또한 변경이 일어난 소스파일을 실제로 탐지할 수 있는 경우에만 빈에 적용된다. 예를 들면 파일시스템에서 동적언어 소스파일의 최종 수정일을 확인하는 코드를 사용하는 등이다.

27.3.1.3 인라인 동적 언어 소스 파일

동적 언어 지원은 스프링 빈 정의에 직접 삽입한 동적 언어 소스파일 기능도 제공한다. 더 자세히 얘기하면 <lang:inline-script/> 요소로 스프링 설정파일에서 바로 동적 언어 소스를 정의할 수 있다. 예제를 보면 인라인 스크립트 기능을 확실히 알 수 있을 것이다.

<lang:groovy id="messenger">
  <lang:inline-script>
package org.springframework.scripting.groovy;

import org.springframework.scripting.Messenger

class GroovyMessenger implements Messenger {

    String message
}
  </lang:inline-script>
  <lang:property name="message" value="I Can Do The Frug" />
</lang:groovy>

스프링 설정파일에 동적 언어 소스를 정의하는 좋은 방법인가에 대한 부분은 제쳐놓고 <lang:inline-script/> 요소는 일부 시나리오에서 유용할 수 있다. 예를 들어 스프링 MVC Controller에 스프링 Validator 구현체를 빠르게 추가하고자 할 때 인라인 소스가 유용한 경우이다. (예시는 Section 27.4.2, “스크립트로 작성한 밸리데이터(Validator)”를 참고해라.)

inline: 문법으로 스프링 XML 설정파일에서 JRuby로 작성한 빈 소스를 바로 정의하는 예제가 다음에 나와 있다. ('<' 문자를 표시하려고 <를 사용했다. 이러면 <![CDATA[]]>로 인라인 소스를 감싸는 것이 더 좋다.)

<lang:jruby id="messenger" script-interfaces="org.springframework.scripting.Messenger">
  <lang:inline-script>
require 'java'

include_class 'org.springframework.scripting.Messenger'

class RubyMessenger &lt; Messenger
  def setMessage(message)
    @@message = message
  end

  def getMessage
    @@message
  end
end
  </lang:inline-script>
  <lang:property name="message" value="Hello World!" />
</lang:jruby>


27.3.1.4 동적 언어로 작성한 빈의 컨텍스트의 생성자 주입 이해하기

스프링 동적 언어 지원과 관련해서 알아야 할 아주 중요한 내용이 있다. 즉, (현재는) 동적 언어로 작성한 빈에 생성자 인자를 제공할 수 없다.(그래서 동적 언어 빈에는 생성자 주입을 사용할 수 없다.) 생성자와 프로퍼티에 관련한 이 특수한 처리를 100% 이해하기 위해 동작하지 않는 다음 코드와 설정을 보자.

// from the file 'Messenger.groovy'
package org.springframework.scripting.groovy;

import org.springframework.scripting.Messenger

class GroovyMessenger implements Messenger {
  GroovyMessenger() {}

  // 이 생성자는 생성자 주입을 사용할 수 없다
  GroovyMessenger(String message) {
  this.message = message;
  }

  String message

  String anotherMessage
}
<lang:groovy id="badMessenger"
  script-source="classpath:Messenger.groovy">

  <!-- 이다음의 생성자 인자는 GroovyMessenger에 주입되지 *않는다* -->
  <!-- 사실 이는 스키마를 따르도록 하지도 않는다 -->
  <constructor-arg value="이는 동작하지 *않을 것이다*" />

  <!-- only 프로퍼티 값은 동적 언어로 작성한 객체에 주입된다 -->
  <lang:property name="anotherMessage" value="Passed straight through to the dynamic-language-backed object" />
</lang>

개발자들은 대부분 setter 주입을 선호하므로 실제로 이 제약사항이 처음 느낌만큼 크게 문제 되지는 않는다.(이에 대한 논의는 나중을 위해 남겨두자.)

27.3.2 JRuby 빈

JRuby 홈페이지에서...

“ JRuby는 Ruby 프로그래밍 언어를 100% 순수히 자바로만 구현한 구현체이다. ”
선택권을 제공하는 스프링의 철학에 따라 스프링의 동적 언어도 JRuby 언어로 정의한 빈을 지원한다. JRuby 언어는 아주 직관적인 Ruby 언어에 기반을 두고 있어서 인라인 정규표현식, 블락(클로저)과 특정 도메인 문제를 훨씬 쉽게 개발할 수 있는 다른 기능들을 모두 지원한다.

스프링의 JRuby 동적 언어 지원 구현체는 무슨 일이 이뤄지는지에 관심을 둔다. 요소의 'script-interfaces' 속성값에 지정한 모든 인터페이스를 구현하는 JDK 동적 프락시를 스프링이 생성한다. (그래서 'script-interfaces' 속성값에 최소 하나의 인터페이스를 반드시 해야 한다. 이를 통해 JRuby기반의 빈을 사용할 때 프로그램이 연결한다.)

JRuby로 작성한 빈을 사용한 완전한 예제를 보자. 이번 장 앞부분에서 정의한 Messenger 인터페이스의 JRuby 구현체가 다음에 나와 있다.(보기 쉽게 아래 다시 작성했다.)

JRuby 라이브러리 의존성
스프링의 JRuby 스크립트 지원을 사용하려면 애플리케이션 클래스패스에 다음 라이브러리가 필요하다.(명시된 버전은 JRuby 스크립트 지원 개발 시에 스프링 팀이 사용한 버전일 뿐이다. 해당 라이브러리의 다른 버전도 아마 잘 동작할 것이다.)

  • jruby.jar
  • cglib-nodep-2.1_3.jar


package org.springframework.scripting;

public interface Messenger {
  String getMessage();
}
require 'java'

class RubyMessenger
  include org.springframework.scripting.Messenger

  def setMessage(message)
    @@message = message
  end

  def getMessage
    @@message
  end
end

# 이 마지막 라인은 핵심은 아니다(하지만 아래를 참고해라)
RubyMessenger.new

다음은 RubyMessenger JRuby 빈의 인스턴스를 정의하는 스프링 XML이다.

<lang:jruby id="messageService"
    script-interfaces="org.springframework.scripting.Messenger"
    script-source="classpath:RubyMessenger.rb">

  <lang:property name="message" value="Hello World!" />
</lang:jruby>

JRuby 소스의 마지막 라인을 봐라 ('RubyMessenger.new') 스프링의 동적 언어 지원에서 JRuby를 사용하는 경우 JRuby 소스의 실행 결과인 동적 언어로 작성한 빈으로 사용하려는 JRuby 클래스를 인스턴스화하고 새로운 인스턴스를 반환하는 것이 좋다. 이는 다음과 같이 소스파일의 마지막 라인으로 JRuby 클래스의 새로운 인스턴스를 인스턴스화해서 할 수 있다.

require 'java'

include_class 'org.springframework.scripting.Messenger'

# 클래스 정의는 앞과 같다...

# RubyMessenger 클래스의 새로운 인스턴스를 인스턴스화하고 반환한다.
RubyMessenger.new

이 부분을 빠뜨린다고 큰 문제가 생기는 것은 아니지만, 스프링이 인스턴스화할 클래스를 찾으려고 JRuby 클래스의 타입 표현으로 세밀하게(리플렉션하게) 조사하게 된다. 크게 보면 이는 너무 빨라서 느끼지 못할 정도이지만 위 JRuby 스크립트의 마지막 한 라인을 둠으로써 쉽게 피할 수 있는 부분이다. 해당 라인을 적지 않거나 스프링이 스크립트에서 인스턴스화할 JRuby 클래스를 찾을 수 없다면 JRuby 인터프리터가 소스를 실행하자마자 불투명한 ScriptCompilationException가 바로 던져질 것이다. 이를 식별하는 핵심 문구로 예외를 발생시킨 이유를 즉시 찾을 수 있다. (그래서 스프링 컨테이너가 동적 언어로 작성한 빈을 생성할 때 예외를 던지고 해당 스택트레이스에 다음 문구가 있다면 이슈를 발견하고 쉽게 수정할 수 있을 것이다.)

org.springframework.scripting.ScriptCompilationException: Compilation of JRuby script returned ''

이를 수정하려면 JRruby 동적 언어로 작성한 빈으로 노출할 클래스의 새로운 인스턴스를 인스턴스화 하면 된다(위에서 이미 봤다.) JRuby 스크립트에서 원하는 만큼 많은 클래스와 객체를 정의할 수도 있다. 중요한 것은 전체적으로 하나의 객체(스프링이 설정할)를 반드시 반환해야 한다는 것이다.

JRuby로 작성한 빈을 사용하는 예시들을 보려면 Section 27.4, “시나리오”를 참고해라.

27.3.3 Groovy 빈

Groovy 홈페이지에서...

"Groovy는 Java 2 플랫폼에서 동작하는 애자일한 동적 언어로 Python, Ruby, Smalltalk같은 언어에서 사람들이 무척 좋아하던 많은 기능을 자바 개발자들이 자바 문법으로 사용하도록 한다."

이번 장을 처음부터 읽었다면 Groovy 동적 언어로 작성한 빈의 예제를 이미 보았을 것이다. 여기서는 다른 예제를 보자.(스프링 테스트 슈트의 예제를 다시 사용한다.)

Groovy 라이브러리 의존성
스프링의 Groovy 스크립트 지원을 사용하려면 애플리케이션 클래스패스에 다음 라이브러리가 있어야 한다.

  • groovy-1.5.5.jar
  • asm-2.2.2.jar
  • antlr-2.7.6.jar


package org.springframework.scripting;

public interface Calculator {
  int add(int x, int y);
}

Groovy에서 Calculator 인터페이스의 구현체가 다음에 나와 있다.

// 'calculator.groovy' 파일
package org.springframework.scripting.groovy

class GroovyCalculator implements Calculator {
  int add(int x, int y) {
    x + y
  }
}
<-- 'beans.xml' 파일 -->
<beans>
  <lang:groovy id="calculator" script-source="classpath:calculator.groovy"/>
</beans>

마지막으로 위의 설정을 실행해볼 작은 애플리케이션이다.

package org.springframework.scripting;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
  public static void Main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
    Calculator calc = (Calculator) ctx.getBean("calculator");
    System.out.println(calc.add(2, 8));
  }
}

위 프로그램을 실행하면 (놀랍지는 않지만) 10이 나온다. (예제가 흥미로운가? 이 예제의 의도는 개념을 설명하려는 것이다. 더 복잡한 예제의 동적 언어 쇼케이스 프로젝트를 참고하고 이 장 뒷부분에서 Section 27.4, “시나리오”를 살펴본다.)

Groovy 소스파일마다 하나 이상의 클래스를 정의하지 않는 것이 중요하다. 이는 Groovy에서 완전히 유효하지만 (분명히)좋지 않은 방법이다. 일관성을 위해서 소스파일마다 하나의 (public) 클래스를 작성하는 표준 자바 관례를 따라야 한다.(저자의 의견)

27.3.3.1 콜백으로 Groovy 객체 커스터마이징하기

GroovyObjectCustomizer 인터페이스는 Groovy로 작성한 빈을 생성하는 과정에 로틱을 추가할 수 있는 훅(hook)을 제공하는 콜백이다. 예를 들어 이 인터페이스의 구현체로 필요한 초기화 메서드를 호출하거나 프로퍼티의 기본값을 설정하거나 커스텀 MetaClass를 지정할 수 있다.

public interface GroovyObjectCustomizer {
  void customize(GroovyObject goo);
}

스프링 프레임워크는 Groovy로 작성한 빈의 인스턴스를 인스턴스화한 뒤 빈이 정의되었다면 지정한 GroovyObjectCustomizer에 생성한 GroovyObject를 전달할 것이다. 제공받은 GroovyObject 참조로 무엇이든 할 수 있다. 대부분의 사람들이 이 콜백으로 커스텀 MetaClass를 설정할 것이고 이에 대한 예제가 아래 나와 있다.

public final class SimpleMethodTracingCustomizer implements GroovyObjectCustomizer {
  public void customize(GroovyObject goo) {
    DelegatingMetaClass metaClass = new DelegatingMetaClass(goo.getMetaClass()) {

     public Object invokeMethod(Object object, String methodName, Object[] arguments) {
      System.out.println("Invoking '" + methodName + "'.");
      return super.invokeMethod(object, methodName, arguments);
     }
    };
    metaClass.initialize();
    goo.setMetaClass(metaClass);
  }
}

Groovy의 메타프로그래밍에 대한 논의는 스프링 레퍼런스 매뉴얼의 범위를 벗어난다. Groovy 레퍼런스 메뉴얼의 관련 부분을 참고하거나 온라인에서 검색해 봐라. 메타 프로그래밍에 대한 많은 글을 볼 수 있을 것이다. 사실 스프링 2.0 네임스페이스 지원을 사용하고 있다면 GroovyObjectCustomizer를 사용하기는 쉽다.

<!-- 다른 빈과 마찬가지로 GroovyObjectCustomizer를 정의한다 -->
<bean id="tracingCustomizer" class="example.SimpleMethodTracingCustomizer" />
  <!-- ... 그리고 'customizer-ref' 속성으로 원하는 Groovy 빈에 연결한다 -->
  <lang:groovy id="calculator"
    script-source="classpath:org/springframework/scripting/groovy/Calculator.groovy"
    customizer-ref="tracingCustomizer" />

스프링 2.0 네임스페이스 지원을 사용하고 있지 않더라도 GroovyObjectCustomizer 기능을 사용할 수 있다.

<bean id="calculator" class="org.springframework.scripting.groovy.GroovyScriptFactory">
  <constructor-arg value="classpath:org/springframework/scripting/groovy/Calculator.groovy"/>
  <!-- GroovyObjectCustomizer를 정의한다 (내부 빈으로) -->
  <constructor-arg>
    <bean id="tracingCustomizer" class="example.SimpleMethodTracingCustomizer" />
  </constructor-arg>
</bean>

<bean class="org.springframework.scripting.support.ScriptFactoryPostProcessor"/>


27.3.4 BeanShell 빈

BeanShell 홈페이지에서...

"BeanShell은 자바로 작성된 동적 언어 기능을 가진 자바소스 인터프리터로 작고 무료고 내장할 수 있다. BeanShell은 동적으로 표준 자바 문법을 실행하고 Perl이나 JavaScript처럼 느슨한 타입, 커맨드, 메서드 클로저같은 일반적으로 편리한 스크립트 기능으로 자바 문법을 확장한다."

Groovy와는 달리 BeanShell로 작성한 빈 정의를 사용하려면 (약간의) 추가적인 설정이 필요하다. 스프링의 BeanShell 동적 언어 지원 구현체에는 흥미로운 동작이 일어난다. 요소의 'script-interfaces' 속성값으로 지정한 인터페이스를 구현하는 JDK 동적 프락시를 스프링이 생성한다.(그래서 'script-interfaces' 속성에 최소한 하나 이상의 인터페이스를 반드시 제공해야 하고 BeanShell로 작성한 빈을 사용할 때 인터페이스를 (적절히) 프로그래밍해야 한다.) 즉 BeanShell로 작성한 객체의 모든 메서드 호출은 JDK 동적 프락시 호출 메커니즘을 사용한다.

이번 장 초반에서 정의한 Messenger 인터페이스(편의상 아래 다시 적어놨다.)를 구현한 BeanShell로 작성한 빈을 사용하는 전체 예제를 보자.

BeanShell 라이브러리 의존성
스프링의 BeanShell 스크립트 지원을 사용하려면 애플리케이션 클래스 패스에 다음 라이브러리가 있어야 한다.

  • bsh-2.0b4.jar
  • cglib-nodep-2.1_3.jar


package org.springframework.scripting;

public interface Messenger {
  String getMessage();
}

다음은 Messenger 인터페이스의 BeanShell '구현체'이다. (이 용어는 여기서는 약간 막연하다)

String message;

String getMessage() {
  return message;
}

void setMessage(String aMessage) {
  message = aMessage;
}

다음은 위 '클래스'의 '인스턴스'를 정의한 스프링 XML이다.(여기서도 아주 막연한 용어이다.)

<lang:bsh id="messageService" script-source="classpath:BshMessenger.bsh"
  script-interfaces="org.springframework.scripting.Messenger">

  <lang:property name="message" value="Hello World!" />
</lang:bsh>

BeanShell로 작성한 빈을 사용하는 다른 시나리오를 보려면 Section 27.4, “시나리오”를 참고해라.

27.4 시나리오

스크립트 언어로 스프링이 관리하는 빈을 작성하는 것이 유용한 시나리오는 물론 많고 다양하다. 이번 부분에서는 스프링에서 동적 언어 지원으로 가능한 두 가지 유즈케이스를 설명한다.

27.4.1 스크립트로 작성한 스프링 MVC 컨트롤러

동적 언어로 작성한 빈을 사용해서 이득이 되는 클래스 중 하나가 스프링 MVC 컨트롤러다. 순수한 스프링 MVC 애플리케이션에서 웹 애플리케이션의 탐색 가능한 흐름은 아주 큰 범위이고 스프링 MVC 컨트롤러에서 은닉화된 코드로 결정된다. 웹 애플리케이션의 탐색 가능한 흐름과 다른 표현 계층의 로직은 이슈를 처리하거나 사업적 요구사항의 변경에 따라 갱신되어야 한다. 하나 이상의 동적 언어 소스 파일을 수정하고 실행 중인 애플리케이션의 상태에 바로 적용되는 변경사항을 보면 필요한 변경사항을 적용하기가 훨씬 쉬울 것이다.

스프링 같은 프로젝트가 지지하는 경량 아키텍처 모델을 생각해 보자. 보통은 정말 얇은 표현 계층을 가지면서 애플리케이션의 많은 비즈니스 로직을 도메인 계층과 서비스 계층 클래스에 담으려고 할 것이다. 동적 언어로 작성한 빈으로 스프링 MVC 컨트롤러를 작성하면 텍스트 파일을 수정하고 저장하기만 하면 표현 계층의 로직을 변경할 수 있을 것이다. 이러한 동적 언어 소스파일의 변경사항은 해당 파일로 작성한 빈에 자동으로 반영될 것이다. (설정에 따라 다를 수 있다.)

Note
동적 언어로 작성한 빈의 변경사항을 자동으로 '선택'해서 적용하려면 '갱신 가능한 빈'기능을 활성화해야 한다. 이 기능에 대한 전체 내용은 Section 27.3.1.2, “갱신 가능한(Refreshable) 빈”를 참고해라.


Groovy 동적 언어로 구현한 org.springframework.web.servlet.mvc.Controller의 예제가 아래 나와 있다.

// from the file '/WEB-INF/groovy/FortuneController.groovy'
package org.springframework.showcase.fortune.web

import org.springframework.showcase.fortune.service.FortuneService
import org.springframework.showcase.fortune.domain.Fortune
import org.springframework.web.servlet.ModelAndView
import org.springframework.web.servlet.mvc.Controller

import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

class FortuneController implements Controller {

  @Property FortuneService fortuneService

  ModelAndView handleRequest(
      HttpServletRequest request, HttpServletResponse httpServletResponse) {

    return new ModelAndView("tell", "fortune", this.fortuneService.tellFortune())
  }
}
<lang:groovy id="fortune"
     refresh-check-delay="3000"
     script-source="/WEB-INF/groovy/FortuneController.groovy">
  <lang:property name="fortuneService" ref="fortuneService"/>
</lang:groovy>


27.4.2 스크립트로 작성한 밸리데이터(Validator)

동적 언어로 작성한 빈의 유연성을 통해 스프링 애플리케이션 개발에서 이점을 취할 수 있는 또 하나의 부분이 유효성 검사(validation)다. 자바와 비교해서 타입이 느슨한 동적 언어(인라인 정규표현식도 지원한다)를 사용해서 복잡한 유효성 검사 로직을 표현하기가 더 쉬울 것이다.

다시 말하지만 동적 언어로 작성한 빈으로 밸리데이터를 개발하면 텍스트파일을 수정하고 저장하는 것만으로 유효성 검사 로직을 변경할 수 있다. 모든 변경사항이 (설정에 따라 다르지만) 자동으로 운영 중인 애플리케이션에 반영될 것이고 애플리케이션을 재시작할 필요가 없다.

Note
동적 언어로 작성한 빈의 변경사항을 자동으로 '적용'하려면 갱신 가능한 빈 기능을 활성화해야 할 것이다. 이 기능에 대한 자세한 내용은 Section 27.3.1.2, “갱신 가능한(Refreshable) 빈”를 참고해라.


Groovy 동적 언어로 구현한 스프링 org.springframework.validation.Validator의 예제가 다음에 나와 있다. (Validator 인터페이스에 대한 내용은 Section 6.2, “스프링의 Validator 인터페이스를 사용하는 유효성검사”를 참고해라.)

import org.springframework.validation.Validator
import org.springframework.validation.Errors
import org.springframework.beans.TestBean

class TestBeanValidator implements Validator {
  boolean supports(Class clazz) {
    return TestBean.class.isAssignableFrom(clazz)
  }

  void validate(Object bean, Errors errors) {
    if(bean.name?.trim()?.size() > 0) {
      return
    }
    errors.reject("whitespace", "Cannot be composed wholly of whitespace.")
  }
}


27.5 그 밖의 내용

이 마지막 장에서는 동적 언어 지원에 대한 잡다한 내용을 다룬다.

27.5.1 AOP - 스크립트로 작성한 빈의 어드바이스

스크립트로 작성한 빈을 어드바이스하도록 스프링 AOP 프레임워크를 사용할 수 있다. 사실 스프링 AOP 프레임워크는 어드바이스하는 빈이 스크립트로 작성한 빈이라는 것을 알지 못하므로 사용할 모든 AOP 사용 사례와 기능은 스크립트로 작성한 빈과도 잘 동작할 것이다. 스크립트로 작성한 빈을 어드바이스할 때 알아두어야 할 (소소한)내용이 딱 하나 있는데 클래스 기반의 프락시는 사용할 수 없고 인터페이스 기반의 프록시를 반드시 사용해야 한다는 것이다.

물론 스크립트로 작성한 빈을 어드바이스만 할 수 있는 것은 아니고 지원되는 동적 언어로 관점 자체를 작성하고 다른 스프링 빈을 어드바이스할 때 스크립트로 작성한 빈도 사용할 수 있다. 이는 동적 언어 지원에서 정말로 진보된 사용방법이다.

27.5.2 범위 제어

아주 명확하게 보이지는 않더라도 스크립트로 작성한 빈도 다른 빈처럼 범위를 줄 수 있다. 여러 <lang:language/> 요소의 scope 속성으로 다른 일반적인 빈처럼 해당 스크립트성 빈의 범위를 제어할 수 있다. (기본 범위는 다른 '일반적인' 빈과 마찬가지로 singleton이다. )

Grooby 빈을 정의할 때 scope 속성을 사용해서 prototype 범위로 정의한 예제가 다음에 나와 있다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:lang="http://www.springframework.org/schema/lang"
     xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-3.0.xsd">

  <lang:groovy id="messenger" script-source="classpath:Messenger.groovy" scope="prototype">
    <lang:property name="message" value="I Can Do The RoboCop" />
  </lang:groovy>

  <bean id="bookingService" class="x.y.DefaultBookingService">
    <property name="messenger" ref="messenger" />
  </bean>
</beans>

스프링 프레임워크의 범위에 대한 내용은 Chapter 4, IoC 컨테이너의 Section 4.5, “빈(Bean) 범위”를 참고해라.

27.6 추가 자료

이번 장에서 설명한 여러 동적 언어에 대한 추가 자료를 아래 링크에서 볼 수 있다.

스프링 커뮤니티에서 적극적인 일부 회원들이 이번 장에서 다룬 것 외의 추가적인 동적언어를 다수 추가했다. 스프링 배포판에서 지원하는 언어 목록에 언어를 추가하는 공헌을 할 수 있지만 Spring Modules project에서 선호하는 스크립트를 찾을 수 있기를 바란다.

2014/09/09 01:24 2014/09/09 01:24