🔗 의존성 주입과 의존관계 역전 원칙
요즘 의존성 주입(DI, Dependency Injection) 과 의존관계 역전 원칙(DIP, Dependency Inversion Principle) 에 대해 공부하고 있다.
이 개념들을 제대로 이해하면 코드의 결합도를 낮추고 유지보수하기 쉬운 구조를 만들 수 있다고 하는데, 아직 머릿속이 정리되지 않은 느낌이다.
일단 하나씩 정리해보자.
🏗️ 의존성 주입(DI, Dependency Injection)이란?
어떤 프로그램에서 "A가 B를 직접 생성하고 사용한다" 라는 구조를 생각해보자.
이 경우, B가 변하면 A도 영향을 받는다. 즉, A는 B에 강하게 의존 하고 있는 것이다.
class B {
public void go() {
System.out.println("B의 go() 함수");
}
}
class A {
public void go() {
new B().go(); // A가 직접 B를 생성하고 사용
}
}
위 코드에서 B
클래스의 go()
메서드 이름을 변경하면, A
클래스도 반드시 변경해줘야 한다.
이게 바로 강한 결합(의존성이 높음) 상태이다.
🔄 의존성 주입을 사용하면?
의존성 주입(DI) 을 사용하면 A가 B를 직접 생성하지 않고, 외부에서 B를 주입받을 수 있다.
class B {
public void go() {
System.out.println("B의 go() 함수");
}
}
class A {
private B b; // A는 B를 직접 생성하지 않는다.
public A(B b) { // 생성자를 통해 B를 주입받음
this.b = b;
}
public void go() {
b.go();
}
}
public class Main {
public static void main(String[] args) {
B b = new B();
A a = new A(b); // 외부에서 B를 생성하고 A에 주입
a.go();
}
}
이제 B
가 변경되더라도 A
를 직접 수정할 필요가 없다.
즉, A와 B의 결합도를 낮춰 모듈을 더 유연하게 만들 수 있다.
🏛️ 의존관계 역전 원칙(DIP, Dependency Inversion Principle)
의존성 주입을 적용할 때는 의존관계 역전 원칙(DIP) 이라는 개념이 적용된다.
이 원칙을 따르려면 두 가지 규칙 을 지켜야 한다.
1️⃣ 상위 모듈은 하위 모듈에 의존해서는 안 된다.
→ 둘 다 추상화(인터페이스) 에 의존해야 한다.
2️⃣ 추상화는 세부사항에 의존해서는 안 된다.
→ 세부 사항(구현체)은 추상화에 따라 달라져야 한다.
🤔 의존성 역전이 필요한 이유
기존에는 다음과 같이 상위 모듈(Project
)이 하위 모듈(BackendDeveloper
, FrontendDeveloper
)에 의존하는 구조였다.
class BackendDeveloper {
public void develop() {
System.out.println("백엔드 개발 중...");
}
}
class FrontendDeveloper {
public void develop() {
System.out.println("프론트엔드 개발 중...");
}
}
class Project {
private BackendDeveloper backendDeveloper;
private FrontendDeveloper frontendDeveloper;
public Project() {
this.backendDeveloper = new BackendDeveloper();
this.frontendDeveloper = new FrontendDeveloper();
}
public void start() {
backendDeveloper.develop();
frontendDeveloper.develop();
}
}
이렇게 구현하면 프로젝트가 특정 개발자(백엔드, 프론트엔드)에 직접 의존 하기 때문에, 새로운 개발자가 추가되면 Project
클래스도 수정해야 한다.
🔄 인터페이스를 이용한 DIP 적용
이를 해결하려면 "개발자"라는 공통적인 개념을 추상화(인터페이스)로 만들고 각 개발자가 이를 구현하면 된다.
interface Developer {
void develop();
}
class BackendDeveloper implements Developer {
@Override
public void develop() {
System.out.println("백엔드 개발 중...");
}
}
class FrontendDeveloper implements Developer {
@Override
public void develop() {
System.out.println("프론트엔드 개발 중...");
}
}
class Project {
private List<Developer> developers;
public Project(List<Developer> developers) {
this.developers = developers;
}
public void start() {
for (Developer developer : developers) {
developer.develop();
}
}
}
public class Main {
public static void main(String[] args) {
List<Developer> devs = new ArrayList<>();
devs.add(new BackendDeveloper());
devs.add(new FrontendDeveloper());
Project project = new Project(devs);
project.start();
}
}
이제 Project
클래스는 Developer
인터페이스에만 의존하고,
어떤 개발자가 추가되든 Project
코드를 수정할 필요가 없다.
이것이 바로 의존관계 역전 원칙이 적용된 구조! 🎉
✅ 의존성 주입의 장점
✔ 결합도가 낮아진다
- 특정 구현체가 아니라 인터페이스 에 의존하기 때문에 모듈 교체가 쉽다.
- 예를 들어
BackendDeveloper
대신AIDeveloper
를 추가해도Project
코드를 바꿀 필요가 없다.
✔ 테스트하기 쉬워진다
Project
클래스에서 가짜 개발자(Mock 객체) 를 주입하여 단위 테스트를 쉽게 할 수 있다.
✔ 코드 유지보수가 편하다
- 코드 변경이 필요할 때 한 곳만 수정하면 되므로 코드 추론이 쉬워진다.
- 예를 들어, 데이터베이스를 MySQL → PostgreSQL 로 변경할 때, 기존 코드를 수정할 필요 없이 주입된 객체만 변경하면 된다.
✔ 마이그레이션(환경 이전)이 용이하다
- 특정 구현체가 아닌 추상화된 개념에 의존하므로 DB 이전, 프레임워크 변경 같은 작업이 용이하다.
⚠️ 의존성 주입의 단점
❌ 코드가 더 복잡해질 수 있다.
- 클래스가 늘어나고, DI 컨테이너(Spring 등)가 필요할 수도 있다.
❌ 런타임 오류 발생 가능성
- 컴파일 시점이 아닌 실행 시점 에 의존성이 주입되므로,
주입되지 않은 객체를 호출할 경우 NullPointerException 이 발생할 수도 있다.
'CS 공부' 카테고리의 다른 글
# 옵저버 패턴 (0) | 2025.02.20 |
---|---|
# 전략 패턴 (0) | 2025.02.20 |
# 이터레이터(iterator) 패턴 (0) | 2025.02.20 |
# 팩토리 패턴 (1) | 2025.02.20 |
# 싱글톤 패턴 구현 방법 7가지 (0) | 2025.02.20 |