스프링 프레임워크 핵심 기술 - IoC 컨테이너

 

스프링 프레임워크

소규모 애플리케이션 또는 기업용 애플리케이션을 자바로 개발하는데 있어 유용하고 편리한 기능을 제공하는 프레임워크입니다.

스프링의 역사
  • 2003년 등장 Java EE 스펙 구현 모음체 + 알파
  • 최근까지 주로 서블릿 기반 어플리케이션 만들 때 사용해 옴.
  • 스프링 5부터는 WebFlux 지원으로 서블릿 기반이 아닌 서버 어플리케이션도 개발 가능
디자인 철학
  • 모든 선택은 개발자의 몫(스프링이 특정 영속화 기술을 강요하지 않음)
  • 다양한 관점을 지향합니다.
  • 하위 호환성을 지킵니다.
  • API를 신중하게 설계합니다.
  • 높은 수준의 코드를 지향합니다.

Ioc 컨테이너

1. 스프링 IoC 컨테이너와 빈

IoC(Inversion of Control): 의존 관계 주입(Dependency Injection)이라고도 하며, 어떤 객체가 사용하는 의존 객체를 직접 만들어 사용하는게 아니라, 주입 받아 사용하는 방법입니다.

스프링 IoC 컨테이너를 사용하는 이유?

여러 개발자들이 스프링 커뮤니티에서 논의하여 만들어낸 여러가지 DI 방법과 베스트 예제들, 노하우가 쌓여있는 프레임워크이기 때문입니다.

스프링 IoC 컨테이너
  • BeanFactory
  • 애플리케이션 컴포넌트의 중앙 저장소
  • 빈 설정 소스로 부터 빈 정의를 읽어들이고, 빈을 구성하고 제공합니다.
빈(Bean)
  • 스프링 IoC 컨테이너가 관리하는 객체
  • 의존성 관리
  • 스코프
    • 싱글톤: 하나만 생성(스프링의 IoC 컨테이너에 등록된 빈들)
    • 프로토타입: 매번 다른 객체 생성
  • 라이프사이클 인터페이스를 사용하여 부가적인 기능 사용 가능
  • 빈은 @Component, @Controller, @Service, @Repository, @Configuration …
ApplicationContext

스프링 프레임워크에서 가장 많이 사용하는 인터페이스 BeanFactory를 상속받아 만들어짐

  • 메시지 소스 처리 기능
  • 이벤트 발행 기능
  • 리소스 로딩 기능

2. ApplicationContext와 다양한 빈 설정 방법

IoC 컨테이너에 객체를 빈을 등록하는 하기위한 다음과 같은 방법들이 있습니다.

기존 스프링 프레임워크 방식의 application.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 id="bookService"
		class="me.yoon.springapplicationcontext.BookService">
		<property name="bookRepository" ref="bookRepository" />
	</bean>
	<bean id="bookRepository"
		class="me.yoon.springapplicationcontext.BookRepository">
	</bean>

    <!-- 자동 스캔 하여 빈 설정: 위의 방식처럼 일일히 등록할 필요가 없음 -->
	<context:component-scan base-package="me.yoon.springapplicationcontext" />
</beans>

xml 파일에서 객체를 등록할 빈들을 직접 정의하고 의존 관계에 있는 빈을 주입하기 위해서는 함께 로 정의합니다.

또는 컴포넌트 스캔 방식을 사용하여 해당 패키지 내의 빈을 자동으로 등록합니다.

@Component 기반 어노테이션을 읽어 등록합니다.(@Controller, @Service, @Repository…)

@Service
public class BookService {
	@Autowired
	BookRepository bookRepository;
}

@Repository
public class BookRepository {
}
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");

xml로 정의한 빈들을 실행하는 코드입니다.

java code configure 방식

xml 방식이 아닌 자바로 빈을 등록하는 방식입니다.

@Configuration
public class ApplicationConfig {
	@Bean
	public BookRepository bookRepository() {
		return new BookRepository();
	}
	
