반응형

스프링 AOP (Aspect-Oriented Programming)

AOP 란, Aspect-Oriented Programming 의 약자로 관점 지향 프로그래밍이라 불린다. 관점 지향은 쉽게 표현하자면 어떤 로직을 핵심적인 관점, 부가적인 관점으로 나누어 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. 여기서 모듈화란, 공통한 로직이나 기능을 하나로 묶는 것을 뜻한다.

 

AOP 에서 관점을 기준으로 로직을 모듈화한다는 것은 코드들을 부분적으로 나누어 모듈화하겠다는 뜻이다. 이 때, 소스 코드상 다른 부분에서 반복해서 사용되는 코드들을 발견할 수 있는데 이것을 Crosscutting Concerns (흩어진 관심사) 라고 부른다.

 

Crosscutting Concerns 의 모듈화

위와 같이 흩어진 관심사들을 Aspect 로 모듈화하고 핵심적인 비즈니스 로직에서 분리해 재사용하는 것이 AOP 의 취지이다.

 

OOP 와 AOP 개념 비교

더보기
  • OOP : 비즈니스 로직의 모듈화
    • 모듈화의 핵심 단위는 비즈니스 로직
  • AOP : 인프라 혹은 부가기능의 모듈화
    • 각 모듈들의 주요 목적 외 필요한 부가적인 기능들
    • 대표적 예 : 로깅, 트랜잭션, 보안 등

 

AOP 주요 개념

  • Aspect : 위에서 설명한 흩어진 관심사를 모듈화한 것
  • Target : Aspect 를 적용한 곳 (클래스, 메소드, ...)
  • Advice : 실질적으로 어떤 동작을 해야할 지에 대한 것
  • JoinPoint : 어드바이스가 적용될 위치
  • PointCut : 부가기능이 적용될 대상을 선정하는 방법의 표현
  • Proxy : 타겟을 감싸서 타겟의 요청을 대신 받아주는 래핑 오브젝트
  • Introduction : 타겟 클래스에 코드 변경없이 신규 메소드나 멤버 변수를 추가하는 기능
  • Weaving : 지정된 객체에 Aspect 를 적용해 새로운 프록시 객체를 생성하는 과정
반응형

POJO 구성 방식

11. 스프링 환경 및 프로파일마다 다른 POJO 로드

자바 구성 클래스를 여러 개 생성하고 각 클래스마다 POJO 인스턴스 / 빈을 묶는다. 이렇게 작성한 구성 클래스에 @Profile 을 붙임으로써 프로파일 별로 다른 빈 인스턴스 구성을 할 수 있다.

 

POJO 의 초깃값은 애플리케이션의 제작 단계에 따라 달라질 수 있다. 예를 들어 개발 단계나 운영 단계에서 사용하고자 하는 DB 의 경로가 달라질 수 있고, 이러한 설정들을 달리 적용해야 할 빈들이 존재할 수 있다. 이런 경우에 @Profile 을 사용해 개발자가 시나리오에 맞는 구성 클래스를 읽어 들여 실행하게 할 수 있다.

 

간단한 예시로, 자바 구성 클래스에 @Profile 을 붙여 여러 버전으로 나누는 코드를 작성한다.

@Configuration
@Profile("develop")
public class ConfigurationDevelop {
	
    @Bean
    public Product product() {
    	// ...
    }
}
@Configuration
@Profile("deploy")
public class ConfigurationDeploy {
	
    @Bean
    public Product product() {
    	// ...
    }
}

 

위와 같이 구성 클래스를 @Profile 을 나눠 작성한다. @Profile 의 속성으로 위의 예제에서는 하나의 속성값만을 정의했지만, 두 개 이상의 속성값을 정의하는 것도 가능하고 이 경우는 {} 로 값들을 묶어 정의할 수 있다. 이렇게 작성한 프로파일들을 활성화하기 위해서 어플리케이션 콘텍스트 환경을 가져온 뒤 다음과 같이 setActiveProfiles() 메소드를 호출한다.

AnnotationConfigApplicationContext context = ...;
context.getEnvironment().setActiveProfiles("develop");

혹은 자바 런타임 플래그로 프로파일을 명시하는 것도 가능한데, 'develop' 프로파일을 활성화하려면 다음 플래그를 추가하는 것도 가능하다.

-Dspring.profiles.active=develop

 

어떤 프로파일도 로드되지 않는 불상사를 막기 위해 기본 프로파일을 지정하는 것이 가능하다. 어플리케이션 콘텍스트 환경의 메소드를 통해 이를 지정하려면 setDefaultProfiles() 메소드를 사용할 수 있고, 플래그로 지정하고자 할 경우는 spring.profiles.active 대신 spring.profiles.default 로 사용할 수 있다.

 

12. POJO 에게 IoC 컨테이너 리소스 알려주기

빈이 IoC 컨테이너 리소스를 인지하게 하려면 Aware 인터페이스를 구현한다. 스프링은 이 인터페이스를 구현한 빈을 감지해 대상 리소스를 세터 메소스로 주입한다. 자주 쓰이는 스프링 Aware 인터페이스는 다음과 같다.

Aware 인터페이스 대상 리소스 타입
BeanNameAware IoC 컨테이너에 구성한 인스턴스의 빈 이름
BeanFactoryAware 컨테이너 서비스를 호출하는 데 사용
ApplicationContextAware 컨테이너 서비스를 호출하는 데 사용
MessageSourceAware 텍스트 메시지 해석하는 데 사용
ApplicationEvent
PublisherAware
애플리케이션 이벤트를 발행하는 데 사용
ResourceLoaderAware 외부 리소스를 로드하는 데 사용
EnvironmentAware ApplicationContext 인스턴스에 묶인 org.springframework.core.env.Environment 인스턴스

