들어가며
IOC에 대한 개념이 이해가 갔다면 이제 스프링의 IOC에 대해 알아보자. 스프링은 앞서 말했던 코드들중에서 AppConfig
와 같은 역할을 한다고 보면 된다.
class AppConfig {
public UserDao userDao() {
return new MySqlUserDao();
}
public UserService userService() {
return new UserService(userDao());
}
}
AppConfig
는 UserService
의 생성과 생명주기를 관리한다. 그렇다면 스프링은 어떤 객체의 생성과 생명주기를 담당할까?
Bean
스프링에서는 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 빈(Bean)이라고 부른다. 스프링 빈은 스프링 컨테이너가 생성, 관계설정, 사용 등의 제어를 담당함으로써 제어의 역전(IoC)이 적용된 오브젝트를 의미한다.
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(),discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
정말 간단한 예시로, 위 코드와 같이 어노테이션으로 빈 설정을 해줄 수 있다.
Bean Factory
이러한 스프링 빈의 생성과 관계설정 같은 제어를 맡는 IoC 오브젝트를 빈 팩토리(Bean Factory)라고 하며, 이는 스프링 컨테이너의 최상위 인터페이스로서 스프링 빈을 관리하고 조회하는 역할을 담당하고, getBean()
메소드를 제공한다.
Application Context
반면, Application Context는 단순히 빈을 관리하고 검색하는 기능을 넘어서, 애플리케이션 개발에 필요한 다양한 부가기능을 포함한다. Application Context는 별도의 설정 정보를 참고하여 빈(오브젝트)의 생성과 관계설정 등의 제어 작업을 총괄하며, IoC 방식을 구현한 일종의 빈 팩토리이다.
public class MemberApp {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println(findMember.getName());
}
}
간단한 예시로, 위 코드와 같이 ApplicationContext
에서 등록한 빈 객체를 가지고 와 사용할 수 있다.
이 때문에 스프링에서는 Application Context를 IoC 컨테이너 또는 간단히 스프링 컨테이너라고 부르기도 하고, 때로는 빈 팩토리라고도 지칭한다. 실제로 ApplicationContext 인터페이스는 BeanFactory 인터페이스를 상속함으로써 빈 팩토리의 확장된 버전이라고 할 수 있으며, 이를 구현한 클래스들이 스프링의 대표적인 컨테이너 오브젝트로 활용된다.
ApplicationContext가 제공하는 부가기능
Application Context는 메세지 소스를 활용한 국제화 기능을 제공하여, 예를 들어 한국에서 접근하면 한국어로, 영어권에서는 영어로 출력할 수 있도록 지원한다. 또한, 환경 변수 기능을 통해 로컬, 개발, 운영 환경 등을 구분하여 처리할 수 있으며, 애플리케이션 이벤트 기능을 통해 이벤트를 발행하고 구독하는 모델을 편리하게 지원한다. 아울러, 파일, 클래스패스, 외부 등 다양한 경로에서 리소스를 손쉽게 조회할 수 있는 기능도 함께 제공된다.
Application Context의 동작방식
Application Context는 DaoFactory 클래스를 설정정보로 등록하고, @Bean이 붙은 메소드의 이름을 이용하여 빈 목록을 구성하는 것에서 시작된다. 클라이언트가 Application Context의 getBean() 메소드를 호출하면, 컨테이너는 자신이 보유한 빈 목록에서 해당 이름의 빈을 검색하고, 존재할 경우 빈을 생성하는 메소드를 호출하여 오브젝트를 생성한 후 클라이언트에게 반환한다.
여기서 말하는 클라이언트는 애플리케이션 컨텍스트를 사용하여 빈(bean)을 요청하는 코드를 의미한다. 구체적으로는 애플리케이션의 메인 로직을 담당하는 클래스들, 다른 빈을 사용해야 하는 빈 클래스들, 테스트 코드를이 있다.
싱글톤 레지스트리
싱글톤의 필요성
일반적으로 자바에서 같은 객체를 두개 생성해서 비교해보면 둘이 다른 값이 나온다. 각 객체는 유일한 hashcode 값을 가지기 때문이다. 예를 들어 다음 코드를 보자.
DaoFactory factory = new DaoFactory();
UserDao dao1 = factory.userDao();
UserDao dao2 = factory.userDao();
System.out.println(dao1.equals(dao2));
실행결과는 false 이다. dao1과 dao2의 hashcode 값이 다르기 때문이다. 만약 스프링이 이런 방식으로 설정되어 있다면 곤란할 것이다. 매번 새로운 객체 생성으로 메모리 낭비가 될 수도 있고, 이는 성능저하로 이어질 수 있다. 또한, 매번 새로운 인스턴스가 생성된다면 그에 맞게 의존성 주입도 매번 새롭게 설정되어야 하므로 의존성 관리가 복잡해진다.
그래서 객체를 불러올 때, 새로운 객체를 생성해주는 것이 아닌 같은 객체를 보내주기 위해 스프링은 “싱글턴같은” 객체를 유지한다. 싱글톤 패턴이 아닌 “싱글턴같은” 방식이라고 말하는 이유는 자바에서 싱글톤을 구현했을 때 여러 문제가 있기 때문이다.
NOTE자바에서 싱글톤을 구현하는 방법은 보통 이렇다.
- 클래스 밖에서는 오브젝트를 생성하지 못하도록 생성자를 private으로 만든다.
- 생성된 싱글톤 오브젝트를 저장할 수 있는 자신과 같은 타입의 스태틱 필드를 정의한다.
- 스태틱 팩토리 메소드인 getInstance()를 만들고 이 메소드가 최초로 호출되는 시점에서 한 번만 오브젝트가 만들어지게 한다. 생성된 오브젝트는 스태틱 필드에 저장된다. 또는 스태틱 필드의 초기값으로 오브젝트를 미리 만들어둘 수도 있다.
- 한번 오브젝트(싱글톤)가 만들어지고 난 후에는 getInstance() 메소드를 통해 이미 만들어져 스태틱 필드에 저장해둔 오브젝트를 넘겨준다.
토비의 스프링 vol1
자바 스프링에서 싱글톤의 문제
자바 스프링에서 싱글톤을 사용한다면 여러 문제가 생긴다.
상속이 불가능하다.
싱글톤으로 구현시 private 생성자를 가지고 있기 때문이다. 객체지향적으로 코드를 작성해야 하는 경우에 큰 문제가 생긴다.
테스트하기 어렵다.
싱글톤은 만들어지는 방식이 제한적이기 때문에 테스트에서 사용될 때 Mock 오브젝트 등으로 대체하기가 힘들다.
서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
서버에서 클래스로더를 어떻게 구성하느냐에 따라 달라질 수 있다.(하나 이상의 오브젝트가 만들어질 수도 있다.) 여러 개의 JVM에 분산돼서 설치가 되는 경우에도 각각 독립적으로 오브젝트가 생기기 때문에 싱글톤으로서의 가치가 떨어진다.
싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.
싱글톤의 스태틱 메소드를 이용해 언제든지 싱글톤에 쉽게 접근할 수 있기 때문에 애플리케이션 어디서든지 사용될 수 있고,그러다 보면 자연스럽게 전역 상태로 사용되기 쉽다. 이는 객체지향적이지 못하다.
싱글턴 레지스트리(singleton registry)
그래서 자바에서 싱글턴을 사용하는 것은 한계가 있기 때문에 스프링에서는 “싱글턴같은” 방식을 사용한다. 그게 싱글턴 레지스트리이다. 스프링은 직접 싱글턴 형태의 객체를 만들고 관리하는 기능을 제공한다. 싱글턴 레지스트리의 장점은 기존의 싱글턴과 같이 static 메소드와 private 생성자를 사용한 클래스가 아니라 평범한 자바 클래스를 싱글턴으로 활용하게 해 준다.
기존 자바에서의 싱글턴은 객체지향적인 접근이 불가능하다는 점이 가장 큰 문제였다. 하지만 스프링에서 싱글턴 레지스트리로 인해 스프링이 지지하는 객체지향적인 설계 방식과 디자인 패턴 등을 적용하는 데 아무 문제가 없게 되었다.
싱글턴 레지스트리와 오브젝트 상태
싱글턴 레지스트리는 멀티 스레드 환경이라면 여러 스레드가 동시에 접근하여 사용할 수 있다. 이 말은 즉, R/W를 여러 곳에서 동시에 할 수 있다는 점인데 이건 운영체제에서의 동기화 문제와 비슷한 문제가 발생할 수 있다. 따라서 상태관리에 주의를 기울여야 한다.
기본적으로 싱글턴 레지스트리는 멀티스레드 환경에서 서비스 형태의 객체로 사용될 경우에 상태정보를 담고 있지 않은 stateless로 만들어져야 한다. 만약 그렇지 않다면 여러 사용자가 읽고 쓰는 과정에서 상태정보가 꼬여버릴 가능성이 매우 매우 높다. 하지만 읽기만 한다면 정보가 꼬일 일은 없으므로 상관없다. 읽기 속성만을 가지고 있는 정보라면 싱글턴 레지스트리에서 인스턴스 변수로 사용해도 된다.(예: 서버 정보, jwt값 등)
빈의 스코프(추후 수정 예정: 25.03.13 - )
스프링이 관리하는 객체, 빈은 싱글턴 스코프이다. 싱글턴 스코프는 컨테이너 내에서 한 개의 오브젝트만 만들어져서 강제로 제거되지 않는 한, 스프링 컨테이너가 존재하는 한 계속 유지된다. 스프링에서 만들어지는 대부분의 빈은 싱글턴 스코프를 가진다.
싱글턴 스코프
스프링 빈은 기본적으로 싱글턴 스코프로 생성된다. 이런 싱글턴 스코프로 만들어진 빈은 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
프로토타입 스코프
이 외에도 다른 스코프들이 존재하는데, 대표적으로 프로토타입 스코프가 있다. 프로토타입 스코프는 싱글톤과 달리 컨테이너에 빈을 요청할 때마다 매번 새로운 오브젝트를 만들어준다. 그리고 스프링 컨테이너는 프로토타입 빈의 생성, 의존관계 주입까지만 관여하고 더는 관리하지 않는다.(매우 짧은 범위를 가지고 있다.)
웹 관련 스코프
이 외에도 웹을 통해 새로운 HTTP 요청이 생길 때마다 생성되는 request 스코프, 웹의 세션과 스코프가 유사한 session 스코프도 있다.
스코프 지정 방법
빈 스코프는 다음과 같이 지정할 수 있다.
- 컴포넌트 스캔 자동 등록
@Scope("prototype")
@Component
public class HelloBean {}
- 수동 등록
@Scope("prototype")
@Bean
PrototypeBean HelloBean(){
return new HelloBean();
}
출처
- 토비의 스프링 vol1
- 스프링의 핵심 원리 - 기본편(김영한)