	@Bean
	public BookService bookService() {
        BookService bookService = new BookService();
        bookService.setBookRepository(bookRepository());
		return bookService;
        
        //return new BookService();  @Autowired 
	}
}

or

@Configuration
@ComponentScan(basePackageClasses = Application.class)
// 애플리케이션 클래스 이하에 있는 빈들을 컴포넌트 스캔해서 빈을 등록합니다.
public class ApplicationConfig {
}
@Service
public class BookService {
	@Autowired
	BookRepository bookRepository;
}

@Repository
public class BookRepository {
}
ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);

자바로 등록한 빈을 실행하는 방법입니다.

스프링 부트 방식
@SpringBootApplication
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

@SpringBootApplication 내에 @ComponentScan과 @Configuration이 포함되어 있어 사용자가 직접 설정할 필요없이 빈들을 등록해줍니다.

3. @Autowired

@Autowired가 붙은 의존 객체에 의존성을 주입합니다. 이 때 객체는 빈으로 등록이 되어 있어야 합니다.

@Autowired 사용할 수 있는 위치
생성자 (스프링 4.3 부터는 생략 가능)
@Autowired
public BookService(BookRepository bookRepository) {
	this.bookRepository = bookRepository;
}
세터

레포지토리에 @Repository 빈으로 등록이 안되어 있다면 BookService 인스턴스는 만들 수 있지만 Autowired가 붙어있기 때문에 오류가 납니다. (autowired 때문에 빈을 주입하려는 시도는 하기 때문)

@Autowired(required = false) // required 옵션 default true
public void setBookRepository(BookRepository bookRepository) {
	this.bookRepository = bookRepository;
}
필드
@Autowired
BookRepository bookRepository;
의존성 주입을 하는 경우의 수
  • 해당 타입의 빈이 없는 경우 - 실패
  • 해당 타입의 빈이 한 개인 경우 - 성공
  • 해당 타입의 빈이 여러 개인 경우
    • 빈 이름으로 시도
      • 같은 이름의 빈 찾으면 해당 빈 사용
      • 같은 이름 못 찾으면 실패
같은 타입의 빈이 여러개 일때 (interface 구현체 여러개)
  • @Primary를 붙여서 주입하고 싶은 객체를 설정합니다.

    @Repository @Primary
    public class FirstRepository implements BookRepository{
    }
    
  • 해당 타입의 빈 모두 주입받기 List 형태로 모두 받습니다.

    @Autowired
    List<BookRepository> bookRepositories;
    
  • @Qualifier(빈 이름으로 주입) : 스몰케이스 클래스 이름

    @Service
    public class BookService {
    	@Autowired @Qualifier("firstBookRepository")
    	BookRepository bookRepository;
    }
    
  • 이름으로 주입 받는 방법 : 추천하지는 않는 방법

    @Autowired
    BookRepository myBookRepository;
    
동작 원리

BeanPostProccessor의 구현체인 AutowiredAnnotationBeanProcessor에 의해 @Autowired을 찾아 해당하는 빈을 주입을 해줍니다. 이 과정은 스프링 라이프사이클 과정의 Initialization 전에 수행됩니다.

4. @Component와 Component Scan

컴포넌트 스캔 - 컴포넌트 클래스를 Bean으로 등록하는 과정입니다.

주요 기능 - 스캔의 위치 설정, filter 기능

@Component
  • @Repository, @Service, @Controller, @Configuration

@SpringBootApplication이 붙은 메인 Appication 클래스에서 컴포넌트 스캔이 시작됩니다. Application이 존재하는 패키지부터 이하 패키지까지 모두 스캔을 합니다. 외부의 패키지는 스캔을 하지 않습니다. 컴포넌트 스캔을 할 때 잘 되지 않는다면 스캔의 범위를 잘 살펴보아야 합니다. 모든 @이 붙은 클래스를 빈으로 등록하는 것이 아니고 filter로 거르고 필요한 것만 빈으로 등록합니다. 초기에 싱글톤으로 생성하므로 등록할 Bean이 많을 경우 초기 구동시간이 오래 걸릴 수 있습니다.

컴포넌트 스캔 동작 원리