Aware 인터페이스의 세터 메소드는 스프링이 빈 프로퍼티를 설정한 후, 초기화 콜백 메소드를 호출하기 이전에 호출한다. 순서를 정리하면 다음과 같다.

  1. 생성자나 팩토리 메소드를 호출해 빈 인스턴스 생성
  2. 빈 프로퍼티에 값, 빈 레퍼런스 설정
  3. Aware 인터페이스에 정의한 세터 메소드 호출
  4. 빈 인스턴스를 빈 후처리기의 postProcessBeforeInitialization() 메소드로 넘겨 초기화 콜백 메소드 호출
  5. 빈 인스턴스를 빈 후처리기의 postProcessAfterInitialization() 메소드로 넘김
  6. 컨테이너가 종료되면 폐기 콜백 메소드 호출
NOTE
스프링 최신 버전에서는 @Autowired 를 붙여 ApplicationContext 를 손쉽게 가져올 수 있어 굳이 Aware 인터페이스를 구현할 필요가 없다. 프레임워크나 라이브러리 개발 시에는 Aware 인터페이스를 구현하는 게 나을 수 있지만, 스프링에 종속된 인터페이스를 정말 구현할 필요가 있는지 따져보는 것이 중요하다.

 

13. 어노테이션을 활용한 AOP 적용

AOP (Aspect-Oriented Programming ; 관점 지향 프로그래밍).

AOP 에 대한 개념은 따로 포스팅하도록 하겠다. 우선 스프링에서 어노테이션을 활용해 어떻게 AOP 를 적용하는 지를 먼저 정리하려 한다.

 

자바 클래스에 @Aspect 를 붙이고 메소드 별로 적절한 어노테이션을 붙여 Advice 로 만들어 Aspect 를 정의할 수 있다. 적용 가능한 Advice 어노테이션은 @Before, @After, @AfterReturning, @AfterThrowing, @Around 가 있다. IoC 컨테이너에서 Aspect 어노테이션 기능을 활성화하기 위해서는 구성 클래스 중 하나에 @EnableAspectJAutoProxy 를 붙인다.

 

Aspect 는 여러 타입과 객체에 공통 관심사(예 ; 로깅, 트랜잭션 관리) 를 모듈화한 자바 클래스로, @Aspect 를 붙여 표시한다. AOP 에서 말하는 Aspect 란 어디에서 (PointCut), 무엇을 할 것인지 (Advice) 를 합쳐 놓은 개념이다. 어드바이스는 @Advice 를 붙인 단순 자바 메소드이고, 포인트컷은 어드바이스에 적용할 타입 / 객체를 찾는 표현식이다.

 

@Before 어드바이스

Before 어드바이스는 특정 프로그램 실행 시점 이전의 공통 관심사를 처리하는 메소드이다.

@Aspect
@Component
public class CalculatorLoggingAspect {
	
    private Logger log = LoggerFactory.getLogger(this.getClass());
    
    @Before("execution(* Calculator.add(..))")
    public void logBefore() {
        log.info("The method add() begins");
    }
}

위와 같이 @Before 어노테이션의 값으로 포인트컷 표현식을 적용할 수 있다. 이 포인트컷 표현식은 Calculator 인터페이스의 add() 메소드 실행을 가리킨 것이다. 앞부분의 와일드카드(*) 는 모든 수정자(public, protected, private), 모든 반환형을 매치함을 의미한다. 인수 목록 부분에 쓴 두 점(..) 은 인수 개수는 몇 개든 좋다는 의미이다.

 

NOTE
@Aspect 만 붙여서는 스프링이 자동으로 감지하지 않기 때문에, 해당 POJO 마다 개별적으로 @Component 를 붙여야 한다.

 

포인트컷으로 매치한 실행 지점을 JoinPoint 라고 한다. 포인트컷은 여러 조인포인트를 매치하기 위해 지정한 표현식이고 이렇게 매치된 조인포인트에서 해야 할 일이 바로 어드바이스이다.

 

어드바이스가 현재 조인포인트의 세부 내용에 접근하려면 JoinPoint 형 인수를 어드바이스 메소드에 선언해야 한다. 그러면 메소드명, 인수값 등 자세한 조인포인트 정보를 조회할 수 있다. 다음과 같이 클래스명, 메소드명에 와일드카드를 적용하면 모든 메소드에 예외없이 포인트컷을 적용할 수 있다.

@Aspect
@Component
public class CalculatorLoggingAspect {
	
    private Logger log = LoggerFactory.getLogger(this.getClass());
    
    @Before("execution(* *.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        log.info("The method {}() begins with {} ", 
            joinPoint.getSignature().getName(), 
            Arrays.toString(joinPoint.getArgs()));
    }
}

 

@After 어드바이스

After 어드바이스는 조인포인트가 끝나면 실행되는 메소드이다. 조인포인트가 정상 실행되든, 도중 예외가 발생하든 상관없이 실행된다.

@Aspect
@Component
public class CalculatorLoggingAspect {
	
    private Logger log = LoggerFactory.getLogger(this.getClass());
    
    @After("execution(* *.*(..))")
    public void logAfter(JoinPoint joinPoint) {
        log.info("The method {}() ends", joinPoint.getSignature().getName());
    }
}

 

@AfterReturning 어드바이스

After 어드바이스는 조인포인트의 실행 성공 여부와 관계없이 동작한다. 조인포인트가 값을 반환할 경우에만 메소드를 실행하고자 한다면, 다음과 같이 AfterReturning 어드바이스로 대체할 수 있다.

@Aspect
@Component
public class CalculatorLoggingAspect {
	
    private Logger log = LoggerFactory.getLogger(this.getClass());
    
    @AfterReturning(
    	pointcut = "execution(* *.*(..))",
    	returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        log.info("The method {}() ends with {}", joinPoint.getSignature().getName(), result);
    }
}

조인포인트가 반환한 결과값을 받아오려면 @AfterReturning 의 returning 속성으로 지정한 변수명을 어드바이스 메소드의 인자로 지정한다. 스프링 AOP 는 런타임에 조인포인트의 반환값을 이 인수에 넣어 전달한다. 이 때 포인트컷 표현식은 pointcut 속성으로 따로 지정해야 한다.

 

@AfterThrowing 어드바이스

