DIP (Dependency Inversion Principle)
→ 의존 역전의 원칙
의존 관계를 맺을을 때 변하기 쉬운 것에 의존하기 보다는 변화하지 않는 것에 의존하라!
위의 말은 어떤 의미가 담겨 있을까요?
예를 들어
저희가 쇼핑물에서 사용자가 물건을 구입하면 구입한 내역을 알려주기 위해 알림 기능을 구현한다고 생각해 봅시다.
이메일로 사용자에게 알려주기로 결정을 했고 아래와 같이 기능을 구현 했습니다
class EmailMessenger {
fun sendNotification(message: String): String{
println("Sending email: $message")
}
}
class NotificationService {
private val messenger = EmailMessenger()
fun sendNotification() {
messenger.sendNotification("구입 내역에 대해 안내 드립니다.")
}
}
하지만 문제가 있네요!
문자 알림 서비스를 만들어 달라고 요청이 들어오거나 이메일 메신저 기능을 바꿔달리는 요구사항이 생기면 어떻게 될까요?
NotificationService
클래스는 EmailMessenger
클래스에 의존성을 가지고 있다고 할 수 있는데 EmailMessenger
클래스에 변화가 생기면 EmailMessenger
클래스를 참조하는 모든 클래스를 수정해줘야 하는 상황이 생기겠죠?
한번에 직관적으로 이해가 안되시죠? 어떤 상황일지 궁금하실분 들이 있을 것 같아요
백문불여일견이라는 말이 있듯이 함께 아래 예제를 한번 같이 봐볼까요?
아래 예제 처럼 EmailMessenger
의 sendNoticifation
메소드가 이제 문자열 매개 변수 대신 객체를 받도록 변경 되었다고 가정해 봅시다
data class EmailMessage(val content: String, val id: Int)
class EmailMessenger {
fun sendNotification(message: EmailMessage): String{
println("Sending email: ${message.content}")
}
}
NotificationService 클래스에서는 sendNoticifation 메소드를 호출할 때 문자열 대신 EmailMessenger 객체를 전달하도록 변경해야합니다.
class NotificationService {
private val messenger = EmailMessenger()
fun sendNotification() {
messenger.sendNotification(EmailMessage("구입 내역에 대해 안내 드립니다."))
}
}
이렇게 하나의 클래스가 다른 클래스에 의존하는 경우, 후자가 변경되면 전자도 그에 따라 변경해야 하는 상황이 발생할 수 있습니다.
이런 상황을 피하기 위해 의존성 주입을 사용하여 클래스 간의 결합도를 낮출 수 있습니다.
그러면 첫 번째 예제의 코드에 대해 리팩토링 과정을 거쳐서 조금 더 유지보수가 쉬운 코드로 작성해 봅시다!
우선 공통 부분을 추상화한 Interface를 구현한 뒤
interface Messenger {
fun sendNotification()
}
구현 클래스들을 만들어 줍니다.
class EmailMessenger() : Messenger {
override fun sendNotification(): String {
return "Sending email:"
}
}
class SMSMessenger() : Messenger {
override fun sendNotification(): String {
return "Sending SMS"
}
}
비지니스 로직을 담당하는 클래스를 만들어 준 뒤
class NotificationService(private val messenger: Messenger) {
fun sendNotification(message: String) {
println(messenger.sendNotification() + message)
}
}
아래와 같이 사용할 수 있게 됩니다.
class NotificationServiceTest {
@Test
void sendNotification_givenEmail_shouldReturnEmailMessage() {
// Given
val emailService = NotificationService(EmailMessenger())
// When
val message = emailService.sendNotification("구입 내역에 대해 안내해 드립니다")
// Then
assertEquals("Sending email:구입 내역에 대해 안내해 드립니다", message);
}
@Test
void sendNotification_givenSms_shouldReturnSmsMessage() {
// Given
val emailService = NotificationService(EmailMessenger())
// When
val message = emailService.sendNotification("구입 내역에 대해 안내해 드립니다")
// Then
assertEquals("Sending email:구입 내역에 대해 안내해 드립니다", message);
}
}
위와 같은 설계를 하게 된다면,
외부에서 EmailMessenger의 인스턴스를 생성해 NotificationService 클래스 객체를 만들 때 생성자로 주입을 해주게 된다면 EmailMessenger클래스에 수정이 일어나더라도 NotificationService클래스를 수정할 필요가 없게 됩니다.
위 처럼 코드를 작성하면 NotificationService 클래스는 인제 Messenger 인터페이스에 의존하게 되므로 EmailMessenger 클래스나 SMSMessgenger 클래스과 같은 변하기 쉬운 클래스 보다 더 변하지 않는 Messenger 인터페이스에 의존하게 되므로 코드의 유연성이 높아지게 되었네요.
Messenger 인터페이스의 구현이 추가 되더라도 NotificationService 클래스를 변결할 필요가 없어졌습니다.
DI를 하는 이유나 장점은 뭘까요?
- boilerplate code를 줄여주므로 유연한 프로그래밍이 가능해 집니다.
- 재사용성이 높아지고 유지보수가 더 쉬워집니다.
- 더 편한 유닛 테스트가 가능해집니다.
- 결합도를 낮추어서 확장성을 높여 줍니다.
'Kotlin' 카테고리의 다른 글
Android - Kotlin으로 RecyclerView 구현하기 (0) | 2022.07.29 |
---|---|
[Kotlin] - 코틀린 기본 문법 4 (0) | 2022.07.10 |
[Kotlin] - 코틀린 기본 문법 3 (0) | 2022.07.09 |
[Kotlin] - 코틀린 기본 문법 2 (0) | 2022.07.08 |
[Kotlin] - 코틀린 기본 문법 1 (0) | 2022.07.08 |