다른 빈들이 등록되기 이전에 ConfigurationClassPostProcessor 라는 BeanFactoryPostProccessor에 의해 @이 붙은 클래스를 빈으로 등록합니다.

컴포넌트 스캔 외에 직접 빈을 등록하는 방법이 있으나 많은 빈을 직접 등록하기엔 불편하므로 컴포넌트 스캔을 사용하는게 좋습니다.

5. 빈의 스코프

모든 빈들은 스코프가 있고 기본은 싱글톤 스코프 입니다.

싱글톤 - 애플리케이션 전반에 걸쳐 해당 빈에 인스턴스가 오직 한개

프로토타입: 매번 새로운 객체(인스턴스)를 만들어서 사용해야 하는 스코프

프로토 타입 선언 방식
@Component @Scope("prototype")
public class Proto {
}
프로토타입의 빈이 싱글톤 빈을 참조하면?
  • 프로토타입의 빈은 매번 새로워도 싱글톤 스코프는 매번 같은 인스턴트가 들어오기 때문에 아무 문제가 없습니다.
싱글톤타입의 빈이 프로토타입 빈을 참조하면?
  • 프로토타입의 빈이 바뀌지 않고 한개만 참조합니다.
싱글톤 타입의 빈이 프로토타입 빈을 참조 시 해결방법
해결방법1
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)

프로토를 상속받은 프록시를 거쳐서 프로토타입을 사용해야합니다. 프록시 인스턴스가 빈으로 등록됩니다. 타입은 프로토타입이므로 주입이 가능합니다.

해결방법2
@Component
public class Single {
	@Autowired
	private ObjectProvider<Proto> proto;
	
	public Proto getProto() {
		return proto.getIfAvailable();
	}
}

코드에 스프링 관련 코드가 들어가므로 차라리 해결방법1로 선언하는 방법이 낫습니다. 프로토타입은 그렇게 많이 사용되지는 않는 방식입니다.

싱글톤 객체 사용시 주의할 점
  • 프로퍼티가 공유하므로 다른 쓰레드에서 서로 사용될 수 있으므로 쓰레드 세이프한 방법으로 코딩을 해야합니다.
  • 모든 싱글톤 빈은 ApplicationContext 초기 구동시 인스턴스가 생성됩니다.

6. Environment

프로파일과 프로퍼티를 다루는 인터페이스

ApplicationContext extends EnvironmentCapable

1) 프로파일

빈들의 묶음, 환경에 따라 빈을 다르게 하고 싶은 경우 사용합니다.

알파, 베타, 개발, 프로덕 등 다양한 환경

Environment의 역할은 활성화할 프로파일 확인 및 설정하는 것입니다.

프로파일 정의하기

클래스, 메소드에 정의할 수 있습니다.

@Bean @Profile(“test”)

@Configuration   //@Component @Profile("test")
@Profile("test") //@Profile("!prod") prod 가 아닌 경우
public class TestConfiguration {
	@Bean
    public BookRepository bookRepository() {
        return new TestBookRepository();
    }
}
프로파일 표현식
  • ! (not)
  • & (and)
  • (or)
프로파일 설정하는 옵션

IDE에서 active profiles -> test

VM options -> -Dspring.profiles.active=”test”

2) 프로퍼티

key, value 쌍으로 제공되는 프로퍼티에 접근할 수 있습니다. os 환경변수, -D 옵션 프로퍼티, VM 옵션으로 줄 수 있습니다.

ex) VM options: -Dapp.name=”spring5”

Environment environment = ctx.getEnvironment();
System.out.println(environment.getProperty("app.name"));
application에서 설정

app.properties

app.name="spring"

Application class

@SpringBootApplication
@PropertySource("classpath:/app.properties")
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}
프로퍼티의 우선 순위
  • ServletConfig 매개변수
  • ServletContext 매개변수
  • JNDI(java:comp/env/)
  • JVM 시스템 프로퍼티 (-Dkey=”value”)
  • JVM 시스템 환경 변수 (운영 체제 환경 변수)

7. MessageSource

