[Spring] 토비의 스프링 3.1(2) - IoC/DI
의존성 주입(DI)
이전 글에서 구체 클래스가 아닌 인터페이스에만 의존하기 위해 인스턴스를 외부에서 생성하고, 주입받도록 만들었다. 이렇게 클래스의 의존 관계를 외부에서 주입해주는 것을 의존성 주입(Dependency Injection)이라고 한다.
public class UserRepository {
private ConnectionMaker cm;
public UserRepository(ConnectionMaker cm) {
this.cm = cm;
}
public List<User> getAllUsers() {
/* ~~ 생략 ~~ */
Connection c = cm.getConnection();
PreparedStatement ps = c.prepareStatement("select * from user");
/* ~~ 생략 ~~ */
}
public User getUser(Long id) {
/* ~~ 생략 ~~ */
Connection c = cm.getConnection();
PreparedStatement ps = c.prepareStatement("select * from user where user.id = ?");
/* ~~ 생략 ~~ */
}
}
이렇게 외부에서 의존성을 주입한 덕분에 구체 클래스를 변경해도 클라이언트의 코드는 변하지 않을 수 있게 되었다. 또한, 인터페이스의 사용으로 새로운 구현체들을 만들어 유연하게 기능을 추가할 수도 있게 되었다.
이제 UserRepository를 사용하는 UserService를 보자.
@Service
public class UserService {
UserRepository userRepository = new UserRepository(new MySQLConnectionMaker());
public List<User> getAllUsers() {
return userRepository.getAllUsers();
}
}
MySQLConnectionMaker를 생성하는 코드를 UserRepository에서 없앴더니, UserService에서 똑같은 문제가 발생한다. MySQL을 Oracle로 바꾸면 역시나 UserService의 수정이 필요하다. UserService는 '유저 정보와 관련된 로직의 변경'에 의해서만 수정이 되어야 하는데, DB 변경에 의해서 수정이 필요한 것이다. 이는 '단일 책임 원칙'을 위반한 것이다.
그럼 또 다시 UserRepository와 ConnectionMaker를 외부에서 주입받는 것으로 넘기면 된다. 그런데 이렇게 문제를 해결하는 것이 아니라, 다른 클래스로 떠넘기기만 하면 결국 누군가는 해결을 해 주어야 한다. 그것이 바로 Spring인 것이다.
빈(Bean)
빈은 스프링에서 관리해주는 객체들을 말한다. 스프링을 사용하면 위의 예시처럼 생성자를 호출해 직접 객체를 생성하지 않아도 된다. 빈으로 등록된 객체들은 스프링이 알아서 생성해주고, 의존관계를 주입해준다.
그렇다고 스프링이 프로젝트 내의 모든 클래스들을 생성하고 관리해주는 것은 아니다. 생성과 의존 관계 주입을 스프링에게 맡기고 싶으면 빈으로 등록해줘야 한다. 빈으로 등록하는 방법은 빈의 설정 정보를 알려주는 것인데, XML 파일을 사용하는 방법도 있고, Java 코드를 사용하는 방법도 있다. Java 1.5 버전에서 어노테이션이 추가된 이후 Spring 3.1에서 Java 코드로 빈을 등록하는 방법이 생겼고, 최근에는 Java를 사용하는 것이 일반적이다.
@Configuration
public class Config {
@Bean
public UserService userService(UserRepository userRepository) {
return new UserService(userRepository);
}
@Bean
public UserRepository userRepository(ConnectionMaker connectionMaker) {
return new UserRepository(connectionMaker);
}
@Bean
public ConnectionMaker connectionMaker() {
return new MySQLConnectionMaker();
}
}
이제 Oracle로 DB를 교체하려면 connectionMaker에서 OracleConnectionMaker를 생성해서 반환하면 끝이다.
전략 패턴
DI를 활용하면 전략 패턴을 구현할 수 있다. 전략 패턴이란, 인터페이스를 이용해 전체적인 로직의 흐름을 정의하고, 구체 클래스를 주입해서 다양한 로직을 구현하는 패턴이다. 위의 예시처럼 MySQLConnectionMaker를 사용하면 MySQL의 커넥션을 얻을 수 있고, OracleConnectionMaker를 사용하면 Oracle의 커넥션을 얻는 것처럼 말이다.
제어의 역전(IoC)
지금까지 살펴본 것 처럼 객체의 생성과 의존성 주입 등을 개발자가 작성한 코드가 아니라 Spring같은 프레임워크에서 대신 해주는 것을 제어의 역전이라고 한다. '제어'라는 것은 객체를 생성하고, 의존성을 주입해주고, 메소드 호출을 해주는 등 우리가 일반적으로 main 메소드부터 작성하는 프로그램의 흐름을 말한다. Spring을 사용하면 이러한 모든 것을 Spring이 대신 해주고, 우리는 위에서 작성한 UserRepository 클래스처럼 의존성이 분리된 객체 내부의 동작만 구현하면 되는 것이다. 의존성 주입은 제어의 역전 중에서 의존 관계를 만들어 주는 데 초점을 맞춘 좁은 의미의 개념이라고 볼 수 있다.
IoC 컨테이너
이렇게 빈으로 등록된 객체들을 생성하고, 관리해주는 역할을 BeanFactory 인터페이스가 한다. 그러나 실제로는 BeanFactory에 여러 부가 기능들을 확장한 AppicationContext 인터페이스를 더 많이 사용한다. 이렇게 객체를 생성, 관리해주는 객체를 IoC 컨테이너라고 한다.
IoC 컨테이너에 의해 관리되는 빈들은 기본적으로 싱글톤 객체로 만들어진다. 싱글톤이란 오브젝트가 딱 하나만 만들어져 공유되는 객체를 말한다. 스프링은 대부분 엔터프라이즈 서버를 만드는 경우가 많은데, 싱글톤이 아니라면 생성과 GC가 너무 빈번하게 일어나 성능을 떨어뜨릴 수 있기 때문이다.
싱글톤 객체를 사용할 때는 상태를 갖지 않도록 주의해야 한다. 여러 스레드에서 공유하여 사용하기 때문에 예상치 못하게 상태가 변경되어 비정상적인 결과가 나올 수 있기 때문이다. 따라서 빈으로 등록하려는 클래스에는 빈으로 등록된 객체를 주입받아서 사용하기 위한 필드만 정의하는 것이 좋다.
당연하게도, IoC 컨테이너는 빈으로 등록하지 않은 객체들은 알지 못하기 때문에 의존 관계 주입을 해주지 않는다. 이러한 경우에 빈을 사용하고 싶다면, AppicationContext에서 getBean 메소드를 사용해서 필요한 빈을 직접 검색할 수 있다. 이렇게 외부에서 주입하는 것이 아니라 직접 검색하는 것을 의존성 검색(Dependency Lookup)이라고 한다.
결론적으로 Spring이란 IoC를 이용해 역할과 구현을 나누고, 확장과 수정이 유연한 객체지향적인 설계를 도와주는 도구라고 볼 수 있다.
3줄 요약
- 의존성을 외부에서 주입하면 인터페이스에만 의존하여 기능 확장과 수정이 유연해진다
- Spring은 빈으로 등록된 객체들의 의존성을 주입해준다
- 빈을 등록하고 사용하는 방법을 알아두자