2022. 5. 28. 12:17ㆍ토비의 스터디
스프링의 IoC에 대해 좀 더 깊이 알아보자.
1.7.1 제어의 역전(IoC)과 의존관계 주입
DaoFactory처럼 객체를 생성하고 관계를 맺어주는 등의 작업을 담당하는 기능을 일반화 한 것이 스프링의 IoC 컨테이너다. 스프링 IoC 기능의 대표적인 동작원리는 주로 의존관계 주입이라고 불린다. 스프링이 여타 프레임워크와 차별화돼서 제공해주는 기능은 의존관계 주입이라는 새로운 용어를 사용할 때 분명하게 드러난다.
DI의 핵심은 오브젝트 레퍼런스를 외부로부터 제공(주입)받고 이를 통해 다른 오브젝트와 다양하게 의존관계가 만들어진다는 것이다.
1.7.2 런타임 의존관계 설정
의존관계란?
두 개의 클래스 모듈이 의존관계에 있다고 말할 때에는 항상 방향성이 있어야 한다. 즉 누가 누구에게 의존하는 관계에 있다는 식이어야 한다.
그럼 의존하고 있다는 것은 무슨 의미일까?
A가 B에 의존하고 있는 경우, B가 변하면 그것이 A에 영향을 미친다는 뜻이다. 예를들어, A가 B에 정의된 메소드를 호출해서 사용하는 경우다.
다시 말하지만 의존관계는 방향성이 있다.
UserDao의 의존관계
지금까지 작업한 UserDao는 ConnectionMaker에 의존하고 있는 형태다. ConnectionMaker 인터페이스에 의존하고 있으므로 인터페이스가 변한다면 그 영향을 UserDao가 직접적으로 받게 된다. 하지만 ConnectionMaker를 구현한 클래스가 바뀌어도, 사용하는 메소드가 변해도 UserDao에 영향을 주지 않는다.
이렇게 인터페이스에 대해서만 의존관계를 만들어두면 구현 클래스와는 느슨해지면서 결합도가 낮아진다.
이렇게 모델이나 코드에서 클래스와 인터페이스를 통해 드러나는 의존관계 외에도, 런타임 시에 오브젝트 간에 생성되는 의존관계도 존재한다. 이를 런타임 의존관계라고 하며, 설계 시점의 의존관계가 실체화 된 것이다.
UserDao가 ConnectionMaker의 구체 클래스로 어떤 오브젝트를 사용하는지는 프로그램이 시작되고 런타임 시에 결정된다. 이렇게 런타임 시에 의존관계를 맺는, "실제 사용대상"인 오브젝트를 의존 오브젝트라고 한다.
의존관계 주입은 이렇게 구체적인 의존 오브젝트(실제 사용대상)와 그것을 사용할 주체(클라이언트 오브젝트)를 런타임 시에 연결해주는 작업을 말한다.
의존관계 주입의 핵심은, 설계 시점에는 알지 못했던 두 오브젝트의 관계를 맺도록 도와주는 제 3의 존재가 있다는 것이다. DI에서 말하는 제3의 존재는 관계설정 책임을 가진 코드를 분리해 만들어진 오브젝트라고 볼 수 있다. 대표적으로 DaoFactory가 이에 해당한다.
UserDao의 의존관계 주입
인터페이스를 사이에 두고 UserDao와 ConnectionMaker 구현 클래스 간에 의존관계를 느슨하게 만들긴 하였다만,
관계설정을 분리하기 전에는 여전히 UserDao가 사용할 구체적인 클래스를 알고 있어야 한다는 문제점이 남아 있었다.
public UserDao() {
connectionMaker = new DConnectionMaker(); //구체클래스
}
그래서 관계 설정을 분리해서, DaoFactory라는 클라이언트 오브젝트를 생성하고, 그 안에서 UserDao와 ConnectionMaker의 구체 클래스를 결정해서 주입해줌으로써 해결하였다.
DaoFactory는 런타임 시점에 UserDao가 사용할 ConnectionMaker 타입의 오브젝트를 결정하고 이를 생성한 후에 UserDao의 생성자 파라미터로 주입해서 둘의 런타임 의존관계를 맺게 해준다.
이미 DaoFactory를 만든 시점에 의존관계 주입(DI)을 이용한 셈이다.
DaoFactory는 제3의 존재로써 DI 컨테이너의 역할을 수행한다. 런타임 시에 의존 오브젝트를 사용할 수 있도록 레퍼런스를 전달받는 과정이 마치 메소드(생성자)를 통해 DI 컨테이너가 UserDao에게 주입해 주는 것과 같다고 해서 의존관계 주입이라고 부른다.
1.7.3 의존관계 검색과 주입
스프링이 제공하는 IoC 방법에는 의존관계 주입(DI) 외에도 의존관계 검색(DL)이 있다.
의존관계 검색은 자신이 필요로 하는 의존 오브젝트를 능동적으로 찾는다. 물론 자신이 어떤 클래스의 오브젝트를 사용할지 결정하지는 않는다(IoC에 위배).
런타임 시 관계를 맺을 오브젝트 결정과 생성작업은 외부 컨테이너에 IoC로 맡기고, 가져올 때는 생성자, 메소드를 통한 주입 대신 스스로 컨테이너에 요청하는 방법이다.
예를들어 UserDao의 생성자가 아래와 같다고 해보자.
public UserDao(){
DaoFactory daoFactory = new DaoFactory();
this.connectionMaker = daoFactory.connectionMaker();
}
이 경우에도 UserDao는 자신이 어떤 ConnectionMaker 구체클래스를 사용할지 미리 알 수 없다. 의존 대상은 ConnectionMaker 인터페이스 뿐이다. 런타임 시에 DaoFactory가 만들어주는 오브젝트와 동적으로 런타임 의존관계를 맺는다.
하지만 적용 방법은 외부로부터 주입이 아닌 스스로 DaoFactory에게 요청하는 것이다. 스프링의 IoC 컨테이너인 애플리케이션 컨텍스트는 비슷한 기능으로 getBean()이라는 메소드를 제공한다. 이 메소드가 의존관계 검색에 사용된다.
public UserDao(){
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(DaoFactory.class);
this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
}
의존관계 주입과 검색의 중요한 차이점 하나는, 의존관계 검색 시에는 검색하는 오브젝트는 자신이 스프링의 빈일 필요가 없다는 점이다.
UserDao가 getBean()을 이용해 의존관계를 검색할때, UserDao는 굳이 스프링이 만들고 관리하는 빈일 필요가 없다. ConnectionMaker만 빈이면 된다.
반면 의존관계 주입에서는 당연히 둘 다 컨테이너가 관리하는 빈이어야 한다.
1.7.4 의존관계 주입의 응용
DI 기술의 장점은 무엇일까?
의존관계에 있는 대상이 바뀌거나 변경되더라도 자신은 영향을 받지않으며, 변경을 통한 다양한 확장방법에는 자유롭다는 점이다.
기능 구현의 교환
만약 로컬DB와 운영DB를 번갈아 사용해야 하는 상황이라고 가정해보자. DI 방식을 적용하지 않는다면 로컬 DB로 개발중에는 로컬 구체 클래스에 의존하고, 운영DB를 사용할때는 운영 구체 클래스에 의존하므로 번갈아 사용할때마다 DAO를 무진장 고쳐야 한다.
DI방식을 사용한다면 모든 DAO는 생성 시점에 ConnectionMaker 타입의 오브젝트를 컨테이너로부터 제공받는다.
@Bean
public ConnectionMaker connectionMaker(){
return new LocalDBConnectionMaker();
// return new ProductionDbConnectionMaker();
}
로컬 DB에서 운영DB로 옮겨도 DAO 코드를 단 한줄도 수정할 필요가 없다. 주석된 코드를 해제하고, 기존에 사용한 LocalDBConnectionMaker()를 주석처리하면 된다.
부가기능 추가
다음과 같은 상황이 존재한다고 해보자. DAO가 DB를 얼마나 많이 연결해서 사용하는지 파악하고 싶어서
모든 makeConnection() 메소드를 호출하는 부분에 카운터를 증가시키는 코드를 넣으려고 한다.
듣기만 해도 힘든 상황이다.
다행히 DI 컨테이너라면 아주 간단한 방법으로 카운팅이 가능하다. DAO와 DB 커넥션을 만드는 오브젝트 사이에 연결횟수를 카운팅하는 오브젝트를 추가하는 것이다. 아래 코드를 보자.
public class CountingConnectionMaker implements ConnectionMaker{
int count = 0;
private ConnectionMaker realConnectionMaker;
public CountingConnectionMaker(ConnectionMaker realConnectionMaker){
this.realConnectionMaker = realConnectionMaker;
}
public Connection makeConnection() throws ClassNotFoundException, SQLException{
this.counter++;
return realConnectionMaker.makeConnection();
}
public int getCounter(){
return this.counter;
}
}
CountingConnectionMaker라는 클래스는 ConnectionMaker를 구현해서 만든다. 하지만 내부에서 직접 커넥션을 만들지 않는다. 대신 DAO가 DB 커넥션을 가져올 때마다 호출하는 makeConnection()에서 카운터를 증가시킨다.
카운팅 증가 작업이 끝나면, 실제 DB 커넥션을 만들어주는 realConnectionMaker에 저장된 ConnectionMaker타입 오브젝트의 makeConnection()을 호출해 그 결과를 DAO에 돌려준다.
그럼 설정정보는 아래와 같이 변경된다.
@Configuration
public class DaoFactory{
...
...
@Bean
public UserDao userDao(){
return new UserDao(connectionMaker());
}
@Bean
public ConnectionMaker connectionMaker(){
return new CountingConnectionMaker(realConnectionMaker());
}
@Bean
public ConnectionMaker realConnectionMaker(){
return new DConnectionMaker();
}
}
1.7.5 메소드를 이용한 의존관계 주입
의존관계 주입 시 생성자만 사용해야 하는 것은 아니다. 다른 방법들을 소개한다.
- 수정자 메소드를 통한 의존관계 주입
setter 메소드를 활용한다. setter는 IDE의 자동생성을 사용하도록 하자.
public class UserDao{
private ConnectionMaker connectionMaker;
public void setConnectionMaker(ConnectionMaker connectionMaker){
this.connectionMaker = connectionMaker;
}
}
- 일반 메소드를 통한 의존관계 주입
수정자 메소드와 달리 한번에 여러 개의 파라미터를 받을 수 있다는 장점이 있다(생성자 방식도 가능하다).
'토비의 스터디' 카테고리의 다른 글
[토비의 스프링 3.1] 2.3 개발자를 위한 테스팅 프레임워크 JUnit (0) | 2022.06.03 |
---|---|
[토비의 스프링 3.1] 2.1 UserDaoTest 다시 보기 (0) | 2022.06.01 |
[토비의 스터디 3.1] 1.6 싱글톤 레지스트리와 오브젝트 스코프 (0) | 2022.05.27 |
[토비의 스프링 3.1] 1.5 스프링의 IoC (0) | 2022.05.27 |
[토비의 스프링 3.1] 1.4 제어의 역전(IoC) (0) | 2022.05.26 |