After Throwing 어드바이스는 조인포인트 실행중 예외가 발생할 경우에만 실행된다. 작동 원리는 @AfterReturning 과 같고, 발생한 예외는 @AfterThrowing 의 throwing 속성에 담아 전달할 수 있다. 자바 언어에서 Throwable 은 모든 에러/예외 클래스의 상위 타입이므로 다음과 같이 어드바이스를 적용하면 조인포인트에서 발생한 에러/예외를 모두 가져올 수 있다.

@Aspect
@Component
public class CalculatorLoggingAspect {
	
    private Logger log = LoggerFactory.getLogger(this.getClass());
    
    @AfterThrwoing(
    	pointcut = "execution(* *.*(..))",
    	throwing = "result")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
        log.error("An exception {} has been thrown in {}()", e, joinPoint.getSignature().getName());
    }
}

 

@Around 어드바이스

Around 어드바이스는 가장 강력한 어드바이스이다. 이 어드바이스는 조인포인트를 완전히 장악하기 때문에 앞서 살펴본 어드바이스를 모두 Around 어드바이스로 조합할 수 있다. 심지어 원본 조인포인트를 언제 실행할 지, 실행 자체를 할지 말지, 계속 실행할 지 여부도 제어할 수 있다.

 

다음은 Before, After Returning, After Throwing 어드바이스를 Around 어드바이스로 조합한 예이다. Around 어드바이스의 조인포인트 인자형은 ProceddingJoinPoint 로 고정돼 있고, JoinPoint 하위 인터페이스인 ProceedingJoinPoint 를 이용하면 원본 조인포인트를 언제 진행할 지 그 시점을 제어할 수 있다.

@Aspect
@Component
public class CalculatorLoggingAspect {
	
    private Logger log = LoggerFactory.getLogger(this.getClass());
    
    @Around("execution(* *.*(..))")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    	log.info("The method {}() begins with {}",
            joinPoint.getSignature().getName(),
            Arrays.toString(joinPoint.getArgs()));
            
        try {
            Object result = joinPoint.proceed();
            log.info("The method {}() ends with {}", joinPoint.getSignature.getName(), result);
            return result;
        } catch (IllegalArgumentException e) {
            log.error("Illegal Argument {} in {}()", Arrays.toString(joinPoint.getArgs()),
                joinPoint.getSignature().getName());
            throw e;
        }
    }
}

 

#Reference.

 

스프링 5 레시피(4판)

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

www.hanbit.co.kr

 

반응형

POJO 구성 방식

8. 어노테이션을 이용한 POJO 초기화 / 폐기

어떤 POJO 는 사용하기 전 특정한 초기화 작업, 사용한 후 폐기 작업이 필요하다. 예를 들어 파일을 열거나, 네트워크 / DB 에 접속하거나, 메모리를 할당하는 등 선행 작업이 필요한 경우이다. IoC 컨테이너에서 빈을 초기화하거나 폐기하는 로직을 커스터마이징할 수 있다.

 

자바 구성 클래스의 @Bean 정의 시 initMethod, destroyMethod 속성을 설정함으로써 스프링이 각각을 초기화 / 폐기 콜백 메소드로 인지한다. 혹은 POJO 클래스의 메소드에 @PostConstruct / @PreDestroy 를 붙이는 방법이 있다. 또 @Lazy 를 붙임으로써 Lazy Initialization (주어진 시점까지 빈 생성을 미뤄둠) 을 할 수도 있고, @DependsOn 으로 빈을 생성하기 전에 다른 빈을 먼저 생성하도록 강제할 수 있다.

 

다음과 같이 작업을 수행하고 난 기록을 파일로 기록하는 POJO 클래스를 정의한다.

public class Cashier {
	
    private String fileName;
    private String path;
    private BufferedWriter writer;
    
    // Setter
    
    public void openFile() throws IOException {
    	File targetDir = new File(path);
        if (!targetDir.exists()) {
        	targetDir.mkdir();
        }
        
        File checkoutFile = new File(path, fileName + ".txt");
        if (!checkoutFile.exists()) {
        	checkoutFile.createNewFile();
        }
        
        writer = new BufferedWirter(new OutputStreamWriter(
        	new FileOutputStream(checkoutFile, true)));
    }
    
    // Writer Method
    
    public void closeFile() throws IOException {
    	writer.close();
    }
}

openFile() 메소드는 데이터를 저장할 대상 디렉토리와 파일이 존재하는 지 확인한 뒤, 주어진 시스템 경로에 있는 텍스트 파일을 열어 writer 필드에 할당한다. 기록을 모두 마친 뒤 closeFile() 메소드를 통해 파일을 닫고 시스템 리소스를 반납한다. (기록을 하기 위한 메소드는 생략되었다.)

 

Cashier 빈 생성 이전에 openFile() 메소드를, 폐기 직전에 closeFile() 메소드를 각각 실행하도록 자바 구성 클래스에 빈 정의부를 다음과 같이 설정한다.

@Configuration
public class ShopConfiguration {
	
    @Bean(initMethod = "openFile", destroyMethod = "closeFile")
    public Cashier cashier() {
    	Cashier c1 = new Cashier();
        
        // Call Setter ...
        
        return c1;
    }
}

이로써 Cashier 인스턴스를 생성하기 전에 openFile() 메소드를 먼저 트리거한다. 그리고 빈을 폐기할 때 closeFile() 메소드를 자동 실행하게 된다. 이러한 동작은 다음과 같이 POJO 클래스를 작성해도 똑같이 동작한다.

@Component
public class Cashier {
	
    // Fields and Methods ...
    
    @PostConstruct
    public void openFile() throws IOException {
    	// ... Body
    }
    
    @PreDestroy
    public void closeFile() {
    	// ... Body
    }
}

 

기본적으로 스프링은 모든 POJO 를 미리 초기화한다. 하지만 환경에 따라 빈을 처음으로 요청하기 전까지 초기화 과정을 미루는 게 나을 때도 있다. 이렇듯 나중에 초기화하는 방식을 Lazy Initialization 이라 한다.

 

