Study/Spring

[Spring 5 Recipes] Spring 5 Recipes 2장 정리 #8

꼽냥이 2021. 9. 28. 20:07
반응형

wkdPOJO 구성 방식

20. AspectJ 애스펙트 로드 타임 위빙

스프링 AOP 프레임워크는 제한된 타입의 AspectJ 포인트컷만 지원하며 IoC 컨테이너에 선언한 빈에 한해 애스펙트를 적용할 수 있다. 따라서 포인트컷 타입을 추가하거나, IoC 컨테이너 외부 객체에 애스펙트를 적용하려면 스프링 애플리케이션에서 AspectJ 프레임워크를 끌어써야 한다.

 

위빙 (Weaving) 은 애스펙트를 대상 객체에 적용하는 과정이다. 스프링 AOP 는 런타임에 동적 프록시를 활용해 위빙을 하는 반면 AspectJ 프레임워크는 컴파일 타임 위빙, 로드 타임 위빙을 모두 지원한다. 컴파일 타임 위빙은 애스펙트를 자바 소스 파일에 엮고, 위빙된 바이너리 클래스 파일을 결과물로 내놓는다. 또한 이미 컴파일된 클래스 파일이나 JAR 파일에도 애스펙트를 넣을 수 있는데 이를 포스트 컴파일 타임 위빙이라 한다. 자세한 내용은 AspectJ 문서에서 확인할 수 있다.

 

AspectJ 의 로드타임 위빙 (LTW) 은 JVM 이 클래스 로더를 이용해 대상 클래스를 로드하는 시점에 일어난다. 바이트 코드에 코드를 넣어 클래스를 위빙하려면 특수한 클래스 로더가 필요한데, AspectJ 와 스프링 모두 클래스 로더에 위빙 기능을 부여한 로드 타임 위버를 제공한다. 로드 타임 위버는 간단한 설정으로 바로 사용이 가능하다.

 

스프링 애플리케이션에서의 로드 타임 위빙 처리를 보기 위해 다음의 간단한 캐싱 구조를 살펴보자.

@Aspect
public class ObjectCashingAspect {
	
    private final Map<String, Object> cache = new ConcurrentHashMap<>();
    
    @Around("call(public Object.new(String)) && args(str)")
    public Object cacheAround(ProceedingJointPoint joinPoint, String str) 
    	throws Throwable {
        Object someObject = cache.get(str);
        
        if (someObject == null) {
        	System.out.println("Cache MISS for (" + str + ")");
            someObject = (Object) joinPoint.proceed();
            cache.put(str, someObject);
        } else {
        	System.out.println("Cache HIT for (" + str + ")");
        }
        return someObject;
    }
}

애스펙트 코드를 보면 객체 생성자 호출 시 사용한 인자를 이용해 객체를 맵에 캐시한다. AspectJ 포인트컷 표현식의 call() 안에 생성자를 호출하는 코드를 넣고, 캐시에 저장된 객체가 존재한다면 그 객체를 반환하고 그렇지 않을 경우 생성자를 통해 객체를 새로 생성하도록 한다. 반환값을 변경하기 위해 Around 어드바이스를 사용했다.

 

Call 포인트컷은 스프링 AOP 에서 지원하지 않기 때문에 스프링이 이 어노테이션을 스캐닝할 때 지원하지 않는 포인트컷 호출을 의미하는 에러가 발생한다. 이처럼 스프링 AOP 에서 지원하지 않는 포인트컷을 쓴 애스펙트를 적용하려면 AspectJ 프레임워크를 직접 사용해야 한다. AspectJ 프레임워크는 클래스패스 루트의 META-INF 디렉토리에 있는 aop.xml 파일에 다음과 같이 구성한다.

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN"
  "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">

<aspectj>
  <weaver>
    <include within="com.apress.springrecipes.calculator.*"/>
  </weaver>

  <aspects>
    <aspect
      name="com.apress.springrecipes.calculator.ComplexCachingAspect"/>
  </aspects>
</aspectj>

 

 

21. 스프링에서 AspectJ 애스펙트 구성하기

모든 AspectJ 애스펙트에는 Aspects 라는 팩토리 클래스가 있고, 이 클래스의 정적 팩토리 메소드 aspectOf() 를 호출하면 현재 애스펙트 인스턴스를 액세스할 수 있다. IoC 컨테이너에서는 Aspects.aspectOf(SomeObject.class) 를 호출해 빈을 선언한다.

 

