[인프런] 스프링 핵심원리 기본편-싱글톤 컨테이너

경운·2025년 8월 5일
0

스프링 핵심원리

목록 보기
5/7
post-thumbnail

※ 본 게시글은 인프런 스프링 핵심 원리 - 기본편 강의를 바탕으로 작성하였습니다.
강의 내용을 참고하여 개인적으로 정리한 글입니다.


🐣 싱글톤 패턴

  • 클래스의 인스턴스가 1개만 생성하는 것을 보장
  • 객체 인스턴스가 2개 이상 생성하지 못하게 막아야함
    • private 생성자를 사용해서 외부에서 new 키워드를 사용하지 못하도록 막아야함
package hello.core.singleton;

public class SingletonService {

    // 클래스 레벨에 올라가기 때문에 딱 하나만 생성이 됨
    //1. static 영역에 객체를 딱 1개만 생성
    private static final SingletonService instance = new SingletonService();

    //2. public으로 열어서 객체 인스턴스가 필요하면 static 메서드를 통해서만 조회하도록 허용
    public static SingletonService getInstance() {
        return instance;
    }

    //3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막기
    private SingletonService() {
    }

    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }
}
@Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    void singletonServiceTest() {

        //private으로 생성자 막기 / 컴파일 오류 발생
        //new SingletonService();

        //1. 조회 - 호출할 때 마다 같은 객체 반환
        SingletonService singletonService1 = SingletonService.getInstance();

        //2. 조회 - 호출할 때 마다 같은 객체 반환
        SingletonService singletonService2 = SingletonService.getInstance();

        //같은 인스턴스 반환 (참조값이 같은지 확인)
        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);

        // singletonService1 == singletonService2
        assertThat(singletonService1).isSameAs(singletonService2);
        //same == 참조를 비교
        //equal == 객체의 실제 내용 비교
    }

여기까지만 보았을 때 문제점이 발생한다
1. 싱글톤 패턴을 구현하는 코드 자체가 많이 들어감
2. DIP 위반 → 클라이언트가 구체 클라스에 의존
3. 클라이어느가 구체 클래스에 의존하면 OCP 위반
4. 테스트가 어려움 등등
5. 결론적으로 유연성이 떨어짐


🐣 싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리
@Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer() {
    	//스프링 컨테이너
        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);

        //memberService1 == memberService2
        assertThat(memberService1).isSameAs(memberService2);
    }
  • 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리
  • 스프링 컨테이너는 싱글톤 컨테이너 역할 → 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리

🐣 싱글톤 방식의 주의점 (매우 중요!!)

  • 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안됨
  • 무상태로 설계해야함
    • 특정 클라이언트에 의존적인 필드가 있으면 안됨
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안됨
    • 읽기만 가능해야함 → 값을 수정할 수 있으면 안됨
    • 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 사용
public class StatefulService {
    private int price; //상태를 유지하는 필드 10000 -> 20000

    public void order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);
        this.price = price; //여기가 문제!

    }

    public int getPrice() {
        return price;
    }
@Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);
        
        //ThreadA - A사용자 10000원 주문
        statefulService1.order("userA", 10000);
        //ThreadB - B사용자 20000원 주문
        statefulService2.order("userB", 20000);
        
        //ThreadA - A사용자 주문 금액 조회
        int price = statefulService1.getPrice();
        System.out.println("price = " + price);
  • StatefulServiceprice 필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경
  • 우리가 기대했던 값은 10000 이었으나, 실제 값은 20000원 이라는 결과가 나옴

스프링 빈은 항상 무상태로 설계!!


🐣 @Configuration과 싱글톤

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
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 memberService -> new MemoryMemberRepository()
    //@Bean orderService -> new MemoryMemberRepository()

    @Bean
    public MemberService memberService() {
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService() {
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        //return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}
//@Bean을 사용하면 해당 코드들이 "스프링 컨테이너"에 등록

해당 코드들을 보면 memberRepository()가 3번 나오는 것처럼 보인다
1. @Bean이 붙어있는 memberRepository() 호출
2. memberService() 로직에서 memberRepository() 호출
3. orderService() 로직에서 memberRepository() 호출

하지만 결과는 모두 각각 한 번씩만 호출!

💡@Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤은 보장하지 않는다
스프링 설정 정보는 항상 @Configuration을 사용하도록 하자


0개의 댓글