[Spring] 의존성이란 무엇인가? 강한 결합과 느슨한 결합을 통해 이해하는 의존성
의존성이란?
소프트웨어 개발에서 의존성(Dependency)이란 한 모듈이 다른 모듈의 기능을 사용하거나, 하나의 객체가 다른 객체의 기능을 사용하는 것을 뜻한다. 예를 들어 A 객체의 기능을 실행할 때 B 객체가 필요하다면, "A 객체는 B 객체에 의존한다"라고 한다.
의존성은 의존하는 정도에 따라 강한 결합과 느슨한 결합으로 나눌 수 있다. 이어서 이 두가지 개념을 사용해 의존성에 대해 이해해보도록 하자.
강한 결합을 통해 이해하는 의존성
강한 결합이란?
우리가 SNS앱을 개발하는데, 유저에게 푸시 알림을 보내는 서비스를 만든다고 해보자. 그러면 이런 역할을 하는 PushNotificationService는 다음과 같이 만들 수 있다.
class PushNotificationService {
fun sendNotification(message: String, receiverId: String) {
println("Sending push notification to $receiverId: $message")
}
}
PushNotificationService는 sendNotification이라는 함수를 가진 클래스로, 이 함수는 message를 인자로 받아, "Sending push notification to $receiver: $message"를 출력한다.
이제 이 PushNotificationService을 사용해 댓글이 달렸을 때 유저에게 푸시 알림을 보내도록 해보자.
이를 위해서는 코멘트에 새로운 이벤트가 발생했을 때 처리하는 역할을 하는 NewCommentEventHandler을 다음과 같이 만들고 푸시 알림을 보내는 PushNotificationService 객체를 주입 받으면 된다. 이후 내부의 handle 함수가 호출됐을 때 PushNotificationService가 sendNotification을 실행하도록 하면 된다.
class NewCommentEventHandler(
private val notificationService: NotificationService
) {
fun handle(comment: String, ownerId: String) {
notificationService.sendNotification(
receiverId = ownerId,
message = "New comment registered: $comment"
)
}
}
이제 이 NewCommentEventHandler는 다음과 같이 사용될 수 있다.
fun main() {
val newCommentEventHandlerUsingPush = NewCommentEventHandler(PushNotificationService())
newCommentEventHandlerUsingPush.handle(ownerId = "ownerId", comment = "안녕하세요")
}
위의 코드에서 볼 수 있듯이 NewCommentEventHandler 클래스는 PushNotificationService 클래스를 직접 사용하고 있다.
이렇게 하나의 클래스가 다른 클래스를 직접 참조하는 경우 이 둘은 강하게 결합되어 있다고 한다. 이렇게 강하게 결합된 클래스는 코드의 유연성을 떨어뜨리고, 변경에 취약하게 된다. 왜 그런지 이어서 살펴보자.
강한 결합의 한계
강하게 결합된 경우 변경에 취약하다. 예를 들어 새로운 코멘트가 발생했을 때 푸시가 아닌, 카카오톡을 통해 알림을 줘야 한다는 요청이 들어왔다고 해보자. 그런 경우 먼저 KakaoMessageService가 다음과 같이 만들어지고,
class KakaoMessageService {
fun sendMessage(receiverId: String, message: String) {
println("Sending Kakao message to $receiverId: $message")
}
}
위의 NewCommentEventHandler는 다음과 같이 변경돼야 한다.
class NewCommentEventHandler(
private val kakaoMessageService: KakaoMessageService
) {
fun handle(ownerId: String, comment: String) {
kakaoMessageService.sendMessage(
receiverId = ownerId,
message = "New comment registered: $comment"
)
}
}
실행 코드도 다음과 같이 바뀌어야 한다.
fun main() {
val newCommentEventHandlerUsingKaKao = NewCommentEventHandler(KakaoMessageService())
newCommentEventHandlerUsingKaKao.handle(ownerId = "ownerId", comment = "안녕하세요")
}
하나의 요구사항이 들어왔는데, 전체 코드가 바뀌었다. 이유는 NewCommentEventHandler가 PushNotificationService에 강하게 결합되어 있었기 때문이다. NewCommentEventHandler는 이번에 만든 KakaoMessageService와도 강하게 결합되어 있기 때문에 이후 다른 변경사항이 생긴다면 다시 전부 변경해야 한다.
그렇다면 이 문제를 해결하기 위해서는 어떻게 해야할까? 바로 이를 해결하기 위한 방법은 느슨한 결합을 사용하는 것이다. 이어서 이 방법에 대해 알아보자.
느슨한 결합을 통해 강한 결합의 문제 해결하기
느슨한 결합이란 인터페이스를 사용해 의존성을 줄이는 것이다. 기존에 강한 결합에서 NewCommentEventHandler가 PushNotificationService나 KakaoMessageService 같은 특정 클래스에 의존해 생기던 문제를, 중간에 인터페이스를 하나 두고 이 인터페이스에만 의존하도록 함으로써 문제를 해결하는 것이다.
예를 들어 NewCommentEventHandler와 PushNotificationService, KakaoMessageService의 중간에 NotificationService라는 인터페이스를 두고, PushNotificationService와 KakaoMessageService가 이 인터페이스를 구현하게 하면 된다.
interface NotificationService {
fun sendNotification(receiverId: String, message: String)
}
class PushNotificationService : NotificationService {
override fun sendNotification(message: String, receiverId: String) {
println("Sending push notification to $receiverId: $message")
}
}
class KakaoMessageService: NotificationService {
fun sendMessage(receiverId: String, message: String) {
println("Sending Kakao message to $receiverId: $message")
}
override fun sendNotification(receiverId: String, message: String) {
sendMessage(receiverId = receiverId, message = message)
}
}
이후 NewCommentEventHandler도 NotificationService에만 의존하도록 하면 된다.
class NewCommentEventHandler(
private val notificationService: NotificationService
) {
fun handle(comment: String, ownerId: String) {
notificationService.sendNotification(
receiverId = ownerId,
message = "New comment registered: $comment"
)
}
}
그러면 의존성 그래프는 다음과 같은 모양이 된다.
이렇게 되면 NewCommentEventHandler는 PushNotificationService나 KaKaoMessageService 양쪽을 모두 사용할 수 있게 되며, 이를 통해 유연성이 올라간다.
fun main() {
val newCommentEventHandlerUsingPush = NewCommentEventHandler(PushNotificationService())
newCommentEventHandlerUsingPush.handle("안녕하세요")
val newCommentEventHandlerUsingKaKao = NewCommentEventHandler(KakaoMessageService())
newCommentEventHandlerUsingKaKao.handle("안녕하세요")
}
이렇게 인터페이스를 의존하도록 만드는 것을 의존성을 느슨하게 만든다 혹은 느슨하게 결합하도록 한다고 한다. 하지만, 이렇게 느슨한 결합은 장점도 있지만 단점도 있다. 다음 글에서 이러한 느슨한 결합에 어떤 장점과 단점이 있는지 알아보자.