위에서 작성한 바와 같이 캐시 맵을 미리 구성한 뒤, @Bean 메소드를 만들어 애스펙트를 구성하고 앞서 언급한 팩토리 메소드 Aspects.aspectOf() 메소드를 호출해 애스펙트 인스턴스를 가져온다. 이제 이 애스펙트 인스턴스를 구성하는 구성 클래스를 작성한다.

@Configuration
@ComponentScan
public class ObjectConfiguration {
	
    @Bean
    public ObjectCachingAspect objectCachingAspect() {
    	
        ObjectCachingAspect objectCachingAspect = 
        	Aspects.aspectOf(ObjectCachingAspect.class);
        return objectCachingAspect;
    }
}

 

22. AOP 를 이용해 POJO 를 도메인 객체에 주입하기

IoC 컨테이너 외부에서 생성된 객체는 대부분 도메인 객체로, new 연산자 또는 DB 쿼리의 결과로 생긴다. 이렇게 스프링 밖에서 만든 도메인 객체 안에 스프링 빈을 주입하려면 AOP 의 도움이 필요하다. 도메인 객체는 스프링이 만든 객체가 아니어서 스프링 AOP 로는 주입할 수 없다. 따라서 이런 용도에 알맞게 스프링이 제공하는 AspectJ 애스펙트를 AspectJ 프레임워크에서 가져다 쓰면 된다.

 

복소수 객체의 형식을 전역 포매터로 맞추려 한다. 이 포매터는 형식 패턴을 인수로 받고, 표준 어노테이션 @Component, @Value 로 POJO 를 인스턴스화한다.

@Component
public class ComplexFormatter {
	
    @Value("(a + bi)")
    private String pattern;
    
    public void setPattern(String pattern) {
    	this.pattern = pattern;
    }
    
    public String format(Complex complex) {
    	return pattern.replaceAll("a", Integer.toString(complex.getReal()))
        	.replaceAll("b", Integer.toString(complex.getImaginary()));
    }
}

ComplexFormatter 는 Complex 클래스의 toString() 메소드에서 복소수를 문자열로 바꿀 때 사용한다. ComplexFormatter 를 전달받아야 하므로 세터 메소드를 추가한다.

public class Complex {
	
    private int real;
    private int imaginary;
    
    private ComplexFormatter formatter;
    
    public void setFormatter(ComplexFormatter formatter) {
    	this.formatter = formatter;
    }
    
    public String toString() {
    	return formatter.format(this);
    }
}

하지만 여기서 Complex 객체는 IoC 컨테이너가 생성한 인스턴스가 아니므로 평범한 기법으로 의존체 주입을 할 수 없다. 스프링 애스펙트 라이브러리에 포함된 AnnotationBeanConfigurerAspect 를 이용하면 IoC 컨테이너가 생성하지 않은 객체에도 의존체를 주입할 수 있다.

@Configurable
@Component
@Scope("prototype")
public class Complex {
	
    @Autowired
    public void setFormatter(ComplexFormatter formatter) {
    	this.formatter = formatter;
    }
}

위와 같이 @Configurable 아래에 표준 어노테이션인 @Component, @Scope, @Autowired 를 나열하면 표준 스프링 빈처럼 사용할 수 있지만 여기서 핵심은 @Configurable 이기 때문에 스프링은 @EnableSpringConfigured 라는 편의성 어노테이션을 지원한다.

 

@Configurable 을 붙인 클래스를 인스턴스화하면 애스펙트는 이 클래스와 동일한 타입의 프로토타입 스코프 빈을 찾는다. 그 다음, 빈 정의부 내용에 따라 새 인스턴스를 구성한다. 빈 정의부에 프로퍼티가 선언돼 있으면 새 인스턴스도 애스펙트가 설정한 것과 동일한 프로퍼티를 갖게 된다.

 

24. 스프링 TaskExecutor 로 동시성 적용하기

스프링 TaskExecutor 는 기본 자바의 Executor, CommonJ 의 WorkManager 등 다양한 구현체를 제공하며 필요할 경우 커스텀 구현체를 만들어 쓸 수도 있다. 스프링은 이들 구현체 모두를 자바 Executor 인터페이스로 단일화했다.

 