느긋한 초기화는 시동 시점에 리소스를 집중 소모하지 않아도 되므로 전체 시스템 리소스를 절약할 수 있다. 특히 무거운 작업을 처리하는 POJO 는 느긋한 초기화가 더 어울리는 경우가 있다. 사용 방법은 다음과 같이 POJO 클래스에 @Lazy 를 붙임으로써 느긋한 초기화를 적용할 수 있다. @Lazy 덕분에 다음 클래스는 애플리케이션이 요구하거나 다른 POJO 가 참조하기 전까지는 초기화되지 않는다.

@Component
@Scope("prototype"
@Lazy
public class Product {
	
    // ... Class Body
}

 

POJO 가 늘어나면 그에 따라 초기화 횟수도 증가하고, 여러 자바 구성 클래스에 분산 선언된 많은 POJO 가 서로를 참조하게 되면 Race Condition 이 발생하기 쉽다. B, F 라는 빈의 로직이 C 라는 빈에서 필요하다고 할 때, 아직 스프링이 B, F 빈을 초기화하지 않았는데 C 빈이 먼저 초기화되면 원인불명의 에러가 발생하게 된다.

 

@DependsOn 어노테이션은 어떤 POJO 가 다른 POJO 보다 먼저 초기화되도록 강제하며 설사 그 과정에서 에러가 발생한다 하더라도 헤아리기 쉬운 메시지를 보여준다. 또 @DependsOn 은 빈을 초기화하는 순서를 보장한다.

 

@Configuration 
public class SequenceConfiguration {
	
    @Bean
    @DependsOn("prefixGenerator")
    public SequenceGenerator sequenceGenerator() {
    	SequenceGenerator generator = new SequenceGenerator();
        return generator;
    }
}

 

@DependsOn("prefixGenerator") 를 적용했기 때문에 PrefixGenerator 빈은 항상 SequenceGenerator 보다 먼저 생성된다. @DependsOn 의 속성값은 {} 로 감싼 리스트 형태로 여러 개의 의존성을 설정할 수도 있다.

 

9. PostProcessor 를 통한 POJO 검증 / 수정

빈 후처리기를 이용하면 초기화 콜백 메소드 전후에 원하는 로직을 빈에 적용할 수 있다. 빈 후처리기의 주요한 특징은 IoC 컨테이너 내부의 모든 빈 인스턴스를 대상으로 한다는 점이다. 보통 빈 후처리기는 빈 프로퍼티가 올바른지 체크하거나 어떤 기준에 따라 빈 프로퍼티를 변경 또는 전체 빈 인스턴스를 상대로 어떤 작업을 수행하는 용도로 사용된다.

 

@Required 는 스프링에 내장된 후처리기 RequiredAnnotationBeanPostProcessor 가 지원하는 어노테이션이다. 이 후처리기는 @Required 를 붙인 모든 빈 프로퍼티가 설정되었는지 확인한다.

 

빈 후처리기는 BeanPostProcessor 인터페이스를 구현한 객체이다. 이 인터페이스를 구현한 빈을 발견하면 스프링은 자신이 관장하는 모든 빈 인스턴스에 아래의 두 메소드를 적용한다. 따라서 빈 상태를 조사, 수정, 확인하는 등의 어떠한 로직도 이 메소드에 넣을 수 있다. 이 메소드들은 하는 동작이 없어도 반드시 원본 빈 인스턴스를 반환해야 한다.

@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
	
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) 
    	throws BeansException {
    
    	return bean;
    }
    
    @Override
    publi Object postProcessAfterInitialization(Object bean, String beanName) 
    	throws BeansException {
    
    	return bean;
    }
}

 

IoC 컨테이너는 자신이 생성한 빈 인스턴스를 모두 하나씩 빈 후처리기에 넘긴다. 만일 특정 타입의 빈만 후처리기를 적용하려면 인스턴스 타입을 체크하는 필터를 이용해 원하는 빈에만 후처리 로직을 적용할 수 있다. 예를 들어 Product 형 빈 인스턴스에만 빈 후처리기를 적용하는 예제는 다음과 같이 작성할 수 있다.

@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
	
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) 
    	throws BeansException {
    	if (bean instanceof Product) {
        	// ... Some Code
        }
    	return bean;
    }
    
    @Override
    publi Object postProcessAfterInitialization(Object bean, String beanName) 
    	throws BeansException {
        if (bean instanceof Product) {
        	// ... Some Code
        }
    	return bean;
    }
}

위의 두 메소드는 처리한 빈 인스턴스를 반드시 반환해야 한다. 이를 바꿔 말하면, 원본 빈 인스턴스를 다른 인스턴스로 바꿔치기할 수도 있다는 뜻이다.

 

10. 정적 메소드, 인스턴스 메소드, 스프링 FactoryBean 으로 POJO 생성

자바 구성 클래스의 @Bean 메소드는 정적 팩토리를 호출하거나 인스턴스 팩토리 메소드를 호출해 POJO 를 생성할 수 있다. 다음 ProductCreator 클래스에서 정적 팩토리 메소드 createProduct 는 productId 에 해당하는 상품 객체를 생성한다. 주어진 productId 에 따라 인스턴스화할 실제 상품 클래스를 내부 로직으로 결정한다. 맞는 케이스가 없으면 IllegalArgumentException 예외를 던진다.

public class ProductCreator {
	
    public static Product createProduct(String productId) {
    	if (productId.equals("aaa")) {
        	return new Product("AAA", 2.5);
        } else if (productId.equals("cdrw")) {
        	return new Product("CD-RW", 1.5);
        }
        throw new IllegalArgumentException("Unknown Product ID");
    }
}

 

자바 구성 클래스의 @Bean 메소드에서는 일반 자바 구문으로 정적 팩토리 메소드를 호출해 POJO 를 생성한다.

@Configuration
public class ShopConfiguration {
	
    @Bean
    public Product aaa() {
    	return ProductCreator.createProduct("aaa");
    }
    
    @Bean
    public Product cdrw() {
    	return ProductCreator.createProduct("cdrw");
    }
}

 

다음과 같이 클래스 내에 맵을 구성해 상품 정보를 담아두는 방법도 있다. 인스턴스 팩토리 메소드 createProduct() 는 productId 에 해당하는 상품을 맵에서 찾아 반환한다. 마찬가지로 맞는 케이스가 없으면 예외를 던진다.

