제어의 역전(IoC)- 스프링 IoC/DI (2)

waonderboy·2022년 3월 25일
1

스프링

목록 보기
2/2
post-thumbnail

IoC/DI(2)

지난 포스팅에서 의존성 주입이 무엇인지 코드를 통해서 알아보았다. 그러면 이제 IoC가 무엇인지 왜 DI랑 묶이는지에 대해 알아볼 필요가 있고 이 또한 코드를 통해 살펴볼 것이다. 스프링의 핵심개념 중 첫번째로 IoC/DI가 언급되는데, 그러면 스프링 IoC는 어떻게 사용할 수 있는지 확인해 볼 필요가 있다.


IoC (Inversion of Control) 개념

IoC(제어의 역전)는 말 그대로 제어의 흐름구조가 뒤바뀌는 것이다. 코드를 통해 기본적인 IoC의 개념을 살펴볼 것이고, 그 다음 스프링 IoC에 대해 추가적으로 볼 것이다.


제어의 주체 - 1. 필요 서비스를 사용하는 대상

지난 포스팅에서 의존성 주입 [ 제어의 역전/의존성 주입 - IoC/DI (1) - DI ] 에 대해서 다루었다. 의존성 주입이 안된 예제 코드의 일부를 살펴보면 다음과 같은 코드를 찾아 볼 수 있다.

public class Client {
    private KTInternetService ktInternetService;

    public void connectToInternet() {
        ktInternetService = new KTInternetService();
        ktInternetService.connectTo();
    }
}

해당코드는 의존 Client가 직접 원하는 Internet Service를 결정하고 생성하는 것을 볼 수 있다. 그리고 OCP(Open Closed Principle)을 위반하는 것을 볼 수 있다. 왜 그런지 대해 극단적인 예시를 들어보자.

아래와 같은 상황을 살펴보자. 만약 100개 이상의 통신 서비스가 생기고(그럴 일은 적겠지만), 그 서비스를 사용하기위해 Client 코드에 일일히 추가 해야되는 상황이라면 일일이 코드를 추가해줘야 하고 계약이 만료되어 SKT로 바꾸는 상황이면 많은 코드수정이 필요할 것이다.

public class Client {
    private KTInternetService ktInternetService;
    private KTTvService ktTvService;
    private KTTelephoneService ktTelephoneService;

    public void connectToInternet() {
        ktInternetService = new KTInternetService();
        ktInternetService.connectTo();
    }
    public void connectToTv() {
        ktInternetService = new KTInternetService();
        ktInternetService.connectTo();
    }
    public void connectToTelephone() {
        ktInternetService = new KTInternetService();
        ktInternetService.connectTo();
    }
    .
    .
    // 또 다른 새로운 서비스들이 추가될 수 있다.
}

이와 같은 문제는 서비스를 제공하는 업체(KT, SKT, LG)를 제어하는 권한이 Client 코드에 직접적으로 들어가 있기 때문에 강한 결합이 발생하여 OCP를 위반하는 것이라 볼 수 있다.


제어의 주체 - 2. Factory 클래스

마찬가지로 이번에는 지난 포스팅 [ 제어의 역전/의존성 주입 - IoC/DI (1) - DI ] 에서 의존성 주입이 되어있는 코드를 잠깐 살펴보면 아래와 같은 코드를 확인할 수 있다.

public class Client {
    private InternetService internetService;

    public Client(InternetService internetService) {
        this.internetService = internetService;
    }

    public void connectToInternet() {
        internetService.connectTo();
    }
}
.
.
public class ConnectionTest {
    public static void main(String args[]) {
        Client client1 = new Client(new KTInternetService());
        client1.connectToInternet();
		//....
    }
}

인터페이스를 사용하여 InternetService를 구현했기 때문에 low coupling이 되었고, Clinet는 의존성을 주입 받는 것을 볼 수 있었다. Client 클래스는 코드 내부에서 구현하여 해당 회사(KT)를 직접 선택하는 것이 아니고 ConnectionTest 클래스에 의해 결정되는 것을 알 수 있다. 이는 제어권이 Client에서 ConnectionTest로 넘어감을 의미한다. 때문에 이를 제어의 역전(Inversion of Control)이라 한다.

💡 의존성 주입을 통해 제어의 역전이 일어 났음을 알 수 있다.