동시성은 서버 측 컴포넌트의 중요 요건이지만 Java EE 세계에서는 딱히 표준이라 할 만한 것이 없다. 스레드를 명시적으로 생성하거나 조작하는 행위를 금지하는 내용이 일부 Java EE 명세에 포함되어 있을 뿐이다.

 

자바의 Executor API 는 매우 간단하다.

package java.util.concurrent;

public interface Executor {
    void execute(Runnable command);
}

스레드 관리 기능이 강화된 ExecutorService 이하 인터페이스는 shutdown() 처럼 스레드에 이벤트를 일으키는 메소드를 제공한다. ExecutorService 클래스에는 Future<T> 형 객체를 반환하는 submit() 메소드가 있다. Future<T> 인스턴스는 대개 비동기 실행 스레드의 진행 상황을 추적하는 용도로 쓰인다. Futuer.isDone(), Future.isCancelled() 메소드는 각각 어떤 작업이 완료되었는 지, 취소되었는 지를 확인한다. run() 메소드가 반환형이 없는 Runnable 인스턴스 내부에서 ExecutorService 와 submit() 을 사용할 경우, 반환된 Future 의 get() 메소드를 호출하면 null 또는 전송 시 지정한 값이 반환된다. 다음의 예제에서는 Boolean.TRUE 값이 반환된다.

Runnable task = new Runnable() {
	public void run() {
    	try {
        	Thread.sleep(1000 * 60);
            System.out.println("Done sleeping for a minute, returning! ");
        } catch (Exception e) { ... }
    }
};

ExecutorService executorService = Executors.newCachedThreadPool();

if (executorService.submit(task, Boolean.TRUE).get().equals(Boolean.TRUE)) {
	System.out.println("Job has finished!");
}

 

다음은 Runnable 을 이용해 시간 경과를 나타낸 클래스이다.

public class SampleRunnable implements Runnable {
	
    @Override
    public void run() {
    	try {
        	Thread.sleep(1000);
        } catch (Exception e) {
        	e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
        System.out.println("Hello at %s\n", new Date());
    }
}

위 인스턴스를 자바 Executors 와 스프링 TaskExecutor 에서 사용해보자.

public static void main(String [] args) throws Throwable {
	Runnable task = new SampleRunnable();
    
    ExecutorService cachedThreadPoolExecutorService = 
    	Executors.newCachedThreadPool();
    if (cachedThreadPoolExecutorService.submit(task).get() == null) {
    	System.out.printf(
        	"The cachedThreadPoolExecutorService has succeded at %s\n", 
            new Date());
    }
    
    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(100);
    if (fixedThreadPool.submit(task).get() == null) {
    	System.out.printf(
        	"fixedThreadPool has succeded at %s\n", 
            new Date());
    }
    
    ExecutorService singleThreadExecutorService =
    	Executors.newSingleThreadExecutor();
    if (singleThreadExecutorService.submit(task).get() == null) {
    	System.out.printf(
        	"singleThreadExecutorService has succeded at %s\n", 
            new Date());
    }
    
    ExecutorService es = Executors.newCachedThreadPool();
    if (es.submit(task, Boolean.TRUE).get.equals(Boolean.TRUE)) {
    	System.out.println("Job has finished!");
    }
    
    ScheduledExecutorService scheduledThreadExecutorService =
    	Executors.newScheduledThreadPool(10);
    if (scheduledThreadExecutorService.schedule(task, 30, TimeUnit.SECONDS).get == null) {
		System.out.printf(
        	"scheduledThreadExecutorService has succeded at %s\n", 
            new Date());
    }
    
    scheduledThreadExecutorService.scheduleAtFixedRate(task, 0, 5, TimeUnit.SECONDS);
}

ExecutorService 하위 인터페이스에서 Callable<T> 를 인수로 받는 submit() 메소드를 호출하면 Callable 의 call() 메소드가 반환한 값을 그대로 반환한다.

package java.util.concurrent;

public interface Callable<V> {
    V call() throws Exception;
}

 

Java EE 가 갖는 문제는, Java SE 와는 달리 관리되는 환경에서 컴포넌트에 동시성을 부여하고 스레드를 제어할 수 있는, 간단하면서 이식성이 좋은 표준 방법이 없다는 점이다. 이에 스프링은 자바 5의 Executor 를 상속한 TaskExecutor 인터페이스라는 통합 솔루션을 제공한다.

package org.springframework.core.task;

public interface TaskExecutor extends Executor {
    void execute(Runnable task);
}

이를 앞서 정의한 SampleRunnable 을 TaskExecutor 에 넣어 응용한 예제를 살펴보자. 클라이언트에 해당하는 다음 코드는 단순 POJO 로서 여러 TaskExecutor 인스턴스를 자동으로 연결한다. 이들의 임무는 오로지 Runnable 을 전송하는 일이다.

@Component
public class SpringExecutorDemo {
	