public class ProductCreator {
	
    private Map<String, Product> products;
    
    public void setProducts(Map<String, Product> products) {
    	this.products = products;
    }
    
    public Product createProduct(String productId) {
    	Product product = products.get(productId);
        if (product != null) {
        	return product;
        }
        throw new IllegalArgumentException("Unknown Product ID");
    }
}

 

ProductCreator 에서 상품을 생성하려면 먼저 @Bean 을 선언해 팩토리값을 인스턴스화하고 이 팩토리의 퍼사드 역할을 하는 두 번째 빈을 선언한다. 마지막으로 팩토리를 호출하고 createProduct() 메소드를 호출해 다른 빈들을 인스턴스화한다.

@Configuration
public class ShopConfiguration {
	
    @Bean
    public ProductCreator productCreatorFactory() {
    	ProductCreator factory = new ProductCreator();
        Map<String, Product> products = new HashMap<>();
        products.put("aaa", new Product("AAA", 2.5));
        products.put("cdrw", new Product("CD-RW", 1.5));
        factory.setProducts(products);
        return factory;
    }
    
    // ... Bean Methods
}

 

다음과 같이 팩토리 빈을 직접 작성하는 방법도 가능하다. 할인율과 상품이 주어지면 할인가가 적용된 상품을 생성하는 팩토리 빈을 작성해 보자. 이 빈은 product, discount 두 프로퍼티를 받아 주어진 상품에 할인가를 계산해 적용하고 상품 빈을 새로 만들어 반환한다.

public class DiscountFactoryBean extends AbstractFactoryBean<Product> {
	
    private Product product;
    private double discount;
    
    public void setProduct(Product product) {
    	this.product = product;
    }
    
    public void setDiscount(double discount) {
    	this.discount = discount;
    }
    
    @Override
    public Class<?> getObjectType() {
    	return product.getClass();
    }
    
    @Override
    protected Product createInstance() throws Exception {
    	product.setPrice(product.getPrice() * (1 - discount));
        return product;
    }
}

팩토리 빈은 제네릭 클래스인 AbstractFactoryBean<T> 를 상속하고, createInstance() 메소드를 오버라이드해 대상 빈 인스턴스를 생성한다. 또 자동 연결 기능이 작동하도록 getObjectType() 메소드로 대상 빈 타입을 반환한다.

 

이제 상품 인스턴스를 생성하는 팩토리 빈에 @Bean 을 붙여 DiscountFactoryBean 을 적용한다.

@Configuration
@ComponentScan("com.apress.springrecipes.shop")
public class ShopConfiguration {
	
    @Bean
    public Product aaa() {
    	Product aaa = new Product("AAA", 2.5);
        return aaa;
    }
    
    @Bean
    public DiscountFactoryBean discountFactoryBeanAAA() {
    	DiscountFactoryBean factory = new DiscountFactoryBean();
        factory.setProduct(aaa());
        factory.setDiscount(0.2);
        return factory;
    }
}

 

 

#Reference.

 

스프링 5 레시피(4판)

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

www.hanbit.co.kr

 

반응형

POJO 구성 방식

5. @Scope 를 붙여 POJO 스코프 지정

@Scope 는 빈 스코프를 지정하는 어노테이션이다. 스프링은 기본적으로 IoC 컨테이너에 선언한 빈마다 정확히 하나의 인스턴스를 생성하고 전체 스코프에서 이를 공유한다. 이것이 스프링에서 사용하는 빈의 기본 스코프인 singleton 이다. 전체 스프링 빈 스포크는 다음과 같다.

Scope Description
singleton IoC 컨테이너당 빈 인스턴스 하나를 생성
prototype 요청마다 빈 인스턴스를 새로 생성
request HTTP 요청당 하나의 빈 인스턴스 생성. (WebApplicationContext 만 해당)
session HTTP 세션당 빈 인스턴스 생성. (WebApplicationContext 만 해당)
globalSession 전역 HTTP 세션당 빈 인스턴스 생성. (PortalApplicationContext 만 해당)

 

다음과 같이 동일한 빈에 대해 getBean 을 각각 요청할 경우, 특별히 스코프를 명시하지 않으면 singleton 으로 인스턴스를 생성한다. 고로 (1) 과 (2) 는 모두 "prod2" 를 화면에 출력하게 된다.

Product prod1 = context.getBean("product", Product.class);
Product prod2 = context.getBean("product", Product.class);

prod1.setName("prod1");
prod2.setName("prod2");

// (1)
System.out.println(prod1.getName());
// (2)
System.out.println(prod2.getName());

 

하지만 클래스에 @Scope 를 붙여 해당 빈의 스코프를 명시하게 되면, 해당 클래스의 인스턴스는 스코프에 따라 인스턴스 생성 규칙을 달리 하게 된다. 다음과 같이 클래스에 prototype 스코프임을 명시하게 되면, 위의 예제에서 (1) 은 "prod1" 을, (2) 는 "prod2" 를 출력하게 된다.

@Component
@Scope("prototype")
public class Product { ... }

 

6. 외부 리소스 데이터 사용

스프링이 제공하는 @PropertySource 를 통해 *.properties 파일을 읽어들일 수 있다. 또한 스프링 Resource 인터페이스에 @Value 를 적용하면 어떤 파일이라도 읽어들일 수 있게 된다.

 

예를 들어, 다음과 같이 정의된 option.properties 파일이 존재한다고 하자.

special.discount=0.1
summer.discount=0.15
endofyear.discount=0.2

위와 같은 파일이 존재할 때, 아래와 같이 구성 클래스에 @PropertySource 를 붙여 사용하고자 하는 properties 파일을 명시하면 endOfYearDiscount 라는 필드에 자동으로 파일 내에 정의된 값이 입력되게 된다. @Value 에서 ${key:defaultValue} 와 같이 주입받고자 하는 값을 지정할 수 있으며, defaultValue 는 만약 값을 찾지 못할 경우에 기본으로 갖게 되는 값을 의미한다.

@Configuration
@PropertySource("classpath:option.properties")
public class ProductConfiguration {

