박성철 fupfin@gmail.com|한국스프링사용자모임 대표이면서 작은 보안회사의 개발팀을 이끌고 있다. 사진, 애니메이션, 프로그래밍이 취미다. 8비트 PC와 함께 시작한 프로그래밍에 빠져 지금까지 벗어나지 못했다. 사람들과 만나 관심사를 함께 나누길 좋아하며 프로그래머가 더 행복하게 작업하면서 사기꾼에게 놀아나지 않고 인류에 실제로 이바지할 방법은 없는지 찾는 중이다. 최근 클라우드 애플리케이션 개발을 다룬 『알짜만 골라 배우는 자바 구글앱엔진』을 집필한 바 있다.

스프링을 사용하면 일반 자바 객체(POJO)로 기업용 애플리케이션을 만드는 일이 가능하다. 애플리케이션을 구성하는 IoC/DI 컨테이너와 모듈화된 여러 가지 횡단 관심사를 객체에 장착해 객체를 동적으로 확장하는 AOP로 스프링이 유명하긴 하지만 스프링을 다른 경쟁 기술과 구별 짓는 요소는 이런 기본적인 특징에서 한걸음 더 나아가 수많은 이식 가능한 서비스 추상화 계층을 제공한다는 점이다.

풍부하고 잘 설계된 서비스 추상화 계층을 가지고 있다는 사실은 스프링이 오랫동안 기업용 애플리케이션에 사용되었기 때문이기도 하지만, 무엇보다 애초에 쟁쟁한 벤더의 지지 아래 득세하던(하지만 개발자를 괴롭혔던) 자바 엔터프라이즈 기술에 대한 총체적인 대안과 올바른 방향을 제시하려는 목표를 가지고 만들었기 때문이다. 즉, 스프링은 시간이 갈수록 섬세해지고 더욱 온전해지지만 처음 나올 때에도 그런 모습이었다.

클린 코드에서 밥 아저씨는 외부 API에 애플리케이션이 직접 의존하도록 하지 말고 어댑터(Adapter)로 API를 바꾸거나 래퍼(Wrapper)로 감싸서 애플리케이션이 외부 API의 변화에 영향을 받지 않도록 하라고 충고한다. 

스프링은 우리가 직접 외부 API를 추상화하는 것보다 더 훌륭하게 추상화해 준다. 스프링의 추상화 계층 덕에 애플리케이션의 POJO는 변화무쌍한 외부 컴포넌트의 변화에 거의 영향을 받지 않으며 요구사항 변경 이외의 일로는 바뀌지 않게 된다. 그리고 스프링을 아는 사람은 누구나 알겠지만 스프링은 정말 놀라운 하위 호환성을 유지한다. 스프링 3는 추상화 계층에서도 주목할 만한 개선을 이뤘다. 그 중에서 꽤 고급 기능이면서도 단순한 인터페이스, XML 스키마, 자바 어노테이션을 사용해 쉽게 배우고 적용할 수 있는 몇 가지를 골라 소개하려고 한다.

OXM, 객체-XML 매핑
스프링 OXM은 객체와 XML을 매핑하는 기능으로서 자바 객체를 XML로 변환하는 마샬링(Marshalling)과 역으로 XML에서 객체로 복원하는 언마샬링(Unmarshalling)을 처리한다. 원래는 스프링 웹 서비스의 일부였으나 스프링 배치와 스프링 통합 등 여러 곳에서 널리 사용되면서 스프링 프레임워크에 포함되었다.

<그림 1> 스프링 OXM의 추상화

스프링은 바퀴를 다시 발명하지 않고 스프링의 일관된 애플리케이션 구성 방법을 통해 재사용한다는 철학이 있다. OXM도 이런 스프링의 철학을 그대로 이어받아 이미 성숙한 여러 객체-XML 매핑 기술(JAXB2, Caster, XMLBean, XStream 등)을 아주 단순한 두 자바 인터페이스로 추상화했다. 애플리케이션은 단지 인터페이스만 사용할 뿐 구체적으로 어떤 기술이 사용되는지 거의 알 필요가 없기 때문에 코드를 바꾸지 않고도 객체-XML 매핑 기술을 필요에 따라 마음껏 선택해 사용할 수 있고, 구성과 운영환경을 바꿀 수 있으며, 여러 객체-XML 매핑 기술을 혼합해 쓸 수도 있다. 

