[CS] 의존성 주입과 의존성 역전 원칙

팔랑이·2025년 1월 1일
0

CS

목록 보기
6/19
post-thumbnail

⛳️ 인프런 - cs 지식의 정석 강의를 듣고 학습한 내용입니다.


의존성 주입(DI, Dependency Injection)

의존한다는 것의 의미

A가 B에 의존한다는 것은,

  • B가 변하면 A에 영향을 미치는 것을 뜻한다.
  • 기호로 A → B로 표기함

예시 코드

import java.util.*;

class B {
	public void go() {
		System.out.println("B의 go()함수");
	}
}

class A {
	public void go() {
		new B().go();
	}
}

public class main{
	public static void main(String args[]) {
		new A().go();
	}
}

위 코드에서, class B의 go() 메서드의 이름이 gorani()로 바뀐다면 class A의 코드를 new B().gorani()로 수정해줘야 한다. 이런걸 의존한다고 함.

그렇다면 의존성 주입이란?

여러 하위 모듈들이 메인 모듈에 직접적으로 의존하지 않도록, 그 중간에 의존성 주입자를 넣어 하위 모듈들이 이 중간 모듈(주입자)에 의존하도록 만드는 것이다.

이를 통해 메인 모듈과 하위 모듈들간의 의존성을 조금 더 느슨하게 만들 수 있고, 쉽게 교체 가능한 구조로 만든다.

의존성 주입의 장단점

장점

  • 유지보수와 확장이 쉽다
  • 단위 테스팅과 마이그레이션이 쉽다
  • 의존성 방향이 일관되어 코드를 추론하기가 쉽다.

단점

  • 결국에는 모듈이 더 생기는 것이므로, 복잡도가 증가한다.
  • 종속성 주입이 컴파일 시점이 아니라 런타임에 일어나기 때문에, 컴파일시 종속성 주입에 관한 에러를 잡기 어려워진다.

의존성 역전 원칙(DIP, Dependency Inversion Principle)

SOLID 원칙 중 D에 해당하는 부분이다.

의존성 역전 원칙은,

  • 상위 모듈은 하위 모듈에 의존해서는 안되며, 둘 다 추상화된 인터페이스에 의존해야 한다.
  • 추상화(고수준 모듈)는 세부사항에 의존해서는 안되며, 구현(저수준 모듈)이 추상화에 따라 달라져야 한다.

개발을 할 때 저수준 모듈은 빈번하게 변경되는데, 고수준 모듈이 저수준 클래스에 의존하고 있다면 고수준 클래스가 자주 영향을 받게 된다. 따라서 의존관계를 역전시켜야 하는데, 이를 의존성 역전이라고 한다.

  • 좌: 상위 모듈이 하위 모듈에 직접적으로 의존하는 그림
  • 우: 하위 모듈이 고수준 모듈에서 정의한 추상 타입에 의존하는 그림

Java 예시 코드

예시를 통해 확인해보자.

의존성 역전 전

import java.util.*;

// 하위 클래스
class BackendDeveloper {
    // Backend 개발자는 Java 코드를 작성하는 메서드만 제공
    public void writeJava() {
        System.out.println("자바가 좋아 인터네셔널~");
    }
}

class FrontEndDeveloper {
    // Frontend 개발자는 JavaScript 코드를 작성하는 메서드만 제공
    public void writeJavascript() {
        System.out.println("자바스크립트가 좋아 인터네셔널~");
    }
}

// 상위 클래스
public class Project {
    // BackendDeveloper와 FrontEndDeveloper에 직접 의존
    private final BackendDeveloper backendDeveloper; 
    private final FrontEndDeveloper frontEndDeveloper;

    // 의존성을 직접 주입받음 (강하게 결합됨)
    public Project(BackendDeveloper backendDeveloper, FrontEndDeveloper frontEndDeveloper) {
        this.backendDeveloper = backendDeveloper;
        this.frontEndDeveloper = frontEndDeveloper;
    }

    public void implement() {
        backendDeveloper.writeJava(); //
        frontEndDeveloper.writeJavascript();
    }

    // main
    public static void main(String args[]) {
        // Project가 구체적인 구현에 의존하며, 직접 개발자 객체를 생성
        Project a = new Project(new BackendDeveloper(), new FrontEndDeveloper());
        a.implement(); // Backend와 Frontend의 구현 메서드 호출
    }
}

위의 경우,

  • 보다시피 Project 클래스가 BackendDeveloper와 FrontendDeveloper 라는 구체적인 클래스에 직접 의존한다.
  • 만약 Backend 개발자가 Kotlin으로 전환하거나, Frontend 개발자가 TypeScript로 변경해야 한다면 Project의 코드를 수정해야 하는 상황이 발생
  • 또는 iOS 개발자가 새로 추가된다면 Project 클래스에 또 다른 필드와 메서드를 추가해야 한다.

이런 상황이니 확장과 유지보수가 어렵다.

의존성 역전을 적용하여 개선한 코드

의존성 역전 원칙을 적용하면 다음과 같이 코드를 개선할 수 있다.

import java.util.*;

// 개발자 인터페이스: 고수준 모듈이 의존하는 추상화
interface Developer {
    void develop(); // 개발자가 수행해야 할 작업 정의
}

// Backend 개발자 클래스: Developer 인터페이스 구현
class BackendDeveloper implements Developer {
    @Override
    public void develop() {
        writeJava();
    }

    public void writeJava() {
        System.out.println("자바가 좋아~ 새삥새삥");
    }
}

// Frontend 개발자 클래스: Developer 인터페이스 구현
class FrontendDeveloper implements Developer {
    @Override
    public void develop() {
        writeJavascript();
    }

    public void writeJavascript() {
        System.out.println("자바스크립트가 좋아~ 새삥새삥");
    }
}

// 프로젝트 클래스: 고수준 모듈
public class Project {
    private final List<Developer> developers; // 추상화(Developer 인터페이스)에 의존

    // 의존성 주입: 개발자 목록을 생성자로 받아옴
    public Project(List<Developer> developers) {
        this.developers = developers;
    }

    public void implement() {
        developers.forEach(Developer::develop); // 모든 개발자에게 develop() 호출
    }

    // main
    public static void main(String args[]) {
        List<Developer> dev = new ArrayList<>();
        dev.add(new BackendDeveloper());
        dev.add(new FrontendDeveloper());

        // Project 객체 생성 (의존성 주입)
        Project a = new Project(dev);

        // 프로젝트 구현
        a.implement(); 
        
        // 자바가 좋아~ 새삥새삥
        // 자바스크립트가 좋아~ 새삥새삥
    }
}

개선점 설명

1. 추상화 사용

  • Developer라는 인터페이스를 도입하여 고수준 모듈인 Project가 구체적인 구현에 의존하지 않게 설계
  • Project는 Developer라는 인터페이스만 알면 되며, 개발자들이 어떤 언어로 작업하는지 직접 알 필요가 없다.

2. 의존성 주입

  • Project 클래스에서, 개발자 목록을 생성자로 주입받는다.
  • 이로 인해 새로운 개발자 타입이 추가되더라도 Project 클래스는 변경할 필요가 없다. 예를 들어, iOS 개발자를 추가하려면 다음과 같이 Developer 인터페이스를 구현한 새로운 클래스만 추가하면 된다.
class iOSDeveloper implements Developer {
    @Override
    public void develop() {
        writeSwift();
    }
    
    public void writeSwift() {
    	System.out.println("스위프트가 좋아 새삥새삥");
	}
}
profile
정체되지 않는 성장

0개의 댓글