✅ ApplicationContext와 Factory 객체 생성 비교
DaoFactory
에서 객체를 반환하는 것과 ApplicationContext
에서 객체를 반환해서 사용하는 것이 큰 차이를 보여주지 않는 것 같습니다. 그렇다면 각각 객체를 반환할 때 같은 객체를 반환하는 것일까요? 왜 DaoFactory
처럼 직접 팩토리를 만들어 사용하지 않고 ApplicationContext
를 이용하는지 반환하는 객체를 비교하면서 알아보겠습니다.
- DaoFactoryTest
package com.jhcode.spring.ch1.dao; //== Factory에서 직접 생성한 객체가 같은지 테스트하는 클래스 ==// public class DaoFactoryTest { public static void main(String[] args) { DaoFactory factory = new DaoFactory(); UserDao dao1 = factory.userDao(); UserDao dao2 = factory.userDao(); System.out.println(dao1); System.out.println(dao2); } }
DaoFactory
에서 생성한 객체가 같은지 알아보기 위해 따로 테스트 클래스를 만들었습니다. 이때 userDao() 메소드를 통해 반환받은 dao1과 dao2는 같은 객체일까요? 여기서 같은 객체는 동일성을 지닌 객체인지 묻는 것입니다. 동일한 객체라면 메모리 상의 등록된 객체는 하나이고 참조 변수가 두 개이므로, 해당 객체를 출력할 때 나오는 해쉬코드의 값이 동일할 것입니다.결과에서 알 수 있듯이 객체가 두 개가 생성되었습니다.
DaoFactory
에서 new 키워드를 사용하여 생성된 객체를 반환하기 때문에 충분히 예상할 수 있는 결과라고 생각할 수 있습니다. 그러면ApplicationContext
는 어떨까요?
- DaoFactoryTest
package com.jhcode.spring.ch1.dao; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; //== Factory에서 직접 생성한 객체가 같은지 테스트하는 클래스 ==// public class DaoFactoryTest { public static void main(String[] args) { DaoFactory factory = new DaoFactory(); UserDao dao1 = factory.userDao(); UserDao dao2 = factory.userDao(); System.out.println(dao1); System.out.println(dao2); ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class); UserDao dao3 = context.getBean("userDao", UserDao.class); UserDao dao4 = context.getBean("userDao", UserDao.class); System.out.println(dao3); System.out.println(dao4); } }
ApplicationContext
는 객체를 여러번 호출하더라도 동일한 객체를 반환하는 것을 알 수 있습니다.
✅ 싱글톤 레지스트리로서의 애플리케이션 컨텍스트
애플리케이션 컨텍스트는 우리가 만들었던 DaoFactory
와 비슷한 방식으로 동작하는 IoC 컨테이너입니다. 그러면서 동시에 싱글톤을 저장하고 관리하는 싱글톤 레지스트리(singleton registry)이기도 합니다.
- 왜 싱글톤인가?
스프링은 대규모의 언터프라이즈 서버환경에서 주로 사용하는 기술입니다. 클라이언트의 요청에 따라 매번 새롭게 객체를 생성하면, 요청이 많아질수록 서버의 부하가 커지게 됩니다. 싱글톤으로 객체를 관리할 경우, 객체 간의 일관성을 유지할 수 있고, 메모리 사용을 최적화할 수 있다는 장점이 있습니다. 한 개의 오브젝트만 만들어서 사용하는 것을 싱글톤 패턴이라고 합니다.
다만, 디자인 패턴에 소개된 싱글톤 패턴은 사용하기 까다롭고 여러 가지 문제점이 있습니다. 피해야할 패턴이라는 의미로 안티패턴이라고 부르기도 합니다. 싱글톤 디자인 패턴에 대해 자세하게 알아보겠습니다.
🔹싱글톤 패턴의 한계
—Java에서 싱글톤을 구현하는 방법은 보통 아래와 같다.
- 클래스 밖에서 객체를 생성하지 못하도록 생성자를 private으로 만든다.
- 생성된 싱글톤 오브젝트를 저장할 수 있는 자신과 같은 타입의 static 필드를 정의한다.
getInstance()
를 만들고 이 메소드가 최초로 호출되는 시점에서 한 번만 오브젝트가 만들어지게 한다. 생성된 오브젝트는 static 필드에 저장된다.
- 한 번 객체가 만들어지면
getInstance()
메소드를 통해 이미 만들어져 있는 객체를 반환한다.👉UserDao에 싱글톤 패턴을 적용해보겠습니다.
package com.jhcode.spring.ch1.dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import com.jhcode.spring.ch1.domain.User; public class UserDao { //생성자 : 이전 코드 //public UserDao(ConnectionMaker connectionMaker) { // this.connectionMaker = connectionMaker; //} //== 싱글톤 ==// private static UserDao INSTANCE; private static ConnectionMaker connectionMaker; private UserDao(ConnectionMaker connectionMaker) { this.connectionMaker = connectionMaker; } public static synchronized UserDao getInstance() { if (INSTANCE == null) { INSTANCE = new UserDao(?????); return INSTANCE; } return INSTANCE; }
- private static UserDao INSTANCE
생성된 객체를 필드에 저장하기 위해 선언한다.
- private UserDao(ConnectionMaker connectionMaker)
생성자를 정의한다. ConnectionMaker을 인자로 받는 생성자, 받은 인자를 자기 자신의 필드에 저장한다.
- public static synchronized UserDao getInstance()
synchronized
: 다중 스레드 환경에서 안정성을 보장하기 위해 동기화를 수행하는 것을 의미한다.생성된 객체가 없을 경우에만 새로운 객체를 생성한다. 다만
ConnectionMaker
변수가static
변수가 아니기 때문에 현재 메소드에서 주입할 수 없다. 또한 정의한 생성자가private
이기 때문에DaoFactory
에서도ConnectionMaker
객체를 주입하여UserDao
객체를 생성할 수 없다.- UserDao(ConnectionMaker) is not visble
- static reference to the non-static
👉따라서 싱글톤은 다음과 같은 한계를 가지고 있습니다.
- UserDao(ConnectionMaker) is not visble
- private static UserDao INSTANCE
- private 생성자를 갖고 있기 때문에 상속할 수 없다.
싱글톤 패턴은 생성자를 private으로 제한합니다. 오직 싱글톤 크래스 자신만이 자기 자신의 오브젝트를 만들도록 제한하는 것입니다. 문제는 private 생성자를 가진 클래스는 다른 생성자가 없다면 상속이 불가능합니다. 따라서 객체지향의 장점인 상속과 다형성을 사용할 수 없게 됩니다.
- 테스트하기가 힘들다
전역 상태를 공유하고 의존성이 강한 객체이기 때문에, 싱글톤 객체에 대한 모의(Mock) 또는 대체(Stub) 객체를 생성하여 테스트하는 것이 어렵습니다. 하나의 객체만 만드는 방식이기 때문에 객체를 생성하는 것 자체가 까다롭습니다.
- 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.
사용하는 클라이언트가 정해져 있기 않기 때문에 언제든지 static 메소드를 통해서 싱글톤에 쉽게 접근할 수 있습니다. 아무 객체나 자유롭게 접근하고 수정하고 공유할 수 있는 전역 상태의 객체를 갖는 것은 객체지향 프로그래밍에서 권장하지 않는 모델입니다.
👉그렇기 때문에 스프링에서는 싱글톤의 단점을 보완할 수 있는 싱글톤 레지스트리를 제시합니다.
✅ 싱글톤 레지스트리
스프링은 직접 싱글톤 형태의 객체를 만들고 관리하는 기능을 가진 싱글톤 레지스트리(singleton registry)를 제공합니다. 스프링 컨테이너는 싱글톤을 생성, 관리, 공급하는 싱글톤 관리 컨테이너입니다. 싱글톤 레지스트리의 장점은 static 메소드와 private 생성자를 사용하는 비정상적인 클래스가 아니라 평범한 자바 클래스를 손쉽게 싱글톤 방식으로 활용하게 해준다는 점입니다. 스프링은 IoC 컨테이너 뿐만 아니라, 고전적인 싱글톤 패턴을 대신해서 싱글톤을 만들어주고 관리해주는 것이 싱글톤 레지스트리 방식입니다. 싱글톤 레지스트리 방식을 사용하는 것은 스프링이 빈을 싱글톤으로 만드는 것은 결국 객체의 생성 방법을 결정하는 IoC 컨테이너로서의 역할인 것입니다.
🔹싱글톤 주의점
-싱글톤은 stateless여야 한다.
싱글톤이 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용되는 경우에는 상태 정보를 내부에 갖고 있지 않은 무상태(stateless) 방식으로 만들어야 합니다. 단순 읽기 전용 값을 상태로 가지는 것은 아무런 문제가 없겠지만, 공유되는 상태 정보를 가지면 다른 사용자들이 동시에 사용하게 되었을 때 의도하지 않은 방향으로 값이 바뀔 할 확률이 매우 큽니다. 저장할 공간이 하나뿐이니 서로 값을 덮어쓰고 저장하지 않은 값을 읽어올 수 있습니다. 그래서 주로 파라미터, 로컬 변수, 리턴 값 등을 사용하여 요청 정보, DB 서버 리소스로부터 얻은 정보 등을 처리하면 됩니다. 또한 자신이 사용하는 다른 싱글톤 빈을 저장하려는 용도라면 인스턴스 변수로 사용해도 됩니다. 다음은 UserDao
에서는 ConnectionMaker
빈의 주입 정보를 기억하기 위한 필드를 만든 코드입니다.
package com.jhcode.spring.ch1.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import com.jhcode.spring.ch1.domain.User;
public class UserDao {
//이미 스프링 컨테이너가 관리하고 있기 때문에 사용 중에 변경되지 않는 읽기 전용 인스턴스이다.
private ConnectionMaker connectionMaker;
//스프링 IoC 컨테이너가 관리하지 않기 때문에 싱글톤으로 유지 되지 않는다. 매번 새로운 값으로 바뀌어 문제가 발생할 수 있다.
private Connection con;
private User user;
개별적으로 바뀌는 정보를 가진 클래스를 만들 때는, 개별정보를 로컬 변수로 정의하거나 파라미터로 주고 받으면서 사용해야 안전합니다. 로컬 변수나 파라미터로 주고 받지 않을 경우 멀티스레드 환경에서 해당 변수에 어떤 스레드든 동시에 접근할 수 있기 때문에 값이 변경될 수 있는 가능성이 존재합니다.
ConnectionMaker는 스프링 컨테이너가 관리하는 Bean 객체이기 때문에 사용 중에 변경되지 않는 객체입니다. 하지만 Connection과 User은 스프링 컨테이너가 관리하는 객체가 아니기 때문에 사용하는 중에 값이 변할 수도 있습니다.
이러한 문제의 해결책으로 읽기 전용의 속성을 가진 정보라면 접근 지시자 static final
, final
로 선언하는 편입니다.
✅ 스프링 빈의 스코프
빈이 생성되고, 존재하고, 적용되는 범위를 빈의 스코프(scope)라고 합니다.
- prototype : 싱글톤 스코프와 달리 컨테이너에서 빈을 요청할 때 마다 새로운 객체를 만들어준다.
- request : HTTP 요청이 왔을 때 빈이 생성되고, 요청이 끝났을 때 빈이 반환된다.
- session : 웹 세션과 유사하게 사용할 수 있다.
프로젝트 환경
- IDE : STS3 - 3.9.18.RELEASE
- SpringFramework : 5.3.20
- Java : 11
- Maven
📖토비 스프링 3.1 -p102~111
Uploaded by N2T