하지만, 이러한 코드 또한 문제가 있는데 ConnectionTest에서 Client에 의존성을 주입하고 있으니 SRP(Single Responsibility Principle)에 어긋나고 높은 응집도(high Cohesion)에도 걸맞지 않다는 것이다. 그러면 책임을 분리하기 위해 서비스 연결을 담당하는 TelcoServiceFactory라는 새로운 클래스를 만들어 보자. 또한 여기서 설명을 위해 여러 새로운 조건들을 추가하겠다.


새 조건

  • 통신 서비스를 묶어서 계약하면 가격이 싸기 때문에 한 회사의 통신 서비스만 사용한다고 하자.
  • 회사에서 지원하는 통신 서비스는 TV, 유선전화, 인터넷이 있고 더 늘어날 수 있다.
  • 고객들은 계약이 만료되면 다른 통신 회사와 계약할 수 있다.

코드전체는 깃허브에서 확인할 수 있다.

특정회사와 연결하는 코드이다.

public class KTTelcoServiceProvider implements TelcoServiceProvider {
    public void connectTo() {
        System.out.println(">>> Telecommunication Service Provider, KT");
    }
    public void disconnect() {System.out.println(">>> Disconnect to us."); }
}

회사가 제공하는 서비스 코드이다.

public class TelephoneService implements TelcoService {
    private TelcoServiceProvider telcoServiceProvider;

    public TelephoneService (TelcoServiceProvider telcoServiceProvider) {
        this.telcoServiceProvider = telcoServiceProvider;
    }

    public String showConnectMessage() {
        return "Connect to Telephone service of " + telcoServiceProvider.getClass().getSimpleName();
    }

}

조건을 토대로 책임을 분리하고 낮은 결합도를 유지하며 TelcoServiceFactory 클래스를 만들었다.

public class TelcoServiceFactory {

    public InternetService telcoInternetService() {
        return new InternetService(serviceAgreement());
    }

    public TvService telcoTvService() {
        return new TvService(serviceAgreement());
    }

    public TelephoneService telcoTelephoneService() {
        return new TelephoneService(serviceAgreement());
    }

    public TelcoServiceProvider serviceAgreement() {
        TelcoServiceProvider ktServiceProvider = new KTTelcoServiceProvider();
        ktServiceProvider.connectTo();
        return ktServiceProvider;
    }

    private static class telcoServiceHolder {
        private static final TelcoServiceFactory INSTANCE = new TelcoServiceFactory();
    }

    public static TelcoServiceFactory getInstance() {
        return telcoServiceHolder.INSTANCE;
    }
}

테스트 결과는 다음과 같다.

public class ConnectionTest {
    public static void main(String args[]) {
        Client client = new Client();

        System.out.println("------------------------------------------------------");
        client.connectToTvService();
        client.connectToInternetService();
        client.connectToTelephoneService();
        System.out.println("------------------------------------------------------");
    }
}

결과 :: 
>>> Telecommunication Service Provider, KT
>>> Telecommunication Service Provider, KT
>>> Telecommunication Service Provider, KT
------------------------------------------------------
Connect to Tv service of KTTelcoServiceProvider
Connect to Internet service of KTTelcoServiceProvider
Connect to Telephone service of KTTelcoServiceProvider
------------------------------------------------------

같은 역할을 하는 여러 팩토리는 필요없기 때문에 싱글톤 패턴으로 만들었다. 이제 제어의 권한은 TelcoServiceFactory 클래스에 있고 Client -> ConnectTest -> TelcoServiceFactory 순으로 제어의 역전이 일어났음을 알 수 있다.

이제 Client에 주입되는 객체의 생성은 TelcoServiceFactory 가 담당함을 알 수 있다.

하지만 이런 코드도 개선할 필요가 있다.

💡TelcoServiceFactory는 간단한 IoC 컨테이너이다.



스프링 IoC

TelcoServiceFactory의 문제점

TelcoServiceFactory를 통해 IoC를 했지만 여러 문제가 있다. 하나씩 살펴보자.

  • 개발을 하다보면 여러 팩토리가 생성될 수 있으며, 그럴 때마다 Client는 특정 팩토리를 알아야한다.
  • 팩토리를 요청할 때 마다 인스턴스가 생성될 필요가 없기 때문에 팩토리는 빈을 싱글턴 패턴으로 사용하는게 좋다
  • 마찬가지로 팩토리에서 관리되는 객체들도 싱글톤으로 관리될 필요가 있다
  • 객체가 생성만 되고 소멸되지 않으면 메모리 낭비가 발생할 수 있기 때문에 lifecycle을 관리하는 코드 또한 필요할 수 있다.