국제화(i8n) 기능을 제공하는 인터페이스입니다. 여러 국적의 언어를 사용할 필요가 있을 때 사용합니다.

ApplicationContext extends MessageSource

  • getMessage(String code, Object[] args, String, default, Locale, loc)
messages.properties 영어
greeting=Hello {0}
messages_ko_kr.properties 한글
greeting=안녕, {0}

스프링 부트를 사용하면 설정없이 messages로 시작하는 properties를 읽어와 사용 가능합니다.

@Autowired
MessageSource messageSource;

messageSource.getMessage("greeting", new String[]{"yoon"}, Locale.Korea));
messageSource.getMessage("greeting", new String[]{"yoon"}, Locale.getDefault()));
리로딩 기능이 있는 메세지 소스 사용하기
@Bean
public MessageSource messageSource(){
	var messageSource = new ReloadableResourceBundleMessageSource();
	messageSource.setBasename("classpath:/messages")
	messageSource.setDefaultEncoding("UTF-8");
  messageSource.setCacheSeconds(3);
	return messageSource;
}

스프링 어플리케이션이 실행되고 있을 때 messages 프로퍼티 값을 변경하고 빌드를 하면 변경된 프로퍼티 값으로 리로딩이 됩니다.

8. ApplicationEventPublisher

이벤트 프로그래밍에 필요한 인터페이스를 제공합니다. 옵저버 패턴 구현체

Application extends ApplicationEventPublisher

  • publishEvent(Application event)

이벤트 만들기

스프링 4.2 부터 ApplicationEvent 상속 받지 않아도 됩니다. 스프링이 추구하는 방향. 스프링 소스코드가 자기 코드에 노출되지 않는 것이 POJO 기반의 프로그래밍이라고 할 수 있습니다. 테스트하기 편하고, 코드 유지보수가 쉬습니다.

MyEvent.class 이벤트
public class MyEvent {
	private int data;
	private Object source;

	public MyEvent(Object source, int data) {
		this.data = data;
		this.source = source;
	}
	
	public int getData() {
		return data;
	}
	
	public Object getSource() {
		return source;
	}
}
MyEventHandler.class 이벤트 처리
@Component
public class MyEventHandler {
	@EventListener
  // @Order(Ordered.HIGHEST_PRECEDENCE + '숫자')
  // @Async	비동기실행 순서 보장 x application에 @EnableAsync
	private void myHandler(MyEvent event) {
		System.out.println("이벤트 "  +  event.getData());
	}
}

이벤트를 처리하는 핸들러가 두가지가 있을 때 핸들러는 순차적으로 실행을 합니다. 순서를 중요한 경우에 @Order로 정할 수 있습니다.

@Async 를 사용할 경우 Application에 @EnableAsync

이벤트 사용
@Component
public class AppRunner implements ApplicationRunner{
	@Autowired
	ApplicationEventPublisher publishEvent;
	
	@Override
	public void run(ApplicationArguments args) throws Exception {
		publishEvent.publishEvent(new MyEvent(this, 100));
	}	
}
스프링이 제공하는 기본 이벤트

ContextRefreshedEvent - ApplicationContext 초기화 or 리프레시 ContextStartEvent - ApplicationContext start() 하여 라이프사이클 빈들이 시작신호 받은 시점 ContextClosedEvent - ApplicationContext stop() 하여 라이프사이클 빈들이 정지신호 받은 시점 ContextStoppedEvent - ApplicationContext close() 하여 싱글톤 빈 소멸되는 시점 받은 시점

9. ResourceLoader

리소스를 읽어오는 기능을 제공하는 인터페이스

resources 폴더 내의 리소스(파일)를 불러옵니다.

@Autowired
ResourceLoader resourceLoader;

resourceLoader.getResource("classpath:text.txt")
File.readString(Path.of(resource.getURI())); // java 11에만 있는 메소드
리소스 읽어오기
  • 파일 시스템에서 읽어오기
  • 클래스패스에서 읽어오기
  • URL로 읽어오기
  • 상대/절대 경로로 읽어오기