티스토리 뷰

*웹 어플리케이션에서 클라이언트의 요청에 따른 객체 생성이 빈번히 일어나면 공간 상, 성능 상 큰 제약이 따를 수 있다. 예를 들면 다음과 같이 순수 자바 코드로 AppConfig를 구성하는 경우이다. 

public class AppConfig {
 	public MemberService memberService() {
 		return new MemberServiceImpl(new MemoryMemberRepository());
	 }
 	public OrderService orderService() {
 		return new OrderServiceImpl(
 			new MemoryMemberRepository(),
 			new FixDiscountPolicy());
 	}
}

AppConfig.memberSerivce.~ 의 형태로 여러 클라이언트가 memberService를 호출하면 서로 다른 MemberServiceImpl 객체와 그에 관련되어 있는 객체들까지(위에선 MemoryMemberRepository) 모두 포함해서 계속 생성되고 자바 GC의 작동과 메모리 공간 상 낭비로 이어진다. 

 

 

*이러한 단점을 해결하기 위해 차용한 것이 싱글톤 패턴이다. 

(싱글톤 패턴: 소프트웨어 디자인 패턴 중 하나로, 클래스의 인스턴스가 오직 하나만 생성되도록 보장하는 패턴)

 

싱글톤 패턴을 구성하는 자바 코드는 다음과 같음.

package hello.core.singleton;

public class SingletonService {
	
    //static으로 static 영역에 띄워놓기
    private static final SingletonService instance = new SingletonService();

    public static SingletonService getInstance() {
        return instance;
    }
	
    //private 접근 제한자로 외부에서의 객체 생성 제한
    private SingletonService() {

    }
    
    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }
}

 

*싱글톤 패턴의 단점

- 클라이언트가 구현체에 의존해서 DIP원칙에 위반되고 OCP 원칙도 위반. (ex) 위 코드에서 SingletoneService에 의존하고 있음)

- 싱글톤 패턴 구현에 필요한 코드가 많음.

- 등등의 이유로 유연성이 매우 떨어진다.

 

 

*이러한 싱글톤 패턴의 단점을 모두 해결해주는 스프링 DI 컨테이너

 

-스프링 빈의 기본 등록 방식은 싱글톤 패턴으로 구현되어 있다.(다른 방식으로 바꿀 수도 있음)

//앱 구성을 스프링으로 관리하기 위한 @Configuration
@Configuration
public class AppConfig {

    //스프링 컨테이너에 객체들을 관리 하기 위해 @Bean으로 등록
    //스프링 컨테이너에 등록된 객체들을 '스프링 빈' 이라고 한다
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
    
    ...
}

위와 같이 AppConfig를 구성하면 memberService 객체는 하나만 등록되어 여러 클라이언트들이 이를 공유해서 사용하게 된다. 즉 같은 참조를 하게 된다. 또한 싱글톤 패턴을 코드로 구현할 필요가 없어진다. 

 

단! 싱글톤 패턴을 이용한 스프링 빈의 특성 상 이를 사용할 때 주의할 점이 있다.

---> 한 객체를 여러 클라이언트가 공유하기 때문에, 공유되는 객체 클래스에 '상태'가 들어가 있고 그것을 다른 클라이언트가 접근해서 상태값을 바꿀 수 있다면 큰 문제를 초래할 수 있다. 

package hello.core.singleton;

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;
    }
}

위의 StatefulService가 스프링 컨테이너에 스프링 빈으로 등록 되어 사용된다면, 다음과 같은 문제점을 가질 수 있다.

 

class StatefulServiceTest {

    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);
    }

    static class TestConfig {

        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}

위 코드에서 최종적인 price의 값은 당연히 20000이 되고 사용자A의 주문 금액은 실제로는 10000이었음에도 20000으로 출력된다. 실무에서도 더 복잡한 경우에서 드물지만 일어나는 에러라고 한다. 만약 금액과 관련된 부분에서 문제가 터진다면 큰 사고가 될 수도 있다.

 

따라서, 스프링 빈(싱글톤 패턴을 이용한)을 사용할 때는 다음과 같이 '무상태'로 설계하는 것이 중요하다. 

package hello.core.singleton;

public class StatefulService {

//    private int price; //상태를 유지하는 필드

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

//    public int getPrice() {
//        return price;
//    }
}
class StatefulServiceTest {

    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        //ThreadA: A사용자 10000원 주문
        int userAPrice = statefulService1.order("userA", 10000);
        //ThreadB: B사용자 20000원 주문
        int userBPrice = statefulService2.order("userB", 20000);

        //ThreadA: 사용자A 주문 금액 조회
//        int price = statefulService1.getPrice();
        System.out.println("price = " + userAPrice);
    }

    static class TestConfig {

        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}

 

 

 

참고: 김영한 스프링 핵심 기본 원리(인프런)

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/

'Spring' 카테고리의 다른 글

롬북(Lombok)의 @RequiredArgsConstructor 기능  (0) 2023.08.16
컴포넌트 스캔  (0) 2023.08.15
@Configuration의 역할과 싱글톤  (0) 2023.08.14
좋은 객체 지향 설계 5가지 원칙(SOLID)  (0) 2023.08.09
스프링과 스프링부트  (0) 2023.08.09
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함