2023.09.13 - [DEV book/토비의 스프링 3.1] - [토비의 스프링 3.1] 1장 오브젝트와 의존관계 (1) - 전략 패턴을 이용한 DAO 클래스 리팩토링
아직 1장도 다 안 했는데 .. 3번째 포스팅이다. 양이 어마무시하다. 양도 양인데, 중요하고 기억하고 싶은 내용이 많아서 포스팅이 길어진다.
두 번째 포스팅에서는 DaoFactory를 ApplicationContext에 올려서, 클라이언트에서 getBean()를 이용해 빈을 등록하도록 하였다. 그런데, DaoFactory에서 빈을 생성하는 것과, 스프링 컨테이너에서 빈을 생성하는 것은 어떤 차이가 있는가 ?
이 질문에 대해서, 스프링 컨테이너에는 고유한 하나의 오브젝트를 저장하는 '싱글톤 레지스트리'라는 개념을 소개할 수 있다.
싱글톤 레지스트리 (Singleton registry)
ApplicationContext는 싱글톤을 저장하고 관리한다.그 이유는 자바 엔터프라이즈 기술을 사용하는 서버 환경이기 때문인데, 복잡한 비즈니스 로직에서 생성되는 수많은 오브젝트를 감당하기 위해 도입되었다.
일반적으로 싱글톤 패턴은 아래와 같은 문제점이 있다.
private 생성자로 오브젝트 생성을 막고 있기 때문에, 상속과 다형성의 특징이 적용되지 않는다. 또한, 테스트용 오브젝트를 만들기 어려워서 테스트하기 힘들다. 클래스 로더의 구성에 따라서 서버 환경에서 싱글톤이 보장되기도 어렵고, 프로그램 내에서 전역(State) 상태로 존재하기 때문에 바람직하지 않은 프로그래밍 설계이다.
이러한 이유 때문에, 싱글톤 패턴을 그대로 사용하는 것이 아니라 싱글톤 레지스트리를 만들어서 싱글톤을 생성, 공급한다. 싱글톤 패턴을 적용하지 않은 평범한 자바 클래스를 사용할 수 있다는 점이 장점이다. 빈에 대한 제어권을 IoC 컨테이너에 넘기게 되면, 오브젝트 생성에 대한 모든 권한이 Application Context에 있기 때문에 싱글톤의 장점을 유지하면서도 위에서 언급한 단점을 보완할 수 있다.
싱글톤 레지스트리를 사용하게 됨으로써 주의해야할 점
만약, 여러 스레드에서 싱글톤 오브젝트의 변수를 수정하게 되면, 값이 이상하게 바뀌거나 덮어씌워지게 된다. 이를 방지하기 위해서 상태정보를 내부에 갖고 있지 않은 무상태 방식 (stateless)으로 레지스트리를 구성해야한다. 무상태 방식에서는 파라미터와 로컬 변수, 리턴 값을 이용해서 값을 전달하는 방식으로 정보를 다루며, 독립적인 공간을 사용하기 때문에 여러 스레드에 의해 값이 변질될 우려가 없다.
그럼, 인스턴스 변수를 절대 사용하면 안되는 것인가 ?
그건 아니다. 읽기 전용의 속성을 가진 정보거나, 스프링 컨테이너에 등록된 다른 싱글톤 빈을 저장하려는 용도라면 인스턴스 변수를 사용해도 좋다.
빈의 스코프
빈이 생성되고, 적용되는 범위를 빈의 스코프라고 한다. 스프링 빈은 기본적으로 싱글톤 스코프를 가진다. 싱글톤 스코프는 스프링 컨테이너에 하나의 오브젝틀만 만들어진다.
경우에 따라 프로토타입 스코프를 가질 수 있는데, 컨테이너에 빈을 요청할 때마다 매번 새로운 오브젝트를 만들어준다. 대표적으로 HTTP 요청이 있을 때마다 요청 (request) 스코프, 세션 (session) 스코프가 있다.
의존관계 주입 (DI, Dependency Injection)
스프링 IoC 기능의 대표적인 동작원리는 의존관계 주입이다.
거창한 것이 아니라, 제어의 역전의 개념이 스프링에 사용되는 기술적인 면모를 정확히 표현한 워딩이라고 보면 된다. DI는 오브젝트를 외부로부터 제공 받고, 이를 통해 또 다른 오브젝트와 다이내믹하게 의존 관계가 만들어지는 것이 핵심이다.
'의존 관계'에 대하여
의존 관계라 함은 방향성이 있고, A가 B에 의존하고 있을 때 B의 변화가 A의 변화에 영향을 끼친다.
UserDao는 ConnectionMaker 인터페이스를 의존하고 있다. 그러나 DConnectionMaker와 같은 구체 클래스의 내부 메서드가 변한다고 해도 UserDao는 아무런 변형이 없다. 결합도가 낮은 것이다. UserDao와 ConnectionMaker는 이런 방식으로 의존 관계를 맺고 있다.
이렇게 모델이나 코드에서 의존 관계가 드러날 수 있는 반면에, 런타임 시에 의존 관계가 만들어지는 경우가 있다. UserDao 오브젝트가 만들어졌을 때, 런타임 시에 UserDao가 실제로 사용할 오브젝트를 의존 오브젝트 (dependent object) 라고 한다. 의존 관계 주입은 이렇게 의존 오브젝트와 클라이언트 (의존 오브젝트를 사용하는 주체) 를 런타임 시에 연결하는 것을 말한다.
의존 관계 주입의 조건
먼저, 의존 관계 주입의 핵심은 런타임 시점에서 제 3의 존재 (컨테이너, 팩토리) 가 의존 관계를 결정한다는 것이다.
- 클래스 모델이나 코드에 의존 관계가 드러나지 않는다. 그러기 위해서는 오브젝트(UserDao)는 인터페이스(ConnectinoMaker) 에만 의존하고 있어야 한다.
- 런타임 시점의 의존관계는 제 3의 존재가 결정한다.
- 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공해줌으로써 만들어진다.
DaoFactory를 위 조건에서 바라보자.
우선, UserDao 오브젝트는 ConnectionMaker이라는 인터페이스를 의존한다. 그리고 스프링 컨테이너에 등록되어 있으므로 런타임 시점에서 의존관계가 제 3자 (IoC 컨테이너)에 의해서 결정되고, UserDao의 생성자의 파라미터(외부에서 제공)로 주입된다.
DaoFactory는 두 오브젝트 사이의 런타임 의존 관계 주입 작업을 주도하며, IoC 방식으로 오브젝트를 생성하고 제공한다. 따라서, DaoFactory는 의존 관계를 담당하는 컨테이너, 즉 DI 컨테이너라고 할 수 있다.
DI는 (1) 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 (2) 자신은 수동적으로 주입받은 오브젝트를 사용한다는 점에서 IoC의 개념과 잘 맞아 떨어진다.
의존 관계 검색 (DL, Dependency Lookup)
의존 관계를 맺는 방법이 외부로부터의 주입이 아니라, 스스로 검색을 통해 자신이 필요한 의존 오브젝트를 능동적으로 찾는 IoC 방법이 있다.
스프링 컨테이너가 getBean() 함수의 파라미터로 빈 이름을 통해 빈을 제공할 것을 요청을 받으면, 미리 정해놓은 이름을 검색해서 오브젝트를 찾게 된다. 이 오브젝트도 결국 런타임 의존 관계를 가지기 때문에, 이 과정을 의존관계 검색이라고 한다.
의존 관계 검색 방식에서는 주입과는 다르게 검색하는 오브젝트가 스프링의 빈일 필요가 없다는 차이점이 있다. DI에서는 UserDao가 ConnectionMaker를 호출하기 위해선 빈에 등록되어있어야 하는 반면 의존관계 검색 방법을 사용하면 단순한 오브젝트여도 빈을 제공받는 것이 가능하다.
의존 관계 주입의 사용
기능 구현의 손쉬운 교환
ConnectionMaker에서 DAO의 변환 없이 로컬 DB와 운영 DB를 교체할 수 있다.
부가 기능 추가
DB 연결 횟수를 계산하고 싶다면, ConnectionMaker 인터페이스를 구현한 클래스를 만들어서, DI의 개념을 응용해 런타임 의존관계를 새롭게 정의해줄 수 있다.
public class CountingConnectionMaker implements ConnectionMaker{
int counter = 0;
private ConnectionMaker realConnectionMaker;
public CountingConnectionMaker(ConnectionMaker realConnectionMaker){
this.realConnectionMaker = realConnectionMaker;
}
@Override
public Connection makeConnection() throws ClassNotFoundException, SQLException {
this.counter++;
return realConnectionMaker.makeConnection();
}
public int getCounter() {
return this.counter;
}
}
이 코드에서 DConnectionMaker를 호출할 수 있게 하기 위해 DI 를 사용해 CountingDaoFactory라는 설정용 클래스를 만든다.
@Configuration
public class CountingDaoFactory {
@Bean
public UserDao userDao(){
return new UserDao(connectionMaker());
}
@Bean
public ConnectionMaker connectionMaker(){
return new CountingConnectionMaker(realConnectionMaker());
}
@Bean
public ConnectionMaker realConnectionMaker(){
return new DConnectionMaker();
}
}
(1) connectionMaker()에서 CoutingConnectionMaker 클래스의 오브젝트를 생성한다.
(2) 실제로 어떤 DB를 사용할 지는 realConnnectionMaker() 함수에서 결정하여 오브젝트를 생성하고 반환한다.
(3) (2) 에서 만든 오브젝트를 connectionMaker()에서 만드는 오브젝트의 생성자를 통해 DI한다.
메소드를 이용한 의존관계 주입
지금까지 생성자를 이용해서 의존 관계 주입을 해왔다. 일반 메소드를 사용해 의존 관계를 주입할 수 있는데, 크게 두가지 방법이 있다.
setter 메소드를 이용한 주입
파라미터로 전달된 값을 보통 내부의 인스턴스 변수에 저장한다. 하나의 파라미터만 사용할 수 있다는 단점이 있다.
XML 을 사용하여 설정 정보를 등록하는 경우에는 setter 메소드가 사용하기 가장 편리하다.
setter 메소드를 사용하여 의존 관계를 주입한 경우, DaoFactory에서도 setConnectionMaker 함수를 이용하여 UserDao 와 ConnectionMaker 오브젝트를 연결해줘야 한다.
'DEV book > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] 3장 템플릿 (2) - JdbcContext를 UserDao에서 사용하는 두 가지 방법 (0) | 2023.09.24 |
---|---|
[토비의 스프링 3.1] 3장 템플릿 (1) - 디자인 패턴과 DI를 이용한 DAO 최적화 (0) | 2023.09.23 |
[토비의 스프링 3.1] 2장 테스트 - 단위 테스트와 테스트 코드 개선 (0) | 2023.09.20 |
[토비의 스프링 3.1] 1장 오브젝트와 의존관계 (2) - 제어의 역전 (IoC)과 ApplicationContext (0) | 2023.09.13 |
[토비의 스프링 3.1] 1장 오브젝트와 의존관계 (1) - 전략 패턴을 이용한 DAO 클래스 리팩토링 (0) | 2023.09.13 |