
스프링 컨테이너란
순수 Java만으로는 객체지향 설계원칙인 SOLID중 OCP(개방-폐쇄 원칙)와 DIP(의존관계 역전 원칙)을 지킬 수가 없게된다.
OCP와 DIP를 지키기 위해서는 누군가가 객체에 의존성을 주입해주어야 하는데 그 누군가가 바로 스프링 컨테이너이다.
스프링 컨테이너는 객체를 Component(Bean)으로 만들어 직접 관리하며 객체에 의존성이 필요한 클래스에게 직접 의존성을 주입해준다 이것이 바로 DI(Dependency Injection)이다.
스프링 컨테이너를 이용하여 OCP, DIP를 완전하게 지키며 비로서 완벽한 객체지향 설계원칙을 지키게 된다. 객체지향 설계원칙을 지켜 기획, 요구사항에 변경이 발생하더라도 클라이언트 코드의 수정없이 오로지 스프링컨테이너 설정 코드(@Configuration)만을 수정하여 소프트웨어의 기능, 요구사항을 유연하게 변경할 수 있게된다.
스프링 컨테이너 이전의 의존관계

위 그림에 대해 먼저 간단히 설명하자면
1. MemberRepository는 Memory와 JDBC 두 가지의 구현체가 있어 요구사항에 맞는 저장소를 골라서 사용할 수 있다
2. DiscountPolicy또한 정률할인제, 고정금액 할인제 두 가지를 서비스 목적에 맞춰서 유동적으로 사용할 수 있다.
3. MemberService가 MemberRepository를 의존하는 이유는 join(), find()등의 메서드를 사용하기 위해서는 저장소로부터 저장되어있는 회원의 데이터를 사용해야하기 때문에 MemberService는 MemberRepository를 의존한다.
4. OrderService는 DiscountPolicy와 MemberRepository를 의존하는데 MemberRepository저장소를 통해 회원의 등급을 가져오며, 회원의 등급이 일정 등급 이상일 경우에만 RateDiscountPolicy 또는 FixDiscountPolicy의 할인정책 대상인지를 검증하고 할인을 적용해주어야 하기 때문에 DiscountPolicy와 MemberRepository두 가지 모두 의존한다.
그림을 보면 역할과 구현이 확실하게 분리되어 있으며 잘 지켜진듯 보이지만 코드를보면 그렇지 않음을 알 수 있다. 또한 OCP는 어떻게 지켜지는 것인지 의아할 수 있다. 고정금액할인제를 정률할인제로 바꾸기 위해서는 어쨌든 클라이언트의 코드를 수정해야만 정책을 변경할 수 있게된다. 이는 확장에는 열려있고 변경에는 닫혀있어야하는 OCP원칙에 위배되는 행위이다.
아래 코드를 살펴보자
MemberService는 신경쓰지말고 주문관련 로직만 살펴보자
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
위 코드를 보면
OrderServiceImpl은 멤버변수로 MemberRepository, DiscountPolicy만을 의존하며 인터페이스만을 의존하고 있기떄문에 SOLID원칙이 잘 지켜지고 있는 것으로 보인다.
그리고 현재는 FixDiscountPolicy를 생성했기때문에 고정할인 정책을 사용하고있으며 만약 정률할인정책으로 바꾸기 위해서는
`new FixDiscountPolicy();`를 `new RateDiscountPolicy();`로 단 한 줄만 변경해주면 된다.
하지만 아래 그림을 살펴보자 FixDiscountPolicy를 RateDiscountPolicy로 코드를 변경하는 순간 OrderServiceImpl클래스는 인터페이스를 의존하는 것이 아닌 구현체들을 의존하게 되는것이다.
뿐만 아니라 클라이언트의 코드를 수정했기때문에 SOLID원칙이 잘 지켜지고 있지 않음을 알 수 있다.