    @Value("${endofyear.discount:0}")
    private double enfOfYearDiscount;
}

 

properties 파일 외에 다른 파일 데이터를 사용하고자 할 경우에는 Resource 인터페이스를 사용할 수 있다.

public class BannerLoader {
	
    private Resource banner;
    
    public void setBanner(Resource banner) {
    	this.banner = banner;
    }
    
    @PostConstruct
    public void showBanner() throws IOException {
    	Files.lines(Paths.get(banner.getURI()), Charset.forName("UTF-8"))
        	.ForEachOrdered(System.out::println);
    }
}

위와 같은 BannerLoader 클래스가 존재한다고 하자. showBanner() 메소드는 등록된 banner 를 읽어 파일의 내용을 차례대로 읽어 한 줄씩 출력하는 기능을 수행한다. 이 메소드에 @PostConstruct 를 붙임으로써, 인스턴스가 생성된 뒤 showBanner() 메소드를 호출하게 된다. 아래와 같이 구성 클래스를 작성하면 스프링은 어플리케이션 실행 시 BannerLoader 의 빈 인스턴스를 생성하므로 어플리케이션 실행 시 자동으로 이 메소드를 호출한다.

@Configuration
@PropertySource("classpath:option.properties")
public class ProductConfiguration {
	
    @Value("classpath:banner.txt")
    private Resource banner;
    
    @Bean
    public BannerLoader bannerLoader() {
    	BannerLoader b1 = new BannerLoader();
        b1.setBanner(banner);
        return b1;
    }
}

@Value("classpath:banner.txt") 를 통해 스프링은 banner.txt 파일을 찾고 이를 banner 필드에 주입하고 Resource 빈 객체로 변환할 수 있게 된다.

 

7. 프로퍼티 파일에서 로케일마다 다른 다국어 메시지 해석

MessageSource 인터페이스에는 리소스 번들 메시지를 처리하는 메소드가 정의되어 있다. ResourceBundleMessageSource 는 MessageSource 구현체로, 로케일별로 분릴된 리소스 번들 메시지를 해석할 수 있다.

 

리소스 번들 메시지를 구분 처리하려면 다음과 같이 ReloadableResourceBundleMessageSource 인스턴스를 자바 구성 파일에 정의한다.

@Configuration
public class Configuration {
	
    @Bean
    public ReloadableResourceBundleMessageSource messageSource() {
    	ReloadableResourceBundleMessageSource messageSource =
        	new ReloadableResourceBundleMessageSource();
        messageSource.setBasenames("classpath:messages");
        return messageSource;
    }
}

빈 인스턴스는 반드시 messageSource 라고 명시해야 ApplicationContext 가 알아서 감지할 수 있다. 위의 예에서는 자바 클래스패스 내에서 이름이 messages 로 시작하는 파일들을 찾도록 설정했다. 이렇게 MessageSource 를 정의하고 미국 로케일에서 텍스트 메시지를 찾으면 messages_en_US.properties 리소스 번들 파일을 제일 먼저 찾게 된다. 이런 이름을 가진 파일이 없거나 메시지를 찾지 못한다면 언어에 맞는 messages_en.properties 를 찾고 이 파일마저 없으면 전체 로케일의 기본 파일인 messages.properties 를 선택한다.

 

다음과 같이 ApplicationContext 를 구성해 getMessage() 메소들 메시지를 해석할 수 있게 된다. 이 메소드에서 첫 번째 인자는 메시지 키, 두 번째는 각 메시지의 매개변수 자리에 끼워넣을 값들, 세 번째는 대상 로케일이다.

public static void main(String[] args) throws Exception {
	
    ApplicationContext context = 
    	new AnnotationConfigApplicationContext(Configuration.class);
        
    String alert = context.getMessage("alert.checkout", null, Locale.US);
    
    System.out.println("Message for alert.checkout is " + alert);
}

위와 같이 messages 파일을 클래스패스에 정의하고 어플리케이션 콘텍스트의 getMessage() 메소드를 사용하면 위처럼 메시지를 가져다 쓸 수 있다. 특히 로케일별 메시지를 적용함으로써 여러 지역에서 서비스를 진행할 때 각 지역 별 메시지를 자동으로 출력하도록 할 수 있을 것 같다.

 

#Reference.

 

스프링 5 레시피(4판)

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

www.hanbit.co.kr

 

반응형

POJO 구성 방식

1. 자바로 POJO 구성

@Configuration, @Bean 을 붙인 자바 클래스를 만들거나 @Component, @Repository, @Service, Controller 를 붙인 자바 컴포넌트를 구성함으로써 POJO 클래스를 설계한다. IoC 컨테이너는 어노테이션이 붙은 자바 클래스를 탐색해 애플리케이션의 일부인 것처럼 POJO 인스턴스/빈을 구성한다.

 

@Configuration
public class ConfigurationClass {
	
    @Bean
    public BeanClass beanClass() {
    	BeanClass bean = new BeanClass();
        bean.setName("Hello");
        bean.setNick("World");
        return bean;
    }
}

 

@Configuration 어노테이션이 붙은 구성 클래스에서 @Bean 메소드를 통해 빈을 생성한다. 그 후 어노테이션을 붙인 자바 클래스를 스캐닝하기 위해 IoC 컨테이너를 인스턴스화 해야 한다. 스프링에서는 기본 구현체인 BeanFactory 와 이와 호환이 가능한 고급 구현체인 ApplicationContext, 두 가지 IoC 컨테이너를 제공한다.

 

ApplicationContext context = new AnnotationConfigApplicationContext(ConfigurationClass.Class);

 

구성 클래스에 선언된 빈을 BeanFactory / ApplicationContext 에서 가져오려면 유일한 빈 이름을 getBean() 메소드의 인자로 호출한다. getBean() 메소드는 Object 타입을 반환하므로 실제 타입에 맞게 캐스팅하거나, 메소드의 인자로 빈 클래스 타입을 명시할 수 있다. 빈 이름은 기본적으로는 메소드명을 사용하고, @Bean(name = "") 과 같이 이름을 명시한 경우는 어노테이션에 명시한 이름을 사용한다. 해당 클래스의 빈이 하나뿐일 때는 빈 이름을 생략할 수도 있다.

 

