[OOP] 의존성 역전 원칙(DIP: Dependency Inversion Principle) 개념 및 예제
객체 지향 설계 원칙(SOLID)
객체 지향 언어의 등장 이후 수많은 시행착오와 베스트 프랙티스 속에서 객체 지향 설계 5가지 원칙이 등장했는데, 바로 SOLID다. SOLID는 로버트 C. 마틴(Robert C. Martin)이 2000년대 초반 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙으로 제시한 것을 마이클 페더스(Michael Feathers)가 두문자어로 소개한 것이다.
SOLID는 다음 5가지 원칙의 앞 글자를 따서 부르는 이름이다.
- SRP(Single Responsiblity Principle) : 단일 책임 원칙
- OCP(Open Closed Principle) : 개방 폐쇄 원칙
- LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
- ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
- DIP(Dependency Inversion Principle) : 의존 역전 원칙
이 원칙들은 응집도는 높이고(High Cohesion), 결합도는 낮추라(Loose Coupling)는 고전 원칙을 객체지향의 관점에서 재정립한 것이다.
의존성 역전 원칙(DIP: Dependency Inversion Principle)
“고차원 모듈은 저차원 모듈에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다.”
- 로버트 C. 마틴 -
고차원 모듈
일반적으로 더 큰 규모의 기능을 수행하는 클래스, 패키지 등을 의미한다. 즉, 사용자 인터페이스와 관련된 작업을 하는 Controller나 비즈니스 로직을 처리하는 Service 계층이 고차원 모듈에 속한다.
저차원 모듈
고차원 모듈에서 정의한 기능을 구체적으로 구현하는 클래스 등을 의미한다. 주로 데이터베이스 접근 객체(DAO) 클래스나 파일 처리를 위한 클래스 등이 저차원 모듈에 속한다.
로버트 C. 마틴의 말을 해석하면 의존성 역전 원칙(DIP)은 특정 클래스는 또 다른 특정 클래스를 의존하면 안 되고, 이 두 클래스는 모두 다른 추상 클래스나 인터페이스에 의존해야 한다는 의미다.
특정 클래스가 또 다른 클래스를 의존하는 경우, 해당 클래스의 인스턴스를 사용하게 된다. 만약, 사용하고 있는 클래스가 변경될 경우 이를 의존하는 클래스도 변경이 일어나게 된다. DIP를 통해 모듈 간의 결합도를 낮춰 유지보수가 용이하도록 변경해야 한다.
의존성
의존성은 특정 클래스가 기능을 수행할 때, 다른 클래스의 기능이 필요한 경우를 말한다.
// 저차원 모듈
class A {
public void runA() {
System.out.println("runA");
}
}
// 고차원 모듈
class B {
private A a;
public B() {
this.a = new A(); // 의존 관계 성립(B가 A에 의존)
}
public void runB() {
a.runA();
}
}
public class Main {
public static void main(String[] args) {
B b = new B();
b.runB();
}
}
- A 클래스는 runA() 메서드를 포함하고 있으며, 단순히 “runA”라는 문구를 출력해 주는 메서드다.
- B 클래스에서는 A 클래스의 인스턴스와 runB() 메서드를 포함하고 있으며, 내부에서는 A클래스의 runA() 메서드를 사용하고 있다.
- 위 코드처럼 B 클래스의 기능을 사용하기 위해 A 클래스의 기능도 필요한 경우를 의존성이라 한다.
의존성 역전 원칙 예시
다음과 같이 자동차와 스노우 타이어의 의존 관계가 있다고 가정한다.
- 자동차는 현재 스노우 타이어에 의존하고 있다.
- 스노우 타이어는 계절이 바뀌면 일반 타이어로 교체해야 할 것이다.
코드로 표현하면 다음과 같다.
class SnowTire {
public void print() {
System.out.println("스노우 타이어");
}
}
class Car {
private SnowTire snowTire;
public Car() {
this.snowTire = new SnowTire();
}
public void printTire() {
snowTire.print();
}
}
- Car는 SnowTire에 의존하고 있다.
- 만약, 계절이 바뀌어 스노우 타이어를 일반 타이어로 교체할 경우 자동차는 영향을 받게 된다.
계절이 바뀐 경우
class RegularTire {
public void print() {
System.out.println("일반 타이어");
}
}
class SnowTire {
public void print() {
System.out.println("스노우 타이어");
}
}
class Car {
private RegularTire regularTire; // 수정
public Car() {
this.regularTire = new RegularTire(); // 수정
}
public void printTire() {
regularTire.print(); // 수정
}
}
- 타이어만 교체했을 뿐인데 Car 클래스에서 수정이 일어난다.
- Car 클래스에서 구체적인 Tire 클래스를 의존하고 있기 때문이다.
- 두 클래스 간의 결합도가 높아 유지보수가 어렵다.
이를 개선하기 위해 의존성 역전 원칙을 적용해 보자.
- 자동차가 구체적인 타이어들이 아닌 추상화된 타이어 인터페이스에만 의존하게 변경한다.
- 자동차는 사용하고 있는 타이어를 변경하더라도 자동차는 그 영향을 받지 않는 형태로 구성된다.
- 또한, 구체적인 타이어 클래스들은 타이어 인터페이스에 의존하게 되었다. 의존 관계의 방향이 역전된 것이다.
코드로 표현하면 다음과 같다.
interface Tire {
void print();
}
class RegularTire implements Tire {
@Override
public void print() {
System.out.println("일반 타이어");
}
}
class SnowTire implements Tire {
@Override
public void print() {
System.out.println("스노우 타이어");
}
}
class WideTire implements Tire {
@Override
public void print() {
System.out.println("광폭 타이어");
}
}
class Car {
private Tire tire;
public Car(Tire tire) {
this.tire = tire;
}
public void printTire() {
tire.print();
}
}
- 구체적인 Tire 클래스(Regular, Snow, Wide)는 Tire 인터페이스를 구현하고 있다.
- Car 클래스는 인스턴스를 생성할 때 Tire 타입의 객체를 매개변수로 받는다.
public class Main {
public static void main(String[] args) {
Car regularTireCar = new Car(new RegularTire());
Car snowTireCar = new Car(new SnowTire());
Car wideTireCar = new Car(new WideTire());
regularTireCar.printTire();
snowTireCar.printTire();
wideTireCar.printTire();
}
}
- 타이어를 교체하더라도 Car 클래스에는 어떠한 영향도 가지 않게 되었다.
정리
구체적인 Tire 클래스는 변하기 쉬운 반면에, Car 클래스는 변하지 않는 것처럼 의존성 역전 원칙은 자신보다 변하기 쉬운 것에 의존하지 말라는 메시지를 던진다.
즉, 상위 클래스일수록, 인터페이스일수록, 추상 클래스일수록 변하지 않을 가능성이 높기에 하위 클래스나 구체 클래스가 아닌 상위 클래스, 인터페이스, 추상 클래스를 통해 의존하라는 것이 바로 의존성 역전 원칙이다.
참고 서적
https://link.coupang.com/a/baQPj6
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."