본문 바로가기
Spring

[Spring/스프링] 싱글톤 패턴(Singleton pattern) 과 @Scope prototype

by LasBe 2022. 4. 2.
반응형

목차

    📒 싱글톤 패턴(Singleton pattern) 과 @Scope prototype


    📌 싱글톤 패턴

    public class SingletonPattern {
    
    	private static SingletonPattern sp = new SingletonPattern();
    	
    	// 외부에서 생성자를 사용하지 못하도록 private 접근 지정자 사용
    	private SingletonPattern() {}
    	
    	public static SingletonPattern getInstance() {
    		return sp;
    	}
    	
    	public void print() {
    		System.out.println("싱글톤 패턴입니다");
    	}
    }

    싱글톤 패턴은 클래스의 객체를 하나만 유지해 메모리의 낭비를 막는 디자인 패턴 중 하나입니다.

     

    위 코드와 같이 객체를 static 키워드를 이용해 메모리에 올려놓고 반환 메소드로만 인스턴스를 사용할 수 있게 합니다.

     

    그다음 생성자를 private 접근 지정자로 외부에서 사용할 수 없게 막음으로써 하나의 객체를 유지하도록 할 수 있습니다.

     

    🔎 장점

    처음 실행 시 클래스의 인스턴스를 static 키워드를 통해 고정된 메모리 영역으로 올리고

    올려둔 인스턴스를 모두 공유해서 사용하기 때문에 메모리 효율면에서 메리트가 있습니다.

     

    생성자는 private 접근 지정자를 사용해 외부에서 접근하지 못하도록 함으로써

    클래스의 인스턴스가 단 1개만 보장되어야 할 때 사용할 수 있습니다.

     

    또한 하나의 인스턴스를 공유하다보니 데이터의 공유가 편리하다는 장점이 존재합니다.


    🔎

    하나의 인스턴스로 모두가 사용하다보니 데이터의 공유가 쉽지만

    멀티쓰레딩 환경에서 동시에 사용하다보면 동기화 문제가 생길 수 있어 별도의 처리를 해주어야 합니다.

     

    또한 싱글톤 패턴을 적용한 클래스를 사용할 수록 클래스간 결합도가 높아져 유지보수에 많은 비용을 쏟아야 할 수 있습니다.

     

     

     

    📌 스프링과 싱글톤

    스프링 컨테이너는 빈을 등록할 때 별다른 설정이 없다면 기본적으로 빈을 싱글톤으로 관리합니다.

     

    그렇기 때문에 여러 곳에서 같은 인스턴스를 사용할 때 상태를 변경하는 코드를 작성한다면 문제가 발생할 수 있습니다.

     

    코드로 직접 확인해보겠습니다.


    @Component
    public class Computer {
    	private Boolean power;
    	
    	public Computer() {
    		this.power = true;
    		System.out.println("컴퓨터를 켰습니다");
    	}
    
    	public void offPower() {
    		this.power = false;
    		System.out.println("컴퓨터의 전원을 껐습니다");
    	}
    
    	@Override
    	public String toString() {
    		return "{\"power\":\"" + power + "\"}";
    	}
    }

     

    위 클래스는 인스턴스를 생성시 전원이 켜지고 offPower() 메소드로 전원을 끌 수 있는 컴퓨터입니다.

     

    public class Main {
    	public static void main(String[] args) {
    		ApplicationContext factory =
    				new AnnotationConfigApplicationContext(SingletonConfig.class);
    		
    		Computer c1 = factory.getBean("computer", Computer.class);
    		Computer c2 = factory.getBean("computer", Computer.class);
    		System.out.println("컴퓨터 1 : " + c1);
    		System.out.println("컴퓨터 2 : " + c2);
    		
    		c1.offPower(); // computer1 종료
    		System.out.println("컴퓨터 1 : " + c1);
    		System.out.println("컴퓨터 2 : " + c2);
    	}
    }

    컨테이너에서 빈을 받아와 컴퓨터1, 컴퓨터2 객체를 생성했습니다.

     

    생성자에서 power에 true 값을 넣어줬기 때문에 두 컴퓨터 전부 전원이 켜져있는 상태입니다.

     

    객체 생성 후 컴퓨터1의 전원만 c1.offPower()를 이용해 꺼주었습니다.

    결과를 확인하면 손도 대지 않은 컴퓨터2까지 종료가 된 것을 볼 수 있습니다.

     

    주소를 한번 확인해보겠습니다.

    컨테이너가 인스턴스를 싱글톤으로 관리하기 때문에 두 객체가 같은 주소를 사용하는 것을 볼 수 있습니다.

     

    위와 같은 이유 때문에 스프링에서 싱글톤으로 빈을 관리할 때

    클래스를 변경할 수 없는 무상태(statusless) 방식으로 만들지 않으면 문제가 생길겁니다.

     

    그렇다면 인스턴스를 생성할 때마다 다른 주소를 어떻게 배정하려면 어떻게 해야할까요?

     

     

     

    📌 @Scope

    @Scope는 빈을 리턴하는 방식을 설정할 수 있는 어노테이션입니다.

     

    아래와 같이 여러 값을 사용 가능하며, 지정하지 않았을 때 기본 값은 singleton 입니다.

    • singleton : IoC 컨테이너당 하나의 빈을 리턴
    • prototype : 요구가 있을 때 마다 새로운 빈을 만들어 리턴
    • request : HTTP request 객체당 하나의 빈을 리턴
    • session : HTTP session 당 하나의 빈을 리턴
    • globalSession : 전체 모든 세션에 대해 하나의 빈을 리턴

     

    위에서 확인한 코드의 문제를 해결하려면 Computer 클래스에 prototype을 적용하면 되겠죠?

    @Component
    @Scope(value="prototype")
    public class Computer {
    	private Boolean power;
    	
    	public Computer() {
    		this.power = true;
    		System.out.println("컴퓨터를 켰습니다");
    	}
    
    	public void offPower() {
    		this.power = false;
    		System.out.println("컴퓨터의 전원을 껐습니다");
    	}
    
    	@Override
    	public String toString() {
    		return "{\"power\":\"" + power + "\"}";
    	}
    }

    이 어노테이션은 클래스 위에 @Scope(value="[값]") 방식으로 선언하면 적용됩니다.

     

    그럼 @Scope를 적용한 뒤 결과창을 확인해 보겠습니다.

     

    두 컴퓨터가 리턴받은 주소가 다르다보니

    상태를 변경해도 연쇄적으로 적용받지 않는 모습을

    볼 수 있습니다.

     

     

     

     

    📌 @Scope(value="prototype") 적용되지 않을 때

    두 클래스가 의존관계에 있다고 할 때 프로토타입의 객체가 싱글톤 객체에게 의존성을 가지면 문제가 없지만

    싱글톤 객체가 프로토타입 객체에게 의존성을 가지면 의도한대로 작동하지 않는 문제가 발생합니다.

     

    아래 코드는 싱글톤인 Room 객체를 빈으로 등록할 때, 프로토타입 Computer 클래스를 주입하는 코드입니다.

     

    만약 Room을 getBean으로 여러번 받아올 때, 프로토타입인 Computer를 계속해서 새로 받아올까요?

     

    한번 확인해보겠습니다.

    @Component
    public class Room {
    	private Computer computer;
    	
    	@Autowired
    	public Room(Computer computer) {
    		this.computer = computer;
    	}
    
    	public void print() {
    		System.out.println("Room : "+this+" / Computer : "+computer);
    	}
    }
    
    //////////////////////////////////////////////////////////////////////
    
    @Component
    @Scope(value="prototype")
    public class Computer {
    }​

     

    컴퓨터 클래스에 프로토타입 스코프를 적용하고

    아래에선 Room 객체를 두 번 불러왔습니다.

     

    public class Main {
    	public static void main(String[] args) {
    		ApplicationContext factory =
    				new AnnotationConfigApplicationContext(SingletonConfig.class);
    
    		Room room = factory.getBean("room", Room.class);
    		System.out.print("BeforeRoom = "); room.print();
    		
    		room = factory.getBean("room", Room.class);
    		System.out.print("AfterRoom = "); room.print();
    	}
    }

    Computer 클래스에 프로토타입을 적용해 빈을 새로 받아오면

    싱글톤인 room의 주소는 그대로이고 Computer는 바뀐 주소로 적용될 것이라 생각했지만 완전히 같은 모습을 볼 수 있습니다.

     

    이러한 이유는 Computer 객체가 프로토타입 스코프가 적용되어 있더라도

    싱글톤인 Room 객체가 생성되어 빈으로 등록될 때 의존성을 주입한 Computer 인스턴스만 계속해서 참조하기 때문입니다.

     

    다음은 저희가 의도한대로 Room 빈을 받아올 때 새로운 Computer 인스턴스를 받아오기 위해 프록시 모드를 알아보겠습니다.

     

     

     

    📌 proxyMode

    @Scope 어노테이션에 프록시 모드를 적용하면,

    "프로토타입 객체""참조하는 객체"들이 직접 "프로토타입 객체"를 참조하는 것이 아닌

    프록시로 감싼 "프록시 인스턴스"를 참조하기 때문에 계속해서 참조할 때마다

    "프록시 인스턴스"에서 새로운 인스턴스를 받아올 수 있습니다.

     

    @Component
    @Scope(value="prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public class Computer {
    }​

    Computer 클래스에서 스코프 어노테이션에 proxyMode = ScopedProxyMode.TARGET_CLASS 를 추가했습니다.

     

    그 후 실행해보면 서로 다른 Computer 객체의 주소가 적용된 모습을 확인할 수 있습니다

     

    반응형

    댓글


    오픈 채팅