서론
이번 포스트부터 Spring Framework에 필수적인 기술들을 다뤄볼 겁니다.
Spring Framework에서 가장 핵심 기술을 꼽자면 당연히 IoC(Inversion of Control) 컨테이너입니다! 후에 AOP 기술과도 밀접하기 때문에 반드시 알아야 하는 기술입니다!
Java의 가장 중요한 가치는 '객체지향 프로그래밍'입니다. JEB의 복잡성에 잃어버린 특성을 되찾아 내기 위한 프로젝트가 바로 Spring 프로젝트였죠. 그래서 Spring은 오브젝트에 가장 많은 관심을 두었습니다.
개발자들은 항상 오브젝트의 생성부터 시작해 관계를 맺고, 사용되며, 소멸되기까지의 과정인 생명주기에 포커스를 두어야 합니다! Spring은 항상 확장성 있는 설계를 기조로 두기 때문에 특정 모델을 강요하지 않지만, 강력한 가이드라인을 제공해 줍니다. 이게 바로 IoC Container의 시작이죠.
간단한 애플리케이션 만들기
간단하게 회원들과 물건들을 관리하는 관리자용 UI를 만드려고 합니다. 간단하게 회원들부터 만들어 볼까요?
// 편의를 위해 Lombok를 사용했습니다. 자동으로 Get과 Set 메서드를 만들어주는 기능을 합니다
@Getter @Setter
public class Member {
private Long id;
private String name;
private String password;
// 테스트 편의를 위해 만든 생성자.
public Member(Long id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
}
굳이 DB를 사용할 생각은 없습니다. 간단하게 메모리에서 관리하는 HashMap을 이용해봅시다!
public class MemberRepositoryByMemory {
private static Map<Long, Member> store = new HashMap<>();
public void save(Member member) {
store.put(member.getId(), member);
}
public Member getById(Long id) {
return store.get(id);
}
}
이 데이터들만으로는 뭔가를 할 수 없습니다. 최소한의 상호작용을 하는 비즈니스 로직을 만들어 봅시다.
여기서 키포인트는 데이터를 관리하는 코드와 비즈니스 로직을 담당하는 코드를 물리적으로 분리하는 것입니다!
왜냐하면 데이터 관리 쪽을 수정하고 싶을 때 피치 못할 사정으로 비즈니스 로직을 수정해야 할 수 있고 그 역도 가능해질 수 있기 때문이죠. 이를 방지하기 위해서 하는 것을 추상화의 일종인 관심사의 분리라고 합니다!
public class MemberService {
private final MemberRepositoryByMemory memberRepository = new MemberRepositoryByMemory();
public void join(Member member) {
memberRepository.save(member);
}
public Member findMember(Long id) {
return memberRepository.getById(id);
}
}
마지막으로 실행을 담당할 클래스를 만들어 주어야 합니다. 보통 main( ) 메서드가 들어가 있는 곳이죠.
public class MemberApp {
public static void main(String[] args) {
MemberService memberService = new MemberService();
Member member = new Member(1L, "테스트1", "비밀번호1");
memberService.join(member);
// 일치 여부 테스트
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("find Member = " + findMember.getName());
}
}
(테스트하는 경우에는 Unit을 이용하는 것이 좋으나 여기서 그것이 중요한 것은 아니기 때문에 넘어가겠습니다.)
처음 작성했다면 만족스러울지 몰라도 문제점이 많은 코드들입니다.
항상 유지 보수 관점으로 코드를 보셔야 합니다!
이 코드의 문제점은 다음과 같습니다.
1) 만약 저장소의 형식이 달라진다면 어떻게 할 것인가? (확장성의 부재)
사실상 메모리에 데이터를 담는 방식은 애플리케이션을 종료하면 데이터가 모두 날아가기 때문에 운영목적으로는 적절하지 않습니다. 그래서 본격적으로 DB를 사용하려고 합니다!
문제는 이 MemberRepositoryByMemory 클래스를 완전 새로 짜야한다는 것입니다. (직관성을 위해서 클래스 명부터 바꿔야겠군요.)
public class MemberRepositoryByMemory {
// HashMap을 DB로 변경
private static Map<Long, Member> store = new HashMap<>();
public void save(Member member) {
// SQL 쿼리로 데이터 입력
store.put(member.getId(), member);
}
public Member getById(Long id) {
// SQL 쿼리로 데이터 조회
return store.get(id);
}
}
추가적으로 이 애플리케이션이 인기가 많아져 여러 고객들에게 팔아야 한다 칩시다. 만약 고객들이 각기 다른 DBMS를 사용 중이라면? 또 그때그때 새로운 클래스를 짜야할까요? 굉장히 비효율적입니다.
JAVA를 공부한 후 Spring을 배우는 여러분들이라면 이에 대한 해결책은 알고 있을 겁니다. 크게 두 가지 해결책이 있습니다. 바로 추상 클래스와 인터페이스입니다.
그런데 추상 클래스를 사용하자니, 다중 상속은 불가능해지고 또한 기본적으로 기능을 확장시키는 상속 개념이기 때문에 슈퍼클래스의 내부 변경이 있을 때 모든 서브클래스를 수정하거나, 또 함부로 서브 클래스가 변화하지 않게 제약을 가해야 하는 문제점이 있습니다.
그래서 이런 경우 인터페이스를 많이 채택합니다. 구체적으로 얘기하자면, 인터페이스의 전략 패턴이 되겠네요!
public interface MemberRepository {
void save(Member membeR);
Member getById(Long id);
}
이에 맞춰 MemberRepositoryByMemory에 "implement MemberRepository"만 붙이면 됩니다.
MySQL을 사용하는 MemberRepositoryByDB 클래스 역시 만들어 봅시다.
다른 곳들에서도 확장성을 유지하고 또한 MemberRepositoryByDB 클래스에서도 DBMS는 다양하니 이 역시 고려하면 다이어그램은 다음과 같은 것입니다.
2) 확장성을 확보했더라도, 유연성을 확보하지 못했다.
어찌어찌 확장성을 확보했더라도 여전히 문제점이 남아있습니다. 바로 유연성 문제입니다!
각 클래스들은 사실상 인터페이스가 아닌 인터페이스의 각 구현체에 의존하고 있습니다. (이를 DIP 위반이라 합니다.)
이는 그대로 코드에도 노출이 되어 유연성에 영향을 주게 됩니다.
구현 클래스를 변경해야 할 때 클라이언트 코드도 함께 변경해야 하는 문제점이 생기게 됩니다.
// MemberRepositoryByDB에 의존하고 있다라고 대놓고 코드에 드러남.
private final MemberRepository memberRepository = new MemberRepositoryByMemory();
이를 어떻게 해결해야 할까요? Java에서는 다형성이라는 강력한 무기로 이를 해결합니다.
오브젝트 사이의 관계에서는 특정 클래스를 전혀 몰라도 해당 클래스가 구현한 인터페이스를 사용했다면, 그 클래스의 오브젝트를 인터페이스 타입으로 사용할 수 있게 됩니다!
// MemberService 수정
private MemberRepository memberRepository;
이것이 바로 다형성의 장점입니다!
3) 그렇다면 누가 이를 결정해야 하는데?
마지막 문제입니다. 신나게 모든 것을 설정하고 이제 테스트만 하면 되는데.... 문제가 발생합니다.
바로 NullPointer 예외가 발생합니다. 문제는 아직까지 MemberRepository의 구현체를 확정적으로 결정하지 않았다는 것이죠.
그래서 이를 확정적으로 결정을 하자니... 어디서 해야 할까요? MemberService에서 한다면 2번 과정이 의미가 없어집니다. 아니면 MemberApp에서 하자니... 클라이언트 코드에서 이를 드러내고 싶지는 않습니다. (관심사도 두 개나 겹침)
생각해보니 Connection도 선택을 해야 합니다! 그래서 [인터페이스의 구현체를 선택해줄] 역할만 가지는 다른 클래스를 하나 만들기로 합니다! InterfaceImplFactory라고 하죠.
구현체를 선택해줄 방법은 여러 가지가 있습니다. 생성자를 통해서 선택을 해주거나, 팩토리 메서드를 이용하는 방법이죠. 이런 경우에는 다음 이유로 인해 보통 팩토리 메서드 대신 생성자를 선택을 합니다.
- 애플리케이션 종료 시점까지 불변을 유지해야 하기 때문에, 한번 쓰고 말 생성자를 추천.
- 팩토리 메서드의 경우 public으로 열기 때문에 실수의 우려가 있다!
- 팩토리 메서드보다 단위 테스트에서 누락 시 컴파일 단계에서 실수를 잡아내기 쉽다.
그럼 이제 Service, Repository 레벨에서 생성자를 만들어주고 InterfaceImplFactory를 만들어 봅시다.
그리고 여기서 구현체를 선택하면 됩니다!
public class InterfaceImplFactory {
public MemberService memberService() {
return new MemberService(memberRepository());
}
public MemberRepository memberRepository() {
return new MemberRepositoryByDB(connectionStrategy());
}
public ConnectionStrategy connectionStrategy() {
return new MySQLConnection();
}
}
마침내 각 레벨에서 코드를 수정하지 않고 독립적인 클래스에서 구현체를 선택할 수 있게 되었습니다.
MemberApp에서는 이제 InterfaceImplFactory.memberService( )만 실행하면 되겠네요!
다시 한번 복기해봅시다.
각 레벨의 코드를 보면 더 이상 다른 객체를 생성하고 연결하는 코드가 보이지 않습니다. 단지 생성자만 있을 뿐입니다.
// Service 레벨에서 Repository를 선택했던 코드
private final MemberRepositoryByMemory memberRepository = new MemberRepositoryByMemory();
// Repository 레벨에서 DMBS를 선택했던 코드
private ConnectionStrategy cs = new MySQLConnection();
기존에는 각 클래스들이 구현 클래스들을 생성하고 연결했으며, 자신들의 로직을 실행했습니다.
하지만 InterfaceImplFactory가 만들어지며 각 객체들은 자신의 로직을 실행하는 역할만 담당하고, 기존에 수행하던 구현 객체 생성, 연결(DI)을 InterfaceImplFactory에게 맡겨 버렸습니다. 전체적인 애플리케이션의 제어권이 InterfaceImplFactory에게 있는 것입니다!
이렇게 프로그램의 제어 흐름 구조가 뒤바뀌는 것을 제어의 역전(Inversion of Control)이라고 합니다!
그리고 이렇게 InterfaceImplFactory처럼 객체를 생성, 관리 및 의존관계를 연결해주는 객체들을 IoC 컨테이너 또는 DI 컨테이너라고 합니다.
추가적으로 컨테이너 내부에 존재하는 객체들을 빈(bean)이라고 합니다.
정리해봅시다.
1. IoC Container & Bean
각 객체는 생성자, 팩토리 메서드를 통해서만 객체가 의존관계를 정의할 수 있습니다. 의존관계를 주입하는 것 자체가 제어의 역전된 것이기 때문에 IoC와 의존관계 주입(DI)은 결국 같은 뜻을 가지게 됩니다. 컨테이너는 빈을 생성할 때 이러한 종속성을 주입합니다. (Service Locator 패턴과 같은 메커니즘을 사용하기 때문에 참고하시면 좋을 것 같습니다.)
Spring에서 애플리케이션의 백본을 형성하고 Spring IoC 컨테이너에 의해 관리(생성, 관리, 의존관계 연결)되는 객체를 빈이라고 합니다. Bean과 이들 간의 의존관계는 컨테이너에서 사용하는 구성 메타데이터에 반영됩니다.
컨테이너(ApplicationContext)
'org.springframework.context.ApplicationContext' 인터페이스는 Spring IoC 컨테이너를 나타내며 빈의 생성, 관리, 의존관계 연결을 담당합니다(전체적인 애플리케이션 흐름 제어). 컨테이너는 구성 메타데이터를 읽어 인스턴스화, 구성 및 연결할 객체에 대한 지침을 얻습니다. 참고로 구성 메타데이터는 XML, Java 어노테이션 또는 Java 코드로 표시됩니다.
과거에는 주로 XML을, 현재는 주로 Java 어노테이션 또는 Java 코드로 표시합니다.
생성 과정
1. 설정 정보 입력 (자바 코드)
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemberRepositoryByDB(connectionStrategy());
}
@Bean
public ConnectionStrategy connectionStrategy() {
return new MySQLConnection();
}
}
'new AnnotationConfigApplicationContext(AppConfig.class)'를 통해 AppConfig.class를 설정 정보로 지정.
2. 파라미터로 넘긴 설정 정보를 통해 빈 저장소에 스프링 빈을 등록 (@Bean)
기본적으로 Bean의 이름은 메서드 명을 따르며 @Bean(name = "Bean이름")을 통해 직접 이름 부여가 가능합니다.
나중에 살펴볼 내용이지만, Bean의 이름은 모두 달라야 합니다! 중복될 시 설정에 따라 다른 빈이 무시되거나 덮어 쓰입니다.
3. 스프링 빈 의존관계 설정(설정 준비 및 완료)
설정 정보를 참고해 의존관계를 주입합니다.
다음 다이어그램은 Spring이 작동하는 방식에 대한 상위 레벨 보기를 보여줍니다. 애플리케이션 클래스는 구성 메타데이터와 결합되어 ApplicationContext가 생성되고 초기화된 후 완전히 구성되고 실행 가능한 시스템 또는 애플리케이션을 갖게 됩니다.
설정 정보
설정 정보에 대해 자세히 알아봅시다.
다시 한번 설명하지만 Spring IoC 컨테이너는 구성 메타데이터 형식을 사용합니다. 그리고 이 구성 메타데이터는 Spring 컨테이너에 애플리케이션의 객체를 생성하고 관리하는 방법을 나타냅니다. 종류로는 XML, Java Annotation, Java code가 있다고 했었습니다.
각각을 설명하기 이전에 이러한 형식이 가능한 이유는 BeanDefinition 추상화를 지원하기 때문입니다
인터페이스 형식처럼 스프링 컨테이너는 Bean을 생성할 때 BeanDefinition만 알면 되기 때문에 다른 형식들에 구애받지 않습니다.
메서드 | 기능 |
BeanClassName | 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음) |
factoryBeanName | 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig |
factoryMethodName | 빈을 생성할 팩토리 메서드 지정, 예) memberService |
Scope | 싱글톤(기본값) |
lazyInit | 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연처리 하는지 여부 |
InitMethodName | 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명 |
DestroyMethodName | 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명 |
Constructor arguments, Properties | 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음) |
조금 Deep 한 설정을 위해서라면 BeanDefinition을 직접 생성해서 설정하지만 대부분은 그럴 일이 없습니다.
XML 방식
Spring 3.0을 넘기면서부터 Java Annotation(Spring 2.5)이나 Java code(Spring 3.0)를 지원하기 때문에 잘 쓰이는 방식은 아닙니다.
보통 설정 정보 파일은 resources 디렉터리에 들어갑니다.
XML에서는 최상위 <beans/> 요소 내부의 <bean/> 요소로 구성됩니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="빈 이름이 들어갑니다" class="의존관계를 설정할 구현체 완전한 주소가 들어갑니다.">
<!-- collaborators and configuration for this bean go here -->
<constructor-arg name="그 객체의 생성자 파라미터 변수명" ref="변수명" />
</bean>
<bean id="memberService" class="springcorepost.post.service.MemberService">
<constructor-arg name="memberRepository" ref="memberRepository" />
</bean>
<bean id="memberRepository" class="springcorepost.post.repository.MemberRepositoryByDB">
<constructor-arg name="connectionStrategy" ref="connectionStrategy" />
</bean>
<bean id="connectionStrategy" class="springcorepost.post.connection.MySQLConnection">
</bean>
</beans>
이 xml파일을 실행시키기 위해서는 ApplicationContext 인터페이스 구현체인 GenericXmlApplicationContext나 ClassPathXmlApplicationContext 등을 활용해 실행하시면 됩니다.
ApplicationContext context = new GenericXmlApplicationContext("appconfig.xml");
Java Annotation 방식
Annotation방식은 XML 형식에서 사용되는 <beans/> <bean/>들을 @Bean 어노테이션으로 대체하는 것입니다.
Java Code 방식의 경우 직관성은 매우 뛰어나지만, 만약 Bean의 수가 수십~수백 개가 넘어가버린다면, 이를 입력하는 것 자체가 비생산적인 일이 될 수 있습니다.
그래서 Spring은 설정 정보가 따로 없어도 어노테이션을 사용해 "이 클래스는 Bean이다."라고 마킹을 하는 기능을 제공하는데 이를 컴포넌트 스캔이라고 합니다.
생성자에 추가적으로 @Autowired 어노테이션을 부여해 의존관계를 자동으로 주입시킬 수 있습니다.
XML 방식과 Annotation 방식은 조금 고민을 해봐야 합니다. 서로 장단점이 있어 상황에 맞게 사용을 해야 합니다.
종류 | 특징 |
XML 방식 | 장점 : 소스코드를 수정해도 컴파일하지 않고 연결이 가능하다. 단점 : Annotation 방식보다는 조금 더 복잡하다. |
Annotation 방식 | 장점 : 짧고 간결한 구성으로 만들 수 있다. 단점 : 소스코드를 수정할 경우 다시 컴파일을 해야 한다. 더 이상 POJO가 아니고, 구성이 분산되며 제어가 어려워진다. (일부 개발자들의 주장) |
현재는 혼용하는 방식을 사용하기도 합니다.
@Configuration @Bean @Import @DependsOn를 참고하시기 바랍니다.
Java Code 방식
우리가 예제에서 사용했던 방식입니다.
AppConfig.class에 생성할 빈과 의존관계를 모두 쓰는 방식입니다.\
싱글톤 컨테이너
순수한 자바 코드를 사용하든, 스프링의 기능을 이용한 IoC를 사용하든 테스트 결과만 보면 같다는 것을 알 수 있습니다. 오히려 스프링의 기능을 추가했기 때문에 코드가 더 길어지고 복잡하다고 느낄 수 있죠. 그럼 뭐가 좋길래 이러한 작업을 수행할까요?
1. Java 인스턴스
스프링은 온라인 서비스 기술 특히 웹 애플리케이션 개발에 특화되어있습니다.
그리고 이는 많은 사람들의 요청을 감당해야 합니다!
MemberService를 수많은 고객들에게 제공한다고 해봅시다. 그러면 얼마나 많은 MemberService 객체를 만들어야 할까요? 다음 코드를 봅시다. 인스턴스와 힙 메모리에 대해 알고 있다면 당연한 결과입니다.
public class SingleTonTest {
@Test
void 순수자바코드_IoC컨테이너() {
AppConfig appConfig = new AppConfig();
MemberService service1 = appConfig.memberService(); // 1번 클라이언트의 요청에 따라 객체 생성
MemberService service2 = appConfig.memberService(); // 2번 클라이언트의 요청에 따라 객체 생성
// 같은 인스턴스가 아니기 때문에 오류 발생
Assertions.assertThat(service1).isNotSameAs(service2);
}
}
만약 클라이언트 요청 한 번에 5개의 오브젝트가 만들어지며, 트래픽이 초당 500이라면? 매초 2500개의 오브젝트가 만들어질 것입니다. 이는 아무리 생각해도 이는 메모리 낭비입니다! 이를 해결할 방안은 객체를 딱 하나만 생성해 공유하도록 설계를 하는 것입니다! 이를 싱글톤 패턴이라고 합니다.
2. 싱글톤 패턴
1) 싱글톤 패턴 구현
앞서 말했다시피 싱글톤 패턴은 클래스의 인스턴스가 딱 하나만 생성되는 것을 보장하는 설계라고 했습니다.
이 설계를 달성하기 위해서는 인스턴스가 2개 이상 만들어지는 것을 막아야 합니다.
1. 자기 자신을 클래스 레벨로 선언 (Static) => 데이터 영역
2. private 생성자를 사용해서 외부에서 new 키워드를 사용하지 못하게 조치.
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance() {
return instance;
}
// 생성자를 private로 막아 추가로 생성 방지
private SingletonService() { }
}
2) 싱글톤 패턴의 한계
하지만 싱글톤 패턴이라고 문제점이 없는 것은 아닙니다.
1. 싱글톤 패턴 구현 코드 자체가 많아진다.
2. private 생성자 때문에 상속이 불가능해진다.
- 싱글톤 클래스 자신만이 자기 오브젝트를 만들도록 제한하기 때문에 다른 생성자를 만들지 않는 이상 상속이 불가능합니다. 이는 다형성을 적용하지 못하는 문제를 낳게 됩니다.
- Static 필드와 메소드의 사용 역시 동일한 문제를 발생시킵니다.
3. 테스트가 어렵다
- 만들어지는 방식이 제한적이기 때문에 테스트에서 사용될 때 Mock 오브젝트 등으로 대체하기가 어렵습니다.
- 초기화 과정에서 생성자를 통해 오브젝트를 다이내믹하게 주입하기도 어렵기 때문에 직접 오브젝트를 만들어 사용할 수밖에 없다. 이런 경우 테스트용 오브젝트로 대체가 힘들다.
(=> 배보다 배꼽이 커진다.)
4. 클라이언트가 구체 클래스에 의존하기 때문에 OCP 원칙을 위반할 가능성이 높다.
5. 내부 속성을 변경하거나 초기화하기 어렵다
6. 서버 환경에서 싱글톤이 하나만 만들어지는 것을 보장하지 못한다
- 서버에서 클래스 로더를 어떻게 구성하고 있느냐에 따라 싱글톤 클래스임에도 하나 이상의 오브젝트가 만들어질 수 있습니다.
- JVM에 분산돼서 설치되는 경우 역시 각각 독립적으로 오브젝트가 생성되기 때문에 싱글톤의 가치 하락합니다.
이러한 문제점들 때문에 안티 패턴이라 불리기도 합니다.
그렇다면 싱글톤 패턴을 포기해야 하냐? 그건 또 아닙니다. Spring이 이 문제를 해결해 주거든요!
3) 싱글톤 레지스트리
스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공합니다. 이를 싱글톤 레지스트리라고 합니다.
싱글톤 레지스트리의 가장 큰 장점은 스태틱 메서드와 private 생성자를 사용해야 하는 싱글톤 패턴을 적용하지 않아도 평범한 자바 클래스를 스프링 컨테이너에게 넘겨 싱글톤으로 활용하게 해 준다는 것입니다!
덕분에 싱글톤의 성질을 가지지만 public 생성자를 가질 수 있게 됩니다.
스프링은 어떻게 평범한 자바 클래스를 싱글톤으로 보장해주는 걸까요?
비밀은 바로 클래스의 바이트코드를 조작하는 라이브러리에 있습니다.
스프링 빈과 설정 정보를 등록한 AppConfig.class는 사실 스프링 빈입니다!
@Test
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}
그리고 이 인스턴스를 출력하면...
다음 결과가 나오게 됩니다.
일반적인 클래스 뒤에 EnhancerBySpringCGLIB가 붙어있음을 확인할 수 있는데, 이것은 우리가 직접 만든 클래스가 아닌 Spring이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것입니다.
코드 전문을 보고 싶다면 Enhancer.class를 참고하시기 바랍니다.
public void generateClass(ClassVisitor v) throws Exception {
if (스프링 컨테이너에 등록이 되어 있는지 validate) {
return 스프링 컨테이너에서 찾아 반환
} else {
생성 후 컨테이너에 등록
return 반환
}
'@Bean'이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환, 없으면 생성해서 스프링 빈으로 등록 후 반환하는 싱글톤 로직이 동적으로 생성.
설정 정보에서 @Configuration을 제거하면 스프링 빈 등록은 되지만, 싱글톤을 보장하지 않습니다.
결론적으로 스프링 설정 정보는 항상 @Configuration을 사용하면 동적으로 싱글톤을 보장하게 됩니다!
4) 싱글톤 주의점
싱글톤은 멀티스레드 환경이라면 여러 스레드가 동시에 접근해서 사용할 수 있습니다. 그렇기 때문에 상태를 가지는 것(stateful)에 각별한 주의를 기울여야 합니다! 저장공간이 하나밖에 없는 싱글톤 오브젝트를 동시에 수정하는 것은 매우 위험하기 때문에 항상 무상태로 설계해야 합니다! (읽기 전용 값 제외)
무상태 설계
- 특정 클라이언트에 의존적인 필드가 있으면 안 된다.
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안 된다!
- 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
간단한 예시를 만들어 봅시다.
public class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService bean1 = ac.getBean("statefulService", StatefulService.class);
StatefulService bean2 = ac.getBean("statefulService", StatefulService.class);
bean1.order("userA", 10000); //ThreadA : A사용자 10000원 주문
bean2.order("userB", 15000); //ThreadB : B사용자 15000원 주문 (덮어쓰게 됨)
int priceA = statefulService1.getPrice(); //ThreadA : A사용자 주문 금액 조회
System.out.println("priceA = " + priceA);
Assertions.assertThat(bean1.getPrice()).isEqualTo(10000);
}
}
class StatefulService {
private int price; // 상태를 가지는 필드
public void order(String name, int price) {
System.out.println("name = " + name + ", price : " + price);
this.price = price;
}
public int getPrice() {
return price;
}
}
class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
그렇다면 이를 어떻게 해결해야 할까요?
메서드 파라미터와 로컬 변수, 리턴 값을 이용해야 합니다!
이들은 매번 새로운 값을 저장할 독립적인 공간이 만들어지기 때문에 싱글톤이라 해도 여러 스레드가 변수의 값을 덮어쓸 걱정을 하지 않아도 됩니다!
public class StatefulServiceTest {
@Test
public void Stateful하지않은_싱글톤_테스트() throws Exception{
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
NotStatefulService bean1 = ac.getBean("notStatefulService", NotStatefulService.class);
NotStatefulService bean2 = ac.getBean("notStatefulService", NotStatefulService.class);
int userATotalPrice = bean1.order("userA", 10000); //ThreadA : A사용자 10000원 주문, 따로 저장
int userBTotalPrice = bean2.order("userB", 15000); //ThreadB : B사용자 15000원 주문, 따로 저장
System.out.println("priceA = " + userATotalPrice);
Assertions.assertThat(userATotalPrice).isEqualTo(10000);
}
}
class TestConfig {
@Bean
public NotStatefulService notStatefulService() {
return new NotStatefulService();
}
}
class NotStatefulService {
// private int price; 필드 제거
// 메서드 파라미터 + 로컬변수 이용
public int order(String name, int price) {
System.out.println("name = " + name + ", price : " + price);
return price;
}
}
참고자료
Spring Reference : Core Technologies (spring.io)
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' 카테고리의 다른 글
컴포넌트 스캔, 의존관계 자동 주입 (0) | 2021.10.07 |
---|---|
IoC(2) - 빈(Bean) (0) | 2021.10.07 |
스프링의 등장 (0) | 2021.10.04 |