Spring Framework를 배우는 과정에서 가장 중요한 개념중 하나가 제어의 역전(IoC, Inversion of Control)과 의존성 주입(DI, Dependency Injection)이다. 이 두 가지는 객체 지향 프로그래밍의 유연성과 확장성을 높이는 데 핵심적인 역할을 한다.
이 두 개념에 대해 공부한 내용을 상세하게 정리해보도록 하겠다.
IoC를 설명할 때 할리우드 원칙을 예시로 든다고 한다. "Don't call us. We'll call you." 이는 제어의 역전에 대한 비유적 표현으로, 본래 오디션에 떨어진 배우들에게 영화사에서 하던 말이 프로그래밍 용어로 변형되어 사용되고 있다고 한다. 말 그대로 배우들(객체)에게 영화사에서 필요하면 연락할 테니 먼저 연락(호출/생성)하지 말라는 뜻이다.
일반적인 프로그램을 생각해보면 객체의 생명주기(객체의 생성, 초기화, 소멸, 메서드 호출 등)를 클라이언트 구현객체가 직접 관리를 한다. 외부 라이브러리를 호출하더라도 해당 코드의 호출 시점 역시 직접 관리한다. 기존에는 이와 같이 모든 객체 또는 메서드의 호출을 프로그램을 작성하는 프로그래머가 결정하고 관리했다.
스프링 프레임워크는 Controller, Service 같은 객체들의 동작을 우리가 직접 구현하기는 하지만, 해당 객체들이 어느 시점에 호출될 지는 신경쓰지 않는다. 단지 프레임워크가 요구하는대로 객체를 생성하면, 프레임워크가 해당 객체들을 가져다가 생성하고, 메서드를 호출하고, 소멸시킨다. 프로그램의 제어권이 프로그래머에서 시스템으로, 말 그대로 제어가 역전된 것이다.
비유를 해보자면 일반적인 프로그램의 경우엔 스스로 요리를 해먹는 것이라면, IoC는 레스토랑에 가서 요리를 주문하고, 주방장이 모든 조리를 대신 해주는 것과 같다. 개발자는 '무엇을' 필요로 하는지 명시만 하면 된다.
일반적인 방식
public class UserService {
private UserRepository userRepository;
public UserService() {
this.userRepository = new UserRepository();
}
public void process() {
userRepository.save();
}
}
여기서 UserService는 UserRepository를 직접 생성한다. 이는 두 클래스 간의 강한 결합이 발생하며, 변경 시 유연성이 떨어지게 된다.
IoC 적용 방식
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void process() {
userRepository.save();
}
}
IoC를 사용하면 UserRepository 객체는 외부에서 주입되며, UserService는 객체 생성의 제어권을 가지지 않는다. 이렇게 하면 객체 간 결합도를 낮출 수 있다.
먼저 의존성(Dependency)이란 한 객체가 다른 객체를 필요로 하는 관계를 의미한다.
위의 코드에서 예를 들면, UserService는 UserRepository에 의존한다. 즉, UserService는 UserRepository가 없다면 제대로 동작할 수 없습니다.
public class A {
private B b = new B();
}
위의 코드를 보면 A클래스는 B라는 클래스를 필드로 가진다. 즉, A라는 객체를 생성하기 위해서는 반드시 B라는 객체를 생성해야 한다. 그런데 만약 B에 final 필드가 추가되는 변경이 일어난다면, A클래스 내부의 new B() 부분에서 컴파일 에러가 나게 될 것이다. B클래스의 내부에 변경이 일어나서 A클래스에도 영향을 미치게 되는 것이다. 이런경우를 "A가 B에 의존한다." 라고 한다.
의존성 주입(Dependency Injection)은 의존하는 객체를 직접 생성하지 않고, 외부에서 전달받는 것을 의미한다. IoC의 구체적인 구현 방법 중 하나로, 프레임워크가 객체를 생성하고 필요한 객체에 주입해주는 방식이다.
1. 생성자 주입(Constructor Injection)
객체 생성 시 필요한 의존성을 생성자 매개변수로 전달한다.
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
2. 세터 주입 (Setter Injection)
객체 생성 후, setter 메서드를 통해 의존성을 주입한다.
public class UserService {
private UserRepository userRepository;
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
3. 필드 주입(Field Injection)
필드에 직접 의존성을 주입한다. (Spring에서는 @Autowired 사용)
@Autowired
private UserRepository userRepository;
Spring에서 DI 사용 예시
@Component
public class UserRepository {
// 데이터베이스 처리 로직
}
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void process() {
userRepository.save();
}
}
Spring 컨테이너가 UserRepository 객체를 생성하고, 이를 USerSevice 의 생성자에 자동으로 주입해준다.
IoC와 DI의 개념에 대해서 알아보았는데, 두 개념은 서로 비슷한 부분이 많은것 같아 관계를 정리해보도록 하겠다.
IoC는 객체 생성과 의존성 관리를 개발자가 아닌 프레임워크에 맡기는 개념적 원칙이라고 볼 수 있다. 그리고 DI는 IoC를 구현하는 방법 중 하나로, 객체 간의 의존성을 외부에서 주입하는 방식이라고 볼 수 있다. 즉, DI는 IoC의 구체적인 구현 방법이라고 이해할 수 있다.