반응형

 

 

이번 포스팅에서는 스프링의 프레임워크로서의 핵심 작동 원리인 의존성 주입(DI) 및 제어의 역전(IoC) 및 Bean에 대해서 살펴볼 것이다.

다룰 내용은 아래와 같다.

 

  1. 의존성 주입 (DI), 제어의 역전(IoC)의 개념
  2. Spring Boot에서 Bean을 등록하는 방법
    1. 2-1. Configuration + Bean
    2. 2-2. Component
  3. Spring Boot에서 의존성 주입(DI)을 수행하는 방법들
    1. Autowired
    2. 생성자 주입
    3. RequiredArgsConstructor
    4. Qualifier와 Primary

 

 

 


1. 의존성 주입 (DI), 제어의 역전(IoC)의 개념

 

DI (Dependency Injection, 의존성 주입)

 

객체가 의존하는 다른 객체를 직접 생성하는 것이 아닌 외부로부터 받는 것을 뜻한다.

 

의존성

한 객체가 다른 객체에게 의존적인 관계를 나타내는 단어이다.

B 클래스에서 A의 기능을 사용하고 있다 -> B는 A없이 작동할 수 없다 -> B는 A에 의존하고 있다고 표현한다.

 

의존성 주입

객체(B)가 의존 객체(A)가 필요하다면 직접 생성하는 것이 아니라 객체(B)를 관리하고 있는 주체(C)가 객체(B)를 생성할 때 의존 객체(A)를 생성자 등을 통해서 공급해 주는 방법론이다.

이는 객체 간의 느슨한 결합을 추구하게 되며, 유연성 및 유지보수성의 향상을 기대할 수 있다.

 

 

아래의 예시를 한번 살펴보자.

 

 

DI를 사용하지 않는 경우

interface MyLogger{
    //...
}
class MyLoggerImpl implements MyLogger  {
    //...
}

class MyService  {
    private MyLogger logger;
    public MyService() {
        this.logger = new MyLoggerImpl();
    }
}

class MyService2  {
    private MyLogger logger;
    public MyService2() {
        this.logger = new MyLoggerImpl();
    }

}


public class Main {
    public static void main(String[] args) {

        MyService service1 = new MyService();
        MyService2 service2 = new MyService2();
    }
}

 

MyService와 MyService2는 MyLogger의 기능을 사용하고 있으므로 의존하고 있다고 볼 수 있다.

MyService와 MyService2는 인스턴스가 생성될 때 멤버변수로 사용되는 MyLogger를 클래스 내부에서 생성하고 있다. 하지만 다음 방식에서 발생할 수 있는 문제점이 있다.

 

 

interface MyLogger{
    //...
}
class MyLoggerImpl implements MyLogger  {
    //...
}
class MyLoggerImpl2 implements MyLogger  {
    //...
}

class MyService  {
    private MyLogger logger;
    public MyService() {
        /** 일일이 직접 수정해야 함! **/
//        this.logger = new MyLoggerImpl();
        this.logger = new MyLoggerImpl2();
    }
}

class MyService2  {
    private MyLogger logger;
    public MyService2() {
        /** 일일이 직접 수정해야 함! **/
//        this.logger = new MyLoggerImpl();
        this.logger = new MyLoggerImpl2();
    }

}

public class Main {
    public static void main(String[] args) {

        MyService service1 = new MyService();
        MyService2 service2 = new MyService2();
    }
}

 

만약 위의 코드처럼 Service들의 MyLogger의 구현체를 교체하고 싶다면, 사용하고 있던 모든 클래스들의 MyLogger 인스턴스를 생성하는 부분을 바꾸어 주어야 한다. 만약 변경해야 하는 포인트가 여러개가 된다면 강한 결합에 의해서 유지보수하기가 매우 난감해 질 것이다.

 

 

 

 

DI를 사용하는 경우

interface MyLogger{
    //...
}
class MyLoggerImpl implements MyLogger  {
    //...
}
class MyLoggerImpl2 implements MyLogger  {
    //...
}

class MyService  {
    private MyLogger logger;
    public MyService(MyLogger logger) {
        this.logger = logger;
    }
}

class MyService2  {
    private MyLogger logger;
    public MyService2(MyLogger logger) {
        this.logger = logger;
    }

}


public class Main {
    public static void main(String[] args) {

//        MyLogger logger = new MyLoggerImpl();
        MyLogger logger = new MyLoggerImpl2();
        MyService service1 = new MyService(logger);
        MyService2 service2 = new MyService2(logger);
    }
}

 