💡 이러한 문제점들을 것들을 스프링 IoC 컨테이너에서 관리해준다.


스프링 IoC 컨테이너 관련 용어

조금 더 알아보기 전에 용어한번 정리하고 갈필요가 있다.

  • Bean : 스프링 IoC에서 관리하는 오브젝트
  • Bean Factory : Bean을 생성, 조회, 등록, 반환 등 관리한다.
  • ApplicationContext : DI외에도 트랜잭션, AOP 등 여러 서비스를 제공한다 . 간단히 말해 Bean Factory를 상속한 인터페이스이다.
  • Configuration : ApplicationContext나 Bean Factory를 사용하기위한 설정 정보를 의미한다.

스프링 IoC 사용하기

💡 IoC를 구현하는 방식 중 하나가 DI이다. IoC를 구현하는 방식에는 의존성 주입의존성 룩업이라는 방식이 있다. 스프링에서는 주로 DI를 통해 IoC를 지원하기에 IoC/DI가 함께 묶이지만, 의존성 룩업을 통해 컴포넌트에 접근하는 방식도 지원한다.

@SpringBootApplication 어노테이션을 통해 해당 클래스가 메인으로 실행될 것임을 정해주고. @Configuration를 통해 TelcoServiceFactory를 스프링 IoC 컨테이너로 사용할 것임을 알려준다. 또한 @Bean을 통해 스프링 IoC 컨테이너에서 관리 될 객체임을 알려준다.

@SpringBootApplication
public class ConnectionTest {
    public static void main(String args[]) {
        SpringApplication.run(ConnectionTest.class, args);
        Client client = new Client();

        System.out.println("------------------------------------------------------");
        client.connectToTvService();
        client.connectToInternetService();
        client.connectToTelephoneService();
        System.out.println("------------------------------------------------------");

    }
}

@Configuration
class TelcoServiceFactory {

    @Bean
    public InternetService telcoInternetService() {
        return new InternetService(serviceAgreement());
    }

    @Bean
    public TvService telcoTvService() {
        return new TvService(serviceAgreement());
    }

    @Bean
    public TelephoneService telcoTelephoneService() {
        return new TelephoneService(serviceAgreement());
    }

    @Bean
    public TelcoServiceProvider serviceAgreement() {
        TelcoServiceProvider ktServiceProvider = new KTTelcoServiceProvider();
        ktServiceProvider.connectTo();
        return ktServiceProvider;
    }
}

아래는 ApplicationContext를 생성하는 클래스이다.

@Component
public class ApplicationContextProvider implements ApplicationContextAware {
    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    public static ApplicationContext getContext(){
        return context;
    }
}

ApplicationContext에 의해 의존성을 주입 받고있는 Client이다. 제어권은 ApplicationContext에게 있고 이는 스프링이 관리한다.

public class Client {
    private TvService tvService;
    private InternetService internetService;
    private TelephoneService telephoneService;
    ApplicationContext context = ApplicationContextProvider.getContext();

    public Client() {
        this.tvService = context.getBean(TvService.class);
        this.internetService = context.getBean(InternetService.class);
        this.telephoneService = context.getBean(TelephoneService.class);
    }

    public void connectToTvService() {
        System.out.println(this.tvService.showConnectMessage());
    }
    

실행결과는 다음과 같다.

>>> Telecommunication Service Provider, KT

------------------------------------------------------
Connect to Tv service of KTTelcoServiceProvider
Connect to Internet service of KTTelcoServiceProvider
Connect to Telephone service of KTTelcoServiceProvider
------------------------------------------------------

싱글턴 패턴이라 serviceAgreement() Bean 하나만 생성되었기 때문에
>>> Telecommunication Service Provider, KT 라는 결과가 하나만 출력됨을 볼 수 있다.


작성된 스프링 IoC 코드 리팩터링

3.28 수정

  • 문제점
    • Client에서 applicationContext를 생성하여 SRP를 위반 하고 있다.
  • 개선
    • TelcoServiceFactory 클래스를 개선하여 Client에 SRP를 만족하도록 수정하였다.
public class Client {
    private TvService tvService;
    private InternetService internetService;
    private TelephoneService telephoneService;
    
