프로그램의 흐름(객체의 생명주기)을 개발자가 직접 제어하는 것이 아닌, 외부에서 관리하는 것을 말한다.
스프링은 제어의 역전을 지원하여, 개발자 대신 스프링 컨테이너가 객체(Bean)들을 관리해준다.
스프링 컨테이너는 Bean들의 생명주기를 관리하며, 필요에 따라 객체 간 의존성 주입을 해준다.
제어의 역전은 코드의 재사용성과 유지보수성을 높인다.
의존관계는 정적인 클래스 의존관계와 동적인 객체(인스턴스) 의존관계로 분류된다.
정적인 클래스 의존관계
: 애플리케이션을 실행하지 않아도 알 수 있다.
ex, OrderServiceImpl
은 MemberRepository
(인터페이스)와 DiscountPolicy
(인터페이스)에 의존한다. 그러나 실제 어떤 구현체가 OrderServiceImpl
에 주입될지는 알 수 없다.
동적인 객체(인스턴스) 의존관계
: 애플리케이션 실행 시점에 실제 생성된 객체와의 의존관계이다.
ex, OrderServiceImpl
에 MemoryMemberRepository
(구현체), FixDiscountPolicy
(구현체)와의 의존성이 주입된다.
의존관계 주입은 애플리케이션 실행 시점에 객체 간 의존성을 개발자가 아닌 외부에서 주입해주는 것을 말한다.
스프링에서는 스프링 컨테이너가 Bean 설정 정보를 바탕으로 각 클래스의 의존관계를 연결해준다.
의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상을 변경할 수 있다. 즉 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 변경할 수 있다.
BeanFactory
나 ApplicationContext
를 스프링 컨테이너라 한다.
BeanFactory
스프링 컨테이너의 최상위 인터페이스이다.
스프링 빈을 관리하고 조회하는 기능을 제공한다. (ex, getBean()
)
ApplicationContext
BeanFactory
의 기능을 모두 상속받아서 제공한다.
빈을 관리하고 조회하는 기능 외에도 다음과 같은 부가 기능을 제공한다.
즉 ApplicationContext
는 BeanFactory
기능을 모두 상속하여 Bean 객체를 관리, 조회하는 기능 뿐만 아니라, 메시지, 리소스, 이벤트와 관련된 부가 기능까지 제공한다.
애플리케이션을 개발할 때는 빈 관리, 조회 기능 외에도 수많은 부가 기능이 필요하기 때문에 ApplicationContext
를 주로 사용한다.
스프링 컨테이너는 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어 있다.
스프링 컨테이너는 XML을 기반으로 만들 수도 있고, 애노테이션 기반의 자바 설정 클래스로 만들 수도 있다.
스프링이 이렇게 다양한 설정 형식을 지원할 수 있는 이유는 바로 BeanDefinition
(빈 설정 메타정보) 덕분이다.
AnnotationConfigApplicationContext
는
AnnotatedBeanDefinitionReader
를 사용해서 AppConfig.class
를 읽고
@Bean
당 하나씩 메타정보 BeanDefinition
을 생성한다.
GenericXmlApplicationContext
는
XmlBeanDefinitionReader
를 사용해서 appConfig.xml
설정 정보를 읽고
<bean>
당 하나씩 메타정보 BeanDefinition
을 생성한다.
즉 스프링 컨테이너는 자바 코드인지, xml 코드인지 몰라도 된다. 스프링 컨테이너는 오직 BeanDefinition
만 알면 되고, BeanDefinition
을 기반으로 스프링 빈을 생성한다.
public class SingletonService {
//static으로 객체를 1개만 생성해둔다.
private static final SingletonService instance = new SingletonService();
//객체 인스턴스가 필요하면 이 static 메소드를 통해서만 조회할 수 있다.
public static SingletonService getInstance() {
return instance;
}
//생성자를 private으로 선언해서 외부에서 new 키워드로 객체 생성을 못하도록 막는다.
private SingletonService() {
}
}
이렇게 싱글톤 패턴으로 구현하면 고객의 요청이 올 때마다 객체를 새로 생성하지 않고, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다.
그러나 싱글톤 패턴은 다음과 같은 문제를 갖는다.
싱글톤 패턴을 구현하는 코드가 많이 들어간다.
의존관계상 클라이언트가 구체 클래스에 의존한다. (DIP 위반)
private 생성자를 갖고 있어 상속이 불가능하다.
테스트하기 어렵다.
스프링 컨테이너는 싱글톤 패턴의 문제를 해결하면서, 객체 인스턴스를 싱글톤으로 관리한다.
스프링 컨테이너는 싱글톤 컨테이너의 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
스프링 컨테이너 덕분에 싱글톤 패턴을 위한 코드가 들어가지 않으며, DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.
고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있다.
싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.
무상태(stateless)로 설계해야 한다.
특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안되고 가급적 읽기만 가능해야 한다.
공유 값을 설정하면 위험하다. 그 대신에 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
상태를 유지하는 경우는 다음과 같다.
public class StatefulService {
private int price; //상태를 유지하는 필드
public void order(String name, int price) {
this.price = price;
}
public int getPrice() {
return price;
}
}
public class StatefulServiceTest {
@Test
void StatefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService service1 = ac.getBean("statefulService", StatefulService.class);
StatefulService service2 = ac.getBean("statefulService", StatefulService.class);
service1.order("userA", 1000);
service2.order("userB", 2000);
int price = service1.getPrice(); //사용자 A는 1000원을 기대했지만, 2000이 반환된다.
}
static class TestConfig {
@Bean
public StatefulService StatefulService() {
return new StatefulService();
}
}
}
위 코드를 무상태로 설계하면 다음과 같다.
public class StatefulService {
public int order(String name, int price) {
return price;
}
}
public class StatefulServiceTest {
@Test
void StatefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService service1 = ac.getBean("statefulService", StatefulService.class);
StatefulService service2 = ac.getBean("statefulService", StatefulService.class);
int userAPrice = service1.order("userA", 1000);
int userBPrice = service2.order("userB", 2000);
}
static class TestConfig {
@Bean
public StatefulService StatefulService() {
return new StatefulService();
}
}
}
@Configuration
이 달린 클래스는 빈 설정을 담당하는 설정 정보 클래스가 된다.@Configuration
이 붙은 클래스를 설정 정보로 사용한다.@Configuration
이 달린 클래스 내에서, 메소드에 @Bean
을 적용하면 메소드가 반환하는 객체가 스프링 빈으로 등록된다.@Configuration과 바이트코드 조작
@Configuration public class AppConfig { public MemberService memberService() { return new MemberServiceImpl(memberRepository()); } public OrderService orderService() { return new OrderServiceImpl(memberRespository(), discountPolicy()); } public MemberRepository memberRespository() { return new MemoryMemberRepository(); } public DiscountPolicy discountPolicy() { return new FixDiscountPolicy(); } }
- AppConfig의 코드를 보면,
memberRespository()
가 총 3번 호출되어야 한다.
그런데 스프링 컨테이너는 싱글톤을 보장하기 때문에memberRespository()
는 딱 한 번만 호출되고, 이때 생성된MemoryMemberRepository
의 객체가 공유되어 사용된다.
- 이것에 가능한 이유는 AppConfig에
@Configuration
을 적용했기 때문이다.
즉@Configuration
덕분에 객체를 싱글톤으로 관리할 수 있다.
- AppConfig에
@Configuration
을 붙이면, AppConfig도 스프링 빈에 등록된다. 그리고 등록된 AppConfig를 조회해보면hello.core.AppConfig
가 아닌 클래스 명에 xxxCGLIB가 붙은 것을 알 수 있다.@Test void configurationDeep() { ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); AppConfig bean = ac.getBean(AppConfig.class); System.out.println("bean = " = bean.getClass()); }
출력 결과
bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
- 스프링은 CGLIB라는 바이트코드 조작 라이브러리를 사용해서, AppConfig를 상속받는 임의의 다른 클래스를 생성하고, 그 클래스를 스프링 빈으로 등록한다. 그리고 그 임의의 클래스가 싱클톤을 보장해준다.
AppConfig@CGLIB 예상 코드
@Bean public MemberRepository memberRepository() { if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) { return 스프링 컨테이너에서 찾아서 반환; } else { //스프링 컨테이너에 없으면 기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록 return 반환 } }
@Bean
이 붙은 메소드마다 이미 스프링 빈이 존재하면 해당 빈을 반환하고, 스프링 빈이 없으면 생성해서 빈으로 등록하는 코드가 만들어진다. 이 임의의 클래스 덕분에 싱글톤이 보장된다.
AppConfig@CGLIB
는AppConfig
의 자식 타입이기 때문에,AppConfig
타입으로 조회할 수 있는 것이다.
@Configuration을 적용하지 않고, @Bean만 적용하면 어떻게 될까?
- AppConfig에
@Configuration
를 삭제하고@Bean
만 적용하면 어떻게 될까?
- 스프링 빈으로 등록된 AppConfig의 클래스 정보를 조회해보면, AppConfig의 클래스 정보는
hello.core.AppConfig
이다. 즉 CGLIB 기술 없이 순수한 AppConfig로 스프링 빈에 등록된다.
- 따라서
memberRepository()
도 3번 호출되고, 3개의MemoryMemberRepository
객체가 만들어진다. 즉 싱글톤이 보장되지 않는다.
- 결론적으로
@Bean
만 적용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다. 따라서 스프링 설정 정보에는 항상@Configuration
을 적용하자!