스프링 OXM은 여러 객체-XML 매핑 도구가 던지는 예외도 스프링 고유의 XmlMappingException 예외 계층구조로 추상화한다. 모든 도구의 다양한 예외를 일관된 예외로 변환해서 던지기 때문에 애플리케이션은 어떤 도구를 사용하는지 알 필요 없이 정확히 문제를 파악해 처리할 수 있다. 무엇보다 Runtime Exception을 상속받은 예외라서 번잡하게 강제로 예외 처리를 하지 않아도 된다.

이는 스프링 3 내부적으로 스프링 MVC의 XML 뷰나 JMS 등에서 사용된다. 하지만, 애플리케이션 제작자가 원한다면 언제라도 간단히 설정해 직접 사용함으로써 파일로 저장하거나 네트워크로 전송하거나 DB의 XML 타입 컬럼에 넣거나 할 수 있다. 특히 객체-XML 매핑 도구인 캐스터(Caster)는 설정이 간단한데 다음과 같이 애플리케이션 컨텍스트 설정에 빈을 추가하기만 하면 별도 설정이 필요 없고 빈 하나가 마샬링과 언마샬링을 모두 수행한다. 사실, 스프링의 모든 Mashhaler와 Unmashaller의 경우 구현체는 한 클래스가 두 인터페이스를 모두 구현하기 때문에 빈 하나만 등록해서 두 가지 용도로 사용할 수 있다.

<bean id="oxmMarshaller" class="org.springframework.oxm. 

castor.CastorMarshaller"/>

Marshaller 인터페이스
객체를 XML로 변환하는 먀살링은 Marshaller 인터페이스를 구현한 구현체가 처리한다. Marshaller는 marshal이라는 메소드가 하나 있는 아주 단순한 인터페이스이고 인자로 변환할 객체와 결과를 담을 Result 인터페이스의 구현체를 받는다.

public interface Marshaller {
 void marshal(Object graph, Result result) throws 
XmlMappingException,IOException;
}

Result 인터페이스의 구현체로는 DOMResult, SAXResult, StreamResult가 있고 각각 DOM 문서, SAX 처리기, Output Stream을 포장한다. Marshaller를 이용해 객체를 파일로 저장하려면 FileOutputStream을 StreamResult에 담아서 Marshaller의 marshal() 메소드를 호출하면 된다.

@Autowired
private Marshaller marshaller; 
...
 FileOutputStream os = null;
 ...
 os = new FileOutputStream("output.xml");
 this.marshaller.marshal(serializeMe, new StreamResult(os));

Unmarshaller 인터페이스
XML을 객체로 복원하는 언먀살링은 Unmarshaller 인터페이스를 구현한 구현체가 처리한다. Marshaller와 마찬가지로 Unmarshaller는 unmarshal 메소드 하나만 있는 단순한 인터페이스다. 인자로는 Result 대신 Source 인터페이스의 구현체를 받는다. 변환된 객체는 메소드 반환 값으로 얻을 수 있다.

public interface Unmarshaller {
 Object unmarshal(Source source) throws XmlMappingException, IOException;
}

Source 인터페이스의 구현체로는 DOMSource, SAXSource, StreamSource가 있고 각각 DOM 문서, SAX 입력 소스(org. xml.sax.InputSource), InputStream을 포장한다. Unmar shaller로 XML 파일을 읽어 객체로 복원하려면 FileInputStr eam을 StreamSource에 담아 unmarshal 메소드에 전달한다.

@Autowired
private Unmarshaller unmarshaller; 
...    
 FileInputStream is = null;
 ...
 is = new FileInputStream("sample.xml"); 
 Sample sample = (Sample) unmarshaller.unmarshal(new StreamSource(is));

oxm 네임스페이스
스프링 3의 OXM은 객체-XML 매핑 기술을 추상화할 뿐 아니라 쉽게 설정할 수 있는 XML 스키마 기반의 oxm 네임스페이스도 마련해 놓았다. oxm 네임스페이스를 사용하려면 <리스트 1>과 같이 선언해 둬야 한다.

