의존Dependency이란?
정의
OOP에서 하나의 객체는 섬island으로 존재하지 않습니다. 소프트웨어 내부에는 수 많은 객체가 존재하고, 객체들은 각자의 역할과 책임을 다하며 서로 메시지를 주고 받으며 협력하여 프로그램이 동작하게 됩니다.
협력을 한다는 것은 그 협력 대상에 대한 이해를 필요로 하며, 이는 대상 객체에 대해 ‘알고’ 있어야 한다는 것을 의미합니다.
대상 객체에 대해 ‘알고’ 있다는 것은 어떤 의미일까요? 대상 객체를 알고 있다는 것은 해당 객체로부터 기대되는 행동을 알고있다는 의미를 가질 수 있습니다. 일부는 더 나아가 기대되는 행동을 어떻게 수행할지 알고 있다는 의미를 가질 수도 있습니다. 이를 소스코드 레벨에서 표현하면 소스코드 A에 소스코드 B의 코드가 존재한다고도 할 수 있습니다.
대상 객체를 알고 있는 것은 필연적으로 그것에 대해 의존하게 됩니다.
의존성이 생기면 곧 변경에 영향을 받습니다. A가 B에 의존하면 B가 변화할때 그 영향이 A까지 퍼지게 됩니다. 이러한 영향의 전파change propagation는 프로그램을 수정하기 어렵게 만들곤 합니다. 이러한 영향의 전파는 각각의 객체가 다른 객체를 더 많이 알고 있을수록 더욱 심각한 문제를 만들어 냅니다.
따라서 소프트웨어 개발자는 프로그램의 의존성을 관리하여 유지보수가 쉽고 확장성이 높은 프로그램을 만들어야할 필요가 있습니다.
의존성과 UML Class Diagram
UML Class Diagram으로 클래스와 그들간의 관계를 파악할 수 있습니다. 이 때 ‘관계’는 그 특성에 따라 오른쪽 그림과 같이 구분됩니다.
클래스간 관계는 다양한 이유로 형성됩니다. 상속, 구체화, 파라미터 타입, 반환 타입, 필드 타입, 로컬 변수 타입 등 다양한 형태로 클래스는 관계를 맺습니다. 왼쪽 그림은 Document라는 조상 타입이 있고, 이를 상속한 Book 타입과 Email 타입을 표현하고 있습니다. Email과 Book은 Document를 의존하므로 화살표 방향은 자식 타입에서 부모 타입으로 향하고 있습니다. Email의 소스코드와 Book의 소스코드는 Document의 소스코드를 포함하고 있을 것이 자명합니다. 반면 Document는 Email이나 Book에 의존하지 않습니다. Document는 자식 클래스에 관한 어떠한 정보도 가지고 있지 않습니다.
따라서 변경의 방향은 화살표의 역방향으로 이루어집니다. Document의 변경은 Book과 Email에 영향을 미치지만, 그 역은 성립하지 않습니다.
의존성이 높은 프로그램의 특징
의존은 다음과 같은 문제점을 낳습니다.
- Rigidity 경직성 : 코드가 단단하게 묶여있어 변경하기 어려워짐
- Fragility 취약성 : 한 모듈의 수정이 다른 모듈에 영향을 미쳐서 프로그램이 쉽게 깨지는 것
- Immobility 부동성 : 모듈이 쉽게 추출되지 않고 재사용이 어려워지는 것
- Viscosity 점성 : 여러 계층을 거쳐 의존성을 갖는 것
흔히 개발을 할 때 한 부분을 수정하고 싶어서 변경했는데 그 코드를 사용하는 다른 측에서 오류가 발생하여 연쇄적인 수정을 수행해야만 프로그램이 올바르게 돌아가는 경우가 많았을 것이라고 생각합니다. 이러한 문제들이 의존성이 높아서 발생했던 문제들입니다. 이러한 문제들은 컴파일오류를 일으키기도 하고, 런타임에 오류를 일으키기도 합니다.
소프트웨어 모듈은 구체적인concrete 클래스 구현체에 의존할때 그 문제가 가장 심해지며, 인터페이스에 의존하게 된다면 그 문제는 비교적 작아집니다.
구체적인 클래스에 의존하는 관계를 강한 결합Tight coupling이라고 하며, 추상적인 클래스(인터페이스) 등에 의존하는 관계를 약한 결합Loose Coupling이라고 합니다. 결국 개발자들은 변경이 잦은 부분을 약한 결합을 통해 의존도를 줄이는 방향으로 설계를 해나가야 합니다.
의존성의 전이
Dependency 는 직접적으로 연결된 클래스 뿐만 아니라 그 너머에 연결되어있는 모듈에도 의존을 갖습니다. PeriodCondition은 이것이 직접 의존하는 Screening 뿐만 아니라 Movie의 변경에도 영향을 받게 될 것입니다. 물론 이 영향이 어디까지 전파될지 결정하는 것은 개발자의 몫입니다. 개발자는 적절한 캡슐화와 정보은닉을 통해 변경의 영향이 전파되는 정도를 줄일 수 있습니다.
Coupling과 Cohesion
좋은 설계를 위해서는 시스템을 Modularistic하게 만들 필요가 있습니다. Modularistic이란 시스템을 여러개의 모듈로 나누고 이들간의 적절한 책임을 부여하는 것입니다. 이렇게 각 모듈의 관심사를 분리하고, 모듈간의 인터페이스를 작고 간단하게 정의한다면 프로그램의 의존성을 줄일 수 있습니다.
소프트웨어 Modularity의 정도는 Coupling과 Cohesion이라는 척도로 측정될 수 있습니다.
Coupling이란 모듈간의 상호연관관계의 정도를 측정하는 것입니다. 어떠한 모듈이 다른 모듈에 많이 의존하고 있다면 Coupling이 높게 나타나고, 변경에 영향이 더 많이 퍼지게 될 수 있습니다. 또한 모듈 하나를 이해하기 위해 다른 여러 모듈에 대해 알아야할 필요가 생깁니다. 재활용이 곤란해지기도 합니다. 따라서 모듈 간의 연결은 작고 간단한 경로만 가져야 위와 같은 문제 상황이 줄어들 수 있습니다.
Cohesion이란 한 모듈 내에서 그 요소들이 얼마나 관계성 있게 묶여있는가를 측정하는 지표입니다. 서로 관련 있는 책임들이 하나의 단위에 모여있을 때 Cohesion이 높다고 이야기합니다.
예제, LogService
이론적인 내용을 먼저 읽으면 이해가 덜 될 것같아 코드로 든 예시를 먼저 보여드리겠습니다
//LogService
public class LogService {
private SimpleLogDecorator decorator = new SimpleLogDecorator();
public void log(String message) {
System.out.println(decorator.decorate(message));
}
}
public class SimpleLogDecorator {
public String decorate(String message) {
return "[LOG] "+ message;
}
}
LogService.java 는 예시를 위한 아주 간단한 클래스로, void log(String message)
라는 메서드를 갖고 있습니다. 이 메서드는 파라미터로 전달받은 문자열을 출력하는데, 이 과정에서 문자열을 꾸며주는 역할을 하는 객체 SimpleLogDecorator의 도움을 받습니다. SimpleLogDecorator은 문자열 앞에 “[LOG]”를 더해줍니다.
public static void main(String[] args) {
SpringApplication.run(DiApplication.class, args);
LogService logService = new LogService();
logService.log("Hello World!");
}
메인 메서드에서는 LogService를 생성하고 log를 호출합니다. log 메서드의 내부에서는 decorate가 호출되어 결과적으로 “[LOG] Hello World!” 가 출력됩니다.
PM의 요청
악덕 PM S씨는 오늘도 어김없이 추가적인 개발을 요청합니다.
조금 더 복잡한 로깅 데코레이터 클래스를 만들어서 LogService에 적용시켜주세요. 기존의 SimpleLogDecorator는 재사용할 수 있으니 코드를 버리지 말고 남겨주세요
요구사항을 들은 개발자 K 군은 어떻게 문제를 해결할지에 대해 고민한 후 다음과 같은 결정을 내립니다.
ComplexLogDecorator클래스를 만든 후 기능을 개발하고 LogService가 ComplexLogDecorator 를 사용하게 만들어야겠군!
이에대 한 결과물은 다음과 같습니다.
public class ComplexLogDecorator{
public String decorate(String message) {
// complex logic
return "[LOG] very very complex : "; message;
}
}
public class LogService {
private ComplexLogDecorator decorator = new ComplexLogDecorator();
public void log(String message) {
System.out.println(decorator.decorate(message));
}
}
개발자 K씨는 자신의 개발실력에 감탄합니다.
ComplexLogDecorator를 개발한 뒤 LogService의 단 한 줄만 바꿔서 요구사항을 만족시켰군.. 또한 SimpleLogDecorator 도 남겨두었어 움하하
문제점
위와같은 방식의 코드는 언뜻 봤을때 문제없어보이지만, 여러 문제점을 갖고 있습니다.
높은 결합도로 인한 유연성 감소
위와 같은 방식으로 개발했을때는 코드를 변경하기 매우 어렵습니다. 예시 코드는 복잡하지 않으므로 간단히 수정할 수 있었습니다. 하지만 다음 예시를 생각해봅시다.
현재는 LogService에서만 SimpleLogDecorator를 사용하지만, 다른 클래스에서도 SimpleLogDecorator를 사용할 가능성은 열려있습니다.
만약 SimpleLogDecorator를 사용하는 클래스가 100개인 경우, SimpleLogDecorator를 ComplexLogDecorator로 바꿔주기 위해서는 100개의 클래스에 다 들어가 SimpleLogDecorator 부분을 변경해주어야합니다.
바꾸는 것 자체도 일이고, 만약 미처 바꾸지 못한 클래스가 있다면 일부 클래스는 SimpleLogDecorator를 사용하고, 일부 클래스는 기존의 클래스를 사용하는 끔찍한 상황이 발생할 수 있습니다.
이는 생각보다 심각한 문제입니다. 이러한 문제는 우리가 개발한 코드가 아니라 남이 개발한 코드에도 동일하게 적용되는 문제입니다. 가령 스프링부트의 어떤 기능을 커스터마이징해야할 때에 새로운 클래스를 개발했는데 이를 적용시키기 위해서는 스프링부트 패키지 내부에 있는 코드를 뜯어서 수정을 해야할 수도 있습니다. 이는 매우매우 끔찍한 일입니다.
결국 문제는 내가 개발한 기능을 적용시키기 위해서 그 기능을 의존하는 다른 수많은 클래스들을 수정해야한다는 점입니다. 코드 수정시 그 코드에 의존하는 모든 객체에 영향을 끼치게 됩니다.
한 부분의 코드를 수정했는데 그게 굉장히 여러 부분에서 의존하고 있는 부분이라 여러 파일들로부터 컴파일에러가 발생하는 경험은 여러번 경험해보셨을겁니다.
테스팅의 어려움
예시 코드에서 ComplexLogDecorator는 LogService에 내장embedded된 상태입니다. ComplexLogDecorator는 ComplexLogDecorator 없이 실행될 수 없습니다.
따라서 LogService의 코드를 테스팅하기 위해서는, 먼저 ComplexLogDecorator 가 무결하다는 것을 입증해야합니다. ComplexLogDecorator가 무결할때, LogService 자체의 코드가 잘 작성되어있는지 여부를 따질 수 있습니다. ComplexLogDecorator가 에러를 낳는다면 LogService역시 에러를 낳을 것이기 때문입니다. 이 경우 LogService의 코드에는 문제가 없음에도 불구하고 에러를 낳을 것입니다.
따라서 LogService의 코드가 검증하기 위해서는 그것이 강하게 의존하고 있는 클래스들을 먼저 테스팅해야합니다. 이러한 구조는 LogService 자체를 테스팅하기 어렵게 만듭니다.
지금 상황은 LogService가 ComplexLogDecorator 만을 갖고 있지만, 만약 더 많은 객체들을 갖고 있고, 그 객체들이 또 내부적으로 다양한 객체들을 사용중이라면 LogService 를 테스팅하는 것은 매우 어려울 것입니다.
이러한 문제를 해결하기 위해 Dependency Injection을 사용할 수 있습니다.
Dependency Injection
Dependency Injection에서 다루는 개념들을 소개합니다.
DIP (Dependency Inversion Principle)
DIP 는 ‘의존성 역전 원칙’으로, 객체지향의 5원칙 SOLID 중 마지막 원칙에 해당하는 내용입니다.
의존의 역전이라는 것이 어떤 의미인지 먼저 알아보겠습니다.
기존의 소프트웨어는 TOP-DOWN 형식으로 의존성을 가졌습니다.
예를 들어 Controller 계층은 Service 계층을 의존하고, Service 계층은 Repository 계층을 의존하고, Repository는 더 하위 레벨의 Database Module 을 의존하고, 이것은 네트워크 모듈을 의존하고.. 이렇게 하위레벨로 점점 내려가는 의존을 TOP-DOWN 형식의 의존성이라고 합니다.
class LowModule{
void hello {
print("hello");
}
}
class HighModule {
private LowModule lowModule = new LowModule();
public someMethod() {
lowModule.hello();
}
}
DIP 원칙은 이렇게 아래 화살표로 내려가는 의존성을 반대로, 아래에서 위로(BOTTOM-UP) 올려야 한다는 의미입니다. 이 원칙을 지키기 위해서는 두 모듈 사이에 추상화가 필요합니다.
이 추상화 계층을 Java에서는 Interface와 abstract class를 통해 사용합니다. 위 구조를 따른 프로그램에서 High-level Module은 Low-Level Module을 직접 사용하지 않고, 그것을 추상화한 인터페이스만을 갖고 있습니다. High-level module 은 Abstraction Layer를 의존합니다.
Low-level module은 Abstraction Layer 을 상속하여 그 내용들을 구현합니다. 따라서 Low-level Module은 Abstraction Layer 를 의존합니다.
기존에는 Top-down 형식으로 의존성의 화살표가 내려갔다면, DIP 구조에서는 Low-level Module이 상위 계층을 의존하여 의존성이 역전된 모습을 볼 수 있습니다.
코드로 예시를 든다면 다음과 같습니다.
interface AbstractLayer {
void hello();
}
class LowModule implements AbstractLayer {
@Override
void hello {
print("hello");
}
}
class HighModule {
private AbstractLayer abstractLayer;
}
여기서 HighModule을 보면 AbstractLayer 에 의존하고있습니다. 하지만 이 경우 인터페이스는 인스턴스화 할 수 없기 때문에 인스턴스화 할 수 있는 AbstractLayer를 구현한 객체가 필요합니다.
class HighModule {
private AbstractLayer abstractLayer = new LowModule();
public someMethod() {
abstractLayer.hello();
}
}
위 코드처럼 AbstractLayer를 구현한 구현체 클래스를 생성하여 구현체의 메서드를 사용할 수 있습니다. 하지만 위와 같은 구조는 결국 abstractLayer 역할을 하는 구현체를 변경할때 HighModule을 수정해줘야한다는 문제를 그대로 갖고 있습니다.
Injector
결국 문제를 해결하기 위해서는 제 3의 객체가 필요합니다.
의존주입에서 제 3의 객체는 Injector라고 부릅니다.(혹은 다양한 이름으로 부르기도 합니다. ex.DI Container, Assembler, Factory, …) 이는 객체들의 의존관계를 주입 혹은 조립한다는 의미를 갖고 있습니다.
앞에서 봤던 문제들의 본질은 High Module에서 Low Module이라는 구현체를 직접 생성한다는데에서 발생하는 문제였습니다. High Module에서 Low Module을 직접 생성하기 때문에 그 정보를 ‘알아’야하고 따라서 구현체에 의존하는 강한 결합 관계가 발생합니다.
Assembler는 이러한 결합관계를 약한 결합관계로 만들고자 구현체들을 생성하고, 필요로 하는곳에 뿌려주는 역할을 합니다.
class Injector {
private static AbstractLayer abstractLayer = new LowModule();
static AbstractLayer injectAbstractLayer() {
return abstractLayer;
}
}
class HighModule {
private AbstractLayer abstractLayer = Injector.injectAbstractLayer();
public someMethod() {
abstractLayer.hello(); //hello
}
}
Injector를 사용한 예시입니다.
Injector는 AbstractLayer의 레퍼런스를 갖고있으며, 구체적인 구현체를 생성하여 갖고 있습니다. 이후 Injector에게 특정 인터페이스의 구현체를 주입해달라는 요청이 오면, 갖고있던 구현체를 반환해줍니다.
HighModule은 AbstractLayer의 역할을 하는 구현체가 필요합니다. 따라서 Injector의 injectAbstractLayer를 호출하여 어셈블러에게 구현체를 가져다달라고 합니다. 이후 LowModule의 레퍼런스를 주입받게되고, High Module은 Low Module을 인터페이스 레퍼런스를 통해 사용하게 됩니다.
변경된 HighModule의 코드에서는 어떠한 LowModule에 대한 정보도 찾을 수 없습니다. 이 뜻은 곧 LowModule과의 의존성이 제거됐음을 의미합니다. High Module은 AbstractLayer의 역할을 하는 구현체가 어떤 것인지 모르지만, 어떠한 역할을 하는 구현체가 주입됐음만 알고있고 그 객체와 메시지를 주고받으며 소통을 합니다.
이제는 지금까지 봤었던 100개의 클래스를 수정해야하는 문제에서 해방되었습니다. 만약 AbstractLayer의 역할을 수행해야하는 새로운 클래스를 만들고 그 클래스를 사용하고자 하는 곳에 적용시켜주려면 다음과 같이 하면 됩니다.
class NewLowModule implements AbstractLayer {
@Override
void hello() {
System.out.println("HELLO!!!@#!@#!@#!@");
}
}
class Injector {
private AbstractLayer abstractLayer = new NewLowModule();
static AbstractLayer injectAbstractLayer() {
return abstractLayer;
}
}
class HighModule {
private AbstractLayer abstractLayer = Injector.injectAbstractLayer();
public someMethod() {
abstractLayer.hello(); //"HELLO!!!@#!@#!@#!@"
}
}
AbstractLayer를 구현한 구현체 NewLowModule을 생성하고, Injector에게 앞으로 abstractLayer 를 요청하는 곳에 NewLowModule 을 반환해줄 것을 설정해줍니다.
결국 Injector의 코드 한 줄만 수정해주면, AbstractLayer를 사용하는 5조5억개의 클래스는 어떠한 코드를 변경하지 않아도 새롭게 바꿔준 클래스를 사용하게 됩니다.
이를 책임의 관점에서 생각한다면, 기존에는 High Modue에서 사용하는 객체를 “생성”하는 역할과 “사용”하는 역할을 둘 다 가졌다면, 지금은 “생성”하는 역할은 생성만 담당하는 객체(Injector)가 맡고 “사용”의 역할만 갖는다고 이야기 할 수 있겠습니다.
의존 주입의 장점
이제는 상위 모듈이 하위 모듈과 약한 결합관계를 갖기 때문이 변경시 영향이 미미해집니다. 사용하는 객체가 바뀌어도 어떠한 코드의 변경도 필요하지 않습니다.
또한 이제 상위 모듈이 인터페이스를 의존하기 때문에 Mocking이 가능해집니다. Mocking 이란 가짜 객체를 주입해서 테스트하는 기법을 의미합니다.
LogService 문제 해결
LogService 의 문제를 해결하기 위해 LogDecorator의 역할을 추상화한 Interface를 만들겠습니다.
public interface LogDecorator {
String decorate(String message);
}
이후 기존의 코드와 새로만든 Decorator들이 LogDecorator를 구현하게 만들어줍니다.
public class ComplexLogDecorator implements LogDecorator{
@Override
public String decorate(String message) {
// complex logic
return "[LOG] very very complex : "; message;
}
}
public class SimpleLogDecorator implements LogDecorator{
@Override
public String decorate(String message) {
return "[LOG] "+ message;
}
}
Injector 클래스도 만들어줍니다.
public class Injector {
private static LogDecorator logDecorator = new ComplexLogDecorator();
public static LogDecorator getLogDecorator() {
return logDecorator;
}
}
LogService 클래스에서 logDecorator 의존을 주입받게 만들어 줍니다.
class LogService {
private LogDecorator logDecorator = Injector.getLogDecorator();
public void log(String message) {
System.out.println(logDecorator.decorate(message));
}
}
이렇게 의존성 주입을 구현할 수 있습니다.
이제 LogService는 어떠한 구체적인 Decorator 클래스에도 의존하지 않으며, 대신 그것을 추상화한 LogDecorator에만 의존하게 됩니다. 이제 LogDecorator를 사용하는 100개의 클래스는 Injector에 의해 주입받은 구현체를 사용하여, 이제는 새로운 클래스가 생성되더라도 Injector의 코드 한 줄만 수정해준다면 이를 사용하는 모든 객체에서 변경된 객체를 사용할 수 있게 됩니다. 이는 Dependency Injection을 통해 강한 결합을 약한 결합으로 바꾼 결과이며, 이를 통해 프로그램은 더욱 변경에 유연하고 확장 가능한 구조가 될 수 있습니다.
의존주입 나머지 주제
의존 주입 방식
의존 주입을 하는 방식에는 여러가지가 있습니다.
- 생성자 주입 방식
생성자 주입 방식은 객체의 생성 시점에서 필요한 의존성을 갖는 인스턴스들을 주입받는 방식입니다.
객체가 의존하는 구현체가 프로그램 동작 도중 바뀔일이 없다는 특징을 갖고 있습니다.
class LogService {
private final LogDecorator logDecorator;
public LogService(LogDecorator logDecorator) {
this.logDecorator = logDecorator;
}
public void log(String message) {
System.out.println(logDecorator.decorate(message));
}
}
- 메서드 주입 방식 (setter 주입 방식)
setter 메서드를 통해 의존성을 갖는 인스턴스를 주입받는 형식입니다.
프로그램 동작 도중 인스턴스가 바뀔 수 있다는 특징이 있으며, 구현체가 없는 상태도 가능하다는 특징이 있습니다. 구현체가 없는 경우 NPE 가 발생할 수 있으므로 조금은 위험한 방식입니다.
class LogService {
private LogDecorator logDecorator;
public setLogDecorator(LogDecorator logDecorator) {
this.logDecorator = logDecorator;
}
public void log(String message) {
System.out.println(logDecorator.decorate(message));
}
}
- 파라미터 주입 방식
파라미터를 통해 사용하는 인스턴스를 전달받는 형식입니다. 사용하는 객체가 빈번히 교체되어야 하는 경우 유용할 수 있습니다.
class LogService {
public void log(String message, LogDecorator logDecorator) {
System.out.println(logDecorator.decorate(message));
}
}
의존 주입 시 유의점
의존성 순환 문제에 대해 이야기해보겠습니다.
A 클래스가 만약 B 클래스를 의존하고, B 클래스가 A 클래스에 의존하는 형태가 되면 어떻게 될까요?
이 예시는 기존의 Injector 코드로 예시를 들기 어렵습니다. 위 Injector코드는 매우 간략한 코드로, Injector는 추가적으로 싱글톤에 대한 관리와 의존성 순서에 대한 관리 역시 필요합니다. 따라서 여기서 이야기하는 Injector는 위 Injector 코드와는 다름을 먼저 알려드립니다.
먼저 A클래스를 생성한다고 가정해봅시다. A클래스의 의존성 주입은 생성자 방식으로 B클래스를 주입받도록 되어있습니다. 따라서 B클래스를 의존주입받도록 Injector에게 요청을 합니다.
Injector는 B 클래스를 생성하고 주입해줄 책임이 있습니다. 하지만 B 클래스를 생성하기 위해서는 생성자 주입 방식으로 A 클래스를 주입해줘야 합니다.
결국 A를 만드려면 B를 만들어야하고, B를 만들기 위해서는 A를 만들어줘야하는 관계가 발생합니다. 이를 의존성 순환 문제라고 이야기합니다.
의존성이 순환한다는 것은 나의 코드가 객체지향적인 관점에서 무언가 문제가 있다는 것을 의미합니다. 클래스간의 역할과 책임을 잘 분리하고 있는지 확인해봐야 할 것입니다. 일반적으로 순환이 생길때에 두 클래스는 사실 같은 클래스로 묶이는게 맞는 것 아닌지 고민해보는 것이 좋습니다.
더 나은 구조
class LogService {
private LogDecorator logDecorator = Injector.getLogDecorator();
public void log(String message) {
System.out.println(logDecorator.decorate(message));
}
}
이 구조에서 불편함이 느껴지시 않으시나요?
LogService는 LogDecorator 구현체에 대한 의존성이 끊어졌지만, 새롭게 Injector에 의존하게 되었습니다. 이는 Injector의 수정이 Injector를 의존하는 모든 클래스에 영향을 준다는 문제로 이어집니다.
따라서 더 나은 구조로 만든다면 다음과 같습니다.
class LogService {
private final LogDecorator logDecorator;
public LogService(LogDecorator logDecorator) {
this.logDecorator = logDecorator;
}
public void log(String message) {
System.out.println(logDecorator.decorate(message));
}
}
LogService 의 코드를 위와같이 바꾸면, Injector 에 대한 어떠한 내용도 사용하지 않게 됩니다.
이 구조가 정상적으로 동작할 수 있게 하기 위해 LogService 를 생성하는 측에서 Injector를 사용하여 구현체를 가져와 생성자에 넣어주는 작업을 합니다. 이를 통해서 LogService 를 비롯한 웹 컴포넌트들과 Injector간 의존이 끊어졌습니다
public static void main(String[] args) {
SpringApplication.run(DiApplication.class, args);
LogService logService = new LogService(Injector.getLogDecorator());
logService.log("Hello World!");
}
위와같이 LogService를 생성하는 부분에서 생성자를 통해 의존관계를 주입해줍니다.
위 코드 중 LogService logService = new LogService(Injector.getLogDecorator());
는 객체를 생성하고, 그 내부에 필요한 의존관계를 파악한 뒤 이를 주입해주는 일을 합니다. 이러한 일들, 즉 웹 컴포넌트들을 생성하여 라이프사이클을 관리하고 의존관계를 판단하여 주입해주는 일은 스프링 컨테이너에 의해 수행됩니다.
스프링은 좋은 객체지향 설계를 위해 만들어진 프레임워크이고, 지금까지 LogService와 LogDecorator를 어떻게 만들어야 유연한 설계가 될지 고민한 부분은 스프링에서 Spring Container와 Spring Bean이라는 기능을 통해 쉽게 구현할 수 있게 만들어져 있습니다.
다음 글에서는 스프링 컨테이너가 스프링 빈을 등록하는 방법에 대해서 설명해보도록 하겠습니다.
Uploaded by N2T