본문 바로가기

JAVA/Spring

스프링 3 의존관계 주입(DI)의 응용

반응형

의존관계 주입의 응용

런타임 시에 사용 의존관계를 맺을 오브젝트를 주입해준다는 DI 기술의 장점은 무엇?

오브젝트 팩토리가 바로 이 DI방식을 구현한 것이니, 앞서 설명한 모든 객체지향 설계와 프로그래밍의 원칙을 따랐을 때 얻을 수 있는 장점이 그대로 DI 기술에도 적용될 것이다.

코드에는 런타임 클래스에 대한 의존관계가 나타나지 않고, 인터페이스를 통해 결합도가 낮은 코드를 만들므로, 다른 책임을 가진 사용 의존관계에 있는 대상이 바뀌거나 변경되더라도 자신은 영향을 받지 않으며, 변경을 통한 다양한 확장 방법에는 자유롭다는게 지금까지 설명한 장점이었다.

D사와 N사에 UserDao의 코드를 판매하는 경우가 아니더라도 UsrDao와 ConnectionMaker 사이에 적용된 DI는 여러가지 방법으로 유용하게 활용될수 있다.

스프링이 제공하는 기능의 99%가 DI의 혜택을이용하고 있다. DI 없는 스프링도 없다. 그만큼 DI를 활용할 방법은 다양하다.

UserDao가 ConnectionMaker라는 인터페이스에만 의존하고 있다는 건, ConnectionMaker를 구현하기만 하고 있따면 어떤 오브젝트든지 사용할 수 있다는 뜻이다.


기능 구현의 교환

실제 운영에 사용할 데이터베이스는 매우 중요한 자원이다.

평상시에도 항상 부하를 많이 받고 있어서 개발 중에는 저대 사용하지 말아야 한다.

대신 개발중에 개발자 PC에 설치한 로컬 DB로 사용해야 한다고 해보자.

그리고 개발이 진행되다가 어느 시점이 되면 지금까지 개발한 것을 그대로 운영서버로 배치해서 사용할 것이다.

그런데 만약 DI방식을 적용하지 않았다고 해보자.

개발중에 로컬 DB를 사용하도록 해야 하니 로컬 DB에 대한 연결 기능이 있는 LocalDBConnectionMaker라는 클래스를 만들고, 모든 DAO에서 이 클래스의 오브젝트를 매번 생성해서 사용하게 했을 것이다. 

그런데 서버에 배포할 때는 다시 서버가 제공하는 특별한 DB 연결 클래스를 사용해야 한다.

DI를 안햇으니 모든 DAO는 코드에서 이미 클래스인 LocalDBConnectionMaker에 의존하고 있다.

new LocalDBConnectionMaker() 라는 코드가 모든 DAO에 들어있을 것이다.

이를 서버에 배치하는 시점에서 운영서버에서 DB에서 연결할 때 필요한 ProductionDBConnectionMaker라는 클래스로 변경해줘야 한다.

DAO가 100개라면 최소한 100군데의 코드를 수정해야 한다.

하나라도 빼먹거나 잘못 고치면 서버에서 오류가 발생할 것이다.

그리고 다시 개발을 더 진행하려고 한다면 DAO코드를 LocalDBConnectionMaker를 사용하도록 수정해야 한다.

반면에 DI 방식을 적용해서 만들면 모든 DAO는 생성 시점에 ConnectionMaker 타입의 오브젝트를 컨테이너로부터 제공받는다.

구체적인 사용클래스 이름은 컨테이너가 사용할 설정정보에 들어 있다.

@Configuration이 붙은 DaoFactory를 사용한다고 하면 개발자 PC에서는 DaoFactory의 코드를 만들어서 사용하면된다.


개발용 ConnectionMaker

@Bean

public ConnectionMaker connectionMaker() {

return new LocalDBConnectionMaker();

}


서버에 배포할때는 어떤 DAO 클래스와 코드도 수정할 필요가 없다.

단지 서버에 사용할 DaoFactory를 수정해주면된다.


서버용 ConnectionMaker

@Bean

public ConnectionMaker connectionMaker() {

return new ProductionDBConnectionMaker();

}


개발환경과 운영환경에서 DI의 설정정보에 해당하는 DaoFactory만 다르게 만들어두면 나머지 코드에는 전혀 손대지 않고 개발시와 운영시에 각각 다른 런타임 오브젝트에 의존관계를 갖게 해줘서 문제를 해결할 수 있다.

