DI(Dependency Injection  의존성주입)

  표준을 정의할 수 있고, 정의된 표준을 바탕으로 같은 설계를 하게 해준다.

  보통 엔터프라이즈 어플리케이션을 개발할 때는 하나의 처리를 수행하기 위해 여러개의 컴포넌트를 조합해서

  구현하는 경우가 일반적이다.

  이때 사용되는 컴포넌트에는 '공통으로 사용되는 기능을 따로 분리한 컴포넌트', '데이터베이스에 접근하기 위한

  컴포넌트', '외부 시스템이나 서비스에 접속하기 위한 컴포넌트' 등과 같이 다양한 컴포넌트가 있다.

  

  이처럼 하나의 처리를 구현하기 위해 여러개의 컴포넌트를 통합할 때 의존성 주입이라는 접근 방식이 큰 힘을

  발휘한다. 예를들어, 사용자를 등록하는 클래스를 구현한다고 가정하면, 사용자 등록을 위해 필요한 처리 흐름은

  다음과 같다.

    1. 등록하려는 사용자 계정이 이미 등록되어 있는지 확인한다.

    2. 등록하려는 사용자의 패스워드를 해시(Hash)한다.

    3. 사용자 정보를 저장한다.

  

  이 같은 처리를 구현하는데 필요한 인터페이스는 다음과 같다.

  

  사용자 등록을 처리하는 인터페이스

public interface UserService {
	//사용자 정보를 등록한다.
    void register(User user, String rawPassword);
}

 

  패스워드를 해시화하는 인터페이스

public interface PasswordEncoder {
	//패스워드를 해시화한다.
    String encode(CharSequence rawPassword);
}

 

  사용자 정보를 관리하는 인터페이스

public interface UserRepository {
	//사용자 정보를 저장한다.
    User save(User user);
    //사용자 계정명이 일치하는 사용자 수를 카운트한다.
    int countByUsername(String username);
}

 

  사용자 등록을 처리하는 구현 클래스

public class UserServiceImpl implements UserService {
	private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    
    public UserServiceImpl(javax.sql.DataSource dataSource) {
    	//데이터베이스 방식으로 사용자 정보를 관리하는 구현 클래스
        this.userRepository = new JdbcUserRepository(dataSource);
        
        //Bcrypt 알고리즘으로 해시화하는 구현 클래스
        this.passwordEncoder = new BCryptPasswordEncoder();
    }
    
    public void register(User user, String rawPassword) {
    	if(this.userRepository.countByUsername(user.getUsername()) > 0) {
        	//같은 사용자 계정의 사용자가 있다면 예외를 발생시킨다.
            throw new UserAlreadyRegisteredException();
        }
    
    //입력된 원본 패스워드를 해시화 한 후, 사용자 정보로 설정한다.
    user.setPassword(this.passwordEncoder.encode(rawPassword));
    this.userRepository.save(user);
    }
}

 

  위 예제에서는 생성자에서 userRepository와 passwordEncoder를 초기호 ㅏ하기 위해 UserRepository 와

  PasswordEncoder의 구현 클래스를 직접 생성해서 할당한다.

  그래서 UserServiceImpl클래스를 개발하는 단계에서는 의존하는 컴포넌트 클래스가 이미 완성되어 있어야 한다.

  

  이처럼 필요한 컴포넌트를 생성자에서 직접 생성하는 방식은 일단 클래스가 생성되고 나면 이미 생성된

  UserRepository나 PasswordEncoder의 구현 클래스를 교체하는것이 사실상 어려울 수 있다.

  이러한 클래스 간의 관계를 두고 '클래스 간의 결합도가 높다'라고 말한다.

 

  엔터프라이즈 애플리케이션을 개발할 때는 다양한 컴포넌트를 조합하는 것이 일반적이라고 했는데,

  많은 컴포넌트에 의존해야 하는 클래스를 이 같은 방식으로 개발하는것은 상당히 비효율적이다.

  예를들어 의존하는 클래스가 아직 개발중이거나, 미들웨어 벤더가 제공해줘야 하는 클래스가 있는데

  그 벤더의 제품이 완성되지 않았을 수도 있다.

  결국 모든 컴포넌트가 제대로 모양새를 갖추려면 개발의 막바지에 이르러서야 오류없이 조립할 수 있다.

  해결 대안으로는 필요한 컴포넌트가 완성 될 때 까지 임시로 dummy 클래스를 만들어서 대체하는 방법이 있다.

  다만, 언젠가는 제대로 구현해서 교체해야 할 코드이기 때문에 개발규모가 커지면 커질수록 재 작업의 양이 늘어난다.

 

  UserServiceImpl 클래스의 결합도를 낮추려면 우선 생성자 안에서 UserRepository와 passwordEncoder의

  구현 클래스를 직접 생성하는 대신, 생성자의 인수로 받아서 할당하는 방법을 생각해볼 수 있다.

 

  생성자를 활용한 의존 컴포넌트 초기화