    @Autowired
    private SimpleAsyncTaskExecutor asyncTaskExecutor;
    
    @Autowired
    private SyncTaskExecutor syncTaskExecutor;
    
    @Autowired
    private TaskExecutorAdapter taskExecutorAdapter;
    
    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    
    @Autowired
    private SampleRunnable task;
    
    @PostConstruct
    public void submitJobs() {
	    syncTaskExecutor.execute(task);
    	asyncTaskExecutor.submit(task);
        taskExecutorAdapter.submit(task);
        
        for (int i = 0; i < 500; i++) {
        	threadPoolExecutor.submit(task);
        }
    }
    
    public static void main(String [] args) {
    	new AnnotationConfigApplicationContext(ExecutorsConfiguration.class)
        	.registerShutdownHook();
    }
}

 

다음 어플리케이션 컨텍스트를 보면 다양한 TaskExecutor 구현체를 생성하는 방법을 알 수 있다. 대부분 직접 수동으로 만들어도 될 정도로 단순하며, 딱 한 경우에만 팩토리 빈에 위임해 실행을 자동으로 트리거한다.

@Configuration
@ComponentScan
public class ExecutorsConfiguration {

    @Bean
    public TaskExecutorAdapter taskExecutorAdapter() {
        return new TaskExecutorAdapter(Executors.newCachedThreadPool());
    }

    @Bean
    public SimpleAsyncTaskExecutor simpleAsyncTaskExecutor() {
        return new SimpleAsyncTaskExecutor();
    }

    @Bean
    public SyncTaskExecutor syncTaskExecutor() {
        return new SyncTaskExecutor();
    }

    @Bean
    public ScheduledExecutorFactoryBean scheduledExecutorFactoryBean(
    	ScheduledExecutorTask scheduledExecutorTask) {
        ScheduledExecutorFactoryBean scheduledExecutorFactoryBean = new ScheduledExecutorFactoryBean();
        scheduledExecutorFactoryBean.setScheduledExecutorTasks(scheduledExecutorTask);
        return scheduledExecutorFactoryBean;
    }

    @Bean
    public ScheduledExecutorTask scheduledExecutorTask(Runnable runnable) {

        ScheduledExecutorTask scheduledExecutorTask = new ScheduledExecutorTask();
        scheduledExecutorTask.setPeriod(1000);
        scheduledExecutorTask.setRunnable(runnable);
        return scheduledExecutorTask;
    }

    @Bean
    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(50);
        taskExecutor.setMaxPoolSize(100);
        taskExecutor.setAllowCoreThreadTimeOut(true);
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        return taskExecutor;
    }
}

첫 번째 빈 TaskExecutorAdapter 인스턴스는 Executors 인스턴스를 감싼 단순 래퍼라서 스프링 TaskExecutor 인터페이스와 같은 방식으로 다룰 수 있다. 예제에서는 스프링을 이용해 Executor 인스턴스를 구성하고 TaskExecutorAdapter 의 생성자 인자로 전달한다.

 

SimpleAsyncTaskExecutor 는 전송한 작업마다 Thread 를 새로 만들어 제공하며 스레드를 풀링하거나 재사용하지 않는다. 전송한 각 작업은 스레드에서 비동기로 실행된다.

 

SyncTaskExecutor 는 가장 단순한 TaskExecutor 구현체로, 동기적으로 Thread 를 띄워 작업을 실행한 다음, join() 메소드로 바로 연결한다. 사실상 스레딩은 완전히 건너뛰고 호출 스레드에서 run() 메소드를 수동으로 실행한 것이나 다를 것이 없다.

 

ScheduledExecutorFactoryBean 은 ScheduledExecutorTask 빈으로 정의된 작업을 자동 트리거한다. ScheduledExecutorTask 인스턴스 목록을 지정해 여러 작업을 동시에 실행할 수도 있다. ScheduledExecutorTask 인스턴스에는 작업 실행 간 공백 시간을 인자로 넣을 수 있다.

 