또한 QA팀이 테스트용으로 별도의 테스트 DB를 만들어서, 개발한 코드를 테스트할때 쓴다고 하면 DAO코드에 전혀 손댈 필요가 없다.

테스트 DB에 접속하는 방법을 가진 ConnectionMaker 구현 클래스를 만들고, 그것을 테스트에서 사용할 DaoFactory 설정에 넣어주기만 하면된다.

테스트가 수행되는 시점에 테스트용 DB에 연결해주는 오브젝트를 DI 컨테이너가 만들어 모든 DAO가 사용할 수 있도록 DI 해줄것이다.


부가기능 추가
DAO가 DB를 얼마나 많이 연결해서 사용하는지 파악하고 싶다고 해보자.
DB 연결 횟수를 카운팅하기 위해 무식한 방법으로, 모든 DAO의 makerConnection() 메서드를 호출하는 부분에 새로 추가한 카운터를 증가시키는 코드를 넣어야 할까?
그리고 분석 작업이 끝나면 모두 제거하고? 그것은 엄청난 낭비이고 노가다다.
게다가 DAO 코드를 수정한다는건 지금까지 그렇게 피하려고 했던 일이 아닌가.
또한 DB 연결횟수를 세는 일을 DAO의 관심사항이 아니다.
어덯게든 분리돼야 할 책임이기도 하다.
DI 컨테이너에서라면 아주 간단한 방법으로 가능하다.
DAO와 DB 커넥션을 만드는 오브젝트 사이에 연결횟수를 카운팅하는 오브젝트를 하나 더 추가하는 것이다.
뭔가 새로운 기능을 호출 과정에 추가하려고 한다면 분명 그 앞뒤의 코드를 같이 수정해야 하는 것이 아닐까?
꼭 그럴 필요는 없다. DI의 개념을 응용하는 것으로 충분하다.
DI를 이용한다고 했으니 당연히 기존 코드는 수정하지 않아도 된다.
그리고 컨테이너가 사용하는 설정정보만 수정해서 런타임 의존관계만 새롭게 정의해주면 된다.
먼저 CountingConnectionMaker라는 클래스를 만든다. 중요한 것은 ConnectionMaker 인터페이스를 구현해서 만드는 점이다.
DAO가 의존할 대상이 될 것이기 때문이다.

