Outsider's Dev Story: JAVA 카테고리 글 목록https://blog.outsider.ne.kr/Stay Hungry. Stay Foolish. Don't Be Satisfied.2024-03-15T11:09:36+09:00Textcube 1.10.7 : Tempo primo[Spring 레퍼런스] 부록 C. 확장 가능한 XML 작성하기Outsiderhttps://blog.outsider.ne.kr/11562015-07-12T05:01:01+09:002015-07-12T05:01:01+09:00<p>이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.<br><br><br><br></p>
<h2>부록 C. 확장 가능한 XML 작성하기</h2>
<h3>D.1 소개</h3>
<p>2.0 이후 스프링은 빈을 정의하고 구성할 때 스키마에 기반을 두고 기본 스프링 XML 형식을 확장하는 기능을 제공한다. 이번 장에서는 자신만의 커스텀 빈 정의를 작성하는 방법과 이러한 파서를 스프링 IoC 컨테이너와 통합하는 방법에 대해서 자세히 다룬다.</p>
<p>스키마를 이해하는 XML 에디터로 설정파일을 작성할 수 있도록 스프링의 확장성 있는 XML 구성 메커니즘은 XML 스키마에 기반을 두고 있다. 표준 스프링 배포판에 포함된 현재 스프링 XML 구성 확장에 익숙하지 않다면 Appendix C, XML 스키마에 기반을 둔 구성 부록을 먼저 보기 바란다.</p>
<p>다음의 (각각) 간단한 과정을 통해 새로운 XML 구성 확장을 만들 수 있다.</p>
<ol>
<li>커스텀 요소를 설명하는 XML 스키마를 작성한다.</li>
<li>커스텀 NamespaceHandler 구현체를 작성한다. (간단하므로 걱정하지 마라.)</li>
<li>하나 이상의 BeanDefinitionParser 구현체를 작성한다.(여기서 실제 동작이 일어난다.)</li>
<li>스프링에 위의 아티팩트(artifact)를 등록한다.(이 과정도 쉽다.)</li>
</ol>
<p>각 단계에 다른 설명은 이어서 할 것이다. 예를 들어 간단하게 (java.text 패키지의) SimpleDateFormat타입의 객체를 구성할 수 있게 하는 XML 확장(커스텀 XML 요소)을 생성할 것이다. 생성을 완료했을 때 다음과 같이 SimpleDateFormat 타입의 빈 정의를 정의할 수 있다.</p>
<pre class="line-numbers"><code class="language-xml"><myns:dateformat id="dateFormat"
pattern="yyyy-MM-dd HH:mm"
lenient="true"/>
</code></pre>
<p>(이 예제가 너무 간단하다고 걱정하지 마라. 뒤에서 더 자세한 예제를 다룰 것이다. 이 첫 예제의 의도는 기본적인 각 단계를 한번 해보는 것이다.)<br />
<br></p>
<h3>D.2 스키마 작성</h3>
<p>스프링 IoC 컨테이너와 사용하는 XML 구성 확장을 작성하려면 확장에 대해서 설명하는 XML 스키마를 먼저 작성해야 한다. 다음은 SimpleDateFormat 객체를 구성하는데 사용할 스키마이다.</p>
<pre class="line-numbers"><code class="language-xml"><!-- myns.xsd (inside package org/springframework/samples/xml) -->
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.mycompany.com/schema/myns"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
targetNamespace="http://www.mycompany.com/schema/myns"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:import namespace="http://www.springframework.org/schema/beans"/>
<xsd:element name="dateformat">
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="beans:identifiedType">
<xsd:attribute name="lenient" type="xsd:boolean"/>
<xsd:attribute name="pattern" type="xsd:string" use="required"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
</xsd:schema>
</code></pre>
<p>(강조한 라인은 구분할 수 있는 모든 태그의 extension base를 담고 있다.(이는 컨테이너에서 빈의 구분자로 사용할 id 속성이 있다는 말이다.) 스프링이 제공한 'beans' 네임스페이스를 임포트했으므로 이 속성을 사용할 수 있다.)</p>
<p><myns:dateformat/> 요소를 사용해서 XML 애플리케이션 컨텍스트 파일에서 직접 SimpleDateFormat 객체를 구성할 때 위의 스키마를 사용할 것이다.</p>
<pre class="line-numbers"><code class="language-xml"><myns:dateformat id="dateFormat"
pattern="yyyy-MM-dd HH:mm"
lenient="true"/>
</code></pre>
<p>인프라스트럭처 클래스를 생성한 후에는 위 XML이 다음의 XML 코드와 본질에서 완전히 같다. 다시 말하면 두 프로퍼티 셋으로 'dateFormat'라는 이름으로 구분하는 SimpleDateFormat 타입을 컨테이너에서 빈을 생성한 것이다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="dateFormat" class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy-HH-dd HH:mm"/>
<property name="lenient" value="true"/>
</bean>
</code></pre>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
구성 형식을 생성할 때 스키마에 기반을 둔 접근을 통해 스키마를 이해하는 XML 에디터를 가진 IDE와 강력하게 통합할 수 있다. 적절하게 작성된 스키마를 사용하면 목록에서 정의된 여러 구성 옵션 중에서 사용자가 자동완성을 사용할 수 있다.
</font></div>
<p><br></p>
<h3>D.3 NamespaceHandler 작성</h3>
<p>스키마에 추가적으로 NamespaceHandler가 필요하다. NamespaceHandler는 구성 파일을 파싱하는 동안 스프링이 만나는 이 네임스페이스의 모든 요소를 파싱할 것이다. 이 예제에서 NamespaceHandler는 myns:dateformat를 파싱해야 한다.</p>
<p>NamespaceHandler는 딱 세가지 메서드를 제공하는 간단한 인터페이스이다.</p>
<ul>
<li>init() - NamespaceHandler를 초기화할 수 있고 핸들러를 사용하기 전에 스프링이 호출한다.</li>
<li>BeanDefinition parse(Element, ParserContext) - 스프링이 최상위 요소(빈 정의나 다른 네임스페이스 하위에 있지 않은)를 만났을 때 호출된다. 이 메서드는 빈 정의를 등록하고 빈 정의를 반환할 수 있다.</li>
<li>BeanDefinitionHolder decorate(Node, BeanDefinitionHolder, ParserContext) - 스프링이 속성이나 다른 네임스페이스 하위에 있는 요소를 만났을 때 호출된다. 하나 이상의 빈 정의에 데코레이션을 스프링 2.0의 스코프 지원으로 예제에 사용했다. 데코레이션을 사용하지 않고 간단한 예제로 시작하지만 뒤에 나오는 고급 예제에서 데코레이션을 보여줄 것이다.</li>
</ul>
<p>모든 네임스페이스에 대한 NamespaceHandler를 작성하는 것이 가능하지만, 스프링 XML 구성 파일에서 각 최상위 XML 요소가 하나의 빈 정의가 되는 경우이다.(이 예제처럼 하나의 <myns:dateformat/> 요소가 하나의 SimpleDateFormat 빈 정의가 되는 경우를 말한다.) 스프링은 이 시나리오를 지원하는 편의 클래스를 다수 제공하고 있다. 이 예제에서 NamespaceHandlerSupport 클래스를 사용할 것이다.</p>
<pre class="line-numbers"><code class="language-java">package org.springframework.samples.xml;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class MyNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
}
}
</code></pre>
<p>주의력이 깊은 사람이라면 이 클래스에 파싱 로직이 많지 않다는 것을 눈치챌 것이다. 사실 NamespaceHandlerSupport 클래스는 위임의 개념을 사용해서 네임스페이스의 요소를 파싱해야 할 때 이를 위임하는 다수의 BeanDefinitionParser 인스턴스 등록을 지원한다. 이는 관심사를 깔끔하게 분리해서 NamespaceHandler가 네임스페이스의 모든 커스텀 엘리먼트의 다양한 파싱을 다룰 수 있게 하면서 복잡한 XML 파싱 작업은 BeanDefinitionParsers에 위임한다. 다음 과정에서 볼 수 있듯이 이는 각 BeanDefinitionParser가 단인 커스텀 요소를 파싱하는 로직만 가지면 된다는 의미이다.<br />
<br></p>
<h3>D.4 BeanDefinitionParser 작성하기</h3>
<p>NamespaceHandler가 특정 빈 정의 파서(이 예제에서는 'dateformat')가 매핑된 XML 요소를 만났을 때 BeanDefinitionParser를 사용할 것이다. 다시 말하면 BeanDefinitionParser는 스키마에 정의된 개별적인 최상위 XML 요소의 파싱을 담당한다. 이 파서에서 XML 요소에 접근하므로(하위 요소에도 접근한다.) 다음 예제에서 볼 수 있듯이 커스텀 XML 컴텐츠를 파싱할 수 있다.</p>
<pre class="line-numbers"><code class="language-java">package org.springframework.samples.xml;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;
import java.text.SimpleDateFormat;
public class SimpleDateFormatBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
protected Class getBeanClass(Element element) {
return SimpleDateFormat.class;
}
protected void doParse(Element element, BeanDefinitionBuilder bean) {
// 스키마에서 명시적으로 값이 제공되어야 한다고 되어 있으므로 이는 절대 null이 될 수 없다
String pattern = element.getAttribute("pattern");
bean.addConstructorArg(pattern);
// 하지만 이는 선택적인 프로퍼티이다
String lenient = element.getAttribute("lenient");
if (StringUtils.hasText(lenient)) {
bean.addPropertyValue("lenient", Boolean.valueOf(lenient));
}
}
}
</code></pre>
<ol>
<li>하나의 BeanDefinition을 생성하는 다수의 기본 작업을 처리할 때 스프링이 제공하는 AbstractSingleBeanDefinitionParser를 사용한다.</li>
<li>단일 BeanDefinition으로 표현할 타입의 AbstractSingleBeanDefinitionParser 슈퍼클래스를 제공한다.</li>
</ol>
<p>이 간단한 예시에서는 지금 설명한 내용이 해야 할 일 전부이다. 단일 BeanDefinition의 생성은 빈 정의의 유일한 식별자를 설정하고 추출해서 AbstractSingleBeanDefinitionParser 슈퍼클래스가 처리한다.<br />
<br></p>
<h3>D.5 핸들러와 스키마 등록</h3>
<p>코딩은 끝났다! 남은 작업은 여기서 사용한 커스텀 요소를 이해하는 스프링 XML 파싱 인트라스트럭처를 만드는 것 뿐이다. 두 가지 특수한 목적의 프로퍼티 파일에 커스텀 namespaceHandler와 커스텀 XSD 파일을 등록해서 파싱 인트라스트럭처를 만든다. 이 프로퍼티 파일은 모두 애플리케이션의 'META-INF' 디렉터리에 두고 JAR 파일의 바이너리 클래스 옆에 같이 배포할 수도 있다. 스프링 XML 파싱 인프라스트럭처는 이 특수한 프로퍼티 파일을 사용해서 자동으로 새로운 확장(extension)을 선택할 것이다. 이 프로퍼티 파일의 형식은 다음과 같다.<br />
<br></p>
<h4>D.5.1 'META-INF/spring.handlers'</h4>
<p>'spring.handlers' 프로퍼티 파일에는 XML Schema URI와 네임스페이스 핸들러 클래스의 매핑이 포함되어 있다. 이 예제에서는 다음과 같이 작성해야 한다.</p>
<pre class="line-numbers"><code class="language-clike">http\://www.mycompany.com/schema/myns=org.springframework.samples.xml.MyNamespaceHandler
</code></pre>
<p>(':' 문자는 Java 프로퍼티 형식에서 유효한 구분자이므로 URI 내의 ':' 문자는 역슬래시로 이스케이프 해야 한다.)</p>
<p>키-값 쌍의 첫 부분(키 부분)은 커스텀 네임스페이스 확장(extension)와 연관된 URI이고 커스텀 XSD 스키마에서 지정했듯이 'targetNamespace' 속성의 값과 정확하게 일치해야 한다.<br />
<br></p>
<h4>D.5.2 'META-INF/spring.schemas'</h4>
<p>'spring.schemas' 프로퍼티 파일에는 XML Schema 위치('xsi:schemaLocation' 속성 일부분으로 스키마를 사용하는 XML 파일의 스키마 선언과 같이 참조되는)와 classpath 리소스의 매핑이 포함되어 있다. 이 파일은 스키마 파일을 가져오려고 인터넷 접근을 해야 하는 기본 EntityResolver를 스프링이 사용하는 것을 막아야 할 필요가 있다. 이 프로퍼티 파일에서 매핑을 지정한다면 스프링이 클래스패스에서 스키마를 검색할 것이다.(여기서는 'org.springframework.samples.xml' 패키지의 'myns.xsd')</p>
<pre class="line-numbers"><code class="language-clike">http\://www.mycompany.com/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd
</code></pre>
<p>이에 따라 클래스패스에 NamespaceHandler와 BeanDefinitionParser 클래스와 같이 XSD 파일을 배포해야 한다.<br />
<br></p>
<h3>D.6 스프링 XML 구성에서 커스텀 확장(extension) 사용하기</h3>
<p>직접 구현한 커스텀 확장을 사용하는 것은 스프링이 직접 제공하는 'custom' 확장을 사용한 것과 다르지 않다. 이전 단계에서 스프링 XML 구성 파일에 만든 커스텀 <dateformat/> 요소를 사용하는 예시가 다음에 나와 있다.</p>
<pre class="line-numbers"><code class="language-xml"><?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:myns="http://www.mycompany.com/schema/myns"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.mycompany.com/schema/myns http://www.mycompany.com/schema/myns/myns.xsd">
<!-- 최상위 빈 -->
<myns:dateformat id="defaultDateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/>
<bean id="jobDetailTemplate" abstract="true">
<property name="dateFormat">
<!-- 내부(inner) 빈 -->
<myns:dateformat pattern="HH:mm MM-dd-yyyy"/>
</property>
</bean>
</beans>
</code></pre>
<p><br></p>
<h3>D.7 더 충실한 예제</h3>
<p>커스텀 XML 확장에 대한 더 자세한 예제가 다음에 나와 있다.<br />
<br></p>
<h4>D.7.1 커스텀 태그에 커스텀 태그 중첩하기</h4>
<p>이 예제는 다음 구성의 대상을 만족하기 위해 필요한 다양한 아티팩트를 작성하는 방법을 설명한다.</p>
<pre class="line-numbers"><code class="language-xml"><?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:foo="http://www.foo.com/schema/component"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.foo.com/schema/component http://www.foo.com/schema/component/component.xsd">
<foo:component id="bionic-family" name="Bionic-1">
<foo:component name="Mother-1">
<foo:component name="Karate-1"/>
<foo:component name="Sport-1"/>
</foo:component>
<foo:component name="Rock-1"/>
</foo:component>
</beans>
</code></pre>
<p>위 구성은 커스텀 확장을 서로의 내부에 실제로 넣는다. <foo:component/> 요소로 구성한 실제 클래스는 Component 클래스이다.(바로 아래 나온다.) Component 클래스가 'components' 프로퍼티에 setter 메서드를 노출하지 않는 방법을 봐야 한다. 이를 setter 주입으로 Component 클래스의 빈 정의를 구성하는 것은 상당히 어렵다.(거의 불가능에 가깝다.)</p>
<pre class="line-numbers"><code class="language-java">package com.foo;
import java.util.ArrayList;
import java.util.List;
public class Component {
private String name;
private List<Component> components = new ArrayList<Component> ();
// 음, 'components'에는 setter 메서드가 없다
public void addComponent(Component component) {
this.components.add(component);
}
public List<Component> getComponents() {
return components;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
</code></pre>
<p>이 이슈에 대한 일반적인 해결책은 'components' 프로퍼티에 setter 프로퍼티를 노출하는 커스텀 FactoryBean를 생성하는 것이다.</p>
<pre class="line-numbers"><code class="language-java">package com.foo;
import org.springframework.beans.factory.FactoryBean;
import java.util.List;
public class ComponentFactoryBean implements FactoryBean<Component> {
private Component parent;
private List<Component> children;
public void setParent(Component parent) {
this.parent = parent;
}
public void setChildren(List<Component> children) {
this.children = children;
}
public Component getObject() throws Exception {
if (this.children != null && this.children.size() > 0) {
for (Component child : children) {
this.parent.addComponent(child);
}
}
return this.parent;
}
public Class<Component> getObjectType() {
return Component.class;
}
public boolean isSingleton() {
return true;
}
}
</code></pre>
<p>이 예제는 잘 만들어졌고 동작도 잘 동작하지만, 스프링 기능을 최종 사용자에게 너무 많이 노출한다. 이러한 스프링의 기능은 모두 숨긴 채 커스텀 확장을 작성하는 것이 우리가 하려는 것이다. 앞에서 설명한 단계를 충실히 따랐다면 커스텀 태그의 구조를 정의하는 XSD 스키마를 생성하는 것에서 시작할 수 있다.</p>
<pre class="line-numbers"><code class="language-xml"><?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://www.foo.com/schema/component"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.foo.com/schema/component"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:element name="component">
<xsd:complexType>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element ref="component"/>
</xsd:choice>
<xsd:attribute name="id" type="xsd:ID"/>
<xsd:attribute name="name" use="required" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
</xsd:schema>
</code></pre>
<p>그다음 커스텀 NamespaceHandler를 만든다.</p>
<pre class="line-numbers"><code class="language-java">package com.foo;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class ComponentNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser());
}
}
</code></pre>
<p>다음은 커스텀 BeanDefinitionParser의 차례이다. 여기서 만들고 있는 것이 ComponentFactoryBean를 설명하는 BeanDefinition이라는 것을 명심해라.</p>
<pre class="line-numbers"><code class="language-java">package com.foo;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
import java.util.List;
public class ComponentBeanDefinitionParser extends AbstractBeanDefinitionParser {
protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
return parseComponentElement(element);
}
private static AbstractBeanDefinition parseComponentElement(Element element) {
BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean.class);
factory.addPropertyValue("parent", parseComponent(element));
List<Element> childElements = DomUtils.getChildElementsByTagName(element, "component");
if (childElements != null && childElements.size() > 0) {
parseChildComponents(childElements, factory);
}
return factory.getBeanDefinition();
}
private static BeanDefinition parseComponent(Element element) {
BeanDefinitionBuilder component = BeanDefinitionBuilder.rootBeanDefinition(Component.class);
component.addPropertyValue("name", element.getAttribute("name"));
return component.getBeanDefinition();
}
private static void parseChildComponents(List<Element> childElements, BeanDefinitionBuilder factory) {
ManagedList<BeanDefinition> children = new ManagedList<BeanDefinition>(childElements.size());
for (Element element : childElements) {
children.add(parseComponentElement(element));
}
factory.addPropertyValue("children", children);
}
}
</code></pre>
<p>마지막으로 스프링 XML 인프라스트럭쳐에 다양한 아티팩트를 등록해야 한다.</p>
<pre class="line-numbers"><code class="language-clike"># in 'META-INF/spring.handlers'
http\://www.foo.com/schema/component=com.foo.ComponentNamespaceHandler
</code></pre>
<pre class="line-numbers"><code class="language-clike"># in 'META-INF/spring.schemas'
http\://www.foo.com/schema/component/component.xsd=com/foo/component.xsd
</code></pre>
<p><br></p>
<h4>D.7.2 'normal' 요소 상의 커스텀 속성</h4>
<p>커스텀 파서와 관련 아티팩트를 작성하는 것이 어렵지는 않지만 때에 따라서는 좋은 방법이 아니다. 이미 존재하는 빈 정의에 메타데이터를 추가해야 하는 경우를 생각해 보자. 이럴 때 당연히 전체 커스텀 확장을 작성하는 것을 원치 않을 것이고 기존의 빈 정의 요소에 추가적인 요소를 더하기만을 원할 것이다.</p>
<p>다른 예시로 클러스터링 <a href="http://jcp.org/en/jsr/detail?id=107">JCache</a>에 접근하는(그렇다는 것을 알지는 못하는) 서비스 객체에 대한 빈 정의를 정의하는 서비스 클래스를 생각해 보자. 그리고 JCache라는 이름의 인스턴스가 사용하는 클러스터 내에서 eager 모드로 시작하는 것을 보장하기를 원한다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="checkingAccountService" class="com.foo.DefaultCheckingAccountService"
jcache:cache-name="checking.account">
<!-- 그 외 의존성은 여기에 작성한다... -->
</bean>
</code></pre>
<p>'jcache:cache-name' 속성을 파싱했을 때 또 하나의 BeanDefinition을 생성하는 것이 여기서 하려는 것이다. 이 BeanDefinition가 이름을 가진 JCache를 초기화할 것이다. 여기서 'checkingAccountService'에 대한 기존의 BeanDefinition도 수정할 것이므로 새로운 JCache를 초기화하는 이 BeanDefinition에 의존성을 가질 것이다.</p>
<pre class="line-numbers"><code class="language-java">package com.foo;
public class JCacheInitializer {
private String name;
public JCacheInitializer(String name) {
this.name = name;
}
public void initialize() {
// 이름을 가진 캐시를 초기화하려고 다수의 JCache API를 호출한다...
}
}
</code></pre>
<p>이제 커스텀 확장을 보자. 우선, 커스텀 속성을 나타내는 XSD 스키마을 장성한다.(이 경우 아주 쉽다.)</p>
<pre class="line-numbers"><code class="language-xml"><?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://www.foo.com/schema/jcache"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.foo.com/schema/jcache"
elementFormDefault="qualified">
<xsd:attribute name="cache-name" type="xsd:string"/>
</xsd:schema>
</code></pre>
<p>다음으로 관련된 NamespaceHandler를 작성한다.</p>
<pre class="line-numbers"><code class="language-java">package com.foo;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class JCacheNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
super.registerBeanDefinitionDecoratorForAttribute("cache-name", new JCacheInitializingBeanDefinitionDecorator());
}
}
</code></pre>
<p>이제 파서의 차례이다. 이 예제에서 XML 속성을 파싱할 예정이므로 BeanDefinitionParser 대신 BeanDefinitionDecorator를 작성한다.</p>
<pre class="line-numbers"><code class="language-java">package com.foo;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.BeanDefinitionDecorator;
import org.springframework.beans.factory.xml.ParserContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class JCacheInitializingBeanDefinitionDecorator implements BeanDefinitionDecorator {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
public BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder holder, ParserContext ctx) {
String initializerBeanName = registerJCacheInitializer(source, ctx);
createDependencyOnJCacheInitializer(holder, initializerBeanName);
return holder;
}
private void createDependencyOnJCacheInitializer(BeanDefinitionHolder holder, String initializerBeanName) {
AbstractBeanDefinition definition = ((AbstractBeanDefinition) holder.getBeanDefinition());
String[] dependsOn = definition.getDependsOn();
if (dependsOn == null) {
dependsOn = new String[]{initializerBeanName};
} else {
List dependencies = new ArrayList(Arrays.asList(dependsOn));
dependencies.add(initializerBeanName);
dependsOn = (String[]) dependencies.toArray(EMPTY_STRING_ARRAY);
}
definition.setDependsOn(dependsOn);
}
private String registerJCacheInitializer(Node source, ParserContext ctx) {
String cacheName = ((Attr) source).getValue();
String beanName = cacheName + "-initializer";
if (!ctx.getRegistry().containsBeanDefinition(beanName)) {
BeanDefinitionBuilder initializer = BeanDefinitionBuilder.rootBeanDefinition(JCacheInitializer.class);
initializer.addConstructorArg(cacheName);
ctx.getRegistry().registerBeanDefinition(beanName, initializer.getBeanDefinition());
}
return beanName;
}
}
</code></pre>
<p>마지막으로 스프링 XML 인프라스트럭처에 다양한 아티팩트를 등록해야 한다.</p>
<pre class="line-numbers"><code class="language-clike"># in 'META-INF/spring.handlers'
http\://www.foo.com/schema/jcache=com.foo.JCacheNamespaceHandler
</code></pre>
<pre class="line-numbers"><code class="language-clike"># in 'META-INF/spring.schemas'
http\://www.foo.com/schema/jcache/jcache.xsd=com/foo/jcache.xsd
</code></pre>
<p><br></p>
<h3>D.8 추가 자료</h3>
<p>이번 장에서 설명한 XML 스키마와 확장 가능한 XML 지원에 대한 추가 자료를 다음 링크에서 볼 수 있다.</p>
<ul>
<li><a href="http://www.w3.org/TR/2004/REC-xmlschema-1-20041028/">XML Schema Part 1: Structures Second Edition</a></li>
<li><a href="http://www.w3.org/TR/2004/REC-xmlschema-2-20041028/">XML Schema Part 2: Datatypes Second Edition</a></li>
</ul>
<p><strong><a href="https://blog.outsider.ne.kr/1156?commentInput=true#entry1156WriteComment">댓글 쓰기</a></strong></p>[Spring 레퍼런스] 부록 C. XML 스키마에 기반을 둔 구성Outsiderhttps://blog.outsider.ne.kr/11482015-05-25T04:27:05+09:002015-05-25T04:27:05+09:00<p>이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.<br><br><br><br></p>
<h2>부록 C. XML 스키마에 기반을 둔 구성</h2>
<h3>C.1 소개</h3>
<p>이번 부록에서는 스프링 2.0에서 도입되고 스프링 2.5와 3.0에서 강화되고 확장된 XML 스키마에 기반을 둔 구성을 살펴본다.</p>
<p>XML 스키마에 기반을 둔 구성파일로 바꾸는 주요 이유는 스프링 XML 구성을 더 쉽게 작성하는 것이다. '고전적인' <bean/>기반의 접근이 좋지만, 그 특성에 따라 구성에 오버헤드가 발생한다.</p>
<p>스프링 IoC 컨테이너의 관점에서는 모든 것이 빈이다. 모든 것이 빈이라면 같은 방식으로 이를 모두 다룰 수 있으므로 스프링 IoC 컨테이너의 입장에서는 좋은 점이지만 개발자 입장서는 그다지 좋지 않다. 스프링 XML 구성파일에서 정의한 객체는 모두 일반적이고 평범한(generic, vanilla) 빈이 아니다. 보통 각 빈은 어느 정도의 특별한 구성이 필요하다.</p>
<p>스프링 2.0에서 스키마에 기반을 둔 새로운 구성은 이 문제를 해결한다. <bean/> 요소는 계속 사용할 수 있고 원한다면 <bean/> 요소만 사용하면서 완전히 같은 방식의 스프링 XML 구성을 작성할 수 있다. 하지만 새로운 XML 스키마 기반의 구성은 스프링 XML 구성파일을 상당히 읽기 쉽게 만들 수 있고 빈 정의의 의도를 표현할 수 있다.</p>
<p>기존에 존재하는 빈(bean) 태그가 DAO, 서비스계층 객체, 밸리데이터 등의 애플리케이션에 특화된 빈에 적합하다면 새로운 커스텀 태그는 인프라스트럭처나 통합 빈(예를 들어 AOP, 컬렉션, 트랜잭션이나 Mule 등의 서드파티 프레임워크와의 통합)에 사용하기에 최적이라는 점은 꼭 기억해 두어야 한다.</p>
<p>다음의 예시로 스프링 2.0의 XML 스키마 지원이 추가된 것은 좋은 생각이었다는 점을 알 수 있기를 바란다. 개발자들이 이를 도입하기를 권장하고 이 새로운 구성 메커니즘이 완전히 커스터마이징할 수 있고 확장 가능하다는 점을 명심해라. 즉, 애플리케이션의 도메인을 더 잘 나타낼 수 있도록 자신의 도메인에 특화된 구성 태그를 작성할 수 있다. 자신만의 구성태그를 작성하는 방법은 부록 Appendix D, Extensible XML authoring에서 다룬다.</p>
<div style="padding:10px; background-color:#EDE5A6"><font color="#000000">
<b>DTD 지원?</b><br>
지금도 예전의 DTD 방식을 사용하는 스프링 구성 파일을 완전히 지원한다.<br><br>
스프링 XML 구성파일을 작성할 때 새로운 XML 스키마 방식의 접근을 사용하더라도 문제는 전혀 없고 더 간결하고 깔끔한 구성을 사용할 기회를 놓칠 뿐이다. XML 구성이 DTD 기반이냐 스키마 기반이냐에 상관없이 결국은 컨테이너에서 같은 객체 모델이 된다.(보통 하나 이상의 BeanDefinition 인스턴스)
</font></div>
<p><br></p>
<h3>C.2 XML 스키마에 기반을 둔 구성</h3>
<h4>C.2.1 스키마 참조</h4>
<p>기존의 DTD 방식에서 새로운 XML 스키마 방식으로 변경하려면 다음과 같이 바꾸어야 한다.</p>
<pre class="line-numbers"><code class="language-xml"><?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
<!-- <bean/> 정의는 여기에 작성한다 -->
</beans>
</code></pre>
<p>XML 스키마 방식에서 같은 파일은 다음과 같이 작성한다.</p>
<pre class="line-numbers"><code class="language-xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<!-- <bean/> 정의는 여기에 작성한다 -->
</beans>
</code></pre>
<p><br></p>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
'xsi:schemaLocation' 부분은 실제로는 필요 없지만 스키마의 로컬 복사본을 참조하려고(개발단계에서 유용할 수 있다.) 포함할 수 있다.
</font></div>
<p><br></p>
<p>위의 스프링 XML 구성의 일부분은 복사 후 붙여넣기를 해서 사용할 수 있는 상용구문으로 항상 하듯이 <bean/>를 추가해서 사용할 수 있다. 하지만 XML 스키마 방식으로 바꾸는 것은 구성을 더 쉽게 만드는 새로운 스프링 2.0 XML 태그의 장점을 얻으려는 것이다. Section C.2.2, “util 스키마” 섹션에서 일반적인 유틸리티 태그를 사용해서 바로 시작하는 방법을 설명한다.</p>
<p>이번 장의 나머지 부분에서는 새로운 스프링 XML 스키마에 기반을 둔 구성의 예제를 보여준다. (새로운 태그마다 최소한 하나의 예시를 제공한다.) 형식은 before, after 형식을 사용해서 before 부분에서는 이전 방식의 XML을 보여주고(하지만 지금도 완전히 사용 가능한) 바로 이어서 나오는 after 부분에서는 같은 설정을 XML 스키마 방식을 보여준다.<br />
<br></p>
<h4>C.2.2 util 스키마</h4>
<p>처음 다룰 부분은 util 태그의 범위인데 이름에서 알 수 있듯이 util 태그는 컬렉션을 구성하거나 상수를 참조하는 등의 일반적인 유틸리티성 구성 이슈를 다룬다.</p>
<p>util 스키마의 태그를 사용하려면 스프링 XML 구성파일 상단에 다음 코드를 넣어야 한다. util 네임스페이스로 태그를 사용할 수 있도록 다음 예시의 텍스트는 올바른 스키마를 참조하고 있다.</p>
<pre class="line-numbers"><code class="language-xml"><?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:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd">
<!-- 여기에 <bean/> 정의를 작성한다 -->
</beans>
</code></pre>
<p><br></p>
<h4>C.2.2.1 <util:constant/></h4>
<p>Before...</p>
<pre class="line-numbers"><code class="language-xml"><bean id="..." class="...">
<property name="isolation">
<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
</property>
</bean>
</code></pre>
<p>위 구성은 빈의 'isolation' 프로퍼티 값을 'java.sql.Connection.TRANSACTION_SERIALIZABLE' 상수 값으로 설정하려고 스프링의 FactoryBean 구현체인 FieldRetrievingFactoryBean를 사용한다. 이 구성은 문제없이 잘 동작하지만 약간 장황하고 (불필요하게) 스프링의 내부 구조를 최종 사용자에게 노출한다.</p>
<p>다음 XML 스키마 방식은 더 간단하면서도 개발자의 의도('이 상수값을 주입해라')를 명확하게 드러내서 더 읽기가 좋다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="..." class="...">
<property name="isolation">
<util:constant static-field="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
</property>
</bean>
</code></pre>
<p><strong>필드 값으로 빈(bean) 프로퍼티나 생성자 인자 설정하기</strong><br />
<a href="http://static.springframework.org/spring/docs/3.0.x/javadoc-api/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.html">FieldRetrievingFactoryBean</a>은 static 필드 값이나 정적이 아닌 필드 값을 가져오는 FactoryBean이다. 이는 보통 public static final 상수를 가져오는 데 사용한다.(가져와서 또 다른 빈에 프로퍼티 값이나 생성자 인자를 설정하는 데 사용할 수 있다.)</p>
<p><a href="http://static.springframework.org/spring/docs/3.0.x/javadoc-api/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.html#setStaticField(java.lang.String)">staticField</a>를 사용해서 static 필드를 노출하는 방법을 다음 예제에서 보여준다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="myField" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
<property name="staticField" value="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
</bean>
</code></pre>
<p>빈 이름으로 static 필드를 지정하는 것도 편리한 사용방식이다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>
</code></pre>
<p>이는 빈 id에 대한 선택권이 없다는 것을 의미하지만(그래서 이를 참조하는 다른 모든 빈도 이 긴 이름을 사용해야 한다) 이 형식은 매우 간단하게 정의할 수 있고 빈 참조에 id를 지정하지 않아도 되므로 이너 빈으로 사용할 때 아주 편리하다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="..." class="...">
<property name="isolation">
<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
</property>
</bean>
</code></pre>
<p><a href="http://static.springframework.org/spring/docs/3.0.x/javadoc-api/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.html">FieldRetrievingFactoryBean</a> 클래스의 API 문서에 나와 있듯이 다른 빈의 비정적(인스턴스) 필드에 접근하는 것도 가능하다. class.</p>
<p>스프링에서 빈에 프로프니나 생성사 인자로 enum 값을 주입하기는 아주 쉬워서 실제로 아무것도 안해도 되고 스프링 내부에 대해( FieldRetrievingFactoryBean같은 클래스) 알아야 하는 것도 없다. enum 값을 주입하기가 얼마나 쉬운지 예제를 통해서 보자. 다음과 같은 JDK 6 enum을 생각해 보자.</p>
<pre class="line-numbers"><code class="language-java">package javax.persistence;
public enum PersistenceContextType {
TRANSACTION,
EXTENDED
}
</code></pre>
<p>PersistenceContextType타입의 setter를 보자.</p>
<pre class="line-numbers"><code class="language-java">package example;
public class Client {
private PersistenceContextType persistenceContextType;
public void setPersistenceContextType(PersistenceContextType type) {
this.persistenceContextType = type;
}
}
</code></pre>
<p>이에 대한 빈 정의는 다음과 같다.</p>
<pre class="line-numbers"><code class="language-java"><bean class="example.Client">
<property name="persistenceContextType" value="TRANSACTION" />
</bean>
</code></pre>
<p>이는 (JDK 1.4와 JDK 1.3의) 전통적이면서 타입 세이프 하게 만들어진 enum에서도 잘 동작한다. 스프링은 enum 클래스의 상수와 일치하는 문자열 프로퍼티 값을 자동으로 찾을 것이다.<br />
<br></p>
<h4>C.2.2.2 <util:property-path/></h4>
<p>Before...</p>
<pre class="line-numbers"><code class="language-xml"><!-- 대상 빈은 name으로 참조한다 -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
<property name="age" value="10"/>
<property name="spouse">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="11"/>
</bean>
</property>
</bean>
<!-- 이는 'testBean' 빈의 'age' 프로퍼티의 값인 10이 된다 -->
<bean id="testBean.age" class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
</code></pre>
<p>위 구성은 스프링의 FactoryBean 구현체 PropertyPathFactoryBean을 사용해서 'testBean' 빈의 'age' 프로퍼티와 같은 값을 가진 (int 타입의) 'testBean.age'라는 빈을 생성한다.</p>
<p>After...</p>
<pre class="line-numbers"><code class="language-xml"><!-- 대상 빈은 name으로 참조한다 -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
<property name="age" value="10"/>
<property name="spouse">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="11"/>
</bean>
</property>
</bean>
<!-- 이는 'testBean' 빈의 'age' 프로퍼티의 값인 10이 된다 -->
<util:property-path id="name" path="testBean.age"/>
</code></pre>
<p><property-path/> 태그의 'path' 속성의 값은 'beanName.beanProperty' 형식을 따른다.</p>
<p><strong>빈 프로퍼티나 생성자 인자를 설정할 때 <util:property-path/> 사용하기</strong></p>
<p>PropertyPathFactoryBean는 FactoryBean이고 주어진 대상 객체의 프로퍼티 경로를 평가한다. 대상 객체는 직접 지정할 수도 있고 빈 이름으로 지정할 수도 있다. 지정한 값은 다른 빈 정의에서 프로퍼티 값이나 생성자 인자로 사용할 수 있다.</p>
<p>다음은 name으로 다른 빈을 기준으로 path를 사용하는 예제이다.</p>
<pre class="line-numbers"><code class="language-xml">// 대상 빈은 name으로 참조된다
<bean id="person" class="org.springframework.beans.TestBean" scope="prototype">
<property name="age" value="10"/>
<property name="spouse">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="11"/>
</bean>
</property>
</bean>
// 이는 'person' 빈의 'spouse.age' 프로퍼티의 값인 11이 된다
<bean id="theAge"
class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
<property name="targetBeanName" value="person"/>
<property name="propertyPath" value="spouse.age"/>
</bean>
</code></pre>
<p>이 예제에서 path는 내부 빈을 기준으로 평가된다.</p>
<pre class="line-numbers"><code class="language-xml"><!-- 이는 내부 빈의 'age' 프로퍼티의 값인 12가 된다 -->
<bean id="theAge" class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
<property name="targetObject">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="12"/>
</bean>
</property>
<property name="propertyPath" value="age"/>
</bean>
</code></pre>
<p>빈 네임이 프로퍼티 경로인 단축형태도 존재한다.</p>
<pre class="line-numbers"><code class="language-xml"><!-- 이는 'person' 빈의 'age' 프로퍼티의 값인 10이 된다 -->
<bean id="person.age"
class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
</code></pre>
<p>이 형식에서는 빈 이름을 선택할 수 없어서 이에 대한 모든 참조도 같은 id(경로)를 사용해야 한다. 물론 내부 빈으로 사용한다면 참조할 필요조차 없다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="..." class="...">
<property name="age">
<bean id="person.age" class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
</property>
</bean>
</code></pre>
<p>실제 정의에서 결과 타입을 구체적으로 지정할 수도 있다. 대부분 경우 이는 필요 없지만 일부의 경우에는 사용할 수 있다. 이 기능에 대한 자세한 내용은 Javadoc을 참조해라.<br />
<br></p>
<h4>C.2.2.3 <util:properties/></h4>
<p>Before..</p>
<pre class="line-numbers"><code class="language-xml"><!-- 제공한 위치에서 로드한 값으로 java.util.Properties 인스턴스를 생성한다 -->
<bean id="jdbcConfiguration" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="location" value="classpath:com/foo/jdbc-production.properties"/>
</bean>
</code></pre>
<p>위 구성은 스프링의 FactoryBean 구현체인 PropertiesFactoryBean를 사용해서 제공된 Resource 위치에서 로드한 값으로 java.util.Properties 인스턴스를 인스턴스화 한다.</p>
<p>After...</p>
<pre class="line-numbers"><code class="language-xml"><!-- 제공한 위치에서 로드한 값으로 java.util.Properties 인스턴스를 생성한다 -->
<util:properties id="jdbcConfiguration" location="classpath:com/foo/jdbc-production.properties"/>
</code></pre>
<p><br></p>
<h4>C.2.2.4 <util:list/></h4>
<p>Before...</p>
<pre class="line-numbers"><code class="language-xml"><!-- 제공한 'sourceList'에서 로드한 값으로 java.util.List 인스턴스를 생성한다 -->
<bean id="emails" class="org.springframework.beans.factory.config.ListFactoryBean">
<property name="sourceList">
<list>
<value>pechorin@hero.org</value>
<value>raskolnikov@slums.org</value>
<value>stavrogin@gov.org</value>
<value>porfiry@gov.org</value>
</list>
</property>
</bean>
</code></pre>
<p>위 구성은 스프링의 FactoryBean 구현체 ListFactoryBean을 사용해서 제공한 'sourceList'에서 가져온 값으로 초기화한 java.util.List 인스턴스를 생성한다.</p>
<p>After...</p>
<pre class="line-numbers"><code class="language-xml"><!-- 제공한 값으로 java.util.List 인스턴스를 생성한다 -->
<util:list id="emails">
<value>pechorin@hero.org</value>
<value>raskolnikov@slums.org</value>
<value>stavrogin@gov.org</value>
<value>porfiry@gov.org</value>
</util:list>
</code></pre>
<p>명시적으로 정확히 List 타입을 제어할 수도 있는데 이는 <util:list/> 요소의 'list-class' 속성을 사용해서 인스턴스화한다. 예를 들어 java.util.LinkedList를 인스턴스화 하기를 원한다면 다음의 구성을 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-xml"><util:list id="emails" list-class="java.util.LinkedList">
<value>jackshaftoe@vagabond.org</value>
<value>eliza@thinkingmanscrumpet.org</value>
<value>vanhoek@pirate.org</value>
<value>d'Arcachon@nemesis.org</value>
</util:list>
</code></pre>
<p>'list-class' 속성을 지정하지 않으면 컨테이너가 List 구현체를 선택할 것이다.<br />
<br></p>
<h4>C.2.2.5 <util:map/></h4>
<p>Before...</p>
<pre class="line-numbers"><code class="language-xml"><!-- 제공한 'sourceMap'에서 로드한 값으로 java.util.Map 인스턴스를 생성한다 -->
<bean id="emails" class="org.springframework.beans.factory.config.MapFactoryBean">
<property name="sourceMap">
<map>
<entry key="pechorin" value="pechorin@hero.org"/>
<entry key="raskolnikov" value="raskolnikov@slums.org"/>
<entry key="stavrogin" value="stavrogin@gov.org"/>
<entry key="porfiry" value="porfiry@gov.org"/>
</map>
</property>
</bean>
</code></pre>
<p>위 구성은 스프링의 FactoryBean 구현체 MapFactoryBean를 사용해서 제공한 'sourceMap'에서 가져온 키-값 쌍으로 초기화한 java.util.Map 인스턴스를 생성한다.</p>
<p>After...</p>
<pre class="line-numbers"><code class="language-xml"><!-- 제공한 키-값 쌍으로 java.util.Map 인스턴스를 생성한다 -->
<util:map id="emails">
<entry key="pechorin" value="pechorin@hero.org"/>
<entry key="raskolnikov" value="raskolnikov@slums.org"/>
<entry key="stavrogin" value="stavrogin@gov.org"/>
<entry key="porfiry" value="porfiry@gov.org"/>
</util:map>
</code></pre>
<p>명시적으로 정확히 Map 타입을 제어할 수도 있는데 이는 <util:map/> 요소의 'map-class' 속성을 사용해서 인스턴스화 한다. 예를 들어 java.util.TreeMap를 인스턴스화 하기를 원한다면 다음 구성을 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-xml"><util:map id="emails" map-class="java.util.TreeMap">
<entry key="pechorin" value="pechorin@hero.org"/>
<entry key="raskolnikov" value="raskolnikov@slums.org"/>
<entry key="stavrogin" value="stavrogin@gov.org"/>
<entry key="porfiry" value="porfiry@gov.org"/>
</util:map>
</code></pre>
<p>'map-class' 속성을 지정하지 않는다면 컨테이너가 Map 구현체를 선택할 것이다.<br />
<br></p>
<h4>C.2.2.6 <util:set/></h4>
<p>Before...</p>
<pre class="line-numbers"><code class="language-xml"><!-- 제공한 'sourceSet'에서 로드한 값으로 java.util.Set 인스턴스를 생성한다 -->
<bean id="emails" class="org.springframework.beans.factory.config.SetFactoryBean">
<property name="sourceSet">
<set>
<value>pechorin@hero.org</value>
<value>raskolnikov@slums.org</value>
<value>stavrogin@gov.org</value>
<value>porfiry@gov.org</value>
</set>
</property>
</bean>
</code></pre>
<p>위 구성은 스프링의 FactoryBean 구현체 SetFactoryBean를 사용해서 제공된 'sourceSet'에서 가져온 값으로 초기화한 java.util.Set 인스턴스를 생성한다.</p>
<p>After...</p>
<pre class="line-numbers"><code class="language-xml"><!-- 제공한 값으로 java.util.Set 인스턴스를 생성한다 -->
<util:set id="emails">
<value>pechorin@hero.org</value>
<value>raskolnikov@slums.org</value>
<value>stavrogin@gov.org</value>
<value>porfiry@gov.org</value>
</util:set>
</code></pre>
<p>명시적으로 정확히 Set 타입을 제어할 수도 있는데 이는 <util:set/> 요소의 'set-class' 속성을 사용해서 인스턴스화한다. 예를 들어 java.util.TreeSet를 인스턴스화 하기 원한다면 다음 구성을 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-xml"><util:set id="emails" set-class="java.util.TreeSet">
<value>pechorin@hero.org</value>
<value>raskolnikov@slums.org</value>
<value>stavrogin@gov.org</value>
<value>porfiry@gov.org</value>
</util:set>
</code></pre>
<p>'set-class' 속성을 지정하지 않으면 컨테이너가 Set 구현체를 선택할 것이다.<br />
<br></p>
<h4>C.2.3 jee 스키마</h4>
<p>jee 태그는 JNDI 객체를 찾고 EJB 참조를 정의하는 등의 Java EE (Java Enterprise Edition)와 관련된 구성 이슈를 다룬다.</p>
<p>jee 스키마의 태그를 사용하려면 스프링 XML 구성 파일 상단에 다음 부분을 추가해야 한다. 다음 예시의 내용은 올바른 스키마를 참조해서 jee 네임스페이스의 태그를 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-xml"><?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:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.0.xsd">
<!-- 여기에 <bean/> 정의를 작성한다 -->
</beans>
</code></pre>
<p><br></p>
<h4>C.2.3.1 <jee:jndi-lookup/> (간단버전)</h4>
<p>Before...</p>
<pre class="line-numbers"><code class="language-xml"><bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="jdbc/MyDataSource"/>
</bean>
<bean id="userDao" class="com.foo.JdbcUserDao">
<!-- 스프링이 자동으로 캐스팅할 것이다. (평소처럼) -->
<property name="dataSource" ref="dataSource"/>
</bean>
</code></pre>
<p>After...</p>
<pre class="line-numbers"><code class="language-xml"><jee:jndi-lookup id="dataSource" jndi-name="jdbc/MyDataSource"/>
<bean id="userDao" class="com.foo.JdbcUserDao">
<!-- 스프링이 자동으로 캐스팅할 것이다. (평소처럼) -->
<property name="dataSource" ref="dataSource"/>
</bean>
</code></pre>
<p><br></p>
<h4>C.2.3.2 <jee:jndi-lookup/> (단일 JNDI 환경설정을 이용)</h4>
<p>Before...</p>
<pre class="line-numbers"><code class="language-xml"><bean id="simple" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="jdbc/MyDataSource"/>
<property name="jndiEnvironment">
<props>
<prop key="foo">bar</prop>
</props>
</property>
</bean>
</code></pre>
<p>After...</p>
<pre class="line-numbers"><code class="language-xml"><jee:jndi-lookup id="simple" jndi-name="jdbc/MyDataSource">
<jee:environment>foo=bar</jee:environment>
</jee:jndi-lookup>
</code></pre>
<p><br></p>
<h4>C.2.3.3 <jee:jndi-lookup/> (다중 JNDI 환경 설정을 이용)</h4>
<p>Before...</p>
<pre class="line-numbers"><code class="language-xml"><bean id="simple" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="jdbc/MyDataSource"/>
<property name="jndiEnvironment">
<props>
<prop key="foo">bar</prop>
<prop key="ping">pong</prop>
</props>
</property>
</bean>
</code></pre>
<p>After...</p>
<pre class="line-numbers"><code class="language-xml"><jee:jndi-lookup id="simple" jndi-name="jdbc/MyDataSource">
<!-- 라인별로 구분되는 환경의 키-값 쌍 (표준 Properties 형식) -->
<jee:environment>
foo=bar
ping=pong
</jee:environment>
</jee:jndi-lookup>
</code></pre>
<p><br></p>
<h4>C.2.3.4 <jee:jndi-lookup/> (복합 버전)</h4>
<p>Before...</p>
<pre class="line-numbers"><code class="language-xml"><bean id="simple" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="jdbc/MyDataSource"/>
<property name="cache" value="true"/>
<property name="resourceRef" value="true"/>
<property name="lookupOnStartup" value="false"/>
<property name="expectedType" value="com.myapp.DefaultFoo"/>
<property name="proxyInterface" value="com.myapp.Foo"/>
</bean>
</code></pre>
<p>After...</p>
<pre class="line-numbers"><code class="language-xml"><jee:jndi-lookup id="simple"
jndi-name="jdbc/MyDataSource"
cache="true"
resource-ref="true"
lookup-on-startup="false"
expected-type="com.myapp.DefaultFoo"
proxy-interface="com.myapp.Foo"/>
</code></pre>
<p><br></p>
<h4>C.2.3.5 <jee:local-slsb/> (간단 버전)</h4>
<p><jee:local-slsb/> 태그는 EJB Stateless SessionBean애 대한 참조를 구성한다.</p>
<p>Before...</p>
<pre class="line-numbers"><code class="language-xml"><bean id="simple"
class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
<property name="jndiName" value="ejb/RentalServiceBean"/>
<property name="businessInterface" value="com.foo.service.RentalService"/>
</bean>
</code></pre>
<p>After...</p>
<pre class="line-numbers"><code class="language-xml"><jee:local-slsb id="simpleSlsb" jndi-name="ejb/RentalServiceBean"
business-interface="com.foo.service.RentalService"/>
</code></pre>
<p><br></p>
<h4>C.2.3.6 <jee:local-slsb/> (복합 버전)b</h4>
<pre class="line-numbers"><code class="language-xml"><bean id="complexLocalEjb"
class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
<property name="jndiName" value="ejb/RentalServiceBean"/>
<property name="businessInterface" value="com.foo.service.RentalService"/>
<property name="cacheHome" value="true"/>
<property name="lookupHomeOnStartup" value="true"/>
<property name="resourceRef" value="true"/>
</bean>
</code></pre>
<p>After...</p>
<pre class="line-numbers"><code class="language-xml"><jee:local-slsb id="complexLocalEjb"
jndi-name="ejb/RentalServiceBean"
business-interface="com.foo.service.RentalService"
cache-home="true"
lookup-home-on-startup="true"
resource-ref="true">
</code></pre>
<p><br></p>
<h4>C.2.3.7 <jee:remote-slsb/></h4>
<p><jee:remote-slsb/> 태그는 remote EJB Stateless SessionBean에 대한 참조를 구성한다.</p>
<p>Before...</p>
<pre class="line-numbers"><code class="language-xml"><bean id="complexRemoteEjb"
class="org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean">
<property name="jndiName" value="ejb/MyRemoteBean"/>
<property name="businessInterface" value="com.foo.service.RentalService"/>
<property name="cacheHome" value="true"/>
<property name="lookupHomeOnStartup" value="true"/>
<property name="resourceRef" value="true"/>
<property name="homeInterface" value="com.foo.service.RentalService"/>
<property name="refreshHomeOnConnectFailure" value="true"/>
</bean>
</code></pre>
<p>After...</p>
<pre class="line-numbers"><code class="language-xml"><jee:remote-slsb id="complexRemoteEjb"
jndi-name="ejb/MyRemoteBean"
business-interface="com.foo.service.RentalService"
cache-home="true"
lookup-home-on-startup="true"
resource-ref="true"
home-interface="com.foo.service.RentalService"
refresh-home-on-connect-failure="true">
</code></pre>
<p><br></p>
<h4>C.2.4 lang 스키마</h4>
<p>lang 태그는 스프링 컨테이너의 빈을 JRuby나 Groovy 같은 동적 언어로 작성한 경우 노출 객체를 다룬다.</p>
<p>이러한 태그(와 동적 언어 지원)은 Chapter 27, 동적 언어 지원 챕터에서 자세하게 다루었다. 동적 언어 지원과 lang 태그에 대한 자세한 내용은 이 챕터를 참고해라.</p>
<p>lang 스키마의 태그를 사용하려면 스프링 XML 구성파일의 상단에 다음 코드를 넣어야 한다. 다음 예시의 코드는 올바른 스키마를 참조해서 lang 네임스페이스의 태그를 사용할 수 있게 한다.</p>
<pre class="line-numbers"><code class="language-xml"><?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">
<!-- 여기에 <bean/> 정의를 작성한다 -->
</beans>
</code></pre>
<p><br></p>
<h4>C.2.5 jms 스키마</h4>
<p>jms 태그는 스프링의 MessageListenerContainers처럼 JMS와 관련된 빈의 구성을 다룬다. 이러한 태그는 Section 22.6, “JMS 네임스페이스 지원”라는 제목의 JMS 챕터에서 자세히 설명했다. JMS 지원과 jms 태그에 대한 자세한 내용은 해당 챕터를 참고하기 바란다.</p>
<p>jms 스키마의 태그를 사용하려면 스프링 XML 구성파일 상단에 다음 코드를 넣어야 한다. 다음 예시의 코드는 올바른 스키마를 참조해서 jms 네임스페이스의 태그를 사용할 수 있게 한다.</p>
<pre class="line-numbers"><code class="language-xml"><?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:jms="http://www.springframework.org/schema/jms"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms-3.0.xsd">
<!-- 여기에 <bean/> 정의를 작성한다 -->
</beans>
</code></pre>
<p><br></p>
<h4>C.2.6 tx (트랜잭션) 스키마</h4>
<p>tx 태그는 스프링의 광범위한 트랜잭션 지원의 모든 빈의 구성을 다룬다. 이 태그는 Chapter 11, 트랜잭션 관리 장에서 다루었다.</p>
<div style="padding:10px; background-color:#EDE5A6"><font color="#000000">
<b>Tip</b><br>
스프링 배포판에 포함된 'spring-tx-3.0.xsd' 파일을 보기를 권장한다. (물론) 이 파일은 스프링 트랜잭션 구성의 XML 스키마로 기본 속성 등을 포함해서 tx 네임스페이스의 다양한 태그를 모두 다룬다. 이 파일은 문서에 포함되어 있으므로 DRY (Don't Repeat Yourself) 원리를 지키기 위해 여기서 다시 얘기하지는 않는다.
</font></div>
<p><br></p>
<p>tx 스키마의 태그를 사용하려면 스프링 XML 구성파일 상단에 다른 코드를 넣어야 한다. tx 네임스페이스를 사용할 수 있도록 다음 예시의 텍스트는 올바른 스키마를 참조하고 있다.</p>
<pre class="line-numbers"><code class="language-xml"><?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<!-- 여기서 <bean/> 정의를 한다 -->
</beans>
</code></pre>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
tx 네임스페이스로 태그를 사용하는 경우 aop 네임스페이스의 태그도 사용할 수 있다. (스프링의 선언적인 트랜잭션 지원이 AOP로 구현되었으므로) 위 XML에 aop 스키마를 참조하기에 필요한 코드가 포함되어 있으므로 aop 네임스페이스의 태그를 사용할 수 있다.
</font></div>
<p><br></p>
<h4>C.2.7 aop 스키마</h4>
<p>aop 태그는 스프링의 AOP와 관련된 모든 것을 다룬다. 이 태그는 스프링의 프락시 기반 AOP 프레임워크와 AspectJ AOP 프레임워크와의 스프링 통합을 포함한다. 이 태그는 Chapter 8, 스프링의 관점 지향 프로그래밍 장에서 자세히 다루었다.</p>
<p>aop 스키마의 태그를 사용하려면 스프링 XML 구성 파일 상단에 다음 코드를 넣어야 한다. 다음 코드에서 올바른 스키마를 참조하고 있으므로 aop 네임스페이스의 태그를 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-xml"><?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<!-- 여기서 <bean/> 정의를 한다 -->
</beans>
</code></pre>
<p><br></p>
<h4>C.2.8 context 스키마</h4>
<p>context 태그는 최종 사용자에게 중요한 빈이 아니라 일반적인 빈이 아니라 BeanfactoryPostProcessors 처럼 스프링에서 동작에 문제가 있는 빈과 관련된 plumbing과 관련된 ApplicationContext 구성을 다룬다. 다음 코드가 올바른 스키마를 참조하므로 context 네임스페이스의 태그를 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-xml"><?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:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<!-- 여기서 <bean/> 정의를 한다 -->
</beans>
</code></pre>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
context 스키마는 스프링 2.5에서만 도입되었다.
</font></div>
<p><br></p>
<h4>C.2.8.1 <property-placeholder/></h4>
<p>이 요소는 ${...} 플레이스홀더를 지정한 프로퍼티 파일(Spring resource location처럼)로 처리해서 교체하게 한다. 이 요소는 PropertyPlaceholderConfigurer를 설정하는 편리한 방법이다. PropertyPlaceholderConfigurer를 더 세세하게 제어해야 한다면 명시적으로 직접 정의해라.<br />
<br></p>
<h4>C.2.8.2 <annotation-config/></h4>
<p>(사용할 수 있는 경우) JSR 250의 @PostConstruct, @PreDestroy, @Resource와 (사용할 수 있는 경우) JPA의 @PersistenceContext, @PersistenceUnit와 마찬가지로 스프링의 @Required와 @Autowired같은 빈 클래스가 탐지한 다양한 어노테이션을 스프링 인프라가 활성화한다. 아니면 사용할 어노테이션에 대한 개별 BeanPostProcessors를 명시적으로 활성화할 수 있다.</p>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
이 요소는 스프링의 @Transactional 어노테이션을 활성화 하지 않는다. @Transactional 어노테이션을 활성화하려면 <tx:annotation-driven/> 요소를 사용해라.
</font></div>
<p><br></p>
<h4>C.2.8.3 <component-scan/></h4>
<p>이 요소는 Section 4.9, “어노테이션기반의 컨테이너 설정”에 잘 나와 있다.<br />
<br></p>
<h4>C.2.8.4 <load-time-weaver/></h4>
<p>이 요소는 Section 8.8.4, “스프링 프레임워크에서 AspectJ를 사용한 로드타임 위빙(Load-time weaving)”에 잘 나와 있다.<br />
<br></p>
<h4>C.2.8.5 <spring-configured/></h4>
<p>이 요소는 Section 8.8.1, “스프링에 도메인 객체를 의존성 주입시 AspectJ 사용하기”에 잘 나와 있다.<br />
<br></p>
<h4>C.2.8.6 <mbean-export/></h4>
<p>이 요소는 Section 23.4.3, “<context:mbean-export/> 요소”에 잘 나와 있다.<br />
<br></p>
<h4>C.2.9 tool 스키마</h4>
<p>tool 태그는 커스텀 설정 요소에 대한 도구에 국한된 메타데이터를 추가하고자 할 때 사용한다. 그 후 이 메타데이터를 이해할 수 있는 도구가 이 데이터를 사용해서 원하는 무엇이든 할 수 있다.(유효성 검사 등)</p>
<p>tool 태그는 현재 리뷰를 진행하고 있으므로 이번 스프링 릴리즈에는 문서로 만들어 져 있지 않다. 당신이 서드파티 벤더라서 이 리뷰 과정에 참여하고 싶다면 스프링 메일링 리스트에 메일을 보내라. 현재 지원하는 tool 태그는 스프링 소스 배포판의 'src/org/springframework/beans/factory/xml' 디렉토리에서 'spring-tool-3.0.xsd' 파일에 나와 있다.<br />
<br></p>
<h4>C.2.10 beans 스키마</h4>
<p>마지막으로 얘기하지만 무시할 수 없는 태그가 beans 스키마의 태그이다. 스프링 프레임워크의 아주 초창기부터 같은 태그가 존재하고 있다. beans 스키마의 태그는 Section 4.4.2, “의존성과 설정에 대한 자세한 내용”에서 아주 광범위하게 다루었으므로(전체 장에 걸쳐서 다루었다.) 여기서 beans 스키마의 다양한 태그의 예시를 보여주진 않는다.</p>
<p>스프링 2.0에서 beans 태그 자체에 새로 추가된 것 중 하나는 임의의 빈 메타데이터에 대한 아이디어이다. 스프링 2.0에서는 <bean/> XML 정의에 다수의 키/값 쌍을 추가하거나 추가하지 않을 수 있다. 이 여분의 메타데이터로 할 수 있는 일은 완전히 작성한 커스텀 로직에 달려 있다.(그리고 Appendix D, Extensible XML authoring 부록에서 설명한 대로 보통 커스텀 태그를 작성한 경우에만 사용한다.</p>
<p>상위의 <bean/> 컨텍스트에서 <meta/> 태그를 사용한 예제가 아래 나와 있다.(메타데이터를 해석하는 로직 없이는 아무런 효과가 없다는 점을 명심해라.)</p>
<pre class="line-numbers"><code class="language-xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="foo" class="x.y.Foo">
<meta key="cacheName" value="foo"/>
<property name="name" value="Rick"/>
</bean>
</beans>
</code></pre>
<p>위 예제에서 빈 정의를 사용하고 제공한 메타데이터로 캐시 인프라스트럭처를 구성할 어떤 로직이 있다고 가정할 수 있다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1148?commentInput=true#entry1148WriteComment">댓글 쓰기</a></strong></p>[Spring 레퍼런스] 부록 B. 고전적인 Spring AOP 사용방법Outsiderhttps://blog.outsider.ne.kr/11282015-03-11T02:08:38+09:002015-03-11T02:08:38+09:00<p>이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.<br><br><br><br></p>
<h2>부록 B. 고전적인 Spring AOP 사용방법</h2>
<p>이번 부록에서는 저수준 스프링 AOP API와 스프링 1.2 애플리케이션에서 사용하는 AOP 지원을 설명한다. 새로운 애플리케이션에서는 AOP장에서 설명한 스프링 2.0 AOP를 사용하길 권장하지만, 기존 애플리케이션에서 작업하거나 책, 기사를 읽는 경우 스프링 1.2 방식의 예제를 볼 수도 있다. 스프링 2.0은 스프링 1.2와 하위 호환성을 완전히 유지하고 있고 이번 부록에서 설명하는 모든 내용은 스프링 2.0에서도 지원한다.<br />
<br></p>
<h3>B.1 스프링의 포인트컷(Pointcut) API</h3>
<p>스프링이 결정적인 포인트컷의 개념을 어떻게 다루는지 살펴보자.<br />
<br></p>
<h4>B.1.1 개념</h4>
<p>스프링의 포인트컷 모델은 어드바이스 타입과 독립적으로 포인트컷을 재사용할 수 있게 한다. 그래서 다른 어드바이스를 대상으로 같은 포인트컷을 사용할 수 있다.</p>
<p>org.springframework.aop.Pointcut 인터페이스가 특정 클래스와 메서드를 대상으로 하는 어드바이스에 사용하는 핵심 인터페이스이다. 아래 전체 인터페이스가 나와 있다.</p>
<pre class="line-numbers"><code class="language-java">public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
</code></pre>
<p>Pointcut 인터페이스를 두 부분으로 분리해서 클래스와 메서드에 걸맞는 부분을 재사용할 수 있고 세밀한 구성(composition) 작업을 할 수 있다.(다른 메서드 매처(matcher)와 함께 "union"을 수행하는 등)</p>
<p>포인트컷을 주어진 대상 클래스의 세트로 제한할 때 ClassFilter 인터페이스를 사용한다. matches() 메서드가 항상 true를 반환하면 모든 클래스에 매칭될 것이다.</p>
<pre class="line-numbers"><code class="language-java">public interface ClassFilter {
boolean matches(Class clazz);
}
</code></pre>
<p>MethodMatcher가 보통은 더 중요하다. 전체 인터페이스가 아래 나와 있다.</p>
<pre class="line-numbers"><code class="language-java">public interface MethodMatcher {
boolean matches(Method m, Class targetClass);
boolean isRuntime();
boolean matches(Method m, Class targetClass, Object[] args);
}
</code></pre>
<p>이 포잇트컷이 대상 클래스의 해당 메서드에 매칭될 것인지를 테스트할 때 matches(Method, Class) 메서드를 사용한다. 메서드 호출시마다 테스트가 필요한 경우를 피하려고 AOP 프락시를 생성할 때 이 평가를 수행할 수 있다. 2개의 인자를 가진 matches 메서드는 해당 메서드에 true를 반환하고 MethodMatcher의 isRuntime() 메서드가 true를 반환한다면 3개의 인자를 가진 matches 메서드는 메서드 호출시마다 호출될 것이다. 대상 어드바이스가 실행되기 전에 메서드 호출에 전달된 인자를 바로 보고 포인트컷을 활성화한다.</p>
<p>대부분의 MethodMatcher는 정적이라서 MethodMatcher의 isRuntime() 메서드는 false를 반환한다. 이 경우 3개의 인자를 가진 matches 메서드는 절대 호출되지 않는다.</p>
<div style="padding:10px; background-color:#EDE5A6"><font color="#000000">
<b>Tip</b><br>
가능하다면 포인트컷을 정적으로 만들어서 AOP 프레임워크가 AOP 프락시를 생성할 때 포인트컷 평가 결과를 캐시하도록 해라.
</font></div>
<p><br></p>
<h4>B.1.2 포인트컷에서의 작업(operation)</h4>
<p>스프링은 포인트컷에서 작업(operation)을 지원한다. 특히 union과 intersection가 있다.</p>
<ul>
<li>Union은 어느 한쪽의 포인트컷에 매칭되는 메서드를 의미한다.</li>
<li>Intersection는 두 포인트컷에 모두 매칭되는 메서드를 의미한다.</li>
<li>보통은 Union이 더 유용하다.</li>
<li>org.springframework.aop.support.Pointcuts 클래스의 정적 메서드나 같은 패키지의 ComposablePointcut 클래스를 사용해서 포인트컷을 구성할 수 있다. 하지만 AspectJ를 사용한다면 포인트컷 표현식이 보통은 더 간단해질 것이다.</li>
</ul>
<h4>B.1.3 AspectJ 표현식 포인트컷</h4>
<p>2.0부터는 스프링이 사용하는 포인트컷 중에 가장 중요한 타입은 org.springframework.aop.aspectj.AspectJExpressionPointcut이다. 이는 AspectJ 포인트컷 표현식 문자열을 파싱하려고 AspectJ가 제공하는 라이브러리를 사용하는 포인트컷이다.</p>
<p>지원하는 AspectJ 포인트컷에 대한 얘기는 이전 장을 참고해라.<br />
<br></p>
<h4>B.1.4 편리한 포인트컷 구현체</h4>
<p>스프링은 편리한 포인트컷 구현체를 여러 가지 제공한다. 일부는 독창적으로 사용할 수 있고 일부는 애플리케이션에 특화된 포인트컷의 하위클래스로 만들어졌다.</p>
<h4>B.1.4.1 정적(Static) 포인트컷</h4>
<p>정적 포인트컷은 메서드와 대상 클래스에 기반을 두고 있어서 메서드의 인자를 고려할 수 없다. 정적 포인트컷은 대부분의 경우에 만족스럽다.(가장 좋은 방법이기도 하다) 스프링이 메서드를 처음 호출할 때 정적 포인트컷을 딱 한 번만 평가하고 이후에는 메서드 호출하더라도 포인트컷을 다시 평가하지 않도록 할 수 있다.</p>
<p>스프링에 포함된 정적 포인트컷 구현체를 보자.</p>
<p><strong>정규 표현식 포인트컷</strong></p>
<p>정적 포인트컷을 지정하는 명확한 방법의 하나는 정규표현식을 사용하는 것이다. 스프링 외의 여러 AOP 프레임워크도 정규표현식을 지원한다. org.springframework.aop.support.Perl5RegexpMethodPointcut은 일반적인 정규표현식 포인트컷으로 Perl 5 정규표현식 문법을 사용한다. Perl5RegexpMethodPointcut 클래스는 정규표현식 매칭을 할 때 Jakarta ORO에 의존한다. 스프링은 JDK 1.4x에서 지원하는 정규표현식을 사용하는 JdkRegexpMethodPointcut 클래스도 제공한다.</p>
<p>Perl5RegexpMethodPointcut 클래스를 사용해서 패턴 문자열의 목록을 제공할 수 있다. 이 중에서 일치하는 패턴이 있다면 포인트컷은 true로 평가할 것이다.(그래서 결과는 이러한 포인트컷을 효과적으로 합친 결과이다.)</p>
<p>사용방법은 아래 나와 있다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="settersAndAbsquatulatePointcut"
class="org.springframework.aop.support.Perl5RegexpMethodPointcut">
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>
</code></pre>
<p>스프링은 Advice도 참조할 수 있도록(Advice는 interceptor, before advice, throws advice가 될 수 있다는 점을 기억해라) RegexpMethodPointcutAdvisor라는 편리한 클래스를 제공한다. 내부에서 스프링은 JdkRegexpMethodPointcut를 사용할 것이다. RegexpMethodPointcutAdvisor로 아래 나와 있는 대로 포인트컷과 어브다이스를 모두 하나의 빈으로 캡슐화해서 연결을 간소화한다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="settersAndAbsquatulateAdvisor"
class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice">
<ref local="beanNameOfAopAllianceInterceptor"/>
</property>
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>
</code></pre>
<p>RegexpMethodPointcutAdvisor는 어떤 Advice 종류와도 사용할 수 있다.</p>
<p><strong>Attribute 주도 포인트컷</strong></p>
<p>정적 포인트컷의 중요한 종류는 메타데이터 주도(metadata-driven) 포인트컷이다. 이는 메타데이터 속성(attribute)의 값(보통은 소스 수준의 메타데이터)을 사용한다.</p>
<h4>B.1.4.2 동적 포인트컷</h4>
<p>동적 포인트컷은 정적 포인트컷보다 평가하는데 더 큰 비용이 들고 메서드의 인자를 정적인 정보처럼 고려할 수 있다. 그래서 메서드 호출시마다 평가해야 한다. 인자가 다양하므로 결과는 캐시할 수 없다.</p>
<p>대부분의 예제는 control flow 포인트컷이다.</p>
<p><strong>흐름 제어(Control flow) 포인트컷</strong></p>
<p>스프링 흐름 제어 포인트컷은 AspectJ의 cflow 포인트컷보다 덜 강력하지만, 개념적으로는 유사하다. (다른 포인트컷으로 매칭된 조인 포인트 하에서 포인트컷을 실행하도록 하는 방법은 현재 없다.) 흐름 제어 포인트컷은 현재 콜스택(call stack)에서 매칭한다. 예를 들어 com.mycompany.web 패키지나 SomeCaller 클래스에서 메서드가 조인포인트를 호출했다면 흐름 제어 포인트컷이 호출된다. 흐름 제어 포인트컷은 org.springframework.aop.support.ControlFlowPointcut 클래스로 지정한다.</p>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
흐름 제어 포인트컷은 다른 동적 포인트컷보다도 런타임 실행 비용이 상당히 더 크다. Java 1.4에서 다른 동적 포인트컷보다 5배 정도의 비용이 더 든다.
</font></div>
<p><br></p>
<h4>B.1.5 포인트컷 슈퍼클래스</h4>
<p>스프링은 자신만의 포인트컷을 구현할 수 있도록 유용한 포인트컷 슈퍼클래스를 제공한다.</p>
<p>정적 포인트컷이 가장 유용하므로 다음과 같이 StaticMethodMatcherPointcut의 하위 클래스를 만들 것이다. StaticMethodMatcherPointcut를 구현할 때는 딱 하나의 추상 메서드만 구현하면 된다.(동작을 수정하려고 다른 메서드를 오버라이드하는 것도 가능하지만)</p>
<pre class="line-numbers"><code class="language-java">class TestStaticPointcut extends StaticMethodMatcherPointcut {
public boolean matches(Method m, Class targetClass) {
// 커스텀 criteria가 일치한다면 true를 반환한다
}
}
</code></pre>
<p>동적 포인트컷의 슈퍼클래스도 있다.</p>
<p>스프링 1.0 RC2 이상의 모든 어드바이스 타입을 사용해서 커스텀 포인트컷을 사용할 수 있다.<br />
<br></p>
<h4>B.1.6 커스텀 포인트컷</h4>
<p>스프링 AOP의 포인트컷이 언어 기능(AspectJ처럼)이 아니라 자바 클래스이므로 정적이든 동적이든 커스텀 포인트컷을 정의할 수 있다. 스프링의 커스텀 포인트컷은 상황에 따라 꽤 복잡해 질 수 있다. 하지만 가능하다면 AspectJ 포인트컷 표현식 언어를 사용하기를 권장한다.</p>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
스프링의 차기 버전에서는 예를 들면 "대상 객체의 인스턴스 변수를 바꾸는 모든 메서드" 처럼 JAC가 제공하는 "세만틱 포인트컷(semantic pointcuts)"을 지원할 것이다.
</font></div>
<p><br></p>
<h3>B.2 스프링의 어드바이스(Advice) API</h3>
<p>스프링 AOP가 어드바이스를 다루는 방법을 살펴보자.<br />
<br></p>
<h4>B.2.1 어드바이스 생명주기</h4>
<p>각 어드바이스는 스프링 빈(bean)이다. 어드바이스 인스턴스를 모든 어드바이스된 객체 사이에 공유할 수도 있고 각 어드바이스된 객체 내에서 유일할 수도 있다. 이를 각각 per-class와 per-instance 어드바이스라고 한다.</p>
<p>Per-class 어드바이스를 가장 많이 사용한다. 이는 트랜잭션 어드바이저같은 일반적인 어드바이스에 적절하다. 프락시 된 객체의 상태나 새로운 상태 추가에 따라 동작하지 않고 메서드와 인자에 따라 동작한다.</p>
<p>Per-instance 어드바이스는 믹스인을 지원하는 인트로덕션(introduction)에 적절하다. 이 경우에 어드바이스는 프락시 된 객체에 상태를 추가한다.</p>
<p>같은 AOP 프락시에서 공유된 어드바이스와 per-instance 어드바이스를 섞어서 사용할 수 있다.<br />
<br></p>
<h4>B.2.2 스프링의 어드바이스 타입</h4>
<p>스프링은 다양한 어드바이스 타입을 제공하고 임의의 어드바이스 타입을 지원하도록 확장도 가능하다. 표준 어드바이스 타입과 기본 개념을 살펴보자.</p>
<h4>B.2.2.1 Interception around advice</h4>
<p>스프링에서 가장 기본적인 어드바이스 타입은 interception around advice이다.</p>
<p>스프링은 메서드 인터셉션을 사용해서 around advice의 AOP Alliance interface와 호환성이 있다. around advice를 구현한 MethodInterceptor는 다음 인터페이스를 구현해야 한다.</p>
<pre class="line-numbers"><code class="language-java">public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
</code></pre>
<p>invoke() 메서드의 MethodInvocation 인자는 호출되는 메서드, 대상 조인 포인트, AOP 프록시, 메서드의 인자를 노출한다. invoke() 메서드는 호출의 결과(조인포인트(join point)의 반환값)를 반환해야 한다.</p>
<p>간단한 MethodInterceptor 구현체는 다음과 같다.</p>
<pre class="line-numbers"><code class="language-java">public class DebugInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("Before: invocation=[" + invocation + "]");
Object rval = invocation.proceed();
System.out.println("Invocation returned");
return rval;
}
}
</code></pre>
<p>MethodInvocation의 proceed() 메서드의 호출을 봐라. 이는 인터셉터 체인을 따라 조인포인트로 진행한다. 대부분의 인터셉터는 이 메서드를 호출하고 그 반환 값을 반환할 것이다. 하지만 다른 around advice처럼 MethodInterceptor는 다른 값을 반환하거나 proceed 메서드를 호출하지 않고 예외를 던질 수 있다. 그렇지만 그럴듯한 이유가 없다면 이렇게 할 이유가 없다!</p>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
MethodInterceptor는 다른 AOP Alliance 호환 AOP 구현체와 상호운용성이 있다. 이번 장의 뒷부분에 나오는 어드바이스 타입들은 공통의 AOP 개념을 구현했지만, 스프링에 특화된 방법으로 구현했다. 가장 구체적인 어드바이스 타입을 사용하는 이점을 누리면서 다른 AOP 프레임워크의 관점을 사용하고자 한다면 MethodInterceptor around advice를 사용해라. 포인트컷은 현재 프레임워크간의 상호운용성이 없고 AOP Alliance는 포인트컷 인터페이스를 정의하지 않았다.
</font></div>
<p><br></p>
<h4>B.2.2.2 Before advice</h4>
<p>더 간단한 어드바이스 타입은 before advice다. 이는 메서드에 진입하기 전에만 호출되므로 MethodInvocation 객체가 필요없다.</p>
<p>before advice의 주요 이점은 proceed() 메서드를 호출할 필요가 없으므로 인터셉터 체인을 따라가다가 실패할 가능성이 없다는 점이다.</p>
<p>MethodBeforeAdvice 인터페이스가 아래 나와 있다. (일반적인 객체가 필드 가로채기를 할 수 있고 스프링이 이를 구현할 것 같지는 않지만, 스프링 API 디자인은 어드바이스 이전에 필드를 허용한다.)</p>
<pre class="line-numbers"><code class="language-java">public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method m, Object[] args, Object target) throws Throwable;
}
</code></pre>
<p>반환타입은 void이다. before advice는 조인포인트를 실행하기 전에 임의의 동작을 추가할 수 있지만 반환 값은 바꿀 수 없다. before advice가 예외를 던지면 인터셉터 체인을 추가로 실행하지 않는다. 예외는 인터셉터 체인을 거꾸로 따라 전파될 것이다. 예외가 unchecked 예외이면(아니면 호출된 메서드의 시그니처에서) 클라이언트에게 직접 전달될 것이다. 그렇지 않으면 unchecked 예외에서 AOP 프락시가 감쌀 것이다.</p>
<p>다음은 스프링에서 모든 메서드의 호출 횟수를 세는 before advice 예시이다.</p>
<pre class="line-numbers"><code class="language-java">public class CountingBeforeAdvice implements MethodBeforeAdvice {
private int count;
public void before(Method m, Object[] args, Object target) throws Throwable {
++count;
}
public int getCount() {
return count;
}
}
</code></pre>
<div style="padding:10px; background-color:#EDE5A6"><font color="#000000">
<b>Tip</b><br>
before advice를 어떤 포인트컷과도 사용할 수 있다.
</font></div>
<p><br></p>
<h4>B.2.2.3 Throws advice</h4>
<p>Throws advice는 조인 포인트가 예외를 던지면 조인포인트의 반환 후에 호출된다. 스프링은 타입이 있는 throws advice를 제공한다. 이는 org.springframework.aop.ThrowsAdvice 인터페이스가 아무런 메서드도 가지지 않는 다는 것을 의미한다. 주어진 객체가 하나 이상의 타입이 있는 throws advice 메서드를 구현했는지 구별하는 태그 인터페이스이다. 이는 다음과 같은 형식이 되어야 한다.</p>
<pre class="line-numbers"><code class="language-java">afterThrowing([Method, args, target], subclassOfThrowable)
</code></pre>
<p>마지막 인자만 필수값이다. 어드바이스 메서드가 메서드와 인자에 관심있냐에 따라 메서드 시그니처는 한 인자나 네 개의 인자를 가질 수 있다. 다음 클래스는 throws advice의 예시이다.</p>
<p>RemoteException가 던져질 때(하위 클래스를 포함해서) 아래 어드바이스를 호출한다.</p>
<pre class="line-numbers"><code class="language-java">public class RemoteThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// 원격 예외로 어떤 작업을 한다
}
}
</code></pre>
<p>ServletException가 던져질 때 다음 어드바이스를 호출한다. 앞의 어드바이스와는 달리 4개의 인자를 선언했으므로 호출한 메서드, 메서드 인자, 대상 객체에 접근한다.</p>
<pre class="line-numbers"><code class="language-java">public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// 모든 인자로 어떤 작업을 한다
}
}
</code></pre>
<p>마지막 예제에서 RemoteException와 ServletException를 모두 처리하는 한 클래스에서 이 두 메서드를 어떻게 사용할 수 있는지 설명한다. 하나의 클래스에 다수의 throws advice 메서드를 합칠 수 있다.</p>
<pre class="line-numbers"><code class="language-java">public static class CombinedThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// 원격 예외로 어떤 작업을 한다
}
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// 모든 인자로 어떤 작업을 한다
}
}
</code></pre>
<p>Note: throws advice 메서드가 예외를 던진다면 원래의 예외를 오버라이드할 것이다.(예: 사용자에게 던지는 예외를 바꾼다.) 보통 오버라이딩된 예외는 RuntimeException가 될 것이고 이는 어떤 메서드 시그니처와도 호환성을 가진다. 하지만 throws advice 메서드가 체크드 예외를 던진다면 대상 메서드에서 선언된 예외와 일치해야 하므로 특정 대상 메서드 시그니처와 커플링이 생길 것이다. 대산 메서드 시그니처와 호환되지 않고 선언되지 않은 체크드 예외를 던지지 마라!</p>
<div style="padding:10px; background-color:#EDE5A6"><font color="#000000">
<b>Tip</b><br>
throws advice는 어떤 포인트컷과도 사용할 수 있다.
</font></div>
<p><br></p>
<h4>B.2.2.4 After Returning advice</h4>
<p>스프링에서 after returning advice는 아래 나와 있는 것처럼 org.springframework.aop.AfterReturningAdvice 인터페이스를 구현해야 한다.</p>
<pre class="line-numbers"><code class="language-java">public interface AfterReturningAdvice extends Advice {
void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable;
}
</code></pre>
<p>after returning advice는 반환 값(수정할 수 없다), 호출된 메서드, 메서드 인자와 대상에 접근한다.</p>
<p>다음 after returning advice는 예외를 던지지 않고 성공적으로 이뤄진 모든 메서드 호출의 횟수를 센다.</p>
<pre class="line-numbers"><code class="language-java">public class CountingAfterReturningAdvice implements AfterReturningAdvice {
private int count;
public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable {
++count;
}
public int getCount() {
return count;
}
}
</code></pre>
<p>이 어드바이스는 실행경로를 바꾸지 않는다. 이 어드바이스가 예외를 던진다면 값을 반환하는 대신 인터셉터 체인에 예외를 던질 것이다.</p>
<div style="padding:10px; background-color:#EDE5A6"><font color="#000000">
<b>Tip</b><br>
After returning advice는 어떤 포인트컷과도 사용할 수 있다.
</font></div>
<p><br></p>
<h4>B.2.2.5 인트로덕션(introduction) 어드바이스</h4>
<p>스프링은 introduction advice를 특별한 종류의 인터셉트 어드바이스로 다룬다.</p>
<p>Introduction은 IntroductionAdvisor와 다음 인터페이스를 구현하는 IntroductionInterceptor를 필요로 한다.</p>
<pre class="line-numbers"><code class="language-java">public interface IntroductionInterceptor extends MethodInterceptor {
boolean implementsInterface(Class intf);
}
</code></pre>
<p>AOP Alliance MethodInterceptor 인터페이스에서 상속받은 invoke() 메서드는 introduction을 반드시 구현해야 한다. 이 말은 호출된 메서드가 인트로덕션이 적용된 인터페이스에 있을 때 인트로덕션 인터셉터가 메서드 호출을 다루게 된다는 의미이다. 이는 proceed()를 호출할 수 없다.</p>
<p>인트로덕션 어드바이스는 어떤 포인트컷과도 사용할 없고 메서드가 아니라 클래스 수준에만 적용된다. 인트로덕션 어드바이스는 다음 메서드를 가지는 IntroductionAdvisor만 함께 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-java">public interface IntroductionAdvisor extends Advisor, IntroductionInfo {
ClassFilter getClassFilter();
void validateInterfaces() throws IllegalArgumentException;
}
public interface IntroductionInfo {
Class[] getInterfaces();
}
</code></pre>
<p>MethodMatcher가 없으므로 인트로덕션 어드바이스와 관련된 Pointcut도 없다. 클래스 필터링만 적합하다.</p>
<p>getInterfaces() 메서드는 이 어드바이저가 인트로덕션한 인터페이스를 반환한다.</p>
<p>구성한 IntroductionInterceptor가 구현할 수 있는 인트로덕션이 된 인터페이스인지 아닌지를 보기 위해 내부적으로 validateInterfaces() 메서드를 사용한다.</p>
<p>스프링 테스트 슈트의 간단한 예시를 보자. 다음 인터페이스를 하나 이상의 객체에 인트로덕션하려고 한다고 가정해보자.</p>
<pre class="line-numbers"><code class="language-java">public interface Lockable {
void lock();
void unlock();
boolean locked();
}
</code></pre>
<p>이는 <strong>mixin</strong>을 설명한다. 어드바이즈가 적용된 객체를 Lockable(류의 타입)로 캐스팅해서 lock, unlock 메서드를 호출할 수 있기를 원한다. lock() 메서드를 호출하면 모든 setter 메서드가 LockedException를 던지기를 원한다. 그래서 객체에 대해서 전혀 모르더라도 객체를 불변상태로 만들 수 있는 관점을 추가할 수 있다. 이는 AOP의 좋은 예시이다.</p>
<p>우선, 무거운 작업을 하는 IntroductionInterceptor가 필요할 것이다. 이 경우 간편한 org.springframework.aop.support.DelegatingIntroductionInterceptor 클래스를 확장한다. IntroductionInterceptor를 직접 구현할 수도 있지만, 대부분의 경우 DelegatingIntroductionInterceptor를 사용하는 것이 최상의 선택이다.</p>
<p>DelegatingIntroductionInterceptor는 인트로덕션이 된 인터페이스의 실제 구현체에 인트로덕션을 위임하도록 설계되어서 인터셉션의 사용은 감추면서 생성자 인자로 어떤 객체에도 위임을 설정할 수 있다. 이것이 기본적인 위임(인자가 없는 생성자를 사용하는 경우)이다. 그래서 아래 예제에서 위임은 DelegatingIntroductionInterceptor의 하위클래스인 LockMixin이다. 해당 위임(기본 위임)인 DelegatingIntroductionInterceptor 인스턴스는 위임(IntroductionInterceptor)으로 구현된 모든 인터페이스를 찾고 이 인터페이스에 인트로덕션을 지원할 것이다. LockMixin같은 하위클래스가 노출되면 안되는 인터페이스를 감추도록 suppressInterface(Class intf) 메서드를 호출하게 하는 것이 가능하다. 하지만 IntroductionInterceptor가 얼마나 많은 인터페이스를 지원할 준비가 되었는 지와는 관계없이 사용한 IntroductionAdvisor가 실제로 노출되는 인터페이스를 제어할 것이다. 인트로덕션이 된 인터페이스는 같은 인터페이스의 모든 구현체를 대상한테서 감출 것이다.</p>
<p>LockMixin는 DelegatingIntroductionInterceptor를 서브클래스화 하고 Lockable를 구현한다. 슈퍼클래스는 인트로덕션으로 지원할 수 있는 Lockable을 자동으로 선택하므로 지정할 필요가 없다. 이 방법으로 다수의 인터페이스를 인트로듀스할 수 있다.</p>
<p>locked 인스턴스 변수의 사용을 잘 봐라. 이는 대상 객체에 보관되는 추가 상태를 효과적으로 추가하는 방법이다.</p>
<pre class="line-numbers"><code class="language-java">public class LockMixin extends DelegatingIntroductionInterceptor
implements Lockable {
private boolean locked;
public void lock() {
this.locked = true;
}
public void unlock() {
this.locked = false;
}
public boolean locked() {
return this.locked;
}
public Object invoke(MethodInvocation invocation) throws Throwable {
if (locked() && invocation.getMethod().getName().indexOf("set") == 0)
throw new LockedException();
return super.invoke(invocation);
}
}
</code></pre>
<p>때로는 invoke() 메서드를 오버라이드 할 필요가 없다. DelegatingIntroductionInterceptor 구현체(메서드가 인트로듀스되었다면 위임 메서드를 호출하고 인트로유스되지 않았다면 조인포인트로 진행한다.)로 보통은 충분하다. 이 경우에 locked 모드라면 호출할 수 있는 setter 메서드가 없다는 것을 추가로 확인해야 한다.</p>
<p>필요한 인트로덕션 어드바이저는 간단한데 별도의 LockMixin 인스턴스를 보관하고 인트로듀스된 인터페이스(여기서는 Lockable이다)를 지정하는 것이 해야하는 전부이다. 더 복잡한 예시에서는 인트로덕션 인터셉터(prototype으로 정의될 것이다.)를 참조해야 할 수도 있다. 여기서는 LockMixin과 관련된 설정이 없으므로 new로 생성한다.</p>
<pre class="line-numbers"><code class="language-java">public class LockMixinAdvisor extends DefaultIntroductionAdvisor {
public LockMixinAdvisor() {
super(new LockMixin(), Lockable.class);
}
}
</code></pre>
<p>이 어드바이저를 아주 간단히 적용할 수 있는데 필요한 설정이 없다.(하지만 이는 필수적인데 IntroductionAdvisor없이 IntroductionInterceptor는 사용할 수 없다.) 일반적으로 인트로덕션을 사용할 때처럼 어드바이저는 상태를 가져야 하므로 인스터스마다 존재해야 한다. 다른 LockMixinAdvisor 인스턴스가 필요하므로 어드바이즈된 객체마다 LockMixin가 필요하다. 이 어드바이저가 어드바이즈된 객체의 상태를 구성한다.</p>
<p>다른 어드바이저처럼 Advised.addAdvisor()메서드나 XML 설정(권장하는 방법)을 사용해서 이 어드바이저를 프로그래밍으로 적용할 수 있다. 이후에 나오는 모든 프락시 생성방법("auto proxy creators"를 포함해서)은 인트로덕션과 상태를 가진 믹스인을 제대로 다룬다.<br />
<br></p>
<h3>B.3 스프링의 어드바이저 API</h3>
<p>스프링에서 Advisor는 포인트컷 표현식과 연관된 단일 어드바이스 객체를 가진 관점이다.</p>
<p>특수한 경우의 인트로덕션를 제외하고 모든 어드바이저는 어떤 어드바이스와 함께 사용할 수 있다. org.springframework.aop.support.DefaultPointcutAdvisor가 가장 일반적으로 사용하는 어드바이저 클래스다. 예를 들면 MethodInterceptor, BeforeAdvice, ThrowsAdvice와 함께 사용할 수 있다.</p>
<p>스프링에서 어드바이저와 어드바이스 타입을 같은 AOP 프록시에 믹스인할 수 있다. 예를 들어 하나의 프록시 설정에서 interception around advice, throws advice, before advice를 사용할 수 있다. 스프링이 자동으로 필요한 인터셉터 체인을 만들 것이다.<br />
<br></p>
<h3>B.4 AOP 프록시를 생성할 때 ProxyFactoryBean 사용하기</h3>
<p>비즈니스 객체에 Spring IoC 컨테이너(ApplicationContext나 BeanFactory)를 사용 중이라면(꼭 사용해야 한다!) 스프링의 AOP FactoryBean 중 하나를 사용하고자 할 것이다. (이는 간접계층(layer of indirection)을 도입해서 다른 타입의 객체들을 생성할 수 있도록 하는 팩토리 빈이라는 점을 유념해라.)</p>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
스프링 2.0 AOP 지원도 내부에서 팩토리 빈을 사용한다.
</font></div>
<p>스프링에서 AOP 프록시를 생성하는 기본적인 방법은 org.springframework.aop.framework.ProxyFactoryBean를 사용하는 것이다. 이를 통해 적용할 포인트컷과 어드바이스에 대한 완전한 제어권과 순서에 대한 제어권을 가질 수 있다. 하지만 이러한 제어권이 필요 없다면 더 선호되는 간단한 방법이 있다.<br />
<br></p>
<h4>B.4.1 기초</h4>
<p>다른 스프링 FactoryBean 구현체처럼 ProxyFactoryBean는 간접계층을 도입한다. foo라는 이름의 ProxyFactoryBean를 정의했다면 foo를 참조하는 객체가 ProxyFactoryBean 인스턴스가 아니고 getObject() 메서드의 ProxyFactoryBean의 구현체가 생성한 객체이다. getObject() 메서드는 대상 객체를 감싸는 AOP 프록시를 생성할 것이다.</p>
<p>AOP 프록시를 생성할 때 ProxyFactoryBean나 다른 IoC에 특화된 클래스를 사용할 때 가장 중요한 이점 중 하나는 IoC로 어드바이스나 포인트컷도 관리할 수 있다는 점이다. 이는 다른 AOP 프레임워크에서는 하기 힘든 접근방식을 가능하게 하는 강력한 기능이다. 예를 들어 의존성 주입으로 가능한 플러거블(pluggability) 기능의 모든 이점을 사용해서 어드바이스 자체가 애플리케이션 객체(다른 AOP 프레임워크에서 사용할 수 있어야 하는 대상객체를 제외하고)를 참조할 수 있다.<br />
<br></p>
<h4>B.4.2 JavaBean 프로퍼티</h4>
<p>스프링이 제공하는 FactoryBean 구현체와 마찬가지로 ProxyFactoryBean 클래스는 JavaBean이고 이 클래스의 프로퍼티들은 다음의 상황에 사용한다.</p>
<ul>
<li>프락시할 대상을 지정한다.</li>
<li>CGLIB을 사용할 지 여부를 지정한다.(아래의 내용과 Section 9.5.3, “JDK기반과 CGLIB기반의 프록시”를 참고해라.)</li>
</ul>
<p>몇몇 핵심 프로퍼티는 org.springframework.aop.framework.ProxyConfig에서 상속받는다. (스프링에서 모든 AOP 프락시 팩토리의 슈퍼클래스다.) 이 핵심 프로퍼티들은 다음과 같다.</p>
<ul>
<li>proxyTargetClass: 대상 클래스의 인터페이스가 아니라 대상 클래스가 프락시 된 경우 true이다. 이 프로퍼티 값을 true로 설정하면 CGLIB 프락시를 생성할 것이다. (Section 9.5.3, “JDK기반과 CGLIB기반의 프록시”도 참고해라.)</li>
<li>optimize: CGLIB로 생성한 프락시에 적극적인 최적화(aggressive optimizations)를 적용할 것인지를 제어한다. 관련 AOP 프락시가 어떻게 최적화를 하는지 완전히 이해하지 못했다면 이 설정은 조심스럽게 사용해야 한다. 이 설정은 현재 CGLIB 프락시에만 사용되므로 JDK 동적 프락시에는 영향을 주지 않는다.</li>
<li>frozen: 프락시 구성이 frozen이면 구성을 더는 변경할 수 없다. 이는 약간의 최적화를 하거나 콜러(caller)가 프락시를 다루지 (Advised 인터페이스로) 못하게 하고 싶은 경우에 유용하다. 이 프로퍼티의 기본 값이 false이므로 다른 어드바이스를 추가하는 등의 변경을 할 수 있다.</li>
<li>exposeProxy: 대상에서 프락시에 접근할 수 있도록 현재 프락시를 ThreadLocal에 노출할 것인지를 결정한다. 대상이 프락시를 획득해야 하고 exposeProxy 프로퍼티가 true이면 대상이 AopContext.currentProxy() 메서드를 사용할 수 있다.</li>
<li>aopProxyFactory: 사용할 AopProxyFactory 구현체로 동적 프락시나 CGLIB이나 다른 프락시 전략을 사용해야 하는지를 커스터마이징하는 방법을 제공한다. 기본 구현체는 동적 프락시나 CGLIB을 적절하게 선택할 것이다. 이 프로퍼티를 사용할 필요가 없어야 한다. 이는 스프링 1.1에서 새로운 프락시 타입을 추가할 수 있도록 만든 것이다.</li>
</ul>
<p>ProxyFactoryBean에 한정된 프로퍼티에는 다음과 같은 것들이 있다.</p>
<ul>
<li>proxyInterfaces: 문자열로 된 인터페이스 이름의 배열. 이를 제공하지 않으면 대상 클래스의 CGLIB 프락시를 사용할 것이다.(뒷부분의 Section 9.5.3, “JDK기반과 CGLIB기반의 프록시”도 참고해라.)</li>
<li><p>interceptorNames: 적용할 Advisor, 인터셉터, 다른 어드바이스 이름의 문자열 배열. 순서가 중요해서 앞에 작성한 것을 먼저 제공하게 된다. 이는 목록의 첫 인터셉터가 가장 먼저 호출을 가로챌 수 있다는 의미이다.</p>
<p>현재 팩토리에서 이름은 bean 이름이다.(조상 팩토리의 bean 이름을 포함한다.) 여기서 빈(bean) 참조를 사용하면 ProxyFactoryBean이 어드바이스의 싱글톤 설정을 무시하게 되므로 여기서 빈(bean) 참조를 사용할 수 없다.</p>
<p>별표(*)로 인터셉터 이름을 추가할 수 있다. 별표 앞부분으로 시작하는 이름의 모든 어드바이저 빈을 가리킬 것이다. 이 기능의 예제를 Section 9.5.6, “'전역' 어드바이저 사용하기”에서 볼 수 있다.</p></li>
<li><p>singleton: getObject() 메서드를 여러 번 호출하더라도 팩토리가 하나의 객체를 반환해야 하는지를 지정한다. 다수의 FactoryBean 구현체가 이러한 메서드를 제공한다. 기본값은 true이고 상태를 가진 어드바이스를 사용하고자 한다면 (예시로 상태를 가진 믹스인) singleton 값을 false로 설정하고 프로토타입 어드바이스를 사용해라.</p></li>
</ul>
<h4>B.4.3 JDK와 CGLIB에 기반을 둔 프록시</h4>
<p>이번 장에서는 ProxyFactoryBean가 특정 대상 객체(프락시 할) JDK와 CGLIB에 기반을 둔 프락시 중 하나를 생성하는 방법을 자세히 설명한다.</p>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
JDK나 CGLIB에 기반을 둔 프락시를 생성하는 ProxyFactoryBean의 동작이 스프링 1.2.x와 2.0에서 달라졌다. ProxyFactoryBean는 이제 TransactionProxyFactoryBean 클래스의 자동탐지(auto-detecting) 인터페이스와 비슷한 기능을 한다.
</font></div>
<p>프락시 할 대상 객체의 클래스(이후에는 대상 클래스라고 지칭한다.)가 어떤 인터페이스도 구현하지 않았다면 CGLIB에 기반을 둔 프락시를 생성할 것이다. 이는 JDK 프락시가 인터페이스에 기반을 두므로 인터페이스가 없다는 것은 JDK 프락시가 불가능하다는 것을 의미하므로 가장 이해하기 쉬운 경우이다. 그냥 대상 빈에 연결하고 interceptorNames 프로퍼티로 인터셉터 목록을 지정해라. CGLIB에 기반을 둔 프락시는 ProxyFactoryBean의 proxyTargetClass 프로퍼티를 false로 설정하더라도 생성될 것이다.(물론 자연스럽지 않다. 좋아져 봤자 중복이고 그렇지 않으면 혼란스러우므로 빈 정의에서 제거하는 것이 제일 좋다.)</p>
<p>대상 클래스가 하나 이상의 인터페이스를 구현했다면 생성되는 프락시 타입은 ProxyFactoryBean 설정을 따른다.</p>
<p>ProxyFactoryBean의 proxyTargetClass프로퍼티를 true로 설정했다면 CGLIB에 기반을 둔 프락시가 생성될 것이다. 이는 합리적이고 최소 놀람의 법칙을 따른 것이다. ProxyFactoryBean의 proxyInterfaces 프로퍼티를 하나 이상의 정규화된 인터페이스 명으로 설정하더라도 proxyTargetClass 프로퍼티를 true로 설정하면 CGLIB에 기반을 둔 프락시에 영향을 끼칠 것이다.</p>
<p>ProxyFactoryBean의 proxyInterfaces 프로퍼티를 하나 이상의 정규화된 인터페이스 명으로 설정했다면 JDK에 기반을 둔 프락시가 생성될 것이다. 생성된 프락시는 proxyInterfaces 프로퍼티에서 지정한 모든 인터페이스를 구현할 것이다. 대상 클래스가 proxyInterfaces 프로퍼티에서 지정한 것보다 훨씬 많은 인터페이스를 구현하게 되더라도 동작도 잘하고 문제없지만 반환된 프락시는 여분의 인터페이스를 구현하지 않을 것이다.</p>
<p>ProxyFactoryBean의 proxyInterfaces 프로퍼티를 설정하지 않았고 대상 클래스가 하나 이상의 인터페이스를 구현했다면 ProxyFactoryBean는 대상 클래스가 인터페이스를 최소 하나는 구현했다는 사실을 자동으로 탐지하고 JDK에 기반을 둔 프락시를 생성할 것이다. 여기서 실제로 프락시 될 인터페이스는 대상 클래스가 구현한 인터페이스 전부이다. 이는 대상 클래스가 구현한 모든 인터페이스의 목록을 proxyInterfaces 프로퍼티에 작성한 것과 같지만, 훨씬 손쉽고 오타가 날 가능성도 적다.<br />
<br></p>
<h4>B.4.4 프록시 인터페이스</h4>
<p>동작하는 간단한 ProxyFactoryBean의 예시를 보자. 이 예제에는 다음이 포함되어 있다.</p>
<ul>
<li>프락시할 대상 빈. 이 빈은 아래 예제에서 "personTarget" 빈 정의이다.</li>
<li>어드바이스를 제공하는 데 사용할 어드바이저와 인터셉터.</li>
<li>프락시할 대상 객체(여기서는 personTarget 빈)과 인터페이스를 지정하는 AOP 프락시 빈 정의와 적용할 어드바이스</li>
</ul>
<pre class="line-numbers"><code class="language-xml"><bean id="personTarget" class="com.mycompany.PersonImpl">
<property name="name"><value>Tony</value></property>
<property name="age"><value>51</value></property>
</bean>
<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
<property name="someProperty"><value>Custom string property value</value></property>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
</bean>
<bean id="person"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces"><value>com.mycompany.Person</value></property>
<property name="target"><ref local="personTarget"/></property>
<property name="interceptorNames">
<list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
</list>
</property>
</bean>
</code></pre>
<p>interceptorNames 프로퍼티는 현재 팩토리의 인터셉터와 어드바이저의 빈(bean) 이름의 문자열 목록을 담고 있다. 어드바이저, 인터셉터, before/after/returning/throws 어드바이스 객체를 사용할 수 있다. 어드바이저의 순서는 중요하다.</p>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
목록에 왜 빈 참조를 사용하지 않는지 궁금할 수도 있다. 이는 ProxyFactoryBean의 singleton 프로퍼티가 false이면 독립적인 프락시 인스턴스를 반환할 수 있어야 하기 때문이다. 어드바이스 중 prototype인 것이 있다면 독립적인 인스턴스를 반환해야 하므로 팩토리에서 prototype 인스턴스를 얻어낼 수 있어야 한다. 그래서 참조를 담고 있으면 안 된다.
</font></div>
<p>앞에서 "person" 빈 정의를 다음과 같이 Person 구현체 대신 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-java">Person person = (Person) factory.getBean("person");
</code></pre>
<p>같은 IoC 컨텍스트의 다른 빈은 평범한 자바 객체처럼 타입 의존성을 명시적으로 지정할 수 있다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="personUser" class="com.mycompany.PersonUser">
<property name="person"><ref local="person" /></property>
</bean>
</code></pre>
<p>이 예제에서 PersonUser 클래스는 Person 타입의 프로퍼티로 노출될 것이다. 예상대로 "실제" person 구현체 대신 AOP 프락시를 투명하게 사용할 수 있다. 하지만 그 클래스는 동적 프락시 클래스가 될 것이므로 Advised 인터페이스로 캐스팅할 수 있다.(뒤에서 다시 얘기한다.)</p>
<p>다음과 같이 익명 내부 빈(inner bean)을 사용해서 대상과 프락시간의 차이를 숨길 수도 있다. ProxyFactoryBean 정의만 다르다. 어드바이스는 통일성만을 위해서 사용한 것이다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="myAdvisor" class="com.mycompany.MyAdvisor">
<property name="someProperty"><value>Custom string property value</value></property>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces"><value>com.mycompany.Person</value></property>
<!-- 대상의 지역 참조가 아닌 내부 빈을 사용해라 -->
<property name="target">
<bean class="com.mycompany.PersonImpl">
<property name="name"><value>Tony</value></property>
<property name="age"><value>51</value></property>
</bean>
</property>
<property name="interceptorNames">
<list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
</list>
</property>
</bean>
</code></pre>
<p>이는 Person 타입의 객체가 딱 하나만 있다는 장점이 있어서 애플리케이션 컨텍스트의 사용자가 어드바이즈가 안된 객체에 대한 참조를 얻지 못하게 하고 싶을 때나 스프링 IoC의 애매한 자동 연결을 피하고 싶을 때 유용하다. 그리고 ProxyFactoryBean 정의가 자기 충족적(self-contained)일때의 장점이기도 하다. 하지만 테스트 시나리오 등 팩토리에서 어드바이즈 되지 않은 대상을 얻을 수 있는 경우가 유용한 경우도 실제로 있다.<br />
<br></p>
<h4>B.4.5 프록시 클래스</h4>
<p>하나 이상의 인터페이스 말고 클래스를 프락시 해야 할 때는 어떻게 하는가?</p>
<p>앞의 예제에서 Person 인터페이스가 없다고 해보자. 어떤 비즈니스 인터페이스도 구현하지 않은 Person 클래스를 어드바이즈 해야 한다. 이럴 때 스프링이 동적 프락시가 아니라 CGLIB 프락시를 사용하도록 설정할 수 있는데 예제에서 ProxyFactoryBean의 proxyTargetClass 프로퍼티를 true로 설정하면 된다. 클래스를 사용하는 대신 인터페이스를 작성하는 것이 최선이지만 인터페이스를 구현하지 않은 클래스를 어드바이스할 수 있다는 것은 레거시 코드를 사용하는 경우 유용할 수 있다. (보통 스프링은 규법적(prescriptive)이지 않다. 좋은 방법을 적용하기 쉽게 하면서도 특정 접근을 강제하지 않는다.)</p>
<p>원한다면 인터페이스가 있더라도 모든 경우에 CGLIB을 사용하도록 강제할 수 있다.</p>
<p>런타임시에 대상 클래스의 하위 클래스를 생성해서 CGLIB 프락시가 동작한다. 스프링은 이 생성한 하위 클래스에 원래 대상의 메서드 호출을 위힘하도록 설정한다. 이 하위 클래스는 어드바이스에서 위빙을 하는 Decorator 패턴을 구현하는데 사용한다.</p>
<p>보통 CGLIB 프락시는 사용하기에 투명해야 하지만 이때 고려해야 할 부분이 있다.</p>
<ul>
<li>Final 메서드는 오버라이드 할 수 없으므로 어드바이스 할 수 없다.</li>
<li>클래스 패스에 CGLIB 2 바이너리가 필요하다. 동적 프락시는 JDK에서 사용할 수 있다.</li>
</ul>
<p>CGLIB 프락시와 동적 프락시 사이에는 약간의 성능 차이가 있다. 스프링 1.0부터는 동적 프락시가 약간 더 빠르지만 앞으로는 달라질 수도 있다. 이 경우 성능을 결정적인 고려사항이 되면 안 된다.<br />
<br></p>
<h4>B.4.6 'global' 어드바이저의 사용</h4>
<p>인터페이스 이름에 별표(asterisk)를 붙이면 별표 앞부분과 일치하는 빈(bean) 이름을 가진 모든 어드바이저가 어드바이저 체인에 추가될 것이다. 이는 'global' 어드바이저의 표준 셋을 추가할 필요가 있을 때 편리한 방법이다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="service"/>
<property name="interceptorNames">
<list>
<value>global*</value>
</list>
</property>
</bean>
<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>
</code></pre>
<h3>B.5 간결한 프록시 정의</h3>
<p>특히 트랜잭션이 적용된 프락시를 정의하는 경우 비슷한 프락시 정의를 많이 작성하게 될 것이다. 내부 빈과 함께 부모 빈과 자식 빈의 정의를 사용해서 더 명확하고 간단한 프락시 정의를 작성할 수 있다.</p>
<p>먼저 프락시에 대한 부모(template) 빈 정의를 생성한다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="txProxyTemplate" abstract="true"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
</code></pre>
<p>이 자체로는 절대 인스턴스화 되지 않을 것이고 실제로 불완전한 상태이다. 그다음 생성해야 하는 각 프락시는 자식 빈(bean) 정의이고 이는 내부 빈 정의로 프락시의 대상은 감싸므로 대상은 어떻게든 절대 사용되지 않을 것이다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="myService" parent="txProxyTemplate">
<property name="target">
<bean class="org.springframework.samples.MyServiceImpl">
</bean>
</property>
</bean>
</code></pre>
<p>당연히 부모 템플릿의 프로퍼티를 오버라이드 할 수 있다. 이 경우에는 트랜잭션 전파 설정을 오버라이드 한다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="mySpecialService" parent="txProxyTemplate">
<property name="target">
<bean class="org.springframework.samples.MySpecialServiceImpl">
</bean>
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="store*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
</code></pre>
<p>위 예제에서 이전에 설명했던 대로 명시적으로 abstract 속성을 사용해서 부모 빈 정의를 abstract로 표시했으므로 부모 빈은 절대 인스턴스화되지 않을 것이다. 애플리케이션 컨텍스트(간단한 빈 팩토리는 아니다)은 기본적으로 모든 싱글톤을 미리 인스턴스화 할 것이다. 그래서 템플릿으로만 사용할 (부모) 빈 정의가 있고 이 정의가 클래스를 지정하고 있다면 반드시 abstract 속성을 true로 설정해야 한다는 점이 중요하다(최소한 싱글톤 빈에는). 그렇지 않으면 애플리케이션 컨텍스트가 이를 미리 인스턴스화 하려고 할 것이다.<br />
<br></p>
<h3>B.6 ProxyFactory로 AOP 프록시를 프로그래밍적으로 생성하기</h3>
<p>스프링에서 AOP 프락시를 프로그래밍 적으로 생성하는 것은 쉽다. 스프링 IoC에 의존성을 갖지 않고 스프링 AOP를 사용할 수 있다.</p>
<p>다음 예제에서는 대상 객체에 한 인터셉터와 하나의 어드바이저를 가진 프락시를 생성하는 것을 보여준다. 대상 객체를 자동으로 프록시 함으로써 인터페이스를 구현한다.</p>
<pre class="line-numbers"><code class="language-java">ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addInterceptor(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();
</code></pre>
<p>첫 단계는 org.springframework.aop.framework.ProxyFactory 타입의 객체를 생성하는 것이다. 앞의 예시처럼 대상 객체로 이 타입의 객체를 생성하거나 대체 생성자에 프록시될 인터페이스를 지정할 수 있다.</p>
<p>인터셉터와 어드바이저를 추가할 수 있고 ProxyFactory의 생명주기 동안 이를 조작할 수 있다. IntroductionInterceptionAroundAdvisor를 추가한다면 프록시가 여분의 인터페이스를 구현하도록 할 수 있다.</p>
<p>ProxyFactory에는(AdvisedSupport에서 상속받은) before 어드바이스나 throws 어드바이스 같은 다른 어드바이스 타입을 추가할 수 있는 편의 메서드도 있다. AdvisedSupport는 ProxyFactory와 ProxyFactoryBean의 슈퍼 클래스다.</p>
<div style="padding:10px; background-color:#EDE5A6"><font color="#000000">
<b>Tip</b><br>
애플리케이션 대부분에서 AOP 프록시 생성을 IoC 프레임워크와 통합하는 것이 좋은 방법이다. 보통은 AOP로 자바 코드에서 설정을 빼내는 것(externalize)을 추천한다.
</font></div>
<p><br></p>
<h3>B.7 어드바이즈된 객체 조작하기</h3>
<p>AOP 프락시를 생성하더라도 org.springframework.aop.framework.Advised 인터페이스로 이를 조작할 수 있다. 어떤 AOP 프락시라도 이 인터페이스로(또는 이를 구현한 다른 인터페이스) 캐스팅할 수 있다. 이 인터페이스는 다음 메서드를 가진다.</p>
<pre class="line-numbers"><code class="language-java">Advisor[] getAdvisors();
void addAdvice(Advice advice) throws AopConfigException;
void addAdvice(int pos, Advice advice)
throws AopConfigException;
void addAdvisor(Advisor advisor) throws AopConfigException;
void addAdvisor(int pos, Advisor advisor) throws AopConfigException;
int indexOf(Advisor advisor);
boolean removeAdvisor(Advisor advisor) throws AopConfigException;
void removeAdvisor(int index) throws AopConfigException;
boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;
boolean isFrozen();
</code></pre>
<p>getAdvisors() 메서드는 해당 팩토리에 추가된 모든 어드바이저, 인터셉터, 그 외 어드바이스 타입마다 Advisor를 반환할 것이다. Advisor를 추가하면 이 인덱스에서 반환한 어드바이저는 추가한 객체가 될 것이고 인터셉터나 다른 어드바이스 타입을 추가하면 스프링이 항상 true를 반환하는 포인트컷을 가진 어드바이저로 이를 감쌀 것이다. 그래서 MethodInterceptor를 추가하면 이 인덱스가 반환한 어드바이저는 모든 클래스 메서드와 매칭되는 포인트컷과 MethodInterceptor를 반환하는 DefaultPointcutAdvisor가 될 것이다.</p>
<p>어떤 Advisor든 추가할 때 addAdvisor() 메서드를 사용할 수 있다. 보통 포인트컷과 어드바이서를 가진 어드바이저는 어떤 어드바이스나 포인트컷과도 사용할 수 있는(하지만 인트로덕션과는 사용할 수 없다.) 일반적인(generic) DefaultPointcutAdvisor가 될 것이다.</p>
<p>기본적으로 일단 프록시가 생성되고 나면 어드바이저나 인터셉터를 추가하거나 삭제할 수 있다. 해당 팩토리에 존재하는 프록시는 인터페이스 변경사항을 보여주지 않으므로 인트로덕션 어드바이저를 추가하거나 삭제할 수 없다는 점이 유일한 제약사항이다. (팩토리에서 새로운 프록시를 받아서 이 문제를 피할 수 있다.)</p>
<p>AOP 프록시를 Advised 인터페이스로 캐스팅해서 그 어드바이스를 검사하고 조작하는 간단한 예제다.</p>
<pre class="line-numbers"><code class="language-java">Advised advised = (Advised) myObject;
Advisor[] advisors = advised.getAdvisors();
int oldAdvisorCount = advisors.length;
System.out.println(oldAdvisorCount + " advisors");
// 포인트컷없는 인터셉처 처럼 어드바이스를 추가한다
// 프락시 된 모든 메서드에 매칭될 것이다
// 인터셉터, before, after returning, throws 어드바이스를 사용할 수 있다
advised.addAdvice(new DebugInterceptor());
// 포인트컷으로 선택할 수 있는 어드바이스를 추가한다
advised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice));
assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length);
</code></pre>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
합당한 사용 사례가 확실히 있음에도 프로덕션에서 비즈니스 객체의 어드바이스를 수정하려고 어드바이저할 수 있는지(advisable, 농담이 아니다)한지가 궁금할 수 있다. 하지만 이는 테스트 등 개발에서는 아주 유용할 수 있다. 테스트하고자 하는 메서드 호출 내에서 가져오는 인터셉터나 다른 어드바이스 형식으로 테스트 코드를 추가할 수 있다는 점이 아주 유용하다는 것을 종종 경험한다. (예를 들어 해당 메서드를 위해 생성한 트랜잭션 내부에서 트랜잭션을 롤백하기 전에 데이터베이스가 제대로 갱신되었는지 확인하는 SQL을 실행하려고 이 어드바이스를 얻을 수 있다.)
</font></div>
<p><br></p>
<p>프록시를 어떻게 만들었느냐에 따라 frozen 플래그를 보통 설정할 수 있는데 Advised isFrozen() 메서드가 true를 반환하면 추가하거나 제거하는 등 어드바이스를 수정하는 어떤 시도라도 AopConfigException이 발생할 것이다. 어드바이즈된 객체의 상태를 변경할 수 없게 하는 기능은 보안 인터셉터를 제거하는 코드의 호출을 차단하는 등의 경우에 유용하다. 이는 스프링 1.1에서 런타임 어드바이스 수정이 필수사항이 아닌 경우 적극적인(aggressive) 최적화를 허용하는데도 사용할 수 있다.<br />
<br></p>
<h3>B.8 "autoproxy" 기능 사용하기</h3>
<p>지금까지는 ProxyFactoryBean나 비슷한 팩토리 빈을 사용해서 AOP 프록시를 명시적으로 생성했다.</p>
<p>스프링에서도 선택한 빈(bean) 정의를 자동으로 프락시 할 수 있는 "autoproxy" 빈 정의를 사용할 수 있다. 이는 스프링 "bean post processor" 인프라스트럭처에 기분을 두고 있고 컨테이너 로딩처럼 모든 빈 정의의 수정을 활성화 할 수 있다.</p>
<p>이 모델에서 자동 프락시 인프라스트럭처를 구성하려고 XML 빈 정의 파일에 전용 빈 정의를 구성할 수 있다. 이를 통해 자동 프락시가 가능한 대상을 선언할 수 있어서 ProxyFactoryBean를 사용할 필요가 없다.</p>
<p>이를 위한 두 가지 방법이 있다.</p>
<ul>
<li>현재 컨텍스트에서 특정 빈을 참조하는 autoproxy creator를 사용한다.</li>
<li>따로 고려해 볼 만한 autoproxy 생성의 특수한 경우가 있다. autoproxy 생성은 소스 수준의 메타데이터 속성으로 발생한다.</li>
</ul>
<h4>B.8.1 autoproxy 빈 정의</h4>
<p>org.springframework.aop.framework.autoproxy 패키지에서 다음의 표준 autoproxy creator를 제공한다.</p>
<h4>B.8.1.1 BeanNameAutoProxyCreator</h4>
<p>BeanNameAutoProxyCreator 클래스는 리터럴 값이나 와일드카드와 일치하는 이름의 빈에 대한 AOP 프락시를 자동으로 생성하는 BeanPostProcessor이다.</p>
<pre class="line-numbers"><code class="language-xml"><bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames"><value>jdk*,onlyJdk</value></property>
<property name="interceptorNames">
<list>
<value>myInterceptor</value>
</list>
</property>
</bean>
</code></pre>
<p>ProxyFactoryBean가 그러하듯 프로토타입 어드바이저에 대한 올바른 동작을 하게 하는 인터셉터의 목록 대신 interceptorNames 프로퍼티가 존재한다. 이름을 가진 "interceptors"는 어드바이저나 어떤 어드바이스 타입도 될 수 있다.</p>
<p>보통 자동 프락시가 그러하듯 BeanNameAutoProxyCreator 사용하는 주요 이유는 최소한의 설정으로 여러 객체에 일관성 있게 같은 설정을 적용하는 것이다. 여러 객체에 선언적인 트랜잭션을 적용하는 것이 가장 많이 쓰는 경우이다.</p>
<p>앞의 예제에서 "jdkMyBean"와 "onlyJdk"처럼 매칭된 이름을 가진 빈 정의는 대상 클래스에 대한 평범한(plain old) 빈 정의이다. BeanNameAutoProxyCreator가 AOP 프락시를 자동으로 생성할 것이다. 매칭된 모든 빈에 같은 어드바이스를 적용할 것이다. (앞의 예제에서 인터셉터를 사용하는 대신) 어드바이저를 사용한다면 포인트컷은 다른 빈에 다르게 적용되리라는 것을 유념해라.</p>
<h4>B.8.1.2 DefaultAdvisorAutoProxyCreator</h4>
<p>더 일반적이면서 훨씬 강력한 auto proxy creator가 DefaultAdvisorAutoProxyCreator이다. 이는 autoproxy 어드바이저의 빈 정의에서 특정 빈 이름을 지정하지 않고도 현재 컨텍스트에서 가능한 어드바이저를 바로 적용한다. BeanNameAutoProxyCreator처럼 DefaultAdvisorAutoProxyCreator도 일관된 설정과 중복 제거라는 장점이 있다.</p>
<p>이 방법을 사용하면 다음의 과정을 포함한다.</p>
<ul>
<li>DefaultAdvisorAutoProxyCreator 빈 정의를 지정한다.</li>
<li>같은 컨텍스트나 관련 컨텍스트에서 원하는 만큼의 어드바이저 지정한다. 이는 반드시 Advisor여야 하고 인터셉터나 다른 어드바이스일 수는 없다. 이는 후보 빈 정의에 각 어드바이스의 자격을 검사하려면 평가할 포인트컷이 반드시 존재해야 하기 때문이다.</li>
</ul>
<p>각 비즈니스 객체(여기서는 "businessObject1"와 "businessObject2")에 적용해야 하는 어드바이스가 어떤 것인지 보기 위해(존재한다면) DefaultAdvisorAutoProxyCreator는 각 어드바이저에 있는 포인트컷을 자동으로 평가할 것이다.</p>
<p>이는 각 비즈니스 객체에 원하는 만큼의 어드바이저를 자동으로 적용할 수 있다는 의미이다. 비즈니스 객체의 메서드와 매칭되는 어드바이스의 포인트컷이 없다면 해당 비즈니스 객체는 프락시 되지 않을 것이다. 새로운 비즈니스 객체에 빈 정의가 추가되었으므로 필요하다면 자동으로 프락시 될 것이다.</p>
<p>보통 오토 프락시는 호출자(caller)나 의존성에서 어드바이즈되지 않은 객체를 얻을 수 없게 하는 장점이 있다. 이 ApplicationContext에서 getBean("businessObject1")을 호출하면 대상 비즈니스 객체가 아니라 AOP 프락시를 반환한다.("내부 빈(inner bean)"도 이 장점을 제공한다는 것을 앞에서 보여줬다.)</p>
<pre class="line-numbers"><code class="language-xml"><bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
<property name="transactionInterceptor" ref="transactionInterceptor"/>
</bean>
<bean id="customAdvisor" class="com.mycompany.MyAdvisor"/>
<bean id="businessObject1" class="com.mycompany.BusinessObject1">
<!-- Properties omitted -->
</bean>
<bean id="businessObject2" class="com.mycompany.BusinessObject2"/>
</code></pre>
<p>DefaultAdvisorAutoProxyCreator는 다수 비즈니스 객체에 같은 어드바이스를 일관적으로 적용하고자 할 때 유용하다. 인프라 정의를 하고 나면 특정 프락시 구성을 포함하지 않고도 새로운 비즈니스 객체를 간단히 추가할 수 있고 구성을 아주 약간만 바꿔서 아주 쉽게 추가적인 관점(추적(tracing) 관점이나 성능 모니터링 관점 등)에 둘 수도 있다.</p>
<p>DefaultAdvisorAutoProxyCreator는 필터링(같은 팩토리에서 다르게 구성한 다수의 AdvisorAutoProxyCreator를 사용해서 특정 어드바이저만 평가하도록 작명관례를 사용해서)와 오더링(ordering)을 제공한다. 어드바이저는 순서가 중요하다면 올바른 순서를 보장하기 위해 org.springframework.core.Ordered를 구현할 수 있다. 앞의 예제에서 사용한 TransactionAttributeSourceAdvisor는 구성 가능한 순서(order) 값을 가진다. 기본 설정은 순서 없음(unordered)이다.</p>
<h4>B.8.1.3 AbstractAdvisorAutoProxyCreator</h4>
<p>이 클래스는 DefaultAdvisorAutoProxyCreator의 슈퍼클래스다. 그럴 일은 없지만 어드바이저 정의로 DefaultAdvisorAutoProxyCreator 프레임워크의 동작을 커스터마이징하는 것으로 불충분할 때 이 클래스의 하위클래스를 만들어서 자신만의 자동 프락시 생성자를 만들 수 있다.<br />
<br></p>
<h4>B.8.2 메타데이터 주도 자동 프락시 사용하기</h4>
<p>메타데이터가 주도하는 자동 프락시는 특히 중요하다. 이 방식은 .NET ServicedComponents와 유사한 프로그래밍 모델을 제공한다. EJB처럼 XML 배포 디스크립터를 사용하는 대신 트랜잭션 관리와 다른 엔터프라이즈 서비스에 대한 구성을 소스 수준의 속성에 담는다.</p>
<p>이 경우 메타데이터 속성을 이해하는 Advisor와 조합해서 DefaultAdvisorAutoProxyCreator를 사용한다. 관련 메타데이터는 자동프락시 생성 클래스가 아니라 후보 어드바이저의 포인트컷 부분에 작성한다.</p>
<p>사실 이 방식은 DefaultAdvisorAutoProxyCreator의 특수한 경우이지만 별도로 다룰 가치가 있다.(메타데이터를 인식하는 코드는 AOP 프레임워크가 아니라 어드바이저의 포인트컷이 담고 있다.)</p>
<p>JPetStore 예제 애플리케이션의 /attributes 디렉터리에 속성(attribute) 주도 자동프락시가 나와 있다. 이 경우에는 TransactionProxyFactoryBean를 사용할 필요가 없고 메타데이터를 인식하는 포인트컷을 사용하므로 비즈니스 객체에 transactional 속성을 정의하는 것으로 충분하다. /WEB-INF/declarativeServices.xml에 있는(이는 일반적이다) 빈 정의에 다음의 코드가 있고 JPetStore 외부에서 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-xml"><bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
<property name="transactionInterceptor" ref="transactionInterceptor"/>
</bean>
<bean id="transactionInterceptor"
class="org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributeSource">
<bean class="org.springframework.transaction.interceptor.AttributesTransactionAttributeSource">
<property name="attributes" ref="attributes"/>
</bean>
</property>
</bean>
<bean id="attributes" class="org.springframework.metadata.commons.CommonsAttributes"/>
</code></pre>
<p>DefaultAdvisorAutoProxyCreator 빈 정의(이름은 중요하지 않으므로 생략 가능하다)는 현재 애플리케이션 컨텍스트에서 적합한 포인트컷을 모두 선택할 것이다. 이 경우 TransactionAttributeSourceAdvisor 타입의 "transactionAdvisor" 빈 정의는 transaction 속성을 가진 클래스나 메서드에 적용될 것이다. TransactionAttributeSourceAdvisor는 생성자 주입으로 TransactionInterceptor에 의존하는데 예제는 자동연결(autowiring)으로 의존성을 해결한다. AttributesTransactionAttributeSource는 org.springframework.metadata.Attributes 인터페이스의 구현체에 의존한다. 이 예제에서는 "attributes" 빈이 이를 만족하게 하는데 속성 정보를 가져오는데 Jakarta Commons Attributes API를 사용한다. (애플리케이션 코드는 반드시 Commons Attributes 컴파일 테스크로 컴파일해야 한다.)</p>
<p>JPetStore 예제 애플리케이션의 /annotation 디렉터리에는 JDK 1.5+ 어노테이션을 사용한 자동 프락시의 비슷한 예제가 있다. 다음 설정은 스프링 Transactional 어노테이션의 자동 탐지를 활성화해서 어노테이션이 붙은 빈에 프락시를 만들어 준다.</p>
<pre class="line-numbers"><code class="language-xml"><bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
<property name="transactionInterceptor" ref="transactionInterceptor"/>
</bean>
<bean id="transactionInterceptor"
class="org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributeSource">
<bean class="org.springframework.transaction.annotation.AnnotationTransactionAttributeSource"/>
</property>
</bean>
</code></pre>
<p>여기서 정의한 TransactionInterceptor는 PlatformTransactionManager 정의에 의존하지만 애플리케이션의 트랜잭션 요구사항에 따라 다르므로(이 예제처럼 보통은 JTA이고 Hibernate, JDO, JDBC 등이 될 수 있다.) 이 일반적인 파일에는 포함하지 않았다.(넣을 수는 있지만)</p>
<pre class="line-numbers"><code class="language-xml"><bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
</code></pre>
<div style="padding:10px; background-color:#EDE5A6"><font color="#000000">
<b>Tip</b><br>
선언적인 트랜잭션 관리만 필요한 경우 이러한 일반적인 XML 정의를 사용하면 스프링이 자동으로 transaction 속성을 가진 모든 클래스와 메서드를 프락시 할 것이다. AOP를 직접 사용할 필요가 없게 되고 프로그래밍 모델이 .NET ServicedComponents의 프로그래밍 모델과 유사하다.
</font></div>
<p><br></p>
<p>이 메커니즘을 확장 가능해서 커스텀 속성으로 자동프락시를 할 수 있다. 이를 하려면 다음의 작업이 필요하다.</p>
<ul>
<li>커스텀 속성을 정의한다.</li>
<li>클래스나 메서드에서 커스텀 속성이 있으면 발생하는 포인트컷을 포함해서 필요한 어드바이스를 가진 Advisor를 지정한다. 단순히 커스텀 속성을 선택하는 정적 포인트컷을 구현해서 존재하는 어드바이스를 사용할 수도 있다.</li>
</ul>
<p>이러한 어드바이저를 어드바이즈된 각 클래스에 유일하도록 할 수 있다.(예를 들면 믹스인) 이를 위해서는 빈 정의를 싱글톤이 아니라 프로토타입으로 정의하기만 하면 된다. 예를 들어 앞에 나왔던 스프링 테스트 슈트의 LockMixin 인트로덕션 인터셉터를 다음에 나온 것처럼 대상 믹스인에 속성주도 포인트컷과 함께 사용할 수 있다. JavaBean 프로퍼티로 구성한 일반적인 DefaultPointcutAdvisor를 사용한다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="lockMixin" class="org.springframework.aop.LockMixin"
scope="prototype"/>
<bean id="lockableAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"
scope="prototype">
<property name="pointcut" ref="myAttributeAwarePointcut"/>
<property name="advice" ref="lockMixin"/>
</bean>
<bean id="anyBean" class="anyclass" ...
</code></pre>
<p>속성을 이해하는 포인트컷이 anyBean이나 다른 빈 정의의 메서드와 매칭된다면 믹스인이 적용될 것이다. lockMixin와 lockableAdvisor 정의 모두 프로토타입임을 봐라. myAttributeAwarePointcut 포인트컷은 어드바이즈된 개별 객체의 상태를 보관하지 않으므로 싱글톤으로 정의할 수 있다.<br />
<br></p>
<h3>B.9 TargetSource 사용하기</h3>
<p>스프링은 org.springframework.aop.TargetSource 인터페이스에 나온 TargetSource의 개념을 제공한다. 이 인터페이스는 조인 포인트를 구현한 "대상 객체"의 반환을 담당한다. AOP 프락시가 메서드 호출을 다룰 때마다 대상 인스턴스에 대해 TargetSource 구현체에 질의한다.</p>
<p>보통 스프링 AOP를 사용하는 개발자가 TargetSource를 직접 사용할 필요는 없지만 이는 풀링(pooling)이 되고 핫스왑도 가능하면서 정교한 대상을 강력하게 지원한다. 예를 들어 풀링이 되는 TargetSource는 인스턴스를 관리하는 풀을 이용해서 호출할 때마다 다른 대상 인스턴스를 반환할 수 있다.</p>
<p>TargetSource를 지정하지 않으면 로컬 객체를 감싸는 기본 구현체를 사용한다. 이때는 (예상한 대로) 호출할 때마다 같은 대상을 반환한다.</p>
<p>스프링에서 제공하는 표준 대상 소스(target source)를 보고 어떻게 사용할 수 있는지 살펴보자.</p>
<div style="padding:10px; background-color:#EDE5A6"><font color="#000000">
<b>Tip</b><br>
커스텀 대상 소스를 사용하는 경우에는 보통 대상을 싱글톤 빈 정의가 아니라 프로토타입 빈 정의로 정의해야 할 것이다. 이를 통해 필요할 때 스프링이 새로운 대상 인스턴스를 만들게 할 수 있다.
</font></div>
<p><br></p>
<h4>B.9.1 핫스왑이 가능한 대상 소스</h4>
<p>org.springframework.aop.target.HotSwappableTargetSource 호출자(caller)가 AOP 프락시에 대한 참조를 유지하게 하면서 AOP 프락시의 대상이 변경되도록 존재한다.</p>
<p>대상소스의 대상을 변경하면 즉시 영향을 끼친다. HotSwappableTargetSource는 스레드 세이프 하다.</p>
<p>다음과 같이 HotSwappableTargetSource의 swap() 메서드로 대상을 변경할 수 있다.</p>
<pre class="line-numbers"><code class="language-java">HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);
</code></pre>
<p>다음과 같은 XML 정의가 필요하다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="initialTarget" class="mycompany.OldTarget"/>
<bean id="swapper" class="org.springframework.aop.target.HotSwappableTargetSource">
<constructor-arg ref="initialTarget"/>
</bean>
<bean id="swappable" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource" ref="swapper"/>
</bean>
</code></pre>
<p>앞의 swap() 호출은 교환 가능한 빈의 대상을 변경한다. 이 빈에 대한 참조를 가진 클라이언트는 이 변경을 알지 못하지만 새로운 타켓을 바로 사용하기 시작할 것이다.</p>
<p>이 예제에서는 어드바이스를 전혀 추가하지 않았지만 (TargetSource를 사용할 때 어드바이스를 추가할 필요는 없다.) 당연히 임의의 어드바이스와 함께 어떤 TargetSource도 사용할 수 있다.<br />
<br></p>
<h4>B.9.2 대상 소스 풀링</h4>
<p>대상 소스 풀링을 사용해서 상태가 없는 세션 EJB와 유사한 프로그래밍 모델을 제공한다. 이는 같은 인스턴스의 풀을 관리해서 메서드 호출이 풀에서 놀고 있는 객체를 사용하도록 한다.</p>
<p>스프링 플링은 어떤 POJO에도 적용할 수 있다는 점이 스프링의 풀링과 SLSB 풀링간에 중요한 차이점이다. 보통 스프링에서 이 서비스는 비침투적인 방법으로 적용할 수 있다.</p>
<p>스프링은 꽤 효율적인 풀링 구현체를 제공하는 Jakarta Commons Pool 1.3을 지원한다. 이 기능을 사용하려면 애플리케이션의 클래스패스에 commons-pool Jar가 필요하다. 다른 풀링 API를 지원하기 위해 org.springframework.aop.target.AbstractPoolingTargetSource의 하위클래스를 만들 수도 있다.</p>
<p>예시 구성이 다음에 나와 있다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject"
scope="prototype">
... properties omitted
</bean>
<bean id="poolTargetSource" class="org.springframework.aop.target.CommonsPoolTargetSource">
<property name="targetBeanName" value="businessObjectTarget"/>
<property name="maxSize" value="25"/>
</bean>
<bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource" ref="poolTargetSource"/>
<property name="interceptorNames" value="myInterceptor"/>
</bean>
</code></pre>
<p>대상 객체(이 예제에서는 "businessObjectTarget")는 반드시 프로토타입이어야 한다. 대상의 새로운 인스턴스를 생성하려고 PoolingTargetSource 구현체가 풀(pool)의 크기를 필요한 만큼 증가시킬 수 있다. 이 프로퍼티에 대해서 알고 싶다면 AbstractPoolingTargetSource의 javadoc과 하위 구현체를 봐라. "maxSize"느 가장 기본적이고 항상 제공됨을 보장한다.</p>
<p>이 경우 "myInterceptor"가 같은 IoC 컨텍스트에서 정의되어야 하는 인터프리터의 이름이다. 하지만 풀링을 사용할 인터셉터를 지정할 필요는 없다. 풀링만 원하고 다른 어드바이스는 필요 없다면 interceptorNames 프로퍼티를 아예 설정하지 마라.</p>
<p>스프링이 풀링된 어떤 객체라도 인트로덕션으로 풀의 현재 크기와 구성에 대한 정보를 노출하는 org.springframework.aop.target.PoolingConfig 인터페이스로 캐스팅할 수 있도록 구성하는 것이 가능하다. 이와 같은 어드바이저를 정의할 필요가 있다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject" ref="poolTargetSource"/>
<property name="targetMethod" value="getPoolingConfigMixin"/>
</bean>
</code></pre>
<p>AbstractPoolingTargetSource에서 편의 메서드를 호출해서 이 어드바이저를 가져오므로 MethodInvokingFactoryBean을 사용한다. 이 어드바이저의 이름(여기서는 "poolConfigAdvisor")은 풀링된 객체를 노출하는 ProxyFactoryBean에서 interceptor 이름의 목록에 반드시 포함되어 있어야 한다.</p>
<p>다음처럼 캐스팅될 것이다.</p>
<pre class="line-numbers"><code class="language-java">PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());
</code></pre>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
상태가 없는 서비스 객체의 풀링은 보통 필요치 않다. 대부분의 상태가 없는 객체는 자연히 스레드 세이프 하므로 이 풀링인 기본적으로 사용해야 하는 부분이라고 생각지 않고 리소스를 캐시 한다면 인스턴스 풀링은 문제의 소지가 많다.
</font></div>
<p><br></p>
<p>자동프락시를 사용해서 더 간단한 풀링을 사용할 수 있다. 어떤 자동프락시 생성자(autoproxy creator)라도 자동 프락시 생성자가 사용하는 TargetSource를 설정할 수 있다.<br />
<br></p>
<h4>B.9.3 프로토타입 대상 소스</h4>
<p>"prototype" 대상 소스의 설정은 TargetSource 풀링과 비슷하다. 이 경우 메서드 호출할 때마다 대상의 새로운 인스턴스를 생성할 것이다. 최신 JVM에서 새로운 객체를 생성하는 비용이 크지는 않지만 새로운 객체를 연결하는 비용(객체의 IoC 의존성을 만족하게 하면서)은 더 클 수도 있다. 그러므로 아주 좋은 이유가 없이는 이 접근을 사용하지 않아야 한다.</p>
<p>이를 위해 앞의 poolTargetSource 정의를 다음과 같이 수정할 수 있다.(명확하게 이름도 변경했다.)</p>
<pre class="line-numbers"><code class="language-xml"><bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource">
<property name="targetBeanName" ref="businessObjectTarget"/>
</bean>
</code></pre>
<p>대상 빈의 이름을 위한 딱 하나의 프로퍼티만 있다. 일관된 작명을 보장하려고 TargetSource 구현체에서 상속을 사용한다. 대상 소스의 풀링을 사용하려면 대상 빈은 반드시 프로토타입 빈 정의여야 한다.<br />
<br></p>
<h4>B.9.4 ThreadLocal 대상 소스</h4>
<p>유입되는 요청마다(스레드당) 객체를 생성해야 할 때 ThreadLocal 대상 소스가 유용하다. ThreadLocal의 개념은 한 스레드와 함께 리소스를 투명하게 저장하는 JDK의 기능을 제공한다. ThreadLocalTargetSource의 설정은 다른 종류의 대상 소스에서 설명한 것과 거의 같다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource">
<property name="targetBeanName" value="businessObjectTarget"/>
</bean>
</code></pre>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
멀티스레드와 멀티 클래스로더 환경에서 ThreadLocal을 잘못 사용하면 심각한 문제가 발생한다.(메모리 누수가 발생할 수 있다.) 항상 다른 클래스로 스레드로컬을 감싸야 하고 ThreadLocal를 직접 사용하지 않아야 한다.(물론 래퍼 클래스는 제외다.) 스레드에 한정된 리소스를 올바르게 설정하고 해지하는 것을(해지는 ThreadLocal.set(null)를 호출하는 것이다.) 잊지 말아야 한다. 해지가 되지 않으면 동작이 잘못될 수 있으므로 해지는 어떤 경우에서 완료되어야 한다. 스프링은 다른 관리 코드 없이도 ThreadLocal을 항상 사용을 고려할 수 있도록 ThreadLocal 지원을 지원한다.
</font></div>
<p><br></p>
<h3>B.10 새로운 Advice 타입 정의하기</h3>
<p>스프링 AOP는 확장 가능하도록 설계되었다. 인터셉션(interception) 구현 전략은 현재 내부적으로 사용되지만, interception around 어드바이스, before 어드바이스, throws 어드바이스, after returning 어드바이스 에 추가적으로 임의의 어드바이스 타입을 지원하도록 할 수 있다.</p>
<p>org.springframework.aop.framework.adapter 패키지는 핵심 프레임워크를 바꾸지 않고도 새로운 커스텀 어드바이스 타입을 추가할 수 있는 SPI 패키지이다. org.aopalliance.aop.Advice 태그 인터페이스를 구현해야 한다는 것이 커스텀 Advice 타입의 유일한 제약사항이다.</p>
<p>더 자세한 내용은 org.springframework.aop.framework.adapter 패키지의 Javadoc을 참고해라.<br />
<br></p>
<h3>B.11 추가 자료</h3>
<p>스프링 AOP의 추가적인 예제는 스프링의 예시 애플리케이션을 참고해라.</p>
<ul>
<li>JPetStore의 기본 구성은 선언적인 트랜잭션 관리를 위한 TransactionProxyFactoryBean의 사용방법을 보여준다.</li>
<li>JPetStore의 /attributes 디렉터리는 속성주도의 선언적인 트랜잭션 관리의 사용방법을 보여준다.</li>
</ul>
<p><strong><a href="https://blog.outsider.ne.kr/1128?commentInput=true#entry1128WriteComment">댓글 쓰기</a></strong></p>[Spring 레퍼런스] 부록 A. 고전적인 스프링 사용방법Outsiderhttps://blog.outsider.ne.kr/11112014-12-28T23:49:34+09:002014-12-28T23:49:34+09:00<p>이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.<br><br><br><br></p>
<h1>Part VII. 부록</h1>
<p><br></p>
<h2>부록 A. 고전적인 스프링 사용방법</h2>
<p>이 부록에서는 레거시 스프링 애플리케이션을 유지 보수하는 개발자들이 참고할 수 있도록 고전적인 스프링의 몇 가지 사용 패턴을 설명한다. 이러한 사용패턴은 더는 스프링의 기능을 사용하는데 권장하는 방법이 아니고 현재 권장하는 방법은 참조 매뉴얼의 각 부분에서 다루었다.<br />
<br></p>
<h3>A.1 고전적인 ORM 사용방법</h3>
<p>이 부분에서는 레거시 스프링 애플리케이션에서 직면할 수 있는 고전적인 사용 패턴을 설명한다. 현재 권장하는 사용패턴은 Chapter 14, 객체 관계 매핑 (ORM) 데이터 접근 장을 참고하기 바란다.</p>
<h4>A.1.1 하이버네이트(Hibernate)</h4>
<p>현재 권장하는 하이버네이트 사용패턴은 Section 14.3, “하이버네이트(Hibernate)”를 참고해라.</p>
<h4>A.1.1.1 HibernateTemplate</h4>
<p>객체나 비즈니스 서비스에 접근하는 커스텀 데이터 일부가 될 수 있는 메서드에 대한 템플릿의 기본적인 프로그래밍 모델은 다음과 같다. 객체와 관련한 구현체에는 어떠한 제약도 없고 하이버네이트 SessionFactory만 제공하면 된다. SessionFactory는 어디서든 나중에 가져올 수 있지만, 스프링 IoC 컨테이너에서 빈(bean) 참조로 가져오는 걸 선호한다. (setSessionFactory(..) 빈 프로퍼티 세터로) 다음 코드에 스프링 컨테이너의 DAO 정의가 나와 있다. 이 정의는 앞에서 정의한 SessionFactory를 참조하고 있고 DAO 메서드 구현체의 예시이다.</p>
<pre class="line-numbers"><code class="language-xml"><beans>
<bean id="myProductDao" class="product.ProductDaoImpl">
<property name="sessionFactory" ref="mySessionFactory"/>
</bean>
</beans>
</code></pre>
<pre class="line-numbers"><code class="language-java">public class ProductDaoImpl implements ProductDao {
private HibernateTemplate hibernateTemplate;
public void setSessionFactory(SessionFactory sessionFactory) {
this.hibernateTemplate = new HibernateTemplate(sessionFactory);
}
public Collection loadProductsByCategory(String category) throws DataAccessException {
return this.hibernateTemplate.find("from test.Product product where product.category=?", category);
}
}
</code></pre>
<p>HibernateTemplate 클래스는 하이버네이트 Session 인터페이스가 노출하는 메서드와 같은 많은 메서드를 제공하고 추가로 앞에서 본 것처럼 다수의 편의 메서드를 제공한다. HibernateTemplate가 제공하지 않는 메서드를 호출하려고 Session에 접근해야 한다면 다음과 같이 콜백방식을 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-java">public class ProductDaoImpl implements ProductDao {
private HibernateTemplate hibernateTemplate;
public void setSessionFactory(SessionFactory sessionFactory) {
this.hibernateTemplate = new HibernateTemplate(sessionFactory);
}
public Collection loadProductsByCategory(final String category) throws DataAccessException {
return this.hibernateTemplate.execute(new HibernateCallback() {
public Object doInHibernate(Session session) {
Criteria criteria = session.createCriteria(Product.class);
criteria.add(Expression.eq("category", category));
criteria.setMaxResults(6);
return criteria.list();
}
};
}
}
</code></pre>
<p>하이버네이트 데이터 접근에 콜백 구현체를 효과적으로 사용할 수 있다. HibernateTemplate는 Session 인스턴스를 적절하게 여닫는 것을 보장하고 자동으로 트랜잭션에 참여하게 한다. 템플릿 인스턴스는 스레드 세이프하고 재사용성이 있어서 감싸는 클래스의 인스턴스 변수로 담고 있을 수 있다. 단일 find, load, saveOrUpdate, delete 호출처럼 간단한 하나의 작업을 위해 HibernateTemplate는 한 줄짜리 콜백 구현체를 대체할 수 있는 편의 메서드를 제공하고 있다. 게다가 스프링은 SessionFactory를 받는 setSessionFactory(..) 메서드를 제공하는 간편한 HibernateDaoSupport 기반 클래스를 제공하고 하위클래스가 사용할 수 있도록 getSessionFactory()와 getHibernateTemplate()를 제공한다. 이를 사용해서 전형적인 요구사항에 맞는 아주 간단한 DAO 구현체를 작성할 수 있다.</p>
<pre class="line-numbers"><code class="language-java">public class ProductDaoImpl extends HibernateDaoSupport implements ProductDao {
public Collection loadProductsByCategory(String category) throws DataAccessException {
return this.getHibernateTemplate().find(
"from test.Product product where product.category=?", category);
}
}
</code></pre>
<p><br></p>
<h4>A.1.1.2 콜백없이 스프링에 기반을 둔 DAO 구현하기</h4>
<p>DAO를 구현하는데 스프링의 HibernateTemplate를 사용하는 대신 콜백에 하이버네이트 접근 코드를 랩핑하지 않고 더 전통적인 방식으로 데이터 접근 코드를 작성할 수도 있고 이러면서도 여전히 스프링의 일반적인 DataAccessException 계층을 따르고 참여할 수 있다. HibernateDaoSupport 기반 클래스는 현재 트랜잭션이 적용된 Session에 접근할 수 있는 메서드를 제공하고 이러한 시나리오에서 예외를 변환하는 메서드를 제공한다. 비슷한 메서드를 SessionFactoryUtils의 정적 헬퍼로 사용할 수도 있다. 이러한 코드는 getSession(..) 메서드의 'allowCreate' 인자 값으로 'false'를 보통 전달해서 트랜잭션 내에서 실행되도록 강제한다.(트랜잭션이 관리하는 생명주기를 관리하므로 반환된 Session을 닫을 필요가 없도록)</p>
<pre class="line-numbers"><code class="language-java">public class HibernateProductDao extends HibernateDaoSupport implements ProductDao {
public Collection loadProductsByCategory(String category) throws DataAccessException, MyException {
Session session = getSession(false);
try {
Query query = session.createQuery("from test.Product product where product.category=?");
query.setString(0, category);
List result = query.list();
if (result == null) {
throw new MyException("No search results.");
}
return result;
}
catch (HibernateException ex) {
throw convertHibernateAccessException(ex);
}
}
}
</code></pre>
<p>이렇게 하이버네이트에 직접 접근하는 코드의 장법은 데이터 접근 코드 내에서 어떤 체크드 애플리케이션 익셉션이라도 던질 수 있다는 것이다. 이는 HibernateTemplate 클래스가 콜백내에서 언체크드 익셉션만 던질 수 있다는 제약과는 다른 부분이다. 때로는 HibernateTemplate에서 콜백실행 후에 던져진 애플리케이션 익셉션을 대응되는 체크드 입셉션으로 지연시킬 수도 있다. 일반적으로 HibernateTemplate 클래스의 편의 메서드는 많은 경우에 더 간단하고 훨씬 편리하다.<br />
<br></p>
<h4>A.1.2 JDO</h4>
<p>현재 권장하는 JDO 사용패턴은 Section 14.4, “JDO”를 참고해라.<br />
<br></p>
<h4>A.1.2.1 JdoTemplate와 JdoDaoSupport</h4>
<p>JDO에 기반을 둔 각 DAO는 의존성 주입으로 PersistenceManagerFactory를 받는다. 이러한 DAO는 평범한 JDO API로 작성할 수 있고 주어진 PersistenceManagerFactory와 동작할 수 있지만, 보통은 스프링 프레임워크의 JdoTemplate을 사용할 것이다.</p>
<pre class="line-numbers"><code class="language-xml"><beans>
<bean id="myProductDao" class="product.ProductDaoImpl">
<property name="persistenceManagerFactory" ref="myPmf"/>
</bean>
</beans>
</code></pre>
<pre class="line-numbers"><code class="language-java">public class ProductDaoImpl implements ProductDao {
private JdoTemplate jdoTemplate;
public void setPersistenceManagerFactory(PersistenceManagerFactory pmf) {
this.jdoTemplate = new JdoTemplate(pmf);
}
public Collection loadProductsByCategory(final String category) throws DataAccessException {
return (Collection) this.jdoTemplate.execute(new JdoCallback() {
public Object doInJdo(PersistenceManager pm) throws JDOException {
Query query = pm.newQuery(Product.class, "category = pCategory");
query.declareParameters("String pCategory");
List result = query.execute(category);
// 결과 리스트로 추가 작업을 한다
return result;
}
});
}
}
</code></pre>
<p>콜백 구현체는 어떤 JDO 데이터 접근에도 효과적으로 사용할 수 있다. JdoTemplate는 PersistenceManager를 제대로 여닫았다는 것을 보장하고 자동으로 트랜잭션에 참여한다. 템플릿 인스턴스는 스레드 세이프하고 재사용성이 있어서 감싸는 클래스의 인스턴스 변수로 보관할 수 있다. 하나의 find, load, makePersistent, delete 호출 같은 간단한 단일 동작에 대해 JdoTemplate가 한 줄짜리 콜백 구현체로 교체할 수 있는 편의 메서드를 제공한다. 게다가 PersistenceManagerFactory를 받는 setPersistenceManagerFactory(..) 메서드와 하위 클래스가 사용하는 getPersistenceManagerFactory()와 getJdoTemplate()를 제공하는 편리한 JdoDaoSupport 기반 클래스를 스프링이 제공한다. 이를 함께 사용하면 일반적인 요구사항에 대해 아주 간단한 DAO 구현체를 작성할 수 있다.</p>
<pre class="line-numbers"><code class="language-java">public class ProductDaoImpl extends JdoDaoSupport implements ProductDao {
public Collection loadProductsByCategory(String category) throws DataAccessException {
return getJdoTemplate().find(
Product.class, "category = pCategory", "String category", new Object[] {category});
}
}
</code></pre>
<p>스프링의 JdoTemplate와 동작하도록 작성하는 대신 JDO API 수준에서 스프링에 기반을 둔 DAO를 작성할 수 있어서 명시적으로 PersistenceManager를 여닫을 수 있다. 하이버네이트 부분에서 자세히 설명한 대로 이러한 접근의 가장 큰 장점은 데이터 접근 코드에서 체크드 익셉션을 던질 수 있다는 것이다. JdoDaoSupport는 익센션을 잘 변환하면서 트랜잭션이 적용된 PersistenceManager를 가져오고 해지하면서 이러한 시나리오를 지원하는 여러 메서드를 제공한다.<br />
<br></p>
<h4>A.1.3 JPA</h4>
<p>현재 권장하는 JPA 사용패턴은 Section 14.5, “JPA”를 참고해라.<br />
<br></p>
<h4>A.1.3.1 JpaTemplate와 JpaDaoSupport</h4>
<p>JPA에 기반을 둔 각 DAO는 의존성 주입으로 EntityManagerFactory를 받을 것이다. 이러한 DAO는 평범한 JPA로 작성할 수 있고 주어진 EntityManagerFactory나 스프링의 JpaTemplate와 동작한다.</p>
<pre class="line-numbers"><code class="language-xml"><beans>
<bean id="myProductDao" class="product.ProductDaoImpl">
<property name="entityManagerFactory" ref="myEmf"/>
</bean>
</beans>
</code></pre>
<pre class="line-numbers"><code class="language-java">public class JpaProductDao implements ProductDao {
private JpaTemplate jpaTemplate;
public void setEntityManagerFactory(EntityManagerFactory emf) {
this.jpaTemplate = new JpaTemplate(emf);
}
public Collection loadProductsByCategory(final String category) throws DataAccessException {
return (Collection) this.jpaTemplate.execute(new JpaCallback() {
public Object doInJpa(EntityManager em) throws PersistenceException {
Query query = em.createQuery("from Product as p where p.category = :category");
query.setParameter("category", category);
List result = query.getResultList();
// 결과 리스트로 추가 작업을 한다
return result;
}
});
}
}
</code></pre>
<p>JpaCallback 구현체는 어떤 타입의 JPA 데이터 접근이라고 가능하다. JpaTemplate는 EntityManager가 적절하게 열리고 닫히는 것을 보장하고 트랜잭션에 자동으로 참여하게 한다. 게다가 JpaTemplate는 예외를 제대로 다뤄서 리소스가 제대로 정리되고 트랜잭션이 제대로 롤백되었다는 것을 보장한다. 템플릿 인스턴스는 스레드 세이프하고 재사용성이 있어서 감싸진 클래스의 인스턴스 변수로 보관할 수 있다. JpaTemplate는 find, load, merge 등의 단일 동작을 한 줄짜리 콜백 구현체로 교체할 수 있는 편의 메서드를 제공한다.</p>
<p>게다가 스프링은 get/setEntityManagerFactory를 제공하고 하위 클래스가 사용하는 getJpaTemplate()를 제공하는 편리한 JpaDaoSupport 기반 클래스를 제공한다.</p>
<pre class="line-numbers"><code class="language-java">public class ProductDaoImpl extends JpaDaoSupport implements ProductDao {
public Collection loadProductsByCategory(String category) throws DataAccessException {
Map<String, String> params = new HashMap<String, String>();
params.put("category", category);
return getJpaTemplate().findByNamedParams("from Product as p where p.category = :category", params);
}
}
</code></pre>
<p>스프링의 JpaTemplate로 작업할 때와 달리 JPA로 스프링에 기반을 둔 DAO를 작성할 수도 있다. 이때는 명시적으로 EntityManager를 다루어야 한다. 하이버네이트 부분에서 자세히 설명했듯이 이 방법의 주요 장점은 데이터 접근 코드가 체크드 익셉션을 던질 수 있다는 것이다. JpaDaoSupport는 이러한 시나리오를 지원하는 다양한 메서드를 제공해서 예외를 잘 변환하면서 트랜잭션이 적용된 EntityManager를 받고 해제한다.</p>
<p>JpaTemplate은 주로 JdoTemplate와 HibernateTemplate의 형제 관계로 존재해서 이에 익숙한 사람들에게 같은 방식을 제공한다.<br />
<br></p>
<h3>A.2 고전적인 Spring MVC</h3>
<p>...<br />
<br></p>
<h3>A.3 JMS 사용방법</h3>
<p>스프링 JMS 지원의 장점 중 하나는 JMS 1.0.2와 1.1 API 간의 차이점을 사용자가 느끼지 못하게 한다는 점이다.(두 API의 차이점에 대한 내용을 보려면 도메인 단일화의 사이드바를 봐라.) JMS 1.1 API만 사용하는 것이 이제는 일반적이지만 JMS 1.0.2 API에 기반을 둔 클래스를 사용하는 것은 스프링 3.0에서는 폐기(deprecated)되었다. 이번 장에서는 JMS 1.0.2의 폐기된 클래스에 대한 스프링 JMS 지원을 설명한다.</p>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>도메인 단일화(Domain Unification)</b><br>
JMS 명세에는 두 가지 메이저 릴리즈가 있다.(1.0.2와 1.1)
<br>
JMS 1.0.2는 두 가지 종류의 메시지 도메인인 point-to-point (Queues)와 publish/subscribe (Topics)를 정의한다. 1.0.2 API는 도메인마다 병렬 클래스 계층을 제공해서 이 두 가지 메시징 도메인을 반영한다. 그 결과 클라이언트 애플리케이션은 JMS API를 사용할 때 도메인에 특화되게 되었다. JMS 1.1에서는 두 도메인사이에 함수적인 차이점과 클라이언트 API의 차이점을 모두 최소화하는 도메인 단일화라는 개념이 도입되었다. 제거된 함수의 차이점에 대한 예시로 JMS 1.1 프로바이더를 사용할 때 한 도메인에서 트랜잭션을 걸어서 메시지를 소비하고 같은 Session으로 다른 도메인에 메시지를 생성할 수 있다.
<br><br>
<b>Note</b><br>
JMS 1.1 명세는 2002년 4월에 공개되었고 2003년 11월 J2EE 1.4의 일부분이 되었다. 그래서 아직도 널리 쓰이는 일반적인 J2EE 1.3 애플리케이션 서버(BEA WebLogic 8.1이나 IBM WebSphere 5.1 같은)는 JMS 1.0.2에 기반을 두고 있다.
</font></div>
<p><br></p>
<h4>A.3.1 JmsTemplate</h4>
<p>org.springframework.jms.core 패키지에 있는 JmsTemplate102 클래스는 JMS 장에서 설명한 JmsTemplate의 모든 기능을 제공하지만, JMS 1.1 API 대신 JMS 1.0.2 API에 기반을 두고 있다. 그래서 JmsTemplate102를 사용한다면 어떤 JMS 도메인을 사용하는가에 대한 정보로 JmsTemplate를 구성하기 위해 pubSubDomain 불리언 프로퍼티를 설정해야 한다. 기본적으로 이 프로퍼티의 값은 false이고 point-to-point 도메인(Queues)를 사용한다는 것을 의미한다.<br />
<br></p>
<h4>A.3.2 비동기 메시지 수신</h4>
<p>거의 모든 클래스를 메시지 주도 POJO로 노출해서 비동기 메시지 수신을 지원하려고 MessageListenerAdapter는 스프링의 메시지 리스너 컨테이너(message listener containers)와 함께 사용한다. JMS 1.0.2 API를 사용한다면 MessageListenerAdapter102, SimpleMessageListenerContainer102, DefaultMessageListenerContainer102같은 1.0.2에 특화된 클래스를 사용하고자 할 것이다. 이러한 클래스는 JSM 1.1에 기반을 둔 부분과 같은 기능을 제공하지만 JMS 1.0.2 API에만 의존한다는 것만 다르다.<br />
<br></p>
<h4>A.3.3 연결</h4>
<p>ConnectionFactory 인터페이스는 JMS 명세의 일부분이고 JMS와 함께 동작하는 진입점으로 제공된다. 스프링은 ConnectionFactory 인터페이스의 구현체인 SingleConnectionFactory102를 제공한다. 이는 JMS 1.0.2에 기반을 두고 있어서 모든 createConnection() 호출에 같은 Connection을 반환하고 close() 호출은 무시할 것이다. SingleConnectionFactory102이 항상 javax.jms.QueueConnection와 javax.jmsTopicConnection를 명시적으로 구분할 것이므로 어떤 메시징 도메인을 사용하는지 나타내기 위해 pubSubDomain 블리언 프로퍼티를 설정해야 할 것이다.<br />
<br></p>
<h4>A.3.4 트랜잭션 관리</h4>
<p>JMS 1.0.2 환경에서 JmsTransactionManager102 클래스는 하나의 Connection Factory에 대한 JMS 트랜잭션 관리를 지원한다. 이 기능에 대한 자세한 내용은 JMS 트랜잭션 관리의 참고문서를 봐라.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1111?commentInput=true#entry1111WriteComment">댓글 쓰기</a></strong></p>[Spring 레퍼런스] 28장 캐시 추상화Outsiderhttps://blog.outsider.ne.kr/10942014-11-08T05:45:37+09:002014-11-08T05:45:37+09:00<p>이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.<br><br><br><br></p>
<h2>28. 캐시 추상화</h2>
<p><br></p>
<h3>28.1 소개</h3>
<p>3.1 버전부터 스프링 프레임워크는 기존 스프링 애플리케이션에 캐시를 투명하게 추가하도록 지원한다. 트랜잭션 지원과 비슷하게 캐시 추상화도 코드에 주는 영향을 최소화하면서 다양한 캐시 솔루션을 일관성 있게 사용할 수 있게 한다.<br />
<br></p>
<h3>28.2 캐시 추상화 이해하기</h3>
<p>핵심 부분에서 추상화는 Java 메서드에 캐싱을 적용함으로써 캐시에 보관된 정보로 메서드의 실행 횟수를 줄여준다. 즉, 대상 메서드가 실행될 때마다 추상화가 해당 메서드가 같은 인자로 이미 실행되었는지 확인하는 캐싱 동작을 적용한다. 해당 데이터가 존재한다면 실제 메서드를 실행하지 않고 결과를 반환하고 존재하지 않는다면 메서드를 실행하고 그 결과를 캐싱한 뒤에 사용자에게 반환해서 다음번 호출 시에 사용할 수 있게 한다. 이 방법을 통해 비용이 큰 메서드(CPU든 IO든) 해당 파라미터로는 딱 한 번만 실행할 수 있고 다시 메서드를 실행하지 않고도 결과를 재사용할 수 있다. 호출자가 어떤 방해도 받지 않고 캐싱 로직은 투명하게 적용된다.</p>
<div style="padding:10px; background-color:#EDE5A6"><font color="#000000">
<b>Important</b><br>
물론 이 접근방법은 얼마나 많이 호출하든지 간에 입력(혹은 인자)이 같으면 출력(결과)도 같다는 것을 보장하는 메서드에서만 동작한다.
</font></div>
<p><br></p>
<p>캐시 추상화를 사용하려면 개발자가 두 가지 관점을 신경을 써야 한다.</p>
<ul>
<li>캐싱 선언 - 캐시되어야 하는 메서드와 정책을 정한다</li>
<li>캐시 구성 - 데이터를 저장하고 읽을 기반 캐시</li>
</ul>
<p>스프링 프레임워크의 다른 서비스와 마찬가지로 캐싱 서비스는 추상화되어 있고(캐시 구현체가 아니다) 캐시 데이터를 저장하는 실제 스토리지를 사용해야 한다. 즉, 캐시 추상화로 개발자가 캐시 로직은 작성하지 않아도 되지만 실제 스토어를 제공하는 것은 아니다. 통합할 수 있는 두 가지 캐시는 JDK java.util.concurrent.ConcurrentMap와 <a href="http://ehcache.org/">Ehcache</a>이다. 다른 캐시 스토어나 제공자를 연결하려면 Section 28.6, “다른 백엔드 캐시에 연결하기(plugging-in)”를 참고해라.</p>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>캐시 vs 버퍼</b><br>
"버퍼"와 "캐시"라는 용어는 서로 바꿔가며 쓸 수 있다. 하지만 둘은 의미가 다르다. 버퍼는 빠른 엔티티와 느린 엔티티 중간에 데이터를 임시로 저장하는데 보통 사용된다. 어떤 부분이 대기 해야 해서 다른 성능에 영향을 준다면 작은 청크가 아니라 데이터의 전체 블록을 한 번에 옮기게 해서 이 성능 저하를 완화한다. 데이터는 버퍼에서만 읽고 쓰인다. 게다가 버퍼는 버퍼를 알고 있는 최소 한 부분에서는 가시적이다.
반면에 캐시는 숨겨져 있고 관련된 부분이 캐싱 된다는 것을 알지도 못한다. 같은 데이터를 빠르게 여러 번 읽음으로써 성능을 높인다.
캐시와 버퍼의 차이점에 대한 자세한 설명은 <a href="http://en.wikipedia.org/wiki/Cache#The_difference_between_buffer_and_cache">여기</a>에 나와 있다.
</font></div>
<p><br></p>
<h3>28.3 선언적인 어노테이션 기반의 캐싱</h3>
<p>캐싱 선언으로 추상화가 두가지 Java 어노테이션을 제공한다. @Cacheable와 @CacheEvict로 캐시군(cache population)과 캐시 만료(cache eviction)를 실행하는 메서드를 제공한다. 각 어노테이션을 자세히 살펴보자.<br />
<br></p>
<h4>28.3.1 @Cacheable 어노테이션</h4>
<p>이름이 암시하듯이 @Cacheable는 캐시할 수 있는 메서드를 지정하는데 사용한다. 즉, 이 어노테이션을 사용한 메서드는 결과를 캐시에 저장하므로 뒤이은 호출(같은 인자일 때)에는 실제로 메서드를 실행하지 않고 캐시에 저장된 값을 반환한다. 가장 간단한 형식으로는 어노테이션이 붙은 메서드와 연관된 캐시의 이름만 있으면 어노테이션을 선언할 수 있다.</p>
<pre class="line-numbers"><code class="language-java">@Cacheable("books")
public Book findBook(ISBN isbn) {...}
</code></pre>
<p>앞의 코드에서 findBook 메서드는 books라는 캐시와 연결된다. findBook 메서드를 호출할 때마다 해당 호출이 이전에 실행된 적이 있는지 캐시가 확인하고 반복해서 실행하지 않는다. 하나의 캐시만 선언하는 대부분의 경우 어노테이션은 여러 이름을 지정해서 한 캐시를 여러 번 사용할 수 있다. 이 경우 메서드를 실행하기 전에 각 캐시를 확인할 것이고 최소 하나의 캐시에 저장되어 있다면 해당 값을 반환할 것이다.</p>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
이 기능은 기본적으로는 비활성화되어 있다.
</font></div>
<pre class="line-numbers"><code class="language-java">@Cacheable({ "books", "isbns" })
public Book findBook(ISBN isbn) {...}
</code></pre>
<p><br></p>
<h4>28.3.1.1 기본 키 생성</h4>
<p>캐시는 본질적으로 키-밸류 저장소이므로 캐시된 메서드를 호출할 때마다 해당 키로 변환되어야 한다. 캐시 추상화는 다음 알고리즘에 기반을 둔 KeyGenerator를 사용한다.</p>
<ul>
<li>파라미터가 없으면 0을 반환한다.</li>
<li>파라미터가 하나만 있으면 해당 인스턴스를 반환한다.</li>
<li>파라미터가 둘 이상이면 모든 파라미터의 해시를 계산한 키를 반환한다.</li>
</ul>
<p>이 접근은 객체를 리플랙션하는 hashCode()처럼 일반적인 키를 가진 객체와 잘 동작한다. 이러한 경우가 아니고 분산 혹은 유지(persistent)되는 환경이라면 객체가 hashCode를 보관하지 않도록 전략을 변경해야 한다. 사실 JVM 구현체나 운영하는 환경에 따라 같은 VM 인스턴스에서 hashCode를 다른 객체에서 재사용할 수 있다.</p>
<p>다른 기본 키 생성자를 제공하려면 org.springframework.cache.KeyGenerator 인터페이스를 구현해야 한다. 이를 구성하고 나면 전용 키 생성 전략(아래 참고)를 지정하지 않은 모든 선언에서 이 생성자를 사용할 것이다.<br />
<br></p>
<h4>28.3.1.2 커스텀 키 생성 선언</h4>
<p>캐싱은 일반적이므로 대상 메서드는 캐싱 구조에 간단하게 매핑할 수 없는 다양한 시그니처를 가질 가능성이 높다. 이는 여러 인자중 일부만 캐싱하기에 적합한(나머지 인자는 메서드 로직에만 사용된다) 대상 메서드가 있는 경우에 명확해 지려는 의도이다. 예를 들면 다음과 같다.</p>
<pre class="line-numbers"><code class="language-java">@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed
</code></pre>
<p>간단히 말하면 두 boolean 인자는 책을 찾는데 영향을 주고 캐시에는 사용하지 않는다. 게다가 둘 줄 하나만 중요하고 다른 인자는 중요하지 않다면?</p>
<p>이러한 경우 @Cacheable 어노테이션으로 사용자가 key 속성으로 키를 생성하는 방법을 지정할 수 있다. 개발자는 사용할 인자(또는 중첩된 속성)를 선택하거나 작업 실행이나 어떤 코드도 작성하지 않고 인터페이스도 구현하지 않고 임의의 메서드를 호출하려고 SpEL를 사용할 수 있다. 메서드는 코드가 달라짐에 따라 시그니처도 달라질 것이므로 기본 생성자 외의 접근을 추천한다. 기본 전략은 일부 메서드에서 동작할 것이지만 모든 메서드에서 동작할 가능성은 별로 없다.</p>
<p>아래 여러 가지 SpEL 선언의 예시가 있다. SpEL에 익숙하지 않다면 익숙한 것을 사용하거나 Chapter 7, Spring 표현 언어 (SpEL)를 읽어봐라.</p>
<pre class="line-numbers"><code class="language-java">@Cacheable(value="books", key="#isbn"
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(value="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(value="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
</code></pre>
<p>위의 코드는 특정 인자나 그 속성이나 임의의 (stsatic) 메서드를 선택하기가 얼마나 쉬운지 보여준다.<br />
<br></p>
<h4>28.3.1.3 조건부 캐싱</h4>
<p>때로는 메서드를 항상 캐싱하는 것이 적합하지 않을 수 있다.(예를 들어 인자에 따라 다를 수 있다.) 캐시 어노테이션은 true나 false가 되는 SpEL 표현식을 받는 conditional 파라미터로 이러한 기능을 지원한다. 이 표현식이 true이면 메서드를 캐시하고 false이면 메서드가 캐시 되지 않은 것처럼 동작해서 해당 값이 캐시가 되었든 인자가 무엇이든 간에 매번 실행된다. 다음은 name인 32자보다 적은 경우에만 캐시 되는 간단한 예시이다.</p>
<pre class="line-numbers"><code class="language-java">@Cacheable(value="book", condition="#name.length < 32")
public Book findBook(String name)
</code></pre>
<p><br></p>
<h4>28.3.1.4 사용 가능한 캐싱 SpEL 평가 컨텍스트(evaluation context)</h4>
<p>각 SpEL 표현식은 전용 context를 다시 평가한다. 파라미터를 구성하는 것에 추가로 프레임워크는 인자 이름 같은 메타데이터와 관련된 전용 캐시를 제공한다. 다음 표는 컨텍스트에서 사용 가능한 요소가 나와 있어서 키와 조건부(다음 섹션 참고) 계산에 사용할 수 있다.</p>
<p><br><span style="font-weight: bold;">Table 28.1. Cache SpEL에서 사용 가능한 메타데이터</span></p>
<table style="border-collapse: collapse;border: 1.0pt solid;" align="left">
<tbody>
<tr>
<th style="border-collapse: collapse;border: 1.0pt solid;" align="left">이름</th>
<th style="border-collapse: collapse;border: 1.0pt solid;" align="left">위치</th>
<th style="border-collapse: collapse;border: 1.0pt solid;" align="left">설명</th>
<th style="border-collapse: collapse;border: 1.0pt solid;" align="left">예시</th>
</tr>
<tr>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">methodName</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">root object</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">호출되는 메서드의 이름</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">#root.methodName</td>
</tr>
<tr>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">method</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">root object</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">호출되는 메서드</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">#root.method.name</td>
</tr>
<tr>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">target</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">root object</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">호출되는 대상 객체</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">#root.target</td>
</tr>
<tr>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">targetClass</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">root object</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">호출되는 대상 클래스</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">#root.targetClass</td>
</tr>
<tr>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">args</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">root object</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">대상을 호출하는데 사용한 인자(배열)</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">#root.args[0]</td>
</tr>
<tr>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">caches</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">root object</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">현재 실행된 메서드 캐시의 컬렉션</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">#root.caches[0].name</td>
</tr>
<tr>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">argument name</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">evaluation context</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">메서드 인자의 이름. 어떤 이유로든 이름을 사용할 수 없다면(예: 디버깅 정보가 없는 경우) a<#arg>에서 인자 이름을 사용할 수도 있고 #arg은 (0부터 시작하는) 인자의 인덱스를 의미한다.</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">iban나 a0 (p0를 사용하거나 별칭으로 p<#arg> 형식을 사용할 수 있다.).</td>
</tr>
</tbody></table>
<p>.</p>
<h3>28.3.2 @CachePut 어노테이션</h3>
<p>메서드 실행에 영향을 주지 않고 캐시를 갱신해야 하는 경우 @CachePut 어노테이션을 사용할 수 있다. 즉, 메서드를 항상 실행하고 그 결과를 (@CachePut 옵션에 따라) 캐시에 보관한다. @CachePut은 @Cacheable와 같은 옵션을 지원하고 메서드 흐름 최적화보다는 캐시 생성(population)에 사용해야 한다.</p>
<p>같은 메서드에 @CachePut와 @Cacheable 어노테이션을 사용하는 것은 둘이 다른 동작을 가지므로 보통 권장하지 않는다. @Cacheable는 캐시를 사용해서 메서드 실행을 건너뛰고 @CachePut는 캐시 갱신을 하려고 실행을 강제한다. 이는 특정한 코너케이스가 아니라면(두 어노테이션이 서로 조건이 겹치지 않는 경우) 의도치 않은 동작이 발생할 수 있어서 이러한 선언은 하지 말아야 한다.<br />
<br></p>
<h4>28.3.3 @CacheEvict 어노테이션</h4>
<p>캐시 추상화로 캐시 스토어의 생성 뿐만 아니라 제거도 할 수 있다. 이 과정은 캐시에서 오래되거나 사용하지 않는 데이터를 제거하는데 유용하다. @Cacheable와는 달리 @CacheEvict 어노테이션은 캐시를 제거(eviction)하는 메서드를 구분하는데 즉, 캐시에서 데이터를 제거하는 트리거로 동작하는 메서드다. 다른 캐시 어노테이션과 마찬가지로 @CacheEvict는 동작할 때 영향을 끼치는 하나 이상의 캐시를 지정해야 한다. @CacheEvict에서 키나 조건을 지정해야 할 수 있지만 딱 하나의 엔트리(키에 기반을 둔)가 아니라 제거를 할 캐시의 범위를 나타내는 allEntries 파라미터를 추가로 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-java">@CacheEvict(value = "books", allEntries=true)
public void loadBooks(InputStream batch)
</code></pre>
<p>한 지역의 전체 캐시를 모두 지워야 할 때 이 옵션을 편리하게 사용할 수 있다. 각 엔트리를 하나씩 지우는 것이 아니라(비효율적이라 시간이 오래 걸린다) 위 예제처럼 한 번에 모든 엔트리를 제거한다. 이 시나리오에서 지정한 키에 적용되지 않는 것은 프레임워크가 무시할 것이다.(한 엔트리가 아니라 전체 캐시가 제거된다)</p>
<p>beforeInvocation 속성으로 메서드 실행 이후(기본값)나 이전에 제거를 해야 하는 지를 지정할 수도 있다. 메서드 실행 이후에 실행되는 경우는 다른 어노테이션과 같은 의미를 가져서 메서드가 성공적으로 완료되면 캐시에서 동작(여기서는 제거)이 실행된다. 메서드가 실행되지 않거나(캐시 되어서) 예외가 던져지면 제거가 실행되지 않는다. 메서드 실행 이전에 실행되는 경우(beforeInvocation=true)에는 메서드가 호출되기 전에 항상 제거가 발생해서 제거가 메서드 결과에 의존하지 않는 경우에 유용하다.</p>
<p>@CacheEvict를 void 메서드에 사용할 수 있다는 것은 중요한 부분이다. 메서드가 트리거로 동작하므로 반환값은 무시한다.(캐시와 상호작용하지 않으므로) 이는 캐시에 데이터를 넣거나 갱신해서 그 결과가 필요한 @Cacheable와는 다른 점이다.<br />
<br></p>
<h4>28.3.4 @Caching 어노테이션</h4>
<p>@CacheEvict나 @CachePut처럼 같은 계열의 어노테이션을 여러 개 지정해야 하는 경우가 있는데 예를 들어 조건이나 키 표현식이 캐시에 따라 다른 경우이다. 안타깝게도 자바는 이러한 선언을 지원하지 않지만 감싸진(enclosing) 어노테이션을 사용해서(이 경우에는 @Caching) 우회할 수 있다. @Caching에서 중첩된 @Cacheable, @CachePut, @CacheEvict를 같은 메서드에 다수 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-java">@Caching(evict = { @CacheEvict("primary"), @CacheEvict(value = "secondary", key = "#p0") })
public Book importBooks(String deposit, Date date)
</code></pre>
<p><br></p>
<h4>28.3.5 캐시 어노테이션 활성화하기</h4>
<p>캐시 어노테이션을 선언하는 것만으로 자동으로 동작이 실행되지 않는다는 점은 중요하다. 스프링의 다른 기능처럼 선언적으로 기능을 활성화해야 한다.(이는 캐시에 문제가 있다고 의심된다면 코드의 모든 어노테이션을 지우는 대신 설정에서 딱 한 줄만 지워서 캐시를 비활성화할 수 있다는 의미이다.) 실제로는 이 선언으로 스프링이 캐시 어노테이션을 처리하도록 한다.</p>
<pre class="line-numbers"><code class="language-xml"><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
<cache:annotation-driven />
</beans>
</code></pre>
<p>애플리케이션에 추가된 캐시의 동작 방식을 AOP로 변경할 다양한 옵션을 네임스페이스로 지정할 수 있다. 설정은 tx:annotation-driven의 설정과 (의도적으로) 비슷하다.</p>
<p><span style="font-weight: bold;">Table 28.2. <cache:annotation-driven/> 설정</span></p>
<table style="border-collapse: collapse;border: 1.0pt solid;" align="left">
<tbody>
<tr>
<th style="border-collapse: collapse;border: 1.0pt solid;" align="left">속성</th>
<th style="border-collapse: collapse;border: 1.0pt solid;" align="left">기본값</th>
<th style="border-collapse: collapse;border: 1.0pt solid;" align="left">설명</th>
</tr>
<tr>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">cache-manager</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">cacheManager</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">사용할 캐시 관리자의 이름. 위 예제처럼 캐시 관리자의 이름이 cacheManager가 아닐 때만 지정한다.</td>
</tr>
<tr>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">mode</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">proxy</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">기본값인 "proxy" 모드는 스프링의 AOP 프레임워크로 어노테이션이 붙은 빈을 프락시한다.(프락시의 개념에 맞게 프락시를 통해서 오늘 메서드 호출에만 적용한다.) 다른 모드인 "aspectj"는 영향받은 클래스를 스프링의 AspectJ 캐싱 관점으로 위빙한다. (대상 클래스의 바이트코드를 메서드 호출에 적용되도록 수정한다.) AspectJ 위빙을 사용하려면 로딩 타임 위빙(또는 컴파일 타임 위빙) 활성화와 마찬가지로 클래스패스에 spring-aspects.jar가 있어야 한다.(로드 타임 위빙을 설정하는 방법은 Section 8.8.4.5, “스프링 설정”를 참고해라.)</td>
</tr>
<tr>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">proxy-target-class</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">false</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">proxy 모드에만 적용된다. @Cacheable나 @CacheEvict 어노테이션이 붙은 클래스에 어떤 종류의 캐싱 프락시를 생성할지를 제어한다. proxy-target-class 속성을 true로 설정하면 클래스에 기반을 둔 프락시를 생성한다. proxy-target-class가 false이거나 proxy-target-class 속성을 생략하면 표준 JDK 인터페이스에 기반을 둔 프락시를 생성한다. (다양한 프락시 타입에 대한 자세한 내용은 Section 8.6, “프록싱 메카니즘”를 참고해라.)</td>
</tr>
<tr>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">order</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">Ordered.LOWEST_PRECEDENCE</td>
<td style="border-collapse: collapse;border: 1.0pt solid;" align="left">@Cacheable나 @CacheEvict 어노테이션이 붙은 빈에 적용된 캐시 어드바이스의 순서를 정의한다.(AOP 어드바이스 순서에 관한 규칙은 Section 8.2.4.7, “어드바이스 순서”를 참고해라.) 순서를 지정하지 않으면 AOP 하위시스템이 어드바이스의 순서를 결정한다.</td>
</tr>
</tbody></table>
<p>.</p>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
<cache:annotation-driven/>는 선언된 애플리케이션 컨텍스트에서 빈에 붙은 @Cacheable/@CacheEvict만 찾는다. 즉, DispatcherServlet의 WebApplicationContext에 <cache:annotation-driven/>를 선언하면 서비스는 빼고 컨트롤러에서만 @Cacheable/@CacheEvict 빈을 확인한다. 자세한 내용은 Section 16.2, “DispatcherServlet”를 참고해라.
</font></div>
<p><br></p>
<div style="padding:10px; background-color:#EDE5A6"><font color="#000000">
<b>Tip</b><br>
스프링은 인터페이스에 어노테이션을 붙이는 것과는 반대로 구현(concrete) 클래스에만(구현 클래스의 메서드) @Cache* 어노테이션을 붙이기를 권장한다. 물론 인터페이스에(혹은 인터페이스 메서드) @Cache* 어노테이션을 붙일 수 있지만, 인터페이스에 기반을 둔 프락시를 사용할 때만 원하는 대로 동작한다. Java 어노테이션이 인터페이스를 상속받지 않는다는 점은 클래스나 위빙에 기반을 둔 프락시(proxy-target-class="true"나 mode="aspectj")를 사용한다면 프락시나 위빙 인프라가 캐시 설정을 인식하지 못한다는 것을 의미힌다. 그리고 해당 객체는 캐시 프락시로 감싸지지 않을 것이므로 이는 확실히 좋지 않다.
</font></div>
<p><br></p>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>Note</b><br>
프락시 모드(기본값이다)에서는 프락시로 호출되는 외부 메서드만 가로챌 수 있다. 즉, 대상 객체 내의 메서드가 대상 객체의 다른 메서드를 호출하는 자기호출(self-invocation)은 해당 메서드에 @Cacheable 어노테이션이 붙어있더라도 런타임에서 실제 캐싱 되지 않을 것이다.(이러면 aspectj의 사용을 고려해 봐라.)
</font></div>
<p><br></p>
<div style="padding:10px; background-color:#E4E4E4"><font color="#000000">
<b>메서드 가시성(visibility)과 @Cacheable/@CachePut/@CacheEvict</b><br>
프락시를 사용할 때 public 가시성을 가진 메서드에만 @Cache* 어노테이션을 적용해야 한다. protected, private, package-visible 메서드에 이러한 어노테이션을 붙이면 에러는 발생하지 않지만 어노테이션이 붙은 메서드에는 구성한 캐시 설정이 보이지 않는다. public이 아닌 메서드에 어노테이션을 사용해야 한다면 AspectJ(아래 참고)를 고려해 봐라.(AspectJ는 바이트코드 자체를 바꾼다.)
</font></div>
<p><br></p>
<h4>28.3.6 커스텀 어노테이션의 사용</h4>
<p>캐싱 추상화로 어떤 메서드가 캐시 생성과 제거를 실행하는지 확인하기 위한 자신만의 어노테이션을 사용할 수 있다. 이는 캐시 어노테이션을 중복으로 설정할 필요가 없어져서(특히 키나 조건을 지정할 때 유용하다) 템플릿 메커니즘처럼 아주 편리하고 외부 임포트(org.springframework)가 코드에서 사용할 수 없는 경우에 유용하다. stereotype 어노테이션과 유사하게 @Cacheable와 @CacheEvict 어노테이션 모두 다른 어노테이션에 어노테이션을 붙일 수 있는 메타 어노테이션으로 사용할 수 있다. 즉, @Cacheable 선언을 커스텀 어노테이션을 대체할 수 있다.</p>
<pre class="line-numbers"><code class="language-java">@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(value=“books”, key="#isbn")
public @interface SlowService {
}
</code></pre>
<p>앞의 코드에서 @Cacheable 어노테이션이 붙어있는 SlowService 커스텀 어노테이션을 정의했다. 이제 다음 코드를 바꿀 수 있다.</p>
<pre class="line-numbers"><code class="language-java">@Cacheable(value="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
</code></pre>
<p>이 코드를 다음과 같이 바꿀 수 있다.</p>
<pre class="line-numbers"><code class="language-java">@SlowService
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
</code></pre>
<p>@SlowService가 스프링 어노테이션이 아님에도 컨테이너가 런타임시에 자동으로 어노테이션 선언을 찾아서 그 의미를 이해할 수 있다. 앞에서 얘기한 대로 어노테이션 주도 동작을 활성화해야 한다.<br />
<br></p>
<h3>28.4 선언적 XML에 기반을 둔 캐싱</h3>
<p>어노테이션이 선택사항이 아니고(소스에 접근권한이 없거나 외부 코드가 없는 경우) 선언적인 캐싱에 XML을 사용할 수 있다. 그래서 캐싱하려고 메서드에 어노테이션을 붙이는 대신 대상 메서드와 캐싱 지시어를 외부에서 지정한다.(선언적인 트랜잭션 관리 advice와 유사하다) 앞의 예제를 다음과 같이 바꿀 수 있다.</p>
<pre class="line-numbers"><code class="language-xml"><!-- the service we want to make cacheable -->
<bean id="bookService" class="x.y.service.DefaultBookService"/>
<!-- 캐시 정의 -->
<cache:advice id="cacheAdvice" cache-manager="cacheManager">
<cache:caching cache="books">
<cache:cacheable method="findBook" key="#isbn"/>
<cache:cache-evict method="loadBooks" all-entries="true"/>
</cache:caching>
</cache:advice>
<!-- 모든 BookService 인터페이스에 cacheable 동작을 적용한다 -->
<aop:config>
<aop:advisor advice-ref="cacheAdvice" pointcut="execution(* x.y.BookService.*(..))"/>
</aop:config>
...
// 캐시 관리자 정의는 생략한다
</code></pre>
<p>앞의 설정에서 bookService는 캐싱이 가능하다. 여기서 적용되는 캐시 동작은 cache:advice 정의에 은닉화되어 있어서 loadBooks 메서드가 데이터를 제거하는 동안 캐시에 데이터를 보관하는데 findBooks 메서드를 사용하도록 한다. 두 정의 모두 books 캐시에 동작한다.</p>
<p>aop:config 정의는 AspectJ 포인트컷 표현식(자세한 내용은 Chapter 8, 스프링의 관점 지향 프로그래밍에 있다.)을 사용해서 프로그램의 적절한 시점에 캐시 어드바이스를 적용한다. 앞의 예제에서 BookService의 모든 메서드를 검토해서 캐시 어드바이스를 적용한다.</p>
<p>선언적인 XML 캐싱이 어노테이션에 기반을 둔 모든 모델을 지원하므로 어노테이션과 XML 간의 변경은 아주 쉽다. 게다가 한 애플리케이션 내에서 둘을 함께 사용할 수도 있다. XML에 기반을 둔 접근방법은 대상 코드를 건드리지 않지만, XML의 특성상 약간 더 장황하다. 캐싱 대상이 되는 클래스의 오버 로딩된 메서드를 다루는 경우 method 인자가 적절한 식별자가 아니므로 적합한 메서드를 구별하려면 추가적인 노력이 필요하다. 이러면 AspectJ 포인트컷으로 대상 메서드를 골라내서 적절한 캐싱 기능을 적용할 수 있다. 하지만 XML을 사용하면 패키지/그룹/인터페이스 범위의 캐싱을 적용하고(AspectJ 포인트컷 때문에) 템플릿같은 정의를 생성하기가(앞의 예제에서 cache:definitions cache 속성으로 대상 캐시를 정의한 것처럼) 더 쉽다.<br />
<br></p>
<h3>28.5 캐시 스토리지 구성</h3>
<p>캐시 추상화는 JDK ConcurrentMap에 기반을 둔 스토리지와 ehcache 라이브러리의 스토리지와 통합할 수 있다. 이 스토리지를 사용하려면 CacheManager를 적절히 선언하면 된다. CacheManager로 Cache를 제어하고 관리하고 스토리지에서 Cache를 가져와서 사용할 수 있다.<br />
<br></p>
<h4>28.5.1 JDK ConcurrentMap에 기반을 둔 Cache</h4>
<p>JDK에 기반을 둔 Cache 구현체는 org.springframework.cache.concurrent 패키지 아래 존재한다. 이 구현체로 Cache 스토어에 기반을 둔 ConcurrentHashMap를 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-xml"><!-- generic cache manager -->
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="books"/>
</set>
</property>
</bean>
</code></pre>
<p>앞의 코드는 default와 books라는 이름의 중첩된 Concurrent Cache 구현체를 위한 CacheManager를 생성하는데 SimpleCacheManager를 사용한다. 이름은 각 캐시에 직접 설정했다.</p>
<p>애플리케이션이 캐시를 생성하므로 캐시는 애플리케이션의 생명주기를 따른다. 이는 테스트나 간단한 애플리케이션 같은 기본적인 사용 사례에 적합하다. 이 캐시는 잘 확장되고 아주 빠르지만 어떤 관리도 제공하지 않고 보관 기능이나 제거 계약이 존재하지 않는다.<br />
<br></p>
<h4>28.5.2 Ehcache에 기반을 둔 Cache</h4>
<p>Ehcache 구현체는 org.springframework.cache.ehcache 패키지 아래 있다. 마찬가지로 Ehcache 구현체를 사용하려면 CacheManager를 적절히 선언하면 된다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cache-manager-ref="ehcache"/>
<!-- Ehcache 라이브러리 설정 -->
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config-location="ehcache.xml"/>
</code></pre>
<p>이 설정은 스프링 IoC 내에서 ehcache를 구동하고(ehcache 빈을 통해서) 전용 CacheManager 구현체에 연결한다. ehcache에 특화된 전체 설정은 ehcache.xml 리소스에서 읽는다.<br />
<br></p>
<h4>28.5.3 기반 스토어없이 캐시 다루기</h4>
<p>때로는 환경을 바꾸거나 테스트를 할 때 실제 기반 캐시를 구성하지 않고 캐시를 설정해야 하는 경우가 있다. 이는 유효하지 않은 구성이므로 런타임시에 캐시 인프라가 적절한 스토어를 찾을 수 없어서 예외가 던져질 것이다. 이러한 경우 캐시 선언을 제거하는 대신(지루한 작업이다) 캐싱을 수행하지 않는 간단한 더비 캐시를 연결할 수 있다. 즉 캐시된 메서드가 매번 실행되도록 강제한다.</p>
<pre class="line-numbers"><code class="language-xml"><bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
<property name="cacheManagers"><list>
<ref bean="jdkCache"/>
<ref bean="gemfireCache"/>
</list></property>
<property name="addNoOpCache" value="true"/>
</bean>
</code></pre>
<p>앞의 CompositeCacheManager는 여러 CacheManager를 체이닝하고 있고 설정한 캐시 관리자가 다루지 않는 모든 정의에 no op 캐시를 추가한다. (addNoOpManager 플래그로) 즉 (앞에서 설정한) jdkCache나 gemfireCache에서 찾지 못한 모든 캐시 정의는 no op 캐시로 다뤄서 아무런 정보를 저장하지 않아서 대상 메서드가 매번 실행되도록 한다.<br />
<br></p>
<h3>28.6 다른 백엔드 캐시에 연결하기(plugging-in)</h3>
<p>기반 저장소로 사용할 수 있는 많은 캐시 제품이 분명히 존재한다. 이를 연결하려면 CacheManager와 Cache 구현체를 제공해야 하지만 안타깝게도 사용할 수 있는 표준은 존재하지 않는다. 이는 ehcache 클래스가 보여주었듯 스토리지 API 상위의 캐시 추상화 프레임워크에 매핑되는 간단한 <a href="http://en.wikipedia.org/wiki/Adapter_pattern">adapter</a>가 되는 클래스여야 하므로 꽤 어려운 얘기로 들린다. CacheManager 클래스 대부분은 작성해야 할 실제 매핑만 남겨둔 보일러 플레이트 코드를 다루는 AbstractCacheManager와 같은 org.springframework.cache.support 패키지의 클래스를 사용할 수 있다. 스프링과의 통합을 제공하는 라이브러리가 이 약간의 설정 차이를 조만간 메꿔주기를 기대하고 있다.<br />
<br></p>
<h3>28.7 TTL/TTI/Eviction policy/XXX 기능을 어떻게 설정하는가?</h3>
<p>캐시 프로바이더로 직접 설정한다. 케시 추상화는 캐시 구현체가 아니다. 사용하는 제품은 아마 다른 제품이 지원하지 않는(JDK ConcurrentHashMap 예제를 봐라) 다양한 데이터 정책과 여러가지 토폴로지(topologies)를 지원할 것이다. 캐시 추상화에서 이를 노출하는 것은 의존하는 지원체가 없어서 쓸모없을 것이다. 이러한 기능은 캐시를 설정할 때나 캐시의 네이티브 API로 기반 캐시에서 직접 제어해야 한다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1094?commentInput=true#entry1094WriteComment">댓글 쓰기</a></strong></p>