<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:oxm="http://www.springframework.org/ schema/oxm"
  xsi:schemaLocation="http://www.springframework. org/schema/beans
  http://www.springframework.org/schema/ beans/spring-beans-3.0.xsd
   http://www.springframework.org/schema/ oxm
  http://www.springframework.org/schema/ oxm/spring-oxm-3.0.xsd">

<리스트 1> oxm 네임스페이스 사용을 위한 선언

oxm 네임스페이스를 사용해 여러 객체-XM 매핑 기술을 빈으로 등록하고 설정할 수 있다.

JAXB2는 설정하려면 jaxb2-marshaller 요소를 사용한다.

<oxm:jaxb2-marshaller id="marshaller" contextPath= "kr.co.maso.spring.sample.oxm.schema" />

XmlBeans 설정에는 xmlbeans-marshaller 요소가 사용된다.

<oxm:xmlbeans-marshaller id="marshaller"/>

jibx-marshaller 요소는 JiBX 설정에 사용된다.

<oxm:jibx-marshaller id="marshaller" target-class= "kr.co.maso.spring.sample.oxm.schema.MarshallMe"/>

각 요소의 속성은 레퍼런스를 참고하길 바란다.

JSR 303: 빈(Bean) 검증
J2EE 6의 표준으로 제정된 JSR-303은 객체 인스턴스에 저장된 값이 사전에 정한 제약 조건에 부합하는지 확인하는 검증 모델이다. JSR-303은 선언적으로 제약 조건을 지정하는데, 기본으로 자바 5의 어노테이션을 이용한다. 비절차적 방식을 사용하기에 애플리케이션 개발자가 직접 검증 코드를 작성해서 사용하지 않고 프레임워크가 자동으로 검증 작업을 수행하기 좋다는 장점이 있고 어노테이션으로 객체의 필드에 부가 정보를 표시할 수 있어 애플리케이션의 논리를 쉽게 파악하는 데 도움이 된다.

스프링은 내부적으로 MVC에서 양식에 사용자가 입력한 값(또는 원격에서 REST 방식으로 전달되어 온 값)이 미리 정한 제한 조건에 부합하는지 확인하는 데 사용한다. <mvc:annotation-driven/> 설정을 사용하는 것만으로 자동으로 클래스패스에서 JSR-303 구현체를 찾아 사용한다.

@Controller 
public class SomeController {
 @RequestMapping("/action", method=RequestMethod.POST) 
 public void doAction(@Valid SomeForm form) {
 }
}

하지만, 애플리케이션 개발자가 원하면 어떤 계층에서도 JSR-303을 이용한 객체 검증 기능을 사용할 수 있다. 스프링 빈 설정은 다음 한 줄로 충분하다.

<bean id="validator" class="org.springframework.validation. beanvalidation.LocalValidatorFactoryBean" />

이제 검증하고자 하는 객체에 어노테이션을 사용해 선언적으로 제약 조건을 추가하자. 예시로 간단히 살펴볼 객체는 우편번호 객체이다. 우편번호는 두 부분으로 나뉘고 각각 숫자 세 자리로 되어 있다.

public class ZipCode {
 @NotNull 
 @Pattern(regexp="[0-9]{3}")
 private String first;

 @NotNull
 @Pattern(regexp="[0-9]{3}")
 private String second;
 ...
}

ZipCode에 담긴 값이 제약 조건에 부합하는지 검증하려면 먼저 스프링 빈에 등록한 검증기(validator)와 관계를 맺어야 한다.

import javax.validation.Validator;
...
@Autowired
private Validator validator;

주입 받은 검증기를 사용하는 방법은 간단하다.

Set<Constraintviolation<ZipCode>> violations = validator.validate(zipcode);

참고로 스프링 모듈 프로젝트의 일부로 진행되던 스프링 검증 프레임워크는 스프링이 JSR-303을 지원하게 되어 의미를 잃고 개발이 진행되지 않을 예정이다.

표준 제약 조건
JSR 303에 기본으로 정의된 표준 제약 조건은 <표 1>과 같다.

<표 1> JSR 300에 기본 정의된 표준 제약 조건