마지막 ThreadPoolTaskExecutor 는 java.util.concurrent.ThreadPoolExecutor 를 기반으로 모든 기능이 완비된 스레드 풀 구현체이다.

 

TaskExecutor 지원 기능은 애플리케이션 서버에서 하나로 통합된 인터페이스를 사용해 서비스를 스케줄링하는 강력한 수단이다. 모든 애플리케이션 서버에 배포 가능한 확실한 솔루션이 필요할 경우 스프링 쿼츠의 사용을 고려할 수 있다.

 

24. POJO 끼리 애플리케이션 이벤트 주고받기

스프링 애플리케이션 컨텍스트는 빈 간 이벤트 기반 통신을 지원한다. 이벤트 기반 통신 모델에서는 실제로 수신기가 여럿 존재할 가능성이 있기 때문에 송신기는 누가 수신할 지 모른 채 이벤트를 발행한다. 수신기 역시 누가 이벤트를 발행했는 지 알 필요가 없고, 여러 송신기가 발행한 이벤트를 리스닝할 수 있다. 이런 방식으로 수신기와 송신기를 느슨하게 엮을 수 있다.

 

이벤트 기반 통신을 위해서는 제일 먼저 이벤트 자체를 정의할 필요가 있다. 다음 예제에서, Room 을 체크아웃하면 Reception 빈이 체크아웃 시간이 기록된 CheckoutEvent 를 발행한다고 하자.

public class CheckoutEvent extends ApplicationEvent {
	
    private final Room room;
    private final Date time;
    
    public CheckoutEvent(Room room, Date time) {
    	this.room = room;
        this.time = time;
    }
    
    public Room getRoom() {
    	return room;
    }
    
    public Date getTime() {
    	return time;
    }
}

 

이벤트를 인스턴스화한 뒤 다음 애플리케이션 이벤트 퍼블리셔에서 publishEvent() 메소드를 호출하면 이벤트가 발행된다. 이벤트 퍼블리셔는 다음과 같이 ApplicationEventPublisherAware 인터페이스 구현 클래스에서 가져오면 된다.

public class Reception implements ApplicationEventPublisherAware {
	
    private ApplicationEventPublisher applicationEventPublisher;
    
    @Override
    public void setApplicationEventPublisher(
            ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    public void checkout(Room room) throws IOException {
        CheckoutEvent event = new CheckoutEvent(room, new Date());
        applicationEventPublisher.publishEvent(event);
    }
}

 

ApplicationListener 인터페이스를 구현한 애플리케이션 컨텍스트에 정의된 빈은 타입 매개변수에 매치되는 이벤트를 모두 알림받는다. (이런 방식으로 ApplicationContextEvent 같은 특정 그룹 이벤트들을 리스닝한다.)

@Component
public class CheckoutListener implements ApplicationListener<CheckoutEvent> {

    @Override
    public void onApplicationEvent(CheckoutEvent event) {
        // 체크아웃 시 수행할 로직을 여기에 구현합니다.
        System.out.println("Checkout event [" + event.getTime() + "]");
    }
}

스프링 4.2 부터는 ApplicatoinListener 인터페이스 없이 @EventListener 를 붙여도 이벤트 리스너로 만들 수 있다.

@Component
public class CheckoutListener {

    @EventListener
    public void onApplicationEvent(CheckoutEvent event) {
        // 체크아웃 시 수행할 로직을 여기에 구현합니다.
        System.out.println("Checkout event [" + event.getTime() + "]");
    }
}

 

이제 어플리케이션 컨텍스트에 전체 이벤트를 리스닝할 리스너를 등록하면 된다. 등록 절차는 이 리스너의 빈 인스턴스를 선언하거나 컴포넌트 스캐닝으로 감지하면 된다. 애플리케이션 컨텍스트는 ApplicationListener 인터페이스를 구현한 빈과 @EventListener 를 붙인 메소드가 위치한 빈을 인지해 이들이 관심있는 이벤트를 각각 통지한다.

 

#Reference.

 

스프링 5 레시피(4판)

이 책은 스프링 5에 새로 탑재된 기능 및 다양한 구성 옵션 등 업데이트된 프레임워크 전반을 실무에 유용한 해법을 제시하는 형식으로 다룹니다. IoC 컨테이너 같은 스프링 기초부터 스프링 AOP/A

www.hanbit.co.kr