나는 Spring으로 백엔드 개발을 하고있지만 기초부터 배우지 않았다. 유일하게 알던 프레임워크는 Django였고 Java도 모르던 내가 회사에 들어가서 개발 스택을 급하게 바꾸며 “일단 돌아가게만 하자”라는 마음으로 막무가내로 배웠었다. 이제는 spring이 주 프레임워크가 되었지만 기초가 부족하다는 것을 항상 느껴 토비님의 스프링 강의를 들으며 처음부터 다시 학습 중이다.
예전부터 spring을 하며 항상 들어왔던 IoC라는 개념을 제대로 다시 배우게 되어 제대로 정리하고자 한다. 아래의 내용들은 내가 이해한 내용을 바탕으로 다시 정리한 글이다. 잘못 이해한 내용이 있을 수도 있다.
제어의 역전이 뭔데
처음에 한국어로 이 이름을 들었을 땐 무슨 말인지 감도 잡히지 않았다. 내가 무언갈 제어한다는 개념도 모호했고 그걸 역전한다는 것도 이해가 가지 않았다. 예제 코드를 보면서 처음부터 자세히 살펴보자.
내가 도구를 사용해야하는 상황이다. 이 상황에서 가장 기본적으로 사용할 수 있는 코드는 아래와 같을 것이다.
public class Me {
private Tool tool = new Tool();
public void useTool(){
tool.use();
}
}
Me라는 클래스 내에서 Tool이라는 클래스로 오브젝트를 생성하고 use 함수를 통해 사용한다.
이 상황에서는 Tool 이라는 클래스에 대한 제어권을 Me 클래스가 온전히 가지고있다. 즉 내가 도구를 만들고 사용하는 도구의 life cycle이 내 손 안에서 시작되고 사용되고 끝난다.
이 제어권을 역전시켜보자. 이제는 내가 도구를 만들지 않고 외부에서 도구를 받아서 사용한다는 느낌이다.
public class Me {
private Tool tool;
public Me(Tool tool){
this.tool = tool;
}
public void useTool(){
tool.use();
}
}
이렇게 코드를 수정하면 나는 이미 만들어져있는 Tool 오브젝트를 외부에서 받아서 사용하게 된다. 여기부터는 Me 클래스가 Tool 클래스에 대한 완전한 제어권을 가지지 못한다. Tool 오브젝트의 생성은 외부에서 담당하고 Me 클래스는 그것을 사용하기만 하면 된다.
여기까지가 가장 간단한 IoC의 예시이다.
그래서 왜 필요한데
처음에는 이 개념이 왜 필요한지도 몰랐다. 그냥 클래스에서 오브젝트 생성해서 쓰는거랑 결과적으로는 다를게 없기 때문이다.
일단 위 예시를 통해 이해를 해보자. 나의 관심사는 도구를 사용하는 것이다. 내가 도구를 만들 필요가 있을까? 도구를 만들고 그 도구를 관리하는 것은 다른사람에게 맡긴다면 당연히 내 입장에서는 도구를 사용하기 더 편해질 것이다. SoC(Separation of Concerns), 즉 관심사의 분리 차원에서도 제어의 역전은 좋은 효과를 가져온다.
내가가 도구를 만들고 관리까지 해야 한다면 도구를 어떻게 잘 사용할지에 대한 고민에 집중하기 힘들것이다. 따라서 Spring 프레임워크는 개발자가 오브젝트 관리보다는 내부 로직에 더 관심을 쏟을 수 있도록 오브젝트들을 관리해준다.
그리고 객체지향적으로도 IoC는 많은 도움을 준다.
public class Worker {
private Tool tool = new Tool();
public void useTool(){
tool.use();
}
}
내가 Worker라는 클래스를 만들었고 이 worker가 도구를 아주 잘 사용할 수 있도록 완벽하게 코드를 만들었다고 해보자. 이 클래스를 너무 잘 만들어서 내가 진행해온 10개의 프로젝트에 Worker를 사용했다. 그런데 Tool 클래스에 사용된 라이브러리중 하나가 심각한 오류를 발생시켜서 작동하지 못하게 되었다. 작업을 대신할 수 있는 Hammer라는 클래스가 있지만 이 클래스로 바꾸기 위해 지금까지 했던 모든 프로젝트의 Worker 클래스 코드를 수정해야한다. 이런 코드는 portable한 코드라고 하기 어렵다.
Worker 코드를 수정하지 않고 문제를 해결하려면 어떻게 해야할까.
public interface Tool{
void useTool();
}
public class Hammer implements Tool{
@Override
public void useTool(){
...
}
}
public class WorkerFactory {
public Worker worker() {
return new Worker(new Hammer());
}
}
public class Worker {
public Me(Tool tool){this.tool = tool;}
public void useTool(){
tool.use();
}
}
코드 양은 조금 더 많아지긴 했지만 위처럼 코드를 구성하면 도구를 잘 쓰게 완전히 구현해놓은 Worker 코드는 수정하지 않은 채 Worker가 사용할 도구만 외부에서 쥐여주면 Tool의 use 함수를 사용해 더 유동적으로 도구를 바꿔가며 작업을 수행할 수 있을 것이다.
이처럼 구현하면 관심사도 분리해냈고 Worker 클래스를 더 자유롭게 사용할 수 있다.
Spring에서는 어떻게 사용할까?
Spring 프레임워크는 IoC 원칙을 지킴으로써 개발자가 꼭 필요한 로직에만 집중할 수 있는 환경을 만들어준다. Spring에서는 의존성 주입(Dependency Injection - DI)라는 방식으로 IoC를 구현한다. 내가 개념도 모르고 자주 사용했던 방식이 아래와 같은 방식이다.
@RestController
@RequiredArgsConstructor
public class MyController {
private final MyService myService;
}
이렇게 사용하면 Service라는 오브젝트가 알아서 생성되어 controller에서 사용할 수 있게 된다. RequiredArgsConstructor는 final로 지정된 변수들을 정의하는 생성자를 만들어 필요한 객체들을 넣어주게 된다.
MyService라는 객체를 Spring 컨테이너에서 관리하고 Controller는 사용하면 되는 방식으로 IoC가 구현되어있다. DI의 방법에는 여러가지가 있지만 위 방법이 주로 사용되는 방법이다. 다른 방법들은 필드나 생성자나 setter에 @AutoWired 어노테이션을 달아주는 방법이 있다.
여기서 Spring 컨테이너나 Bean에 대한 내용으로 이어지는데 이는 추후에 다룰 예정이다.