맞춤 제약 조건
애플리케이션 개발자가 해당 문제 영역(Problem Domain)에 맞는 제약 조건을 JSR-303 표준에 추가하고 싶다면 @Constraint 어노테이션을 확장해서 원하는 제약 조건을 정의하고 이를 처리할 전용 맞춤 검증기를 만들어 등록한다.

@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = ResidentRegistrationNumberValidator.class)
public @interface ResidentRegistrationNumber{
 String message() default "{kr.co.maso.spring.validation.constraints.rrn}";
 Class<?>[] groups() default {};
 Class<? extends Payload>[] payload() default {};
}

전용 맞춤 검증기는 javax.validation.ConstraintValidator 인터페이스를 구현해야 하는데 스프링은 이 검증기도 일반 스프링 빈처럼 @Autowired 같은 어노테이션으로 다른 스프링 빈과의 관계를 주입 받도록 처리한다.

import javax.validation.ConstraintValidator;
public class ResidentRegistrationNumberValidator implements ConstraintValidator {
 @Autowired 
 private SomeBean someBean;
 ...
 public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
  ...
 }
}

비동기 수행(TaskExecutor) 추상화
스프링 2.5의 TaskExecutor는 자바 1.4와 자바 5 간의 비동기 수행 기능 차이를 추상화하고 자바 1.4에서도 자바 5 수준의 비동기 수행 기능을 사용하도록 만들어졌으나 스프링 3가 완전히 자바 5 기반으로 옮겨 오면서는 기존 코드와의 하위 호환성을 유지하고 자바 5와 자바 6의 비동기 수행을 추상화하는 용도로 성격이 바뀌었다. 스프링의 추상화 계층을 사용하면 이렇게 외부 API(자바 API 포함)가 변하더라도 애플리케이션에는 영향을 거의 미치는 않는다. 언뜻 보면 스프링 비동기 수행 추상화 계층의 역할이 약해진 것 같지만 오히려 자바 1.4의 제한에서 풀려 본격적으로 자바 5 이상의 개선된 동시성 기능을 지원하게 되었다. 무엇보다 자바 5의 어노테이션과 추가된 XML 네임스페이스를 사용해 쉽게 비동기 수행 설정을 할 수 있게 되었다는 점이 반가운 부분이다.

task 네임스페이스를 사용한 TaskExecutor 등록
다음 한 줄만으로 여러 TaskExecutor 구현체 중 운영 환경에서 가장 범용적인 ThreadPoolTaskExecutor가 빈으로 등록된다. 스레드 풀의 크기는 pool-size 속성에 지정한 값으로 설정된다.

<task:executor id="executor" pool-size="20"/>

pool-size 값을 범위로 지정하면 최소값과 최대값을 지정할 수 있다.

<task:executor id="executor" pool-size="0-30"/>

이 외에도 큐 크기 관리에 쓰는 keep-alive, queue-capacity 등의 속성이 있으니 레퍼런스를 참고하길 바란다. pool-size 속성을 범위로 지정하면서 최소값을 0 이상 지정하기 원한다면 queue-capacity 속성을 지정해야 한다.

이렇게 등록한 TaskExecutor는 어떤 빈에도 의존 관계 주입을 통해 얻어 사용할 수 있다. 다음과 같이 TaskExecutor의 execute() 메소드에 비동기로 실행할 작업을 전달하면 execute() 메소드는 지정한 작업을 실행시키기만 하고 즉각 원래 위치로 작업을 끝내며 프로그램은 원래 절차 흐름에 따라 진행된다. 실행된 비동기 작업(여기서는 MyAsyncTask)은 별도의 스레드에서 동시에 실행된다. execute() 메소드로 전달한 객체는 Runnable 인터페이스를 구현해야 한다.

@Autowired
private TaskExecutor taskExecutor;
...
    taskExecutor.execute(new MyAsyncTask());
...

@Async 어노테이션
지금까지 TaskExecutor를 통해 비동기적으로 어떤 작업을 수행하려면 조금 전의 예에서처럼 Runnable을 구현한 객체를 만들어서 TaskExecutor의 execute() 메소드에 전달하는 코드를 직접 프로그래밍해야 했다. 스프링 3에서는 @Async라는 어노테이션을 사용해 간단히 특정 메소드를 비동기로 수행할 수 있다.예를 들어 MyService의 doLongTask()라는 메소드를 비동기로 수행하고 싶다면 이 메소드에 @Async 어노테이션을 단다.

