의존성 역전 원칙 (SOLID 원칙 중 “D” 에 속한다)
- DIP : Dependency Inversion Principle
- 1. DIP 용어를 알아본다.
- 2. DIP 를 적용하지 않은 사례를 알아본다.
- 3. DIP 를 적용한 사례를 알아본다.
- 4. DIP 적용 시 주의사항을 알아본다.
1. DIP 용어를 알아본다.
DIP 에는 “고수준 모듈”과 “저수준 모듈” 이라는 용어가 나오기 때문에 해당 용어에 대한 의미를 알아야 한다.
- 고수준 모듈
의미 있는 단일 기능을 제공하는 모듈이다.
- 저수준 모듈
고수준 모듈에서 정의한 기능을 실제로 구현하는 모듈이다.
2. DIP 를 적용하지 않은 사례를 알아본다.
위 용어를 특정 사례로 보면 아래와 같다.
DIP 원칙을 지키지 않고, 위 사례에 대해 코드를 작성한다면 아래와 같이 코드를 작성할 것이다.
- 1번 기능을 위해 고수준 모듈 service 를 만든다.
- 2, 3번 기능을 처리하기 위한 서비스 코드를 각각 만든다.
- 1번 service 에서 2번, 3번 코드를 호출한다.
위 글을 그림으로 보면 아래와 같다.
위 그림을 코드로 보면 아래와 같다.
class 고수준모듈service(
private val jpaService: JpaService,
private val emailSender: EmailSender
) {
fun 계좌내역을_조회하고_전송한다() {
// 계좌 내역 조회
val 계좌내역리스트 = jpaService.get()
// 계좌 내역 전송
emailSender.send(계좌내역리스트)
}
}
만약, 요구사항이 바뀌어 fax 로도 보내야한다면?
혹은 계좌내역 리스트 조회를 Jpa 가 아닌 Mybatis 를 이용해야한다면?
고수준모듈service 클래스의 코드를 변경해야 한다.
3. DIP 를 적용한 사례를 알아본다.
보통 저수준의 기능에 대한 요구사항이 자주 바뀌게 된다. (보통 그렇다는 것이지 100%는 아니다.)
고수준 모듈 → 저수준 모듈 의존
그래서 고수준 모듈을 변경할 일이 생기게 되는데 DIP 를 적용하면 고수준 모듈을 변경하지 않도록 할 수 있다.
저수준 모듈 → 고수준 모듈 의존
위 특정 사례를 DIP 로 적용한 그림을 먼저 보자.
고수준 서비스에서 필요한 기능들을 인터페이스로 추상화 한 뒤에 의존한다. (고수준 → 고수준 의존)
저수준은 이제 고수준의 인터페이스를 의존하게 된다. (저수준 → 고수준 의존)
코드로 보면 아래와 같다.
class 고수준모듈을 호출해서 사용하는 service {
...
// jpa, email
val foo = 고수준모듈service(JpaRepository, EmailSender)
// jpa, fax
val bar = 고수준모듈service(JpaRepository, FaxSender)
}
// 요구사항이 추가되어도 고수준모듈service 내부는 전혀 바꿀 필요가 없다.
class 고수준모듈service(
private val db조회용Repo: Db조회용Repo,
private val 계좌내역전송Sender: 계좌내역전송Sender
) {
fun 계좌내역을_조회하고_전송한다() {
// 계좌 내역 조회
val 계좌내역리스트 = db조회용Repo.get()
// 계좌 내역 전송
계좌내역전송Sender.send(계좌내역리스트)
}
}
4. DIP 적용 시 주의사항을 알아본다.
그런데, 인터페이스로 추상화 레이어를 추가했다고 해서 갑자기 고수준이 되는것인가? 라고 의문이 들 수도 있다.
조금 애매할 수도 있는데… 관점에 따라서 다르다고 한다.
위 고수준 service 를 관점으로 “계좌 내역을 조회한다” , “계좌내역을 보낸다” 라는 기능을 위해
인터페이스를 추출했다면 “고수준” 이 되는것이고,
(왜냐하면, “조회한다”와 “보낸다” 는 고수준 모듈의 정의인 의미있는 단일 기능이기 때문이다.)
만약, 저수준 EmailSender 의 관점에서 인터페이스를 만든다면 EmailI 가 되었을 것이고,
고수준 service 에서는 fax 보내기 기능이 필요하면 고수준service 코드 내부에
또 FaxI 를 추가해서 사용해야 될 수도 있을것이다.