IOC(Inversion Of Control) 프로그램의 제어 흐름을 반전시키는 소프트웨어 설계 패턴입니다. 프레임워크가 개체 자체가 아니라 개체의 수명 주기와 해당 종속성을 관리하는 책임이 있음을 의미합니다.
스프링에서는 일반적인 Java 객체를 new로 생성하여 개발자가 관리하는 것이 아닌 Spring Container에 모두 맡깁니다. 즉, 개발자 -> 프레임워크로 제어의 객체 관리의 권한이 넘어 갔음을 의미하며 이를 제어의 역전이라고 합니다.
1. 모듈성 향상 및 관심사 분리
DI를 사용하면 애플리케이션의 객체가 종속성과 밀접하게 결합되지 않으므로 보다 모듈화된 방식으로 설계할 수 있습니다. 이는 관심사 분리로 인한 유지보수 및 코드 관리 측면에서 용이합니다.
2. 느슨한 결합
DI를 사용하면 객체가 자체 종속성을 관리할 때와 같은 방식으로 서로 의존하지 않기 때문에 객체 간의 결합이 느슨해집니다. 시스템에 다른 부분에 영향을 주지 않고 종속성을 쉽게 바꾸거나 수정할 수 있습니다.
3 코드 테스트 용이
의존성으로 부터 격리시켜 코드 테스트에 용이합니다. DI를 사용하면 개별 구성 요소에 대한 자동화된 테스트를 독립적으로 작성하기가 더 쉬워집니다. DI를 통해 불가능한 상황을 Mock과 같은 기술을 통하여, 안정적으로 테스트가 가능합니다.
EncodeService
interface EncodingService {
public String encode(String msg);
}
Encorder
package com.example.ioctest.ioc.encode;
public class Encoder {
private EncodingService service;
public Encoder(EncodingService service) {
this.service = service;
}
public String encode(String msg) {
return service.encode(msg);
}
}
Base64, Url Encoder
class UrlEncoder implements EncodingService {
@Override
public String encode(String msg) {
try {
return URLEncoder.encode(msg, StandardCharsets.UTF_8);
} catch (UnsupportedOperationException e) {
e.printStackTrace();
return null;
}
}
}
class Base64Encoder implements EncodingService {
@Override
public String encode(String msg) {
return Base64.getEncoder().encodeToString(msg.getBytes());
}
}
Main
public class Main {
public static void main(String[] args) {
String url = "www.naver.com";
//todo 외부에서 내가 사용하는 객체를 주입하는 것 -> DI
// Encoder encode = new Encoder(new Base64Encoder());
Encoder encoder = new Encoder(new UrlEncoder());
String result = encoder.encode(url);
System.out.println(result);
}
}
외부에서 내가 사용할 객체를 주입하는 것을 의미하며 앞서 예시로 나온 코드처럼 Encoder 생성자에 내가 이용할 객체를 주입하여 사용하는 것입니다. 만약 의존성 주입을 하지 않는다면 어떻게 구현이 될까요?
public class Main {
public static void main(String[] args) throws UnsupportedEncodingException {
String url = "www.naver.com";
// base 64 encoding
Basae64Encoder base64Encoder = new Base64Encoder();
String result = base64Encoder.encode(url);
System.out.println(result);
//url encoding
UrlEncoder urlEncoder = new UrlEncoder();
String urlResult = UrlEncoder.encode(url);
System.out.println(urlResult);
}
}
해당 코드처럼 따로따로 받아 실행하야 합니다. 문제는 해당 객체를 유지보수 혹은 추가하 코드 변화에 대응할 점이 많아진다는 것입니다. 즉, 코드 유지보수 및 간결성이 떨어지게 됩니다.
이전 코드에서 DI를 사용하였지만 그대로 개발자가 관리를 직접하는 경우 입니다. Spring에서의 경우는 개발자가 Spring에게 관리권한을 넘겨줍니다. @Component 어느테이션을 사용하면 bean으로 변경 시킨 이후 Spring에게 관리 권한을 넘깁니다. 넘겨 받은 권한으로 @SpringBootApplication에서 직접 객체를 싱글톤 형태로 만들어 Spring Container에서 관리합니다.
컴포넌트에서 Bean를 만들고 ApplicationContextAware 인터페이스를 구현하여 setApplicationContext를 호출합니다. Bean은 자신의 인스턴스를 관리하는 context가 어떤 인스턴스 인지 확인하고 접근할 수 있습니다. 즉, ApplicationContext는 Bean을 관리합니다. 사용하는 방법은 다음과 같습니다.
@Component
public class ApplicationContextProvider implements ApplicationContextAware {
private static ApplicationContext context;
//todo spring 이 알아서 applicationContext 에다가 주입한다.
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static ApplicationContext getContext() {
return context;
}
}
Application
@SpringBootApplication // spring 실행이 될 때 직접 객체를 싱글톤 형태로 만들어 Spring container에서 관리
public class IocTestApplication {
public static void main(String[] args) {
SpringApplication.run(IocTestApplication.class, args);
ApplicationContext context = ApplicationContextProvider.getContext();
}
}
만들어진 context를 이용하여 IOC 설계 패턴을 사용할 수 있습니다.
Base64Encoder, UrlEncoder
@Component("base64") // -> Spring 에서 bean으로 바꾼다음 관리권한을 Spring에게 관리 권한 위임
class Base64Encoder implements EncodingService {
@Override
public String encode(String msg) {
return Base64.getEncoder().encodeToString(msg.getBytes());
}
}
@Component("url")
class UrlEncoder implements EncodingService {
@Override
public String encode(String msg) {
try {
return URLEncoder.encode(msg, StandardCharsets.UTF_8);
} catch (UnsupportedOperationException e) {
e.printStackTrace();
return null;
}
}
}
Encoder
@Component
public class Encoder {
private EncodingService service;
public Encoder(@Qualifier("base64") EncodingService service) {
this.service = service;
}
public void setEncoder(EncodingService service) {
this.service = service;
}
public String encode(String msg) {
return service.encode(msg);
}
}
Main
@SpringBootApplication
public class IocTestApplication {
public static void main(String[] args) {
SpringApplication.run(IocTestApplication.class, args);
ApplicationContext context = ApplicationContextProvider.getContext();
// Base64Encoder base64Encoder = context.getBean(Base64Encoder.class);
// UrlEncoder urlEncoder = context.getBean(UrlEncoder.class);
//spring형태에서 관리 되는 객체 -> bean
Encoder encoder = context.getBean(Encoder.class);
String url = "www.naver.com";
String result = encoder.encode(url);
System.out.println(result);
encoder.setEncoder(context.getBean(UrlEncoder.class));
// encoder.setEncoder(urlEncoder);
// result = encoder.encode(url);
// System.out.println(result);
}
}
context에서 getBean을 활용하여 접근할 수 있습니다.