해당 글은 토비의 봄 TV를 보고 정리한 것입니다.
먼저 더블 디스패치를 알기전에 디스패치에 대해 간략하게 알아보면,
"어플리케이션이 어떤 메소드를 호출할 것인지 결정하고 실행하는 과정"을 의미한다
그리고 디스패치에는 크게 두가지로 나눠집니다
정적인 디스패치(Static Dispatch) 와 동적인 디스패치(Dynamic Dispatch)
정적인 디스패치는 컴파일타임에 어떤 메소드가 호출될지 이미 정해지고, 동적인 디스패치는 런타임에서 어떤 메소드가 호출될지 결정된다.
정적인 디스패치
e.g)
아래 코드는 반환 타입(Unit)도 같고, 함수명(run)도 같지만, 파라미터가 달라 이미 컴파일 시점에서 다른 함수 취급을 받게된다.
즉 실행되기전에 이미 어떤 함수가 호출될지 결정이 되어 있는 상황이다.
class Service {
companion object {
fun run() {
println("run()")
}
fun run(message: String) {
println(message)
}
}
}
fun main() {
Service.run()
Service.run("Hello word")
}
동적인 디스패치
e.g)
아래 코드를 실행하면 당연히 우리는 MyService 가 프린트 될 것 이라는 것을 알고 있다.
우리는 MyService() 가 service 변수에 할당되어 있으니, 컴파일 시점에 이미 service.run() 가 마치 myService.run() 로 치환되어 있는 것이 아닐까? 하고 해석할 수 있다
하지만 답은 그렇지 않다. 컴파일시점에는 결정되어 있지 않고, 런타임 시점에서 그 객체에 할당되어 있는 타입을 확인하고 run() 메소드를 호출하게 된다. 이때 Service 타입의 service 오브젝트가 MyService 의 run 을 호출하는 행위를 "다이나믹 디스패치" 라고 한다
abstract class Service {
abstract fun run()
}
class MyService: Service() {
override fun run() {
println("MyService")
}
}
class YourService: Service() {
override fun run() {
println("YourService")
}
}
fun main() {
val service: Service = MyService()
service.run()
}
아래 코드에서 s.run() 은 어떤 것이 실행될지 컴파일 시점에 결정되어 있지 않고, 런타임에서 forEach 를 돌아 s 를 꺼내고 그 때 오브젝트에 할당되어 있는 타입을 확인하여 run() 메소드를 호출한다. 이것도 역시 다이나믹 디스패치이다
fun main() {
val services = listOf(MyService(), YourService())
services.forEach { s -> s.run() }
}
이제 본론으로 들어가서 더블 디스패치에 대해 알아보자.
더블 디스패치는 다이나믹 디스패치가 두번 나타나는 것을 의미한다.
먼저 내가 작성한 이미지, 글을 사용하는 모든 SNS 에 포스트하는 코드를 작성한다고 가정해보자.
interface Post {
fun postOn(sns: SNS)
}
class Text : Post {
override fun postOn(sns: SNS) {
println("Text -> ${sns.javaClass.simpleName}")
}
}
class Picture : Post {
override fun postOn(sns: SNS) {
println("Picture -> ${sns.javaClass.simpleName}")
}
}
interface SNS {}
class Facebook : SNS {
}
class Twitter : SNS {
}
fun main() {
val posts = listOf(Text(), Picture())
val sns = listOf(Facebook(), Twitter())
for (p in posts) {
for (s in sns) {
p.postOn(s)
}
}
// or
// posts.forEach { p -> sns.forEach { s -> p.postOn(s) } }
}
결과
Text -> Facebook
Text -> Twitter
Picture -> Facebook
Picture -> Twitter
현재 코드에서는 SNS 가 뭐든 상관없이 Text 타입이라면 Text 클래스의 postOn 로직을 타고, Picture 타입이라면 Picture 클래스의 postOn 로직을 타기 때문에 비즈니스 로직은 2개라고 할 수있다.
하지만 Text -> Facebook 와 Text -> Twitter 인 경우에, 하나의 비즈니스로 처리하지 않고, 각각의 비즈니스 로직이 실행되야한다고 가정해보자.
먼저 가장 간단하게 생각해보자.
아래와 같이 is 키워드로 해당 타입을 체크하면 비즈니스 로직을 분리 시킬 수 있다.
interface Post {
fun postOn(sns: SNS)
}
class Text : Post {
override fun postOn(sns: SNS) {
when(sns) {
is Facebook -> {
println("Text -> ${sns.javaClass.simpleName}")
}
is Twitter -> {
println("Text -> ${sns.javaClass.simpleName}")
}
}
}
}
class Picture : Post {
override fun postOn(sns: SNS) {
when(sns) {
is Facebook -> {
println("Picture -> ${sns.javaClass.simpleName}")
}
is Twitter -> {
println("Picture -> ${sns.javaClass.simpleName}")
}
}
}
}
interface SNS {}
class Facebook : SNS {
}
class Twitter : SNS {
}
fun main() {
val posts = listOf(Text(), Picture())
val sns = listOf(Facebook(), Twitter())
posts.forEach { p -> sns.forEach { s -> p.postOn(s) } }
}
하지만 이런 코드는 몇가지의 문제가 있다.
GooglePlus 가 새로 추가되었다고 가정해보자.
아래 코드는 에러없이 동작하지만, when 문에 is 로 새로운 GooglePlus 타입을 체크하는 로직을 추가하지 않았기 때문에, 결과적으로 오동작하게된다.
물론 enum, sealed 클래스의 대수타입을 활용하여 is 문을 추가하지 않았을 때 자동으로 컴파일에서 에러를 던져줘서 이런 문제를 보완할 수 있지만 결국은 다양한 타입들이 추가되면 추가될수록 when 문이 커진다. 우리는 이런 코드를 작성하고 싶지 않다.
현재 아래코드는 SOLID 원칙 중 OCP(Open close principle - 애플리케이션의 요구 사항이 변경될 때, 이 변경에 맞게 새로운 동작을 추가해 모듈을 확장할 수 있다. 즉, 모듈이 하는 일을 변경할 수 있다.) 를 어기고 있고, 우리는 리스코프의 치환법칙을 활용하여 이를 적절히 보완할 수 있다.
interface Post {
fun postOn(sns: SNS)
}
class Text : Post {
override fun postOn(sns: SNS) {
when(sns) {
is Facebook -> {
println("Text -> ${sns.javaClass.simpleName}")
}
is Twitter -> {
println("Text -> ${sns.javaClass.simpleName}")
}
}
}
}
class Picture : Post {
override fun postOn(sns: SNS) {
when(sns) {
is Facebook -> {
println("Picture -> ${sns.javaClass.simpleName}")
}
is Twitter -> {
println("Picture -> ${sns.javaClass.simpleName}")
}
}
}
}
interface SNS {}
class Facebook : SNS {
}
class Twitter : SNS {
}
class GooglePlus : SNS {
}
fun main() {
val posts = listOf(Text(), Picture())
val sns = listOf(Facebook(), Twitter(), GooglePlus()) // GooglePlus 추가
posts.forEach { p -> sns.forEach { s -> p.postOn(s) } }
}
아래와 같이 작성하게 되면 main 의 postOn 에서 런타임시에 1차 다이나믹 디스패치가 발생하고, postOn의 내부메소드의 run 에서 2차 다이나믹 디스패치가 발생한다. 리스코프의 치환법칙이 아주 잘 적용이 되었다. 아주 인상 깊은 점으로는 run 메서드 안에 this 를 넘기고 있는 것 이다.
interface Post {
fun postOn(sns: SNS)
}
class Text : Post {
override fun postOn(sns: SNS) {
sns.run(this@Text) // run 의 2차 다이나믹 디스패치
}
}
class Picture : Post {
override fun postOn(sns: SNS) {
sns.run(this@Picture) // run 의 2차 다이나믹 디스패치
}
}
interface SNS {
fun run(text: Text)
fun run(picture: Picture)
}
class Facebook : SNS {
override fun run(text: Text) {
println("Text -> Facebook")
}
override fun run(picture: Picture) {
println("Picture -> Facebook")
}
}
class Twitter : SNS {
override fun run(text: Text) {
println("Text -> Twitter")
}
override fun run(picture: Picture) {
println("Picture -> Twitter")
}
}
class GooglePlus : SNS {
override fun run(text: Text) {
println("Text -> GooglePlus")
}
override fun run(picture: Picture) {
println("Picture -> GooglePlus")
}
}
fun main() {
val posts = listOf(Text(), Picture())
val sns = listOf(Facebook(), Twitter(), GooglePlus())
posts.forEach { p -> sns.forEach { s -> p.postOn(s) } } // postOn 의 1차 다이나믹 디스패치
}
위와 같은 방식을 사용하게 되면 어떤 장점 이 있을까 ?
먼저 첫째로 when 문에 is 를 체크하는 부분이 없어졌기 때문에 코드량이 절대적으로 감소했다. when-is 는 객체지향에서 안티패턴이 되는 경우가 많기 때문에, 사용하지 않는 것이 좋다. 굳이 사용해야 한다면 when 안에 조건을 enum 으로 두는 것이 좋다.
두번째로 GooglePlus 같은 새로운 타입이 추가된다고해도 우리는 GooglePlus 클래스를 정의하고 SNS 를 담는 리스트에 추가만 해주면된다. 새로 추가하는 것이 자유롭고, 어떤 코드를 추가하는 시점에서 다른 코드에게 영향을 주지 않기 때문에 매우 객체지향적인 코드가 된다.
마지막으로 n * n 케이스에 각각 비즈니스 로직을 분리하여, 관심사가 분리되고 비즈니스 로직에 집중할 수 있게 되었다.
'Code Paradigm' 카테고리의 다른 글
클린 코더스 강의 요약(Architecture) - 5 (0) | 2020.11.03 |
---|---|
클린 코더스 강의 요약(Form) - 4 (0) | 2020.10.05 |
클린 코더스 강의 요약(Function Structure) - 3 (0) | 2020.07.21 |
클린 코더스 강의 요약(Function) - 2 (0) | 2020.07.20 |
클린 코더스 강의 요약 (OOP) - 1 (0) | 2020.07.19 |