public UserServiceImpl(UserRepository userRepository,
						PassworkdEncoder passwordEncoder) {
		
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
}

 

  이렇게 하면 UserServiceImpl의 소스코드 안에서 UserRepository와 passwordEncoder의 구현 클래스 정보가 제거되어

  UserServiceImpl의 외부에서 UserRepository와 passwordEncoder의 구현체를 쉽게 변경할 수 있게 된다.

 

  애플리케이션에서 UserService를 사용

UserRepository userRepository = new JdbcUserRepository(dataSource);
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
UserService userService = new UserServiceImpl(userRepository, passwordEncoder);
//생략

  먄약 JdbcUserRepository와 BCryptPasswordEncoder가 아직 완성되지 않았다면 앞서 생성자 안에서 구현 클래스를

  직접 생성하던 방식과 달리 UserServiceImpl을 변경하지 않고 JdbUserRepository와 BCryptEncoder의 더미클래스를

  임시로 만들어서 대체하면 개발을 중단없이 계속 할 수 있다.

 

  미완성된 클래스를 더미로 대체

UserRepository userRepository = new DummyUserRepository();
passwordEncoder passwordEncoder = new DummyPasswordEncoder();
UserService userService = new UserServiceImpl (userRepository, passwordEncoder);
//생략

  하지만 이 경우에도 UserServiceImpl이 의존하는 각 컴포넌트는 개발자가 직접 생성해서 주입해야 하기 때문에

  변경이 발생하는 경우의 재작업은 불가피하다.

  

  어떤 클래스가 필요로 하는 컴포넌트를 외부에서 생성한 후, 내부에서 사용 가능하게 만들어 주는 것을

  '의존성 주입(DI)한다', 또는 '인젝션(Injection)'한다' 라고 말한다.

  그리고 이러한 의존성 주입을 자동으로 처리하는 기반을 'DI 컨테이너'라고 한다.

 

DI 컨테이너

  스프링 프레임워크가 제공하는 기능 중 가장 중요한 것이 DI컨테이너 기능이다.

  스프링 프레임워크의 DI컨테이너에 UserService, UserRepository, PasswordEncoder의 인터페이스와

  구현 클래스를 알려주고 의존관계를 정의해주면 UserServiceImpl이 생성될 때 UserRepository와

  PasswordEncoder의 구현 클래스가 자동으로 생성되어 주입된다.

  UserService를 사용하고 싶은 애플리케이션은 DI 컨테이너에서 UserService를 꺼내오기만 하면 되고,

  이 때 UserRepository와 PasswordEncoder는 UserService에 이미 조합 된 상태이다.

 

  DI 컨테이너에서 UserService 꺼내기

ApplicationContext context = ...; //스프링 DI컨테이너
UserService userService = context.getean(UserService.class);
//생략

 

  이렇게 DI 컨테이너를 통해 각 컴포넌트의 인스턴스를 생성하고 통합 관리하면서 얻을 수 있는 장점은

  컴포넌트간의 의존성 해결 뿐만이 아니다.

  어떤 컴포넌트는 반드시 단 하나의 인스턴스만 만들어서 재사용 되도록 싱글턴(singleton)객체로 만들어야 하고

  어떤 컴포넌트는 매번 필요할 때마다 새로운 인스턴스를 사용하도록 프로토타입(prototype)객체로 만들어야 한다.

  이러한 인스턴스의 스코프(scope)관리를 DI컨테이너가 대신한다.

  각 인스턴스가 필요로 하는 공통 처리 코드를 외부에서 자동으로 끼워넣는 AOP기능도 DI컨테이너가 대신 해준다.

 

DI 개요

  의존성 주입이라고도 하며 IoC라고 하는 소프트웨어 디자인 패턴 중 하나다.

 

  DI 컨테이너에서 인스턴스를 관리하는 방식에는 다음과 같은 장점이 있다.

    1. 인스턴스의 스코프를 제어할 수 있다.

    2. 인스턴스의 생명 주기를 제어할 수 있다.

    3. AOP 방식으로 공통 기능을 집어넣을 수 있다.

    4. 의존하는 컴포넌트 간의 결합도를 낮춰서 단위 테스트 하기 쉽게 만든다.

  스프링의 공식 문서에서는 DI가 아닌 IoC컨테이너라고 기재하고 있다.

 

ApplicationContext와 빈 정의

  스프링 프레임워크에서는 ApplicationContext가 DI 컨테이너의 역할을 한다.

  

  DI컨테이너에서 인스턴스 꺼내기

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
//설정클래스(Configuration class)를 인수로 전달하고 DI컨테이너를 생성한다.
//설정클래스는 여러개 정의 할 수도 있다.

