우테코 레벨 2 자동차 경주 미션을 진행하면서, 순수 자바로만 짜여졌던 기존 코드를 스프링을 활용해 웹으로 구현하게 되었다.
따라서 기존 자바 클래스 중 특정 몇몇을 스프링 빈으로 등록시켜 사용하고자 했는데, 이 때 어떤 기준으로 빈으로 등록해야 할까에 대한 고민이 있었다.
이에 대해 공부해보니 스프링 빈으로 등록된 클래스는 기본적으로 싱글톤으로 관리되고 있었다. 따라서 이 관리 개념에 대해 공부가 되어 있다면, 스프링 빈으로 등록할지 말지에 대한 기준을 세울 수 있다고 판단했다!
이전 포스팅에서 자바를 활용해 만들 수 있는 싱글톤 패턴의 장점과 단점에 대해 설명해 보았다. 싱글톤 패턴은 멀티스레드 환경에서 자원의 낭비를 방지하기 위해 필요한 중요 개념이다. 그러나 단순 자바를 활용해 구현한 싱글톤 패턴에는 한계가 있었고 스프링은 어떻게 싱글톤 패턴을 적용하면서 한계를 탈피했는지 정리하고자 했다.
public class JavaSingleTon {
private static JavaSingleTon javaSingleTon = null;
private JavaSingleTon() {
}
public static JavaSingleTon getInstance() {
if (javaSingleTon == null) {
javaSingleTon = new JavaSingleTon();
}
return javaSingleTon;
}
}
자바 싱글톤 패턴은 다음과 같이 만들어진다. 클래스 밖에서 생성을 못하도록 생성자를 private으로 강제하고, 정적 메소드를 통해 객체가 호출되는 최초 시점에 static 영역에 인스턴스가 만들어지고, 만들어진 인스턴스를 이후 요청에서 응답한다.
그러나 자바에서 싱글톤 패턴을 활용해 구현하면 많은 문제에 직면한다.
- private 생성자를 갖고 있는 덕에 상속이 불가능하다
- 만들어지는 방식이 제한적이므로 테스트 시 목 오브젝트 등으로 대체하기가 어렵다.
- 또 서버에서 클래스로더를 어떻게 구성하느냐에 따라, 확실한 싱글톤 보장이 어렵다. 여러 개의 jvm에 분산되어 설치 되는 경우에도 각각 독립적으로 오브젝트가 생기기 때문이다.
- static 메소드를 사용하므로 아무 객체나 자유롭게 접근 수정할 수 있다
-> 즉, 클라이언트가 구체 클래스에 의존해 dip를 위반하고, 확장이 불가능하므로 ocp를 위반한다!
스프링은 이러한 싱글톤 패턴의 장점은 취하고 단점은 피하려 했다.
따라서 위의 예시처럼 기술한 싱글톤 패턴(static 사용, private 생성자 사용 등...)으로 클래스를 구현하지 않고도, 스프링 컨테이너에 등록된 빈을 기본적으로 싱글톤으로 관리한다.
쉽게 말해서 우리가 평범하게 짠 자바 코드도 싱글톤으로 관리할 수 있다는 것이다.
왜냐? 구현한 자바 클래스의 오브젝트 생성, 관계설정, 사용 등의 제어권을 IOC 컨테이너(= 애플리케이션 컨텍스트)에 넘기는 순간,
아무런 제약을 걸지 않을 경우 제어권을 가진 IOC 컨테이너가 스프링 빈으로 등록된 클래스의 인스턴스를 생성하고, 이를 저장해 싱글톤으로 관리 및 응답한다.
클래스의 생애주기 제어권을 모두 IOC 컨테이너에 넘겼기에 가능한 것이다. 따라서 스프링은 IOC 컨테이너이자 싱글톤을 관리하는 싱글톤 레지스트리이다.
따라서 스프링은 객체지향적 설계 방식과 원칙을 지키는데 가진 자바의 한계를 없애준다고 할 수 있다.
import org.springframework.stereotype.Component;
@Component
public class SpringSingleTon {
}
다음과 같이 @Component를 활용하여 해당 클래스를 bean으로 등록시켜 준다.
@SpringBootApplication
public class SingletonApplication {
public static void main(String[] args) {
SpringApplication.run(SingletonApplication.class, args);
ApplicationContext ac = new AnnotationConfigApplicationContext(SpringSingleTon.class);
SpringSingleTon bean1 = ac.getBean(SpringSingleTon.class);
SpringSingleTon bean2 = ac.getBean(SpringSingleTon.class);
System.out.println("bean1 = " + bean1);
System.out.println("bean2 = " + bean2);
}
}
이후 애플리케이션 컨텍스트를 구동시켜 해당 ac에 등록된 SpringSingleTon 클래스의 빈을 조회해봤다.
콘솔을 확인하면 여러 번 호출해도 같은 주소값의 빈이 반환되는 것을 볼 수 있다.
즉, 현재 빈으로 등록된 SpringSingleTon 클래스는 싱글톤으로 관리되는 것이다!
스프링 빈은 하나의 애플리케이션 컨텍스트 안에서만 싱글톤으로 관리된다. 생각해 보면 당연한 일이다. 싱글톤으로 관리하고 생성하는 주체가 애플리케이션 컨텍스트이니까...
@SpringBootApplication
public class SingletonApplication {
public static void main(String[] args) {
SpringApplication.run(SingletonApplication.class, args);
ApplicationContext ac1 = new AnnotationConfigApplicationContext(SpringSingleTon.class);
SpringSingleTon bean1ac1 = ac1.getBean(SpringSingleTon.class);
SpringSingleTon bean2ac1 = ac1.getBean(SpringSingleTon.class);
System.out.println("bean1ac1 = " + bean1ac1);
System.out.println("bean2ac1 = " + bean2ac1);
ApplicationContext ac2 = new AnnotationConfigApplicationContext(SpringSingleTon.class);
SpringSingleTon bean1ac2 = ac2.getBean(SpringSingleTon.class);
SpringSingleTon bean2ac2 = ac2.getBean(SpringSingleTon.class);
System.out.println("bean1ac2 = " + bean1ac2);
System.out.println("bean2ac2 = " + bean2ac2);
}
}
위와 같이 애플리케이션 컨텍스트가 달라지는 순간 다른 인스턴스를 반환하는 것을 확인할 수 있다.
그렇다면 자원을 아끼기 위해 모든 상황과 경우의 수에서 스프링 내의 자바 클래스를 싱글톤 레지스트리에 등록해 사용하는 것이 좋을까?
결론부터 말하면, 그렇게 작성하면 큰일난다. 그 이유에 대해서는 코드를 통해서 확인해보자.
@Component
public class UserPayment {
private int payedPrice;
public void pay(String userName, int payedPrice) {
System.out.println("userName = " + userName + ", payedPrice = " + payedPrice);
this.payedPrice = payedPrice;
}
}
만약 다음과 같이 특정 유저의 지불 금액을 관리하는 클래스가 있다고 가정해 보자. pay(지불)이라는 메소드를 통해서 특정 유저가 얼마를 지불했는지를 저장할 수 있다. 이를 @Component를 통해 스프링 빈으로 등록하면 어떻게 될까?
@SpringBootApplication
public class SingletonApplication {
public static void main(String[] args) {
SpringApplication.run(SingletonApplication.class, args);
ApplicationContext ac = new AnnotationConfigApplicationContext(UserPayment.class);
UserPayment userA = ac.getBean(UserPayment.class);
UserPayment userB = ac.getBean(UserPayment.class);
userA.pay("userA", 10000);
userB.pay("userB", 1000000);
System.out.println("userA.getPayedPrice() = " + userA.getPayedPrice());
}
}
빈 조회를 통해 userA와 userB를 생성한다. userA가 10000원을 지불하도록, userB가 1000000원을 지불하도록 한 후, userA의 지불가격을 조회해 보자. 결과가 대충 짐작 갈 것이다.
처음에 분명 10000원을 지불했던 userA의 지불가격이 1000000원으로 껑충 뛰었음을 확인할 수 있다. 당연하다. userA와 userB는 같은 객체를 바라보고 있으니까! userB가 userA의 값을 변경하게 된 것이다.
만약 내가 분명 10000원을 썼는데, 1000000원이 카드에서 빠져나간다고 생각해보자. 아마 개발자에게 소송을 걸 것이다.
싱글톤은 특정 클라이언트에 의존적인 상태를 가지면 안 된다!
위 UserPayment를 스프링 빈으로 등록해 싱글톤으로 관리하면 안되는 궁극적 이유는 무엇이었을까?
이유는 클래스의 성격 자체가 특정 클라이언트에 의존적이기 때문이다. 따라서 상태를 가지게 설계되었고, 다른 클라이언트가 등장했을 때 이전 클라이언트의 상태를 그대로 가져와 비즈니스가 꼬이게 되었다.
그렇다면 빈으로 관리하려는 클래스에는 인스턴스 변수를 사용하면 안되는 것일까? 꼭 그렇지만은 않다.
정확히 말하면 인스턴스 변수로 주입하는 객체 역시 무상태로 설계된, 자신이 사용하는 다른 싱글톤 빈 클래스일 경우에는 가능하다.
이러한 클래스들은 스프링이 한 번 초기화해주고나면 이후 수정될 일이 존재하지 않기에, 멀티스레드 환경에서 변화할 걱정을 하지 않아도 되기 때문이다.
그렇기 때문에 우리가 @Service로 사용하는 클래스에서 @Repository 등으로 사용하는 클래스를 사용할 수 있는 것이다. 빈으로 사용하는 클래스이기 때문이다.