public class CountingConnectionMaker implements ConnectionMaker {
int counter = 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 인터페이스를 구현했지만 내부에서 직접 DB 커넥션을 만들지 않는다.
대신 DAO가 DB 커넥션을 가져올 때마다 호출하는 makeConnection()에서 DB 연결횟수 카운터를 증가시킨다.
CountingConnectionMaker는 자신의 관심사인 DB 연결횟수 카운팅 작업을 마치면 실제 DB 커넥션을 만들어주는 realConnectionMaker에 저장된 ConnectionMaker 타입 오브젝트의 makeConnection()을 호출해서 그 결과를 DAO에게 돌려준다.
그래야만 DAO가 DB 커넥션을 사용해서 정상적으로 동작할 수 있다.
생성자는 보면 CountingConnectionMaker도 DI를 받는 것을 알 수 있다.
CountingConnectionMaker의 오브젝트가 DI를 받을 오브젝트도 역시 ConnectionMaker인터페이스를 구현한 오브젝트다.
아마 실제 DB 커넥션을 돌려주는 DConnectionMaker클래스의 오브젝트일 것이다.
CountingConnectionMaker가 추가되면서 런타임 의존관계가 어떻게 바뀌는지 살펴보자.

CountingConnectionMaker를 사용하기전의 런타임의 의존관계
UserDao 오브젝트는 ConnectionMaker 타입의 DConnectionMaker오브젝트에 의존한다.

UserDao는 ConnectionMaker의 인터페이스만 의존하기 있기 때문에, ConnectionMaker 인터페이스를 구현하고 있다면 어떤 것이든 DI가 가능하다.
그래서 UserDao오브젝트가 DI받는 대상의 설정을 조정해서 DConnectionMaker 오브젝트 대신 CountingConnectionMaker 오브젝트로 바꿔치기 하는것이다.
이렇게 해두면 UserDao가 DB 커넥션을 가져오려고 할때 CountingConnectionMaker의 makeConnection() 메서드가 실행되고 카운터는 하나씩 증가할것이다.
그렇다고 해서 DB 커넥션을 제공해주지 않으면 DAO가 동작하지 않을테니 CountingConnectionMaker가 다시 실제 사용할 DB 커넥션을 제공해주는 DConnectionMaker를 호출하도록 만들어야 한다.
역시 DI를 사용하면 된다. 이렇게 해서 재구성된 새로운 런타임 의존관계가 만들어진다.

새로운 의존관계를 컨테이너가 사용할 설정정보를 이용해 만든다.
CountingDaoFactory라는 이름의 설정용 클래스를 만든다.
기존의 DaoFactory와 달리 ConnectionMaker() 메서드에서 CountingConnectionMaker 타입 오브젝트를 생성하도록 만든다.
그리고 실제 DB 커넥션을 만들어주는 DConnectionMaker는 이름이 realConnectionMaker()인 메서드에서 생성하게 한다.
그리고 realConnectionMaker() 메서드가 만들어주는 오브젝트는 connectionMaker()에서 만드는 오브젝트의 생성자를 통해 DI해준다.
기존 DAO설정부분은 바꾸지 않아도 된다.
계속해서 connectionMaker() 메서드를 통해 생성되는 오브젝트를 사용하게 한다.

@Configuration
public class CountingDaoFactory {
@Bean
public UserDao userDao() {
return new UserDao(connectionMaker()); //모든 DAO는 여전히 connectionMaker()에서 만들어지는 오브젝트를 DI를 받는다.
}

@Bean
public ConnectionMaker connectionMaker() {
return new CountingConnectionMaker(realConnectionMaker());
}

@Bean
public ConnectionMaker realConnectionMaker() {
return new DConnectionMaker();
}
}

이제 커넥션 카운팅을 위한 실행 코드를 만든다.
기본적으로는 UserDaoTest와 같지만 설정용 클래스를 CountingDaoFactory로 변경해줘야 한다.
DAO를 DL방식으로 가져와 어떤 작업이든 여러 번 실행시킨다.
그리고 CountingConnectionMaker 빈을 가져온다.
설정정보에 지정된 이름과 타입만 알면 특정 빈을 가져올 수 있으니 CountingConnectionMaker 오브젝트를 가져오는 건 간단하다.
CountingConnectionMaker에는 그동안 DAO를 통해 DB 커넥션을 요청한 횟수만큼 카운터가 증개해 있어야 한다.
카운터 값을 가져와서 화면에 출력해보고 DAO의 사용횟수와 일치하는지 확인해보자.

CountingConnectionMaker 테스트용 클래스

public class UserDaoConnectionCountingTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
AnnotaionConfigApplicationContext context = new AnnontationConfigApplicationContext(CountingDaoFactory.class);
UserDao dao = context.getBean("userDao", userDao.class);

//Dao사용코드
for(int i=0; i<10; i++) {
User user = new User();
user.setId(""+i);
user.setName(""+i);
user.setPassword(""+i);
dao.add(user);
}

CountingConnectionMaker ccm = context.getBean("connectionMaker",CountingConnectionMaker.class);
System.out.println("Connection counter : " + ccm.getCounter());
}
}

지금은 DAO가 하나뿐이지만 DAO가 수십, 수백개여도 상관없다.
DI의 장점은 관심사의 분리(SoC)를 통해 얻어지는 높은 응집도에서 나온다.
모든 DAO가 직접 의존해서 사용할 ConnectionMaker 타입 오브젝트는 connectionMaker() 메서드에서 만든다.
따라서 CountingConnectionMaker의 의존관계를 추가하려면 이 메서드만 수정하면 그만이다.
또한 CountingConnectionMaker를 이용한 분석 작업이 모두 끝나면 다시 CountingDaoFactory 설정 클래스를 DaoFactory로 변경하거나 connectionMaker() 메서드를 수정하는 것만으로 DAO의 런타임 의존관계는 이전 상태로 복구된다.
정말 멋지고 편리하지 않는가?
바론 이런 것이 의존관계 주입의 매력을 잘 드러내는 응용 방법이다.
DI의 활용하는 방법은 매우 다양하다.
다시 말하지만 스프링이 제공하는 대부분의 기능은 DI없이는 존재할수도 없은 것들이다.
스프링은 DI를 쳔하게 사용할 수 있도록 도와주는 도구이면서 그 자체로 DI를 적극 활용한 프레임워크이기도 하다.
DI가 정말 중요하다고 좋다고 생각한다면 자기 스스로 먼저 적용하지 않을 이유가 없다.
그래서 스프링을 공부하는 건 DI를 어떻게 활용해야 할지를 공부하는 것이기도 하다.