UserService userService = context.getBean(UserService.class);
//DI컨테이너에서 UserService 인스턴스를 가져온다.

  여기서 AppConfig 클래스는 DI컨테이너에서 설정파일 역할을 하며, 자바로 작성돼 있어서

  자바 컨피규레이션클래스(Java Configuration Class)라고도 한다.

  그리고 이렇게 자바 컨피규레이션 클래스로 설정하는 방식을 자바 기반 설정방식이라고 하며, 다음과 같은 형태로

  작성한다.

 

  자바기반 설정 방식의 예

@Configuration
public class AppConfig {
	@Bean
	UserRepository userRepository() {
		return new UserRepositoryImpl();
	}
	
	@Bean
	PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	@Bean
	UserService userService() {
		return new UserServiceImpl(userRepository(), passwordEncoder());
	}
}

 

  그리고 이 설정을 사용하는 애플리케이션과 ApplicationContext의 관계는 다음과 같다.

  @Configuration 과 @Bean 어노테이션을 사용해서 DI컨테이너에 컴포넌트를 등록하면 애플리케이션은

  DI 컨테이너에서 있는 빈(Bean)을 ApplicationContext 인스턴를 통해 가져올 수 있다.

  스프링 프레임워크에서는 DI 컨테이너에 등록하는 컴포넌트를 빈이라고 하고, 이 빈에 대한 설정(Configuration)

  정보를 '빈 정의(Bean Definition)'라고 한다.

  또한 DI컨테이너에서 빈을 찾아오는 행위를 룩업(lookup)이라고 한다.

 

  DI 컨테이너에서 빈 가져오기

UserService userService = context.getBean(UserService.class);
//가져오려는 빈의 타입(Type)을 지정하는 방법이다.
//지정한 타입에 해당하는 빈이 DI 컨테이너에 오직 하나만 있을 때 사용한다.
//스프링프레임워크 3.0 부터 사용할 수 있다.

UserService userService = context.getBean("userService", UserService.class);
//가져오려는 빈의 이름과 타입을 지정하는 방법.
//지정한 타입에 해당하는 빈이 DI컨테이너에 여러개 있을 때 이름으로 구분하기 위해 사용.
//2.5.6버전까지는 반환값이 Object 타입이라서 원하는 빈의 타입으로 형변환 해야 했지만
//3.0.0버전 이후부터는 형변환 하지 않아도 된다.

UserService userService = (UserService) context.getBean("userService");
//가져오려는 빈의 이름을 지정하는 방법이다.
//반환값이 Object타입이라서 원하는 빈의 타입으로 형변환 해야 한다.
//1.0.0버전부터 사용할 수 있다.

 

  빈 설정 방법

  자바 기반 설정 방식이나 XML 기반 설정 방식만 사용해서 빈을 정의할 수도 있지만 대부분의 경우 자바 기반

  설정 방식과 어노테이션 기반 설정 방식을 조합하거나 XML 기반 설정 방식과 어노테이션 기반 설정 방식의

  조합을 많이 사용한다.

 

  ApplicationContext를 생성하는 다양한 방법

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
//자바 기반의 설정방식으로 AnnotationConfigApplicationContext의 생성자에
//@Configuration 어노테이션이 붙은 클래스를 인수로 전달한다.

ApplicationContext context = new AnnotationConfigApplicationContext("com.example.app");
//어노테이션 개반 설정방식으로 AnnotationConfigApplicationContext의 생성자에
//패키지명을 인수로 전달한다.
//지정된 패키지 이하의 경로에서 컴포넌트를 스캔한다

ApplicationContext contet = 
            new ClassPathXmlApplicationContext("META-INF/spring/applicationContext.xml");
//XML 기반의 설정방식으로 ClassPathXmlApplicationContext의 생성자에 XML파일을 인수로 전달한다.
//경로에 접두어(Prefix)가 생략된 경우에는 클래스패스 안에서 상대경로로 설정파일을 탐색한다.

ApplicationContext context =
            new FileSystemXmlApplicationContext("./spring/applicationContext.xml");
//XML기반의 설정방식으로 FileSystemXmlApplicationContext의 생성자에 XML 파일을 인수로 전달한다.
//경로에 접두어가 생략된 경우에는 JVM의 작업 디렉토리 안에서 상대경로로 설정파일을 탐색한다.

  ApplicationContext는 단독 애플리케이션에서 스프링 프레임워크를 사용하거나 JUnit으로 만든 테스트 케이스 안에서

  스프링 프레임워크를 구동할 때 사용된다.

  웹 애플리케이션에서는 스프링 MVC를 활용하게 되는데, 이때는 웹 환경에 맞게 확장한 WebApplicationContext를

  사용한다.

 

빈 설정

 

+ Recent posts