지난 포스팅에서 의존성 주입이 무엇인지 코드를 통해서 알아보았다. 그러면 이제 IoC가 무엇인지 왜 DI랑 묶이는지에 대해 알아볼 필요가 있고 이 또한 코드를 통해 살펴볼 것이다. 스프링의 핵심개념 중 첫번째로 IoC/DI가 언급되는데, 그러면 스프링 IoC는 어떻게 사용할 수 있는지 확인해 볼 필요가 있다.
IoC(제어의 역전)는 말 그대로 제어의 흐름구조가 뒤바뀌는 것이다. 코드를 통해 기본적인 IoC의 개념을 살펴볼 것이고, 그 다음 스프링 IoC에 대해 추가적으로 볼 것이다.
지난 포스팅에서 의존성 주입 [ 제어의 역전/의존성 주입 - 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를 위반하는 것이라 볼 수 있다.
마찬가지로 이번에는 지난 포스팅 [ 제어의 역전/의존성 주입 - 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라는 새로운 클래스를 만들어 보자. 또한 여기서 설명을 위해 여러 새로운 조건들을 추가하겠다.
새 조건
코드전체는 깃허브에서 확인할 수 있다.
특정회사와 연결하는 코드이다.
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 컨테이너이다.
TelcoServiceFactory를 통해 IoC를 했지만 여러 문제가 있다. 하나씩 살펴보자.
💡 이러한 문제점들을 것들을 스프링 IoC 컨테이너에서 관리해준다.
조금 더 알아보기 전에 용어한번 정리하고 갈필요가 있다.
- Bean : 스프링 IoC에서 관리하는 오브젝트
- Bean Factory : Bean을 생성, 조회, 등록, 반환 등 관리한다.
- ApplicationContext : DI외에도 트랜잭션, AOP 등 여러 서비스를 제공한다 . 간단히 말해 Bean Factory를 상속한 인터페이스이다.
- Configuration : ApplicationContext나 Bean Factory를 사용하기위한 설정 정보를 의미한다.
💡 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 라는 결과가 하나만 출력됨을 볼 수 있다.
3.28 수정
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를 통해 해결 될 수 있다. 이것이 스프링의 가장 큰 장점이고 스프링이 단순 라이브러리가 아닌 프레임 워크라 부르는 이유다. 해결된 단점을 통해 생긴 장점은 다음과 같다.
@Component
말고도 xml
파일 등과 같은 방식으로 스프링 컨테이너가 의존성 주입을 관리할 수 있다.의존성 주입
과, 의존성 룩업
이 있다.Bean Factory
가 이와 같은 역할을 담당하고 ApplicationContext
는 Bean Factory
의 확장된 역할을 한다.전체코드는 깃허브에서 볼 수 있다.
https://github.com/waonderboy/spring-triangle/
- 토비의 스프링 3.1
- 전문가를 위한 스프링 5
- geeksforgeeks