메서드를 이용한 의존관계주입
UserDao의 의존관계 주입을 위해 생성자를 사용했다.
생성자에 파라미터를 만들어두고 이를 통해 DI 컨테이너가 의존할 오브젝트 레퍼런스를 넘겨주도록 만들었다.
그런데 의존관계 주입시 반드시 생성자를 사용해야 하는 것은 아니다.
생성자가 아닌 일반 메서드를 사용할수도 있을 뿐만 아니라, 생성자를 사용하는 방법보다 더 자주 사용된다.
생성자가 아닌 일반 메서드를 이용해 의존 오브젝트와의 관계를 주입해주는데 크게 두가지 방법이 있다.

1. 수정자 메서드를 이용한 주입
수정자(setter) 메서드는 외부에서 오브젝트 내부의 애트리뷰트 값을 변경하려는 용도로 주로 사용된다.
메서드는 항상 set으로 시작한다.
간단히 수정자라고 불리기도 하다.
수정자 메서드의 핵심기능은 파라미터로 전달된 값을 보통 내부의 인스턴스 변수에 저장하는 것이다.
부자걱으로, 입력값에 대한 검증이나 그 밖의 작업을 수행할 수도 있다.
수정자 메서드는 외부로부터 제공받은 오브젝트 레퍼런스를 저장해뒀다가 내부의 메서드에서 사용하게 하는 DI 방식에서 활용하기 적당하다.

2. 일반메서드를 이용한 주입
수정자 메서드처럼 set으로 시작해야하고 한번에 한개의 파라미터만 가질수 있다는 제약이 싫다면 여러개의 파라미터를 갖는 일반 메서드를 DI용으로 사용할 수도 있다.
생성자가 수정자 메서드보다 나은점은 한번에 여러 개의 파라미터를 받을 수 있다는 점이다.
하지만 파라미터의 개수가 많아지고 비슷한 타입이 여러 개라면 실수하기 쉽다.
임의의 초기화 메서드를 이용하는 DI는 적절한 개수의 파라미터를 가진 여러 개의 초기화 메서드를 만들 수도 있기 때문에 한번에 모든 필요한 파라미터를 다 받아야 하는 생성자보다 낫다.

스프링은 전통적으로 메서드를 이용한 DI방법 중에서 수정자 메서드를 가장 많이 사용해왔다.
DaoFactory와 같은 자바코드 대신 XML을 사용하는 경우에는 자바빈 규약을 따르는 수정자 메서드가 가장 사용하기 편리하다.
수정자 메서드 DI를 사용할때는 메서드의 이름을 잘 결정하게 중요하다.
가능한 의미있고 단순한 이름을 사용하자. 이름을 짓는게 귀찮다면 메서드를 통해 DI 받을 오브젝트의 타입 이름을 따르는 것이 가장 무난하다.
예를 들어 ConnectionMaker 인터페이스 타입의 오브젝트를 DI 받는다면 메서드의 이름은 setConnectionMaker()라고 하는 것이다.
특별한 이름을 지정해서 의미를 더 부여해줄 생각이 아니
라면 이 관계를 따르도록 한다.

UserDao도 수정자 메서드를 이용해 DI 하도록 만들어보자.
기존 생성자는 제거한다.
생성자를 대신할 setConnectionMaker()라는 메서드를 하나 추가하고 파라미터로 ConnectionMaker 타입의 오브젝트를 받도록 선언한다.
파라미터로 받은 오브젝트는 인스턴스 변수에 저장해두도록 만든다.
대부분의 IDE는 수정자 메서드를 자동생성하는 기능이 있다.
인스턴스 변수만 정의해두고 자동생성 기능을 사용하면 편리하다.

public class UserDao {
private ConnectionMaker connectionMaker;

public void setConnectionMaker(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
//수정자 메서드의 DI의 전형적인 코드다. setter메서드는 보통 IDE의 자동생성기능을 사용해서 만드는 것이 편리하다.
}

UserDao를 수정자 메서드 DI방식이 가능하다록 변경했으니 DI를 적용하는 DaoFactory의 코드도 함께 수정해줘야 한다.
수정자 메서드 DI를 사용해 UserDao 타입의 빈을 만드는 DaoFactory의 UserDao() 메서드

@Bean
public UserDao userDao() {
UserDao userDao = new UserDao();
userDao.setConnectionMaker(connectionMaker());
return userDao;
}

단지 의존관계를 주입하는 시점과 방법이 달라졌을 뿐 결과는 동일하다.
UserDaoTest를 실행서 개선한 코드의 기능에 문제가 없는지 확인해보자.
실제로 스프링은 생성자, 수정자 메서드, 초기화 메서드를 이용한 방법 외에도 다양한 의존관계 주입 방법을 지원한다.


반응형