반응형

스프링 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 구성 방식

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

 

+ Recent posts