public class MyService {
...
 @Async
 public void doAsyncTask(String str) {
  ...
 }
}

이렇게 @Async 어노테이션을 단 메소드는 그냥 호출하기만 해도 비동기로 실행된다.

@Autowired
MyService myService;
...
 myService.doAsyncTask("This is parameter");
...

이렇게 @Async 어노테이션이 작동하려면 먼저 애플리케이션 컨텍스트 설정에서 task 네임스페이스의 annotation-driven을 요소를 사용해 설정해야 한다. executor 속성에 지정할 빈의 ID는 <task:executor ...>로 등록한 TaskExecutor 빈의 ID이다.

<task:annotation-driven executor="executor"/>

비동기 처리 결과 반환

스프링 3가 온전히 자바 5 기반으로 옮겨가면서 스프링 Task Executor도 자바 5의 Future를 사용해 비동기 처리 결과를 얻을 수 있게 되었다. 값을 반환할 비동기 수행 메소드는  Future 객체를 반환하도록 선언하기만 하면 된다.

@Async
Future<Boolean> doAsyncTaskAndReturnResult() {
 boolean result = false; 
 ...
 return new AsyncResult<Boolean>(result);
}

AsyncResult는 Future를 구현한 비동기 수행 메소드용 반환 값 포장 객체이다. 수행 결과를 반환하려는 비동기 수행 메소드는 결과를 이 객체로 포장해서 반환해야 한다.

일정 작업 추상화
스프링 3에 처음 소개되는 TaskScheduler 인터페이스는 여러 스케줄러를 추상화해서 일관된 방식으로 일정 작업을 처리하도록 만들어졌다. 운영환경에 따라 CommonJ 같은 JSR-236 구현체를 외부에서 공급 받아 사용하기도 하고 애플리케이션 자체 스케줄러를 운영하기도 할 텐데 TaskScheduler를 사용하면 환경이 바뀌더라도 애플리케이션 코드에는 전혀 영향이 없다. TaskScheduler로 실행할 작업은 Runnable 인터페이스를 구현해야 한다.

@Autowired
private TaskScheduler scheduler;
...
 scheduler.scheduleAtFixedRate(new MyScheduledTask(), 60 * 1000); // 1분마다 실행
...

task 네임스페이스를 사용한 일정 작업 설정
XML 네임스페이스 task의 scheduler 요소를 사용하면 간단하게 스케줄러를 등록할 수 있다.

<task:scheduler id="scheduler" pool-size="10"/>

scheduler는 ThreadPoolTaskScheduler를 사용해 스케줄러를 빈으로 등록한다. 설정 옵션은 poo-size 속성 하나뿐이다. 참고로 ThreadPoolTaskScheduler는 TaskExecutor도 구현하고 있기 때문에 이 빈 하나만 등록해서 비동기 수행(TaskExe cutor)과 일정 작업(TaskScheduler) 두 가지 용도로 사용할 수 있다.

등록한 스케줄러를 사용하려면 DI로 주입받아 코드에서 스케줄 작업을 지정하는 방법도 있지만 애플리케이션 컨텍스트 설정에서 XML 네임스페이스를 사용해 특정 빈의 특정 메소드를 일정한 주기로 실행하도록 지정할 수도 있다.

<task:scheduled-tasks scheduler="scheduler">
 <task:scheduled ref="fooService" method= "doScheduledTask" fixed-rate="5000"/>
 <task:scheduled ref="barService" method= "anotherMethod" cron="* 15 */3 * * MON-FRI"/>
</task:scheduled-tasks>

네임스페이스 task의 scheduled-tasks 요소에 scheduled 요소를 필요한 만큼 사용해 일정 작업을 등록할 수 있다. scheduled-tasks의 sscheduler 속성에는 따로 등록한 스케줄러 빈의 ID를 지정한다. 일정에 따라 호출될 메소드 정보는 scheduled의 ref와 method 속성을 사용해 지정한다. method에는 메소드 이름을, ref에는 그 메소드가 있는 빈의 ID를 써 넣는다. 일정을 정의하는 속성에는 fixed-delay, fixed-rate, cron이 있다. fixed-delay는 매번 수행이 끝난 후 지정한 시간이 경과한 후에 수행되도록 할 때, fixed-rate는 특정 주기마다 일정하게 수행하도록 일정을 잡을 때, cron 속성은 unix의 cron 표현식을 사용해 정밀하게 일정을 잡을 때 각각 사용한다. 

