1. 개요
- 토비의 스프링 2주차 스터디
- 1장 오브젝트와 의존관계 (102p ~ 122p)
2. 싱글톤 레지스트리
오브젝트와 관계를 설정하고 리턴하는 순수 자바코드 형태의 '오브젝트 팩토리'와 '어플리케이션 컨텍스트'는 하나의 큰 차이가 있다. 어플리케이션 컨텍스트는 빈을 모두 '싱글톤'으로 만든다는 점이다.
아래 예제를 보면 알 수 있듯이 애플리케이션 컨텍스트에서 조회한 userDao에 대해 getBean을 두 번 호출할 경우 모두 동일한 객체가 리턴되었고, DaoFactory에서 userDao 메서드를 호출할 경우 다른 객체를 리턴하고 있다. (DaoFactory 관련 코드는 1주차 게시글에 기재되어 있음)
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao userDao1 = applicationContext.getBean("userDao", UserDao.class);
UserDao userDao2 = applicationContext.getBean("userDao", UserDao.class);
System.out.println("동일한 객체인가 : " + (userDao1 == userDao2)); // true
DaoFactory daoFactory = new DaoFactory();
UserDao factoryUserDao1 = daoFactory.userDao();
UserDao factoryUserDao2 = daoFactory.userDao();
System.out.println("동일한 객체인가 : " + (factoryUserDao1 == factoryUserDao2)); // false
그렇다면 스프링 애플리케이션 컨텍스트는 왜 객체를 싱글톤으로 생성할까?
3. 싱글톤으로 빈을 생성하는 이유
서버환경에서는 객체를 싱글톤으로 만드는게 성능적으로 좋기 때문이다. 예를들어 클라이언트에서 요청이 올 때마다 관련 오브젝트를 새로 생성한다고 생각해보자. 한 요청에 5개의 오브젝트가 생성되어야 한다면, 100번의 요청에는 500개의 오브젝트가 생성된다. 생성 후 사용되지 않는 객체는 자바의 GC에 의해 처리되는데, 처리해야할 객체가 많으니 GC 횟수가 많아져 서버 리소스가 자주 사용될 것이고, 이는 곧 서버 부하를 가져오게 된다.
만약 싱글톤으로 빈을 생성한다면, 한 요청에 5개의 오브젝트를 생성하는게 아닌 공유하게 될 것이고, GC로 인한 부하도 발생하지 않을 것이다. 이처럼 오브젝트를 싱글톤으로 생성하고 관리하는 것을 '싱글톤 레지스트리'라고 하며, 애플리케이션 컨텍스트는 싱글톤 레지스트리의 역할도 수행한다고 할 수 있다.
3.1. 멀티 스레드 환경에서의 싱글톤
멀티 스레드 환경에서는 하나의 싱글톤 객체에 여러 스레드가 접근하게 된다. 그렇기에 싱글톤 객체의 상태 정보를 수정하는 케이스는 절대 있어서는 안된다. 상태정보는 일반적으로 오브젝트 내 비지니스 로직과 관련되어 사용된다. 상태 정보가 수정된다면 비지니스 로직의 결과 값이 바뀔 수 있다는 뜻인데, 멀티 스레드 환경에서는 상태 값이 덮어 씌워지게 된다. 즉 스레드 각각이 원하지 않는 값을 읽어 올 수 있다는 뜻이다.
그렇기에 싱글톤은 인스턴스 필드 값을 변경하고 유지하는 상태가 아닌 무상태(stateless) 방식으로 만들어야 한다. 단, 읽기 전용의 상태 값이라면 덮어 씌워질 위험이 없으므로 상태 방식으로 만들어도 상관없다.
3.2. 스프링 빈의 스코프
스프링이 관리하는 빈이 생성되고, 존재하고, 적용하는 범위를 빈의 스코프라고 한다. 빈의 스코프는 일반적으로 싱글톤이다. 하지만 경우에 따라 싱글톤 이외의 스코프를 가질 수 있다. 이에 대해선 추후 10장에서 자세히 다룰 예정이라고 한다.
4. 의존관계 주입(DI)
IoC는 스프링만의 용어가 아니다. 1주차에서 공부했듯이 IoC라는 개념은 소프트웨어에서 자주 발견할 수 있는 일반적인 개념이다. 객체지향적인 설계를 할 경우 자연스럽게 IoC를 적용할 수 있다. 때문에 스프링을 IoC 컨테이너라고 하기엔 타 소프트웨어와 확실히 구분되지 않았다. 이러한 이유로 스프링 IoC 방식의 핵심 기능을 나타내는 의존 관계 주입(Dependency Injection)이라는 용어가 만들어져 사용되기 시작했다.
스프링의 동작 원리는 IoC라고 할 수 있지만, 타 프레임워크와 차별화되서 제공되는 핵심 기능은 의존 관계 주입이라고 할 수 있기 때문이다. 그렇다면 의존관계란 뭘까?
5. 의존관계
두 개의 클래스가 의존관계에 있다는 것을 설명할 때에는 방향성도 같이 설명해야한다. 즉, 누가 누구에게 의존하는 관계에 있다는 식이어야 한다는 말이다. UML 모델에서는 두 클래스의 의존관계를 점선으로 표시하며, 아래 그림을 'A가 B에 의존한다'라고 한다.
의존한다는 건 대상이 변하면 자신에게 영향을 미친다는 뜻이다. 여기서는 B가 변하면 A에 영향을 미친다. B의 기능이 추가되거나, 변경되거나, 형식이 바뀌면 그 영향이 A로 전달되기 때문이다.
예를들어 A가 B를 사용하는 경우 B에 정의된 메소드를 호출해서 사용하게 되는데, B의 메소드 형식이 바뀌면 A도 그에 따라 수정해야하고, B의 형식은 그대로지만 기능이 바뀔경우 A의 기능 수행에도 영향을 미칠 수 있다. 반대로 B는 A에 영향을 받지 않지 않는다. B에서 A를 사용하지 않기 때문이다. 일반적으로 이렇게 사용의 관계에 있는 경우 의존 관계가 맺어진다.
5.1. UserDao의 의존관계
지금까지 작업했던 UserDao는 ConnectionMaker에 의존하고 있다. ConnectionMaker 인터페이스가 변한다면 그 영향을 UserDao가 받을 수 있다. 하지만, ConnectionMaker 인터페이스를 구현한 클래스, 즉 DConnectionMaker가 다른 것으로 바뀌어도 UserDao에 영향을 주지 않는다. 인터페이스를 사용했기 때문에 구현 클래스와의 관계가 느슨해졌기 때문이다.
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker){
this.connectionMaker = connectionMaker;
}
...
}
위 그림에서 알 수 있듯이 UserDao 클래스는 ConnectionMaker 인터페이스에게만 의존한다. UserDao 입장에서 DConnectionMaker 클래스의 존재도 알지 못하기에 이 클래스에는 의존하지 않는다.
이처럼 인터페이스를 통한 의존관계를 갖는 경우에는 UserDao가 본인이 사용할 오브젝트가 어떤 클래스로 만든것인지 미리 알 수 없다. DaoFactory와 같이 DI 기능을 담당하는 클래스에 미리 정의해 놓을 순 있으나, UserDao 클래스나 ConnectionMaker의 코드 내에서는 드러나지 않는다. 이를 알 수 있는 시점은 어플리케이션이 실행된 이후인 런타임 시점이다. 이렇게 런타임 시점에 의존 관계를 맺는 대상을 의존 오브젝트라고 하다. 그리고 이렇게 구체적인 의존 오브젝트와 그것을 사용할 오브젝트를 런타임 시에 연결해주는 작업을 의존관계 주입이라고 정의한다.
의존관계 주입
구체적인 의존 오브젝트와 그것을 사용할 오브젝트를 런타임 시에 연결해주는 작업을 말하며, 아래 세가지 조건을 충족한다.
1) 클래스 코드에는 구체적인 의존 오브젝트에 대한 의존관계가 드러나지 않는다. 이를 위해서는 인터페이스에만 의존하고 있어야 한다.
2) 런타임 시점의 의존관계는 컨테이너나 팩토리같은 제 3의 존재가 결졍한다.
3) 의존관계는 사용할 오브젝트에 대한 래퍼런스를 외부에서 제공해줌으로써 만들어진다.
6. 의존관계 주입의 응용
6.1. 기능 구현의 교환
인터페이스 구현체로 기능을 구현하고 있으니, 기능 구현을 교환하려면 인터페이스에 대한 구현체 클래스를 새로 생성 후 DI 하는 부분에서 클래스만 교체하면 된다.
6.2. 부가 기능 추가
DAO가 DB를 얼마나 많이 연결해서 사용하는 지를 파악하기 위해 DB 연결 횟수를 카운팅하는 기능을 추가한다고 가정한다면, DAO 로직마다 Count를 추가하는 게 아닌 DAO와 DB 커넥션을 만드는 오브젝트 사이에 연결 횟수를 카운팅하는 오브젝트를 추가하면 된다. 오브젝트 추가는 DI 설정으로 한다.
public class CountingConnectionMaker implements ConnectionMaker{
int counter = 0;
private ConnectionMaker realConnectionMaker;
CountingConnectionMaker(ConnectionMaker realConnectionMaker){
this.realConnectionMaker = realConnectionMaker;
}
@Override
public Connection makeConnection() throws ClassNotFoundException, SQLException {
this.counter++;
return realConnectionMaker.makeConnection();
}
public int getCounter(){
return this.counter;
}
}
...
@Configuration
public class DaoFactory {
@Bean
public UserDao userDao(){
return new UserDao(countingConnectionMaker());
}
@Bean
public ConnectionMaker countingConnectionMaker(){
return new CountingConnectionMaker(realConnectionMaker());
}
@Bean
public ConnectionMaker realConnectionMaker(){
return new DConnectionMaker();
}
}
...
public class Main {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao userDao = applicationContext.getBean("userDao", UserDao.class);
// DAO 사용 코드
CountingConnectionMaker ccm = applicationContext.getBean("countingConnectionMaker",
CountingConnectionMaker.class);
System.out.println(ccm.getCounter());
}
}
7. 회고
이번 주차에서는 빈의 스코프와 DI에 대해 알아보았다. 이번 기회로 DI에 대한 개념을 재정리 할 수 있게 되었다. DI가 단순히 어떠한 객체를 주입받는다라고 생각했었지만 코드 내에 의존 오브젝트가 드러나서는 안되고, 의존관계를 런타임 시점에 맺어야 한다는 것을 새롭게 알게되었다.
또한 DI 활용 부분을 보며 AOP, 프록시와 같은 성질을 가진 기능들도 DI로 구현이 가능하겠구나 라는 생각이 들면서 이 기능이 스프링에서 제공하는 다양한 부분에 사용되고 있겠구나 라는 생각도 들었다.
'공부 > 토비의 스프링 스터디' 카테고리의 다른 글
[토비의 스프링 스터디] 7주차 / 예외 / 예외 처리 방법 및 전략 (0) | 2023.05.25 |
---|---|
[토비의 스프링 스터디] 5주차 / 템플릿 / 직접 DI (0) | 2023.05.16 |
[토비의 스프링 스터디] 4주차 / 테스트 / JUnit 실습 (0) | 2023.05.09 |
[토비의 스프링 스터디] 3주차 / XML을 통한 DI (0) | 2023.05.02 |
[토비의 스프링 스터디] 1주차 / 스프링이란 / 초난감 DAO 예제 / 스프링 IoC (0) | 2023.04.17 |