즉, 코드를 변경하는 순간 OrderServiceImpl은 DiscountPolicy인터페이스를 의존함과 할인정책을 바꾸기 위해서는 DIscountPolicy구현체의 객체생성 코드도 변경해야하기때문에 RateDiscountPolicy 또는 FixDiscountPolicy도 의존하게 되는것이다.
AppConfig를 이용하여 리팩터링하기
AppConfig라는 의존성을 부여주며 중계역할을 해주는 클래스를 통해 클라이언트의 코드를 고치지않고 오로지 AppConfig(설정 파일)만을 수정하여 할인정책을 바꿀 수 있도록 코드를 리팩터링 할 수 있습니다.
뿐만아니라 AppConfig클래스를 통해 SOLID원칙을 만족하는 애플리케이션 설계를 할 수 있습니다.
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
//return new FixDiscountPolicy();
}
}
AppConfig클래스에서 메서드를 통하여 객체를 생성함과 동시에 의존성을 주입해주게 되면 먼저 DIP를 완벽하게 지키고, 변경에는 닫혀있는 OCP또한 만족하게 됩니다. discountPolicy()메서드에서는 RateDiscountPolicy()를 FixDiscountPolicy로 변경해주기만하면 정책을 바꿔 클라이언트의 코드를 수정할 필요가 없게됩니다.
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
OrderServiceImpl코드에서는 기존에는 필드에서 new생성자를 통해 의존성을 주입했지만 생성자의 파라미터를 통해 의존성을 주입 받게됩니다.
하지만 위의 코드들은 서비스를 요청할 때마다 새로운 객체를 생성하게되고, 만약 5천명의 클라이언트가 서비스를 요청하게 되면 5천개의 객체를 생성해야하므로 이는 비용측면에서 매우 비효율적인 설계가 될 수 있습니다. 웹 애플리케이션은 수많은 클라이언트들이 동시에 많은 양의 서비스를 요청하는 일이 보통입니다.
이때는 '싱글톤 패턴'을 적용하여 비용을 최대한 절감하여 사용할 수 있습니다.
물론 순수 자바코드만으로도 싱글톤 패턴을 적용할 수 있지만, 이는 코드가 지저분해지기도 하며 스프링컨테이너의 싱글톤을 사용한다면 싱글톤패턴 이외에도 여러가지 이점을 얻을 수 있습니다
@Test
@DisplayName("스프링 컨테이너 X, 싱글톤 X")
void singletonTest(){
AppConfig ac = new AppConfig();
MemberService memberService1 = ac.memberService();
MemberService memberService2 = ac.memberService();
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
}

싱글톤 패턴이 적용된 스프링컨테이너
먼저 스프링컨테이너를 사용하기위해 AppConfig클래스를 다시 리팩터링 해주게되면 아래와 같게됩니다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
//return new FixDiscountPolicy();
}
}
AppConfig에 @Configuration와 각각의 메서드에 @Bean어노테이션을 붙여주기만 하면 AppConfig를 스프링 컨테이너로 사용할 수 있습니다.
이제 스프링컨테이너에서 의존성을 관리하게 되며 이는 DIP를 완벽하게 지켜냄을 알 수 있게됩니다. 앞으로 Spring 프레임워크가 의존성을 직접 주입해주게 되며 동시에 역할과 구현을 완벽하게 분리시키기도 했습니다.
이제 싱글톤이 적용되었는지 확인해보기위해 아래와 같은 테스트코드를 작성해보겠습니다
아래 코드가 AppConfig.class를 스프링 컨테이너로 만들어주는 역할을 수행합니다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("스프링 컨테이너 O, 싱글톤 O")
void springSingletonTest(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
assertThat(memberService1).isSameAs(memberService2);
}