    // 수정된 부분
    public Client(TvService tvService, InternetService internetService, TelephoneService telephoneService) {
        this.tvService = tvService;
        this.internetService = internetService;
        this.telephoneService = telephoneService;
    }

    public void connectToTvService() {
        System.out.println(tvService.showConnectMessage());
    }

    public void connectToInternetService() {
        System.out.println(internetService.showConnectMessage());
    }

    public void connectToTelephoneService() {
        System.out.println(telephoneService.showConnectMessage());
    }
}
@Configuration
class TelcoServiceFactory {
	// 추가된 부분
    @Bean
    public Client makeClientService () {
        return new Client(telcoTvService(), telcoInternetService(), telcoTelephoneService());
    }

    @Bean
    public InternetService telcoInternetService() {
        return new InternetService(serviceAgreement());
    }

    @Bean
    public TvService telcoTvService() {
        return new TvService(serviceAgreement());
    }

    @Bean
    public TelephoneService telcoTelephoneService() {
        return new TelephoneService(serviceAgreement());
    }

    @Bean
    public TelcoServiceProvider serviceAgreement() {
        TelcoServiceProvider ktServiceProvider = new KTTelcoServiceProvider();
        ktServiceProvider.connectTo();
        return ktServiceProvider;
    }
}

클라이언트 생성부분이 수정되었다.

@SpringBootApplication
public class ConnectionTest {
    public static void main(String args[]) {
        SpringApplication.run(ConnectionTest.class, args);
        ApplicationContext context = ApplicationContextProvider.getContext();
        // 수정된 부분
        Client client = context.getBean(Client.class);
        System.out.println("------------------------------------------------------");
        client.connectToTvService();
        client.connectToInternetService();
        client.connectToTelephoneService();
        System.out.println("------------------------------------------------------");

    }
}

객체지향 설계 5가지 원칙 (SOLID) 관점에서 작성된 코드를 살펴보자

  • OCP : Client는 어떤 회사의 서비스든 받을 수 있고, 회사가 변경되더라도 Client코드를 바꾸지 않아도 되기 때문에 OCP를 만족한다.
  • DIP : Client는 인터페이스에만 의존하게 되고 TelcoServiceFactory에 의해 의존성 주입을 받기때문에 DIP를 만족한다.
  • SRP : Client는 ApplicationContext를 생성하는 부분이 지워지고 단지 자신의 역할에만 신경을 쓰기 때문에 SRP를 만족한다.

스프링 IoC 장점

위에서 언급한 문제들이 스프링 IoC를 통해 해결 될 수 있다. 이것이 스프링의 가장 큰 장점이고 스프링이 단순 라이브러리가 아닌 프레임 워크라 부르는 이유다. 해결된 단점을 통해 생긴 장점은 다음과 같다.

  • Client는 특정 팩토리를 알 필요가 없다.
  • @Component 말고도 xml파일 등과 같은 방식으로 스프링 컨테이너가 의존성 주입을 관리할 수 있다.
  • 싱글턴으로 객체를 관리하기 때문에 메모리 효율이 좋다.
  • 객체가 생성만 되고 소멸되지 않으면 메모리 낭비가 발생할 수 있기 때문에 lifecycle을 관리하는 코드 또한 필요할 수 있다.



정리

  • IoC(제어의 역전)은 의존하는 객체를 생성하는 권한이 다른 객체로 넘어가는 것이다.
  • IoC로 인한 장점은 결합이 느슨해지고 OCP를 만족한다는 것이다.
  • IoC를 구현하는 방법중에는 의존성 주입과, 의존성 룩업이 있다.
  • 스프링에서는 주로 DI를 통해 IoC를 지원하기에 IoC/DI가 함께 묶이지만, 의존성 룩업을 통해 컴포넌트에 접근하는 방식도 지원한다.
  • 스프링은 빈의 라이프사이클을 관리하기 때문에 단순 라이브러리가 아닌 프레임워크다.
  • Bean Factory가 이와 같은 역할을 담당하고 ApplicationContextBean Factory의 확장된 역할을 한다.

전체코드는 깃허브에서 볼 수 있다.

https://github.com/waonderboy/spring-triangle/







Reference

  • 토비의 스프링 3.1
  • 전문가를 위한 스프링 5
  • geeksforgeeks
profile
wander to wonder 2021.7 ~

0개의 댓글