다음은 생성자를 통해서 의존성을 주입받도록 변경한 코드이다.

이제 Service들은 멤버변수로 생성할 클래스를 외부 클래스로부터 생성자를 통해서 주입받아서 사용하고 있다.

이렇게 된다면, MyLogger를 사용하고 있는 클래스들의 구현체를 변경해야 하는 상황이라도, 클래스들을 모두 수정할 필요 없이, 인스턴스를 주입해주고 있는 곳만을 수정하면 된다

 

이는 코드의 결합도를 낮추어 수정할 부분들이 줄어들고 유지보수성에서 용이해진다.

 

 

 


 

IoC(Inversion of Control, 제어 역전)

 

인스턴스의 생명주기 관리의 측면에서 인스턴스들을 코드 내에서 생성하고, 삭제하는 것은 개발자들의 영역이었는데, 이를 매번 관리하기란 쉽지가 않다.

따라서 Framework가 객체의 생성과 관리, 제공을 위임하게 하고 프레임워크가 인스턴스 생명 제어의 주체가 되게 하는 방법이다.

 

Bean

Bean은 Spring IoC 컨테이너에 의해 관리되는 객체라고 하였다. 프레임워크가 객체의 생명주기와 의존성을 관리하게 하기 위해서 사용.

개발자가 작성한 클래스 + IoC 컨테이너에 등록 = Bean 이라고 이해하면 되겠다.

 

IoC 컨테이너

Spring Core에서 Bean을 관리하는 주체

 

 

1. 프로그래머들은 Framework에 관리를 위임할 클래스들을 Bean으로 등록해둔다.

2. 프레임워크에서는 Bean들을 파악하여, 해당 클래스들을 관리 대상으로 삼는다.

3. Bean(관리 대상)으로 등록된 클래스들은 기본적으로 싱글턴(Singleton)으로 생성된다.(필요 시 프로토타입 등으로 생성될 수 있음),

4. 해당 Bean들간의 의존성 주입또한 Framework에서 처리하게 된다.

 

프로그래머 입장에서는 프레임워크의 규칙만 따라주면 관리와 제어를 프레임워크가 대신 해 주므로 한 작업에 집중할 수 있다.

 

 

아래의 코드의 메인함수에서 하는 행동들을 Spring Framework단에서 수행해준다는 것!

/** Bean으로 등록되었다고 가정 **/
interface MyLogger{
    //...
}
class MyLoggerImpl implements MyLogger  {
    //...
}
class MyService  {
    private MyLogger logger;
    public MyService(MyLogger logger) {
        this.logger = logger;
    }
}
class MyService2  {
    private MyLogger logger;
    public MyService2(MyLogger logger) {
        this.logger = logger;
    }
}
/** Bean으로 등록되었다고 가정 **/

/** Bean으로 등록된 인스턴스들을 자동으로 싱글턴으로 생성해주고, 의존성 주입을 Framework단에서 담당 **/
public class Spring {
    public static void main(String[] args) {
        MyLogger logger = new MyLoggerImpl();
        MyService service1 = new MyService(logger);
        MyService2 service2 = new MyService2(logger);
    }
}

 

 

 

 

https://ko.wikipedia.org/wiki/%EC%9D%98%EC%A1%B4%EC%84%B1_%EC%A3%BC%EC%9E%85

 

 

UML을 통해서도 DI와 IoC를 설명해보겠다.

 

디자인 패턴 관점에서 Injector는 의존성 주입을 수행하는 구성 요소이다. Spring에서는 이 역할을 Spring IoC 컨테이너가 수행한다.

1. IoC 컨테이너는 ServiceA1, ServiceB1, Client의 인스턴스를 생성(주로 Singleton)한다. (Bean으로 등록되었다는 가정)

2. Client는 ServiceA와 ServiceB에 의존하는 관계이므로 IoC에서 생성자 Injection을 통해 의존성을 주입받는다.

 

 

 

 


2. Spring Boot에서 Bean을 등록하는 방법

 

2-1. Configuration + Bean

메서드를 정의하고, 그에 따른 반환값을 Bean으로 등록할 때 사용

public class LoggerLibrary(){
    private int setting;
    public void setSetting(int setting){
        this.setting = setting;
    }
}
@Configuration
public class AppConfig {
    @Bean
    public LoggerLibrary getLogger() {
        LoggerLibrary logger = new LoggerLibrary();
        logger.setSetting(1);
        return logger;
    }
}

 

@Configuration해당 클래스가 스프링의 설정을 정의하는 클래스라는 것을 정의하여 IoC 컨테이너가 인식하게 해준다.

 