BeanClass bean = (BeanClass) context.getBean("beanClass");

// 클래스를 명시함으로써 캐스팅을 하지 않을 수 있음
// bean = context.getBean("beanClass", BeanClass.class);

// 빈 클래스가 하나뿐일 때는 빈 이름을 명시하지 않아도 됨
// bean = context.getBean(BeanClass.class);

 

스프링에는 Persistence, Service, Presentation 이렇게 세 계층이 존재하는데 이를 가리키는 어노테이션이 각각 @Repository, @Service, @Controller 이다. 이 외 범용으로 사용할 수 있는 어노테이션으로 @Component 가 있는데, 쓰임새가 명확하지 않을 때는 @Component 를 사용할 수 있지만 구체적으로 명시하는 편이 용도에 맞는 혜택을 누릴 수 있는 장점이 있다고 한다. (@Repository 는 발생한 Exception 을 DataAccessException 으로 감싸 던지는 등 디버깅 시 유리한 점이 있다.)

 

2. 생성자 호출을 통한 POJO 생성

POJO 클래스에 생성자를 하나 이상 정의한 뒤, 구성 클래스에서 IoC 컨테이너가 사용할 POJO 인스턴스값을 생성자로 설정한다. 그 뒤 IoC 컨테이너를 인스턴스화해 어노테이션을 붙인 자바 클래스를 스캐닝하도록 한다. 그리하면 POJO 인스턴스 / 빈을 애플리케이션 일부처럼 접근할 수 있게 된다.

 

// POJO 클래스 선언 (생성자 정의)
public class Product {
	
    private String name;
    private double price;
    
    public Product(String name, double price) {
    	this.name = name;
        this.price = price;
    }
}

// 구성 클래스 정의
public class ShopConfiguration {
	
    @Bean
    public Product aaa() {
    	Product p = new Product("AAA", 2.5);
        return p;
    }
}

 

3. POJO 레퍼런스와 자동 연결을 통한 상호 작용

POJO / Bean 인스턴스들 사이의 참조 관계는 위와 같이 자바 코드로도 연결을 맺어줄 수 있다. 스프링에서는 필드, Setter 메소드, 생성자, 또는 아무 메소드에 @Autowired 를 붙임으로써 POJO 레퍼런스를 자동으로 연결할 수 있다.

 

@Service
public class TemplateService {

    @Autowired
    private BeanDao beanDao;
    
    // Getter, Setter
}

 

서비스 객체를 생성하는 서비스 클래스틑 실제로 자주 쓰이는 Best Practice 로, DAO 에 대한 직접 호출을 하지 않고 일종의 관문을 두는 것이다. 서비스 객체는 내부적으로 DAO 와 연동해 요청받은 작업을 처리하게 된다.

 

위에서는 필드에 @Autowired 를 적용해 의존성을 주입했지만 다음과 같이 생성자나 메소드와도 자동으로 연결할 수 있다. 또한 컬렉션이나 배열에도 @Autowired 를 적용할 경우 매칭되는 빈을 모두 찾아 자동으로 연결해 준다.

 

public class StringGenerator {
	
    // 필드 주입
    @Autowired
    private PrefixGenerator prefixGenerator;
    
    // 배열형, 컬렉션에 Autowired 적용
    @Autowired
    private PrefixGenerator[] prefixGenerators;
    
    // 생성자 주입
    @Autowired
    public StringGenerator(PrefixGenerator prefixGenerator) {
    	this.prefixGenerator = prefixGenerator;
    }
    
    // Setter 주입
    @Autowired
    public void setPrefixGenerator(PrefixGenerator prefixGenerator) {
    	this.prefixGenerator = prefixGenerator;
    }
}

 

스프링은 기본적으로 @Autowired 를 붙인 프로퍼티에 해당하는 빈을 찾지 못하면 예외를 던진다. 선택적으로 프로퍼티를 적용하고자 할 경우에는 @Autowired(required=false) 와 같이 required 속성값을 지정함으로써 빈을 찾지 못하더라도 그냥 지나치도록 할 수 있다.

 

@Autowired 는 타입을 기준으로 자동으로 빈을 연결하는데, 컨테이너에 호환되는 타입이 여러개 존재할 경우 제대로 연결되지 않을 수 있다. 이러한 문제를 해결하기 위해 @Primary, @Qualifier 를 적용할 수 있다.

 

// @Primary 를 활용
@Component
@Primary
public class PrefixGenerator1 implements PrefixGenerator {
	
    // Methods..
}

// @Qualifier 활용
public class StringGenerator {
	
    @Autowired
    @Qualifier("prefixGenerator1")
    private PrefixGenerator prefixGenerator;
}

 

애플리케이션의 규모가 커질수록 POJO 설정들을 하나의 구성 클래스에 담기 어려워지는데, 보통 POJO 의 기능에 따라 여러 구성 클래스로 나누어 관리한다. 그런데 구성 클래스가 여러 개 공존할 경우 상이한 클래스에 정의된 POJO 를 자동으로 연결하거나 참조하는 것이 쉽지 않다.

 

한 가지 방법은, 구성 클래스가 위치한 경로마다 애플리케이션 컨텍스트를 초기화하는 방법이다. 각 자바 구성 클래스에 선언된 POJO 를 컨텍스트와 레퍼런스로 읽으면 POJO 간 자동으로 연결이 가능하다.

 

ApplicationContext context = 
	new AnnotationConfigApplicationContext(Configuration1.class, Configuration2.class);

 

혹은 @Import 로 구성 파일을 나누는 방법도 존재한다.

 

@Configuration
public class Configuration1 {
	
    @Bean
    private PrefixGenerator prefixGenerator() {
    	// ...
    }
}

@Configuration
@Import(Configuration1.class)
public class Configuration2 {

    @Value("#{prefixGenerator}")
    private PrefixGenerator prefixGenerator;
    
    @Bean
    private StringGenerator stringGenerator() {
    	// ...
    }
}

 

