(인프런) 김영한님의 스프링 핵심 원리 - 기본편 강의를 보고 정리한 글입니다.
스프링으로 전환하기
저번 포스팅에서 작성한 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();
}
}
먼저 설정을 구성한다는 뜻의 어노테이션인 @Configuration을 클래스 레벨에 붙여줍니다.
다음으로 메서드에 @Bean 을 붙이면 스프링 컨테이너에 스프링 빈으로 등록되는데, 등록될 때는 key(메서드명) : value(반환 객체) 형식으로 등록됩니다! 기본적으로 메서드 명을 스프링 빈의 이름으로 사용하지만, 빈 이름은 직접 부여할 수 있답니다. 빈 이름은 항상 다른 이름을 부여해야된다는 것만 명심해주세요!
public class OrderApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
// OrderService orderService = appConfig.orderService();
// 스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
}
}
그리고 위의 코드처럼 사용할 수 있답니다..
갑자기 새로운 코드가 등장해서 놀라셨죠? 천천히 설명해보겠습니다.
- 주석으로 된 부분은 스프링으로 전환하기 전, 순수 자바 코드로 AppConfig를 호출하는 방법입니다! 개발자가 직접 객체를 생성해 의존관계를 주입하는 것입니다.
- ApplicationContext는 스프링 컨테이너를 말합니다. 이제부터는 스프링 컨테이너를 생성하고 스프링 컨테이너를 통해서 사용할 수 있게됩니다.
- 꺼내고 싶을 때는 getBean(메서드 명, 반환 객체); 와 같은 형식으로 꺼낼 수 있답니다.
그럼 스프링 컨테이너가 생성되는 과정을 자세히 살펴봅시다.
new AnnotationConfigApplicationContext(AppConfig.class) 를 통해 생성하는데, 컨테이너를 생성할 때는 구성 정보를 지정해주어야 합니다. 저는 지난 번 예제에서 작성한 AppConfig.class 를 구성 정보로 넣었습니다.
그리고 파라미터로 넘어온 설정 클래스 정보를 보고, 스프링 빈으로 등록합니다.
위에서 설명한 것과 같이 빈 이름은 메서드 명, 빈 객체는 반환 객체로 등록됩니다.
스프링 빈으로 등록되면 의존관계 설정 준비가 된 것이고, 이후 스프링 설정 정보를 참고하여 의존관계를 주입합니다.
(단순히 자바 코드를 호출하는 것처럼 보이지만, 차이가 있습니다. 이 차이에 대해서는 뒤에서 언급하겠습니다.)
위의 과정을 마치면 스프링 컨테이너가 무사히 생성된 것입니다!
BeanFactory와 ApplicationContext
위에서 ApplicationContext를 스프링 컨테이너라고 말했지만, 더 정확하게 말하자면 BeanFactory, ApplicationContext로 구분해서 이야기를 합니다.
BeanFactory
- 스프링 컨테이너의 최상위 인터페이스로 스프링 빈을 관리하고 조회하는 역할 담당합니다.
- getBean() 을 제공해줍니다.
ApplicationContext
- BeanFactory 기능을 상속받아서 제공합니다.
- BeanFactory 뿐만 아니라 다양한 기능을 상속 받아, 수 많은 부가기능을 제공합니다.
.. 너무 길어서 이렇게 캡처하는 것이 최선이었습니다.. 보이시나요? ㅎㅎ;;
참고로 위의 사진에 BeanFactory가 없어서 당황하셨을 수도 있는데, 타고타고 올라가다보면 최상위에 BeanFactory 인터페이스가 있답니다!
어쨌든 ApplicationContext는 빈 관리기능 + 다양한 부가 기능을 제공하여, BeanFactory 대신 ApplicationContext을 사용한답니다.
싱글톤 컨테이너
스프링 컨테이너는 싱글톤 컨테이너라고도 불립니다.
그럼 싱글톤이란 무엇일까요?
싱글톤 패턴이란, 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴을 말합니다. 따라서 객체 인스턴스가 2개 이상 생성되지 않도록 막아야 합니다.
만약 싱글톤을 적용하지 않는다면, 요청이 들어올 때마다 객체를 생성하게 되는데 이것은 메모리 낭비가 심해 비효율적입니다.
그럼 어떻게 싱글톤을 적용할까요?
싱글톤을 적용한 예제 코드를 보며 설명해드리겠습니다.
public class SingletonService {
// 미리 자기 자신을 내부에 static 으로 생성 -> static 영역에서 하나만 존재하게 됨
private static final SingletonService instance = new SingletonService();
// instance 를 조회할 때 사용: getInstance() 를 호출하면 항상 같은 instance 반환
public static SingletonService getInstance() {
return instance;
}
// 외부에서 생성되는 것을 막기 위해 private 생성자를 만든다! (외부에서 new 카워드로 객체 인스턴스가 생성되는 것을 막는다)
private SingletonService() {}
}
답은 간단합니다!
- static 영역에 객체 instance를 미리 생성해서 올려둡니다.
- 외부에서 객체 인스턴스를 생성하지 못하도록 private 생성자를 사용합니다. 그럼 외부에서 new 키워드로 객체를 생성하고 싶어도 접근할 수 없겠죠?
- 만약 외부에서 이 객체 인스턴스가 필요하다면 getInstance() 메서드를 사용해서 접근하면 됩니다!
위의 방법을 적용한다면 클래스의 인스턴스가 단 1개만 생성된답니다!
하지만 이런 싱글톤 패턴에는 다음과 같은 문제점이 존재합니다.
하지만 스프링 컨테이너는 위와 같은 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리한답니다!!!
지금까지 설명한 스프링 빈이 바로 싱글톤으로 관리되는 빈입니다!
그런데 싱글톤 방식을 적용할 때 주의할 점이 있는데요, 바로 stateless 로 설계해야한다는 것입니다!
만약 상태를 유지할 경우, 어떠한 문제가 생기는지 보여드리겠습니다.
public 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 StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(StatefulService.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
//ThreadA: A사용자 10000원 주문
statefulService1.order("userA", 10000); // 10000원 할당
//ThreadB: B사용자 20000원 주문
statefulService2.order("userB", 20000); // 20000원 할당
//ThreadA: 사용자A 주문 금액 조회
int price = statefulService1.getPrice();
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(10000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
A 사용자가 10000원을 주문하고, 이후에 바로 B 사용자가 20000원을 주문했다고 생각해봅시다.
그리고 A 사용자의 주문 금액을 조회하면 얼마가 10000원이 나와야겠죠?
하지만 실행시켜보면, 20000원이 나온답니다!!;;
생각해보면 당연합니다.
statefulService1 과 statefulService2 는 같은 객체를 사용하고 있잖아요! 이 말은 즉, 둘이 모두 동일한 빈을 참조하고 있단 뜻입니다.
따라서 A 사용자가 주문했을 때 price는 분명 10000 이었지만, B 사용자가 주문하면서 20000 으로 변경된 것입니다 (이해가 되시나요?)
stateful로 관리하려면 다음과 같이 코드를 수정하면 됩니다.
public class StatefulService {
private int price; // 상태를 유지하는 필드
public void order(String name, int price) {
System.out.println("name = " + name + "price = " + price);
return price;
}
}
조회할 때는 order() 메서드를 호출하면 되겠지요?
스프링 빈은 항상 무상태(stateless)로 설계해야 한다는 것을 꼭!! 기억해주세요!
'Spring > 스프링 핵심 원리 - 기본편' 카테고리의 다른 글
스프링 핵심 원리 - 기본편 정리 (1) (0) | 2025.03.09 |
---|