위의 실행결과를 통해 알 수 있듯이 memberService1과 memberService2는 같은 객체참조주소를 가지고 있음을 알 수 있습니다.
싱글톤 스프링 컨테이너 주의사항
실무에서 싱글톤 방식을 사용할 때 매우 유의해야 할 점이 있다. 아래에 코드에서 살펴보자
public class StateFulService {
private int price;
public void order(String name, int price){
this.price = price;
System.out.println("name = " + name + " price = " + price);
}
public int getPrice(){
return price;
}
}
public class StatefulTest {
@Test
@DisplayName("싱글톤 패턴의 주의사항")
void statefulSingleton(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StateFulService stateFulService1 = ac.getBean("stateFulService", StateFulService.class);
StateFulService stateFulService2 = ac.getBean("stateFulService", StateFulService.class);
stateFulService1.order("userA", 10000);
stateFulService2.order("userB", 20000);
System.out.println("userA price : " + stateFulService1.getPrice());
}
static class TestConfig{
@Bean
public StateFulService stateFulService(){
return new StateFulService();
}
}
}
위와 같이 코드를 짜게 되면 StateFulService클래스를 스프링 컨테이너로 등록하면 싱글톤 방식이 적용되게 된다.
그러면 자연스레 price필드에는 order서비스를 제공할때 값이 유동적으로 변하게되는데,
stateFulService1.order("userA", 10000);
stateFulService2.order("userB", 20000);
System.out.println("userA price : " + stateFulService1.getPrice());
위 코드와 같이 userA의 서비스를 제공하기 전에 userB가 중간에 끼어들어오게 된다면
userA의 price또한 20000으로 바뀌게 되어 결제금액이 2배로 증가되는 큰 서비스결함이 발생하게 된다.
위와 같이 필드가 공유되는 설계를 StateFull이라 하며 좋은 설계 방식이 아니다.
스프링 빈은 아래와 같이 Stateless 즉, 무상태로 설계되어야 한다.
public class StateFulService {
public int order(String name, int price){
System.out.println("name = " + name + " price = " + price);
return price;
}
}
public class StatefulTest {
@Test
@DisplayName("싱글톤 패턴의 주의사항")
void statefulSingleton(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StateFulService stateFulService1 = ac.getBean("stateFulService", StateFulService.class);
StateFulService stateFulService2 = ac.getBean("stateFulService", StateFulService.class);
int userAPrice = stateFulService1.order("userA", 10000);
int userBPrice = stateFulService2.order("userB", 20000);
System.out.println("userAPrice = " + userAPrice);
System.out.println("userBPrice = " + userBPrice);
}
static class TestConfig{
@Bean
public StateFulService stateFulService(){
return new StateFulService();
}
}
}

@Configuration과 싱글톤
AppConfig 클래스의 코드를 한 번 살펴보자
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
//return new FixDiscountPolicy();
}
}
위 코드에서 보면 memberRepository() 메서드는 적어도 2번 이상 new키워드를 통해 생성되는 것을 알 수 있다.
memberService()에서 한번,
orderService()에서 한번 총 두번 호출되는 것을 확인할 수 있는데 new생성자를 통해 두 번 생성되게 된다면 싱글톤이 깨지게 된다는 것인데 우리는 이전에 싱글톤이 깨지지 않고 같은 참조주소를 반환해준다는 것을 이미 확인했다.
이는 스프링이 CGLIB이라는 바이트코드 조작 라이브러리를 사용해서 스프링 컨테이너가 싱글톤을 유지할 수 있도록 관리해주는 것이다
만약 A라는 Bean이 이미 스프링 컨테이너에 등록되어있다면 스프링 컨테이너는 A Bean을 생성하지 않고,
반대로 A라는 Bean이 스프링 컨테이너에 등록되어있지 않다면 스프링 컨테이너에 A라는 Bean을 생성하여 관리하는 것이다.
위 내용은 인프런 김영한님의 스프링 핵심원리 - 기본편 강의를 기반으로 공부한 내용을 복습한 포스팅입니다.
혹시라도 잘못된 부분이 있다면 댓글로 남겨주시면 감사하겠습니다!
'Framework > Spring' 카테고리의 다른 글
Spring MVC - @RequestMapping (0) | 2024.04.16 |
---|---|
Spring MVC - 프레임워크 사용 전과 후 & @Controller (0) | 2024.04.15 |
Spring MVC - Dispatcher Servlet과 View Resolver (0) | 2024.04.15 |
객체지향 설계를 위한 5가지 원칙 (SOLID) (0) | 2024.03.20 |
스프링이란? with 객체지향 (0) | 2024.03.20 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!