@Bean은 Configuration이 붙은 클래스 내에서 사용되며, 해당 메서드가 빈을 생성하고 반환한다는 것을 나타낸다. 해당 메서드의 반환값이 IoC 컨테이너에 의해서 Bean으로 관리된다.

 

주로 해당 방식은 어떤 객체를 Application에 맞는 설정을 수행한 후 Bean으로 등록할 때 사용된다.

라이브러리 등에서 나온 Java 기반의 설정 클래스들을 직접 정의한 이후 Bean으로 구성하려고 할 때 유용하다.

 

 

 

2-2. Component

@Component
public class MyComponent {
    // 해당 클래스는 스프링 컨테이너에서 빈으로 관리됨
}

 

@Component는 해당 클래스가 스프링 컨테이너에서 관리해야 하는 컴포넌트임을 나타내고, 이를 통해 Bean으로 등록되어 관리된다.

 

해당 방식은 클래스 자체가 빈으로 등록되어 관리되어진다.

이는 주로 개발자들이 직접 작성한 클래스를 스프링 빈으로 등록하여 사용할 때 많이 사용된다. 

@Repository, @Service, @Controller 등도 @Component를 기반으로 한다.

 

 

 

 

 


3. Spring Boot에서 의존성 주입(DI)을 수행하는 방법들

 

3-1. Autowired

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

}
@Service
public class UserService {

    private UserRepository userRepository;
    
    @Autowired
    public void setUserRepository(UserRepository repository){
    	this.userRepository = repository;
    }
    
}

 

@Autowired 키워드를 이용하여, 주입받으려는 멤버변수에 직접, 혹은 Setter 등 메서드를 통하여 의존성을 주입받을 수 있다.

해당 어노테이션이 적용된 필드의 타입을 기반으로 Spring은 적절한 Bean을 찾아서 자동으로 주입해준다.

 

 

 

3-2. 생성자 Injection

@Service
public class UserService {

    private UserRepository userRepository;

//    @Autowired
    public MyService(UserRepository repository) {
        this.userRepository = repository;
    }
    
    
}

 

생성자를 통해서 의존성을 주입받는 방식이다. 만약 생성자가 하나만 있는 경우라면 @Autowired를 생략해도 된다.

IoC 컨테이너에서 해당 생성자의 매개변수를 기반으로 해당 클래스에 필요한 의존성을 파악하고 DI를 수행한다.

 

 

 

3-3. RequiredArgsConstructor

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    //Lombok이 생성자를 자동으로 생성해줌
   
}

 

RequiredArgsConstructor는 Lombok에서 제공해주는 어노테이션으로, 필드에 대한 생성자를 자동으로 생성해주는 역할을 한다.

단, final 키워드가 붙은 필드에 대한 생성자를 자동으로 생성해주므로 의존성을 주입받을 필드에는 final 키워드를 꼭 사용해주어야 한다.

 

 

 

 

3-4. Qualifier와 Primary

 

@Repository
@Primary
public class UserRepositoryImpl implements UserRepository {

}

@Repository
public class UserRepositoryImpl2 implements UserRepository {

}

 

만약 다음과 같이 UserRepository를 구현하는 클래스 두 개가 Bean으로 등록된다고 가정해보자.

위쪽에서는 UserRepository를 타입으로 하여 의존성을 주입받았었는데, 해당 타입의 Bean 두 개 이상일 경우에는 어떻게 해야 할까?

 

@Primary의 역할은 해당 빈을 기본 Bean으로서 지정하여, 의존성 주입 시 명시하지 않는다면 해당 Bean을 디폴트로 사용한다.

 

 

 

@Service
public class UserService {

    private UserRepository userRepository;

    public MyService(@Qualifier("userRepositoryImpl2")UserRepository repository) {
        this.userRepository = repository;
    }
    
    
}

 

@Qualifier은 다음과 같이 명시적으로 어떤 Bean을 주입받을 지 지정할 수 있다. 

UserRepository 타입 중, UserRepositoryImpl2의 이름(Name)에 해당하는 Bean을 주입받겠다는 뜻이다.

 

Spring Bean 이름의 생성 규칙 : 클래스 이름 + 첫 글짜를 소문자로 바꿈

 

 

 

 

@Service
@RequiredArgsConstructor
public class UserService {

    @Qualifier("userRepositoryImpl2")
    private final UserRepository userRepository;

    //Lombok이 생성자를 자동으로 생성해줌
   
}

 

RequiredArgsConstructor을 사용할 때에도 마찬가지이다.

 

 

 

 

반응형

BELATED ARTICLES

more