StringGenerator 빈을 설정하기 위해 prefixGenerator 빈을 설정해야 하는데, 이 구성 클래스 내에 존재하지 않고 다른 자바 구성 클래스에 정의되어 있는 경우에 @Import 를 사용한다. @Import 를 붙이면 해당 클래스에 정의한 POJO 를 모두 현재 구성 클래스의 스코프로 가져올 수 있는데, 그런 뒤 위와 같이 @Value 와 SpEL (Spring Expression Language) 를 사용해 외부 구성 클래스에 선언된 빈을 필드에 주입할 수 있다.

 

4. @Resource 와 @Inject 를 사용한 POJO 자동 연결

@Resource 와 @Inject 는 자바 표준(JSR) 에 근거한 해법으로, @Autowired 와 거의 유사하게 사용이 가능하다. 

 

@Resource 는 우선적으로 @Autowired 와 같이 타입을 기준으로 POJO 를 찾아 자동으로 연결한다. 하지만 타입이 같은 POJO 가 여럿일 때 @Autowired 는 가리키는 대상이 모호해져 @Qualifier 를 추가적으로 사용해야 하지만, @Resource 는 기능상 @Qualifier 와 @Autowired 를 합한 것과 같아 자체적으로 대상을 명확하게 지정할 수 있다.

 

@Inspect 는 @Autowired, @Resource 와 같이 타입으로 우선 POJO 를 찾지만 타입이 같은 POJO 가 여럿일 경우에는 다음과 같이 커스텀 어노테이션을 작성해 적용해야 한다.

 

@Qualifier
@Target({ElementType.TYPE, ...})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAnnotation {
}

 

이 커스텀 어노테이션에 적용된 @Qualifier 는 스프링에서 사용되는 @Qualifier 가 아닌, javax.inject 패키지에 속한 어노테이션이다. 커스텀 어노테이션을 위와 같이 작성한 뒤, 빈 인스턴스를 생성하는 POJO 클래스에 다음과 같이 어노테이션을 붙인 뒤, @Inject 어노테이션과 같이 커스텀 어노테이션을 사용하면 더이상 모호해지는 상황이 발생하지 않는다.

 

@CustomAnnotation
public class PojoClass {
}

public class ServiceClass {

    @Inject @CustomAnnotation
    private PojoClass pojoClass;
}

 

@Autowired, @Resource, @Inject 중 어떤 것을 사용하더라도 결과는 같다. 차이점은 이름을 기준으로 할 경우에 구문이 가장 단순한 @Resource 가 낫고, 타입을 기준으로 할 경우 셋 중 어느 것을 골라도 간편하게 사용이 가능하다.

 

* 개인적인 생각을 덧붙이자면, 어떤 어노테이션을 쓰느냐는 개인의 마음이라고 볼 수 있겠지만 프로젝트에서 사용함에 있어 하나로 통일해서 사용해야 할 필요는 있을 것 같다. 기능적으로 여러 개를 혼용해 써도 문제가 될 것 같지는 않지만, 추후에 코드를 봤을 경우에 어노테이션이 뒤죽박죽 사용되어 있다면 불필요한 혼란을 가져올 수 있을 것 같다.

 

#Reference.

 

스프링 5 레시피(4판)

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

www.hanbit.co.kr

 

반응형

Spring Framework 를 사용해 많지는 않은 프로젝트를 진행해 봤는데, 정작 Spring 자체에 대한 공부는 뒷전으로 한 것 같다는 생각이 문득 들었다. 내부적으로 어떻게 동작하는 지 알고 싶어 공부를 하고자 했는데 어떻게 공부를 시작해야 할 지 잘 모르겠어서 일단 책을 구입했다.

 

공부하기 위한 책으로 스프링 5 레시피 라는 책을 구입했는데, 그 내용에 대해서 앞으로 정리하면서 공부를 진행해 보려 한다.

 

Spring Core

Spring IoC / POJO

IoC (Inversion of Control ; 제어의 역전) 는 스프링 프레임워크의 심장부라고 할 수 있는데, IoC 컨테이너는 POJO 를 구성하고 관리하는 역할을 수행한다. IoC 는 DI (Dependency Injection ; 의존성 주입) 이라고도 불리는데 객체 간의 의존성이 발생하는 것을 IoC 컨테이너가 관리하는 것이라 이해했다.

 

일반적으로 객체의 생성과 실행은 다음의 순서를 따른다.

  1. 객체 생성
  2. 의존성 객체 생성
    클래스 내부에서 생성할 객체의 생성자 호출 등을 통해 직접 생성
  3. 의존성 객체의 메소드 호출

하지만 스프링에서는 객체의 생성을 IoC 컨테이너 (Spring 의 BeanFactory, ApplicationContext) 에서 수행하고 생성된 객체를 주입하는 다음과 같은 순서를 따른다.

  1. 객체 생성
  2. 의존성 객체 주입
    스스로 생성하지 않고, 스프링에서 생성한 객체를 주입받음
  3. 의존성 객체의 메소드 호출

이러한 방식으로 객체의 의존성을 주입받는 것을 제어의 역전이라 표현하며, 이를 통해 객체 간의 결합도를 줄이고 유연성을 높인 코드를 작성할 수 있게 해 가독성을 높이고, 코드 중복을 줄이고, 유지보수를 편하게 할 수 있다 한다.

 

특히, 스프링이 모든 의존성 객체를 실행 시 모두 생성하고 필요한 곳에 주입하는 이와 같은 방식은 각각의 Bean 들이 싱글턴 패턴의 특징을 가지도록 하며 제어의 흐름을 개발자가 갖지 않고 스프링이 맡아 작업을 처리하게 된다.

 

# Reference.

 

스프링 5 레시피(4판)

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

m.hanbit.co.kr

 

Core Technologies

In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do

docs.spring.io

 

[Spring] DI, IoC 정리

DI(Dependency Injection)란 스프링이 다른 프레임워크와 차별화되어 제공하는 의존 관계 주입 기능으로,객체를 직접 생성하는 게 아니라 외부에서 생성한 후 주입 시켜주는 방식이다.DI(의존성 주입)

velog.io

 

+ Recent posts