다른 클래스의 기능을 사용하기 위해서는 보통 생성자를 호출해서 사용한다. 하지만 만약 호출된 클래스에 수정사항이 생긴다면 해당 클래스를 호출한 모든 클래스의 일부 기능, 또는 전체를 같이 수정해야 하는 번거로움이 생길 수 있다.
이러한 것을 클래스 간의 강한 결합이라고 부르는데, 이렇게 강한 결합으로 연결되어 있을 때 프로젝트의 규모가 커지면 커질수록 유지보수가 매우 어려워질 것이다.
때문에 개발자가 직접 코드 내에서 클래스 간의 의존 관계를 맺어주는 것이 아닌 의존성 주입이 필요하게 되었다.
의존성 주입은 클래스 간의 의존 관계를 코드 내에서 직접 설정하지 않고, 외부에서 객체를 주입받아 설정하는 설계 패턴이다.
쉽게 말해 클래스 내에서 어떠한 객체를 새로 생성하기 보다는 부품처럼 외부에서 생성한 것을 끼워주는 방법을 뜻한다.
class Repository {
public String getData() {
return "Some data";
}
}
class Service {
private Repository repository;
public Service() {
this.repository = new Repository();
// 직접 객체를 생성한다.
}
public String process() {
return repository.getData();
}
}
public class Main {
public static void main(String[] args) {
Service service = new Service();
System.out.println(service.process());
}
}
class Repository {
public String getData() {
return "Some data";
}
}
class Service {
private Repository repository;
// 의존성 주입: 생성자 주입
public Service(Repository repository) {
this.repository = repository;
}
public String process() {
return repository.getData();
}
}
public class Main {
public static void main(String[] args) {
Repository repository = new Repository();
// 외부에서 객체 생성
Service service = new Service(repository);
// 생성자로 객체 주입
System.out.println(service.process());
}
}
Service 클래스에서 직접 객체를 생성하지 않고 외부(Main 클래스)에서 객체를 받아와서 사용하고 있다.
하지만 이렇게만 보면 무엇이 장점인지 알 수 없으므로
Repository 클래스를 수정했다고 가정해보자.
Repository 클래스 -> NewRepository 클래스로 수정
class NewRepository {
public String fetchData() {
return "new data";
}
}
class Service {
private NewRepository repository;
// 수정된 부분1
public Service() {
this.repository = new NewRepository();
// 수정된 부분2
}
public String process() {
return repository.fetchData();
// 수정된 부분3
}
}
public class Main {
public static void main(String[] args) {
Service service = new Service();
System.out.println(service.process());
}
}
class NewRepository {
public String fetchData() {
return "new data";
}
}
class Service {
private NewRepository repository;
// 수정된 부분1
public Service(NewRepository repository) {
// 수정된 부분2
this.repository = repository;
}
public String process() {
return repository.fetchData();
// 수정된 부분3
}
}
public class Main {
public static void main(String[] args) {
NewRepository repository = new NewRepository();
// 수정된 부분4
Service service = new Service(repository);
System.out.println(service.process());
}
}
이렇게만 본다면 의존성을 주입한 쪽이 훨씬 수정된 부분이 많고 복잡해 보일 수 있다.
하지만 진정한 이점은 인터페이스를 사용했을 때이다..!
interface Repository {
String getData();
}
class OldRepository implements Repository {
// Repository 인터페이스를 상속받은 클래스1 (기존)
public String getData() {
return "Some data";
}
}
class NewRepository implements Repository {
// Repository 인터페이스를 상속받은 클래스2 (수정)
public String getData() {
return "New data";
}
}
class Service {
private Repository repository;
public Service(Repository repository) {
this.repository = repository;
// 외부에서 주입받음
}
public String process() {
return repository.getData();
}
}
public class Main {
public static void main(String[] args) {
Repository repository = new NewRepository();
// NewRepository 객체를 주입
// 만약 OldRepository를 사용하고 있었다면 이 부분만 수정하면 해결
Service service = new Service(repository);
System.out.println(service.process());
}
}
이렇게 인터페이스로 먼저 Repository를 정의하고,
해당 기능을 Repository를 상속받은 클래스에서 구현하게 된다면 수정을 하더라도 Main클래스에서 하나의 코드만 바꿔주면 끝난다.
(new OldRepository() -> new NewRepository()로 수정)
Service 클래스의 코드는 단 한줄도 건드리지 않고 해결된 것이다!
참고로 개발자가 제어하는 것에서 시스템이 제어하는 것으로 바뀌는 것을 제어의 역전이라고 한다. (= IoC)
🔥 한줄평
매번 봐도봐도 헷갈리는 의존성 주입...