cron 표현식은 6개의 세부 표현식으로 구성되고 각 표현식은 공백으로 구분 짓는다. 각 표현식은 앞에서부터 초, 분, 시, 날짜, 달, 요일에 해당한다. 년도는 생략 가능하다.

@Scheduled 어노테이션
네임스페이스를 사용하면 아주 쉽게 작업 수행 일정을 지정할 수 있지만 자바 5의 어노테이션을 사용해 @Async와 마찬가지로 빈의 메소드에 직접 메타데이터를 지정해 일정에 따라 호출되도록 할 수 있다. 예를 들어 메소드를 한번 실행한 후 5초 후에 계속 반복해서 실행하도록 지정하는 방법은 다음과 같다.

@Scheduled(fixedDelay=5000)
public void doScheduledTask() { 
 ... 
}

XML 네임스페이스를 사용한 설정 방법과 마찬가지로 일정한 주기로 실행되도록 일정을 잡을 수 있다. 이 때 사용하는 속성은 fixedRate다. 그리고 강력한 cron식도 사용 가능하다.

@Scheduled(fixedRate=5000) // 매 5초마다 실행
@Scheduled(cron="* 15,30 0-6 * * MON-FRI") // 근무일 새벽 매시 15분과 30분에 실행

@Scheduled를 사용하려면 먼저 활성화해야 하는데, @Async를 활성화하는 데 사용했던 task 네임스페이스의 annotation-driven 요소가 @Scheduled를 활성화하는 역할도 한다. annotation-driven 요소에 scheduler 속성을 사용해서 등록한 스케줄러 ID를 지정하기만 하면 된다.

<task:annotation-driven scheduler="scheduler"/>

물론 비동기 실행기와 스케줄러를 동시에 설정할 수도 있다.

<task:annotation-driven executor="executor" scheduler= "scheduler />

결론
지금까지 스프링 3에 새로 추가되거나 개선된 몇 가지 기능을 살펴봤다. OXM 덕에 객체와 XML 사이의 변환 작업을 아주 간단하게 처리할 수 있게 되었고 객체-XML 매핑 도구의 종류와 설정에 관계없이 일관된 API를 사용할 수 있게 되었다. JSR-303은 아주 단순하게 객체의 상태를 검증하는 메커니즘을 제공하며 스프링 내부적으로도 쓰고 있지만 원한다면 언제나 검증기를 주입받아 사용할 수 있다. 비동기 실행은 스프링 3가 자바 5로 옮겨오면서 더욱 강력하고 쉬워졌다. 복잡한 자바 API를 몰라도 누구나 비동기 수행의 장점을 누릴 수 있다. 새로 추가된 TaskScheduler도 운영환경에 영향을 받지 않고 한두 가지 설정만으로 일정 작업을 처리하도록 도와준다.

스프링은 그동안 일관된 모습을 유지하면서도 계속해서 사용하기 쉽고 빨리 작업하기 위한 여러 방편을 제공하면서 성장해왔다. 스프링 2에서는 XML 스키마로 애플리케이션 구성을 극적으로 단순하게 했고 2.5에서는 어노테이션을 소개해 한걸음 더 진보했다. 스프링 3는 그동안 여러 스프링 하부 프로젝트에서 개발된 컴포넌트를 스프링 코어로 흡수하고 XML 스키마와 어노테이션 관련 기능을 한층 향상시켜 개발자가 개발 도구가 아닌 애플리케이션 자체에 집중하도록 한다. 스프링 3 덕에 개발 작업이 한층 쉬워졌고 개발자는 그만큼 행복해졌다.


출처 - http://www.imaso.co.kr/?doc=bbs/gnuboard.php&bo_table=article&keywords=%C0%D0%C0%BB%B0%C5%B8%AE%3B%C6%AF%C1%FD&page=4&wr_id=36004



Posted by linuxism
,