Dynamic Method Dispatch
Dependency 의존성
하나의 클래스 A 가 있다.
다른 클래스 B 가 있다.
B 클래스의 내부 구현에서 A 클래스로 인스턴스를 만들었다.
B 클래스 내부에서 만든 A 의 인스턴스로 A 클래스의 메서드도 사용했다.
그렇게 에플리케이션을 잘 사용하다가, A 클래스의 메서드를 삭제했다.
그럼 B 클래스는 어떻게 될까?
당연히 오류를 뱉을 것이다.
B는 A를 사용하고 있었으니까.
이렇게 B 가 A를 사용하고 있는 상태를 B 는 A를 의존한다 라고 하고, 두 클래스는 의존관계가 있으며 이런 것을 의존성이라 한다.
디스패치
디스패치란 어떤 메서드를 호출할 것인가 를 결정하는 과정을 말한다.
즉 메서드의 의존성을 결정하는 과정이라 할 수 있다.
- 정적 디스패치
- 동적 디스패치
정적 디스패치
public class Person {
private int age;
public void print() {
System.out.println("Hello");
}
public void print(String greeting) {
System.out.println(greeting);
}
public void printJob() {
}
}
public class Main {
public static void main(String[] args) {
Person p = new Person();
p.print();
p.print("hi");
}
}
출력 :
Hello
Hi
이 코드에서 p 객체의 오버로딩된 두개의 메서드 print 를 실행했다.
정적 디스패치는 런타임이 아닌 컴파일타임에 컴파일러, 사용자, 바이트코드 모두 어떤 메서드가 실행될 지 아는 것이다.
p 객체의 두 print 메서드 중 어떤 메서드를 실행할 지 컴파일 타임에 결정한다.
바이트 코드를 보자.
L1
LINENUMBER 6 L1
ALOAD 1
INVOKEVIRTUAL com/whiteship/white_ship_study/week6/Dispatch/Person.print ()V
L2
LINENUMBER 7 L2
ALOAD 1
LDC "hi"
INVOKEVIRTUAL com/whiteship/white_ship_study/week6/Dispatch/Person.print (Ljava/lang/String;)V
()V, (Ljava/lang/String;)V 이렇게 다른 메서드를 실행함을 바이트코드는 이미 알고있다.
이런 것을 정적 디스패치라 한다.
동적 디스패치
public abstract class Job {
abstract void printJob();
}
public class Student extends Job {
@Override
public void printJob() {
System.out.println("Student");
}
}
public class Teacher extends Job {
@Override
public void printJob() {
System.out.println("Teacher");
}
}
public class DynamicDispatch {
public static void main(String[] args) {
Job student = new Student();
Job teacher = new Teacher();
student.printJob();
teacher.printJob();
// List<Job> jobList = Arrays.asList(new Teacher(), new Student());
// jobList.forEach(Job::printJob);
}
}
Job 클래스를 상속받는 두 클래스 Student, Teacher 가 있다.
이 두 클래스를 Job 타입 객체로 만들었다.
그리고 두 객체의 printJob() 을 실행시킨다.
이 때, main 메서드 안의 student.printJob(), teacher.printJob() 은 컴파일타임에 어떤 클래스의 printJob 을 실행하는지 알까?
아니, 애초에 Job 이라는 abstract 클래스의 메서드로 접근하는데 컴파일타임에 하위 타입의 printJob() 의 구현을 고려할까?
이번에도 바이트코드를 보자.
L0
LINENUMBER 5 L0
NEW com/whiteship/white_ship_study/week6/Dispatch/Student
DUP
INVOKESPECIAL com/whiteship/white_ship_study/week6/Dispatch/Student.<init> ()V
ASTORE 1
L1
LINENUMBER 6 L1
NEW com/whiteship/white_ship_study/week6/Dispatch/Teacher
DUP
INVOKESPECIAL com/whiteship/white_ship_study/week6/Dispatch/Teacher.<init> ()V
ASTORE 2
L2
LINENUMBER 8 L2
ALOAD 1
INVOKEVIRTUAL com/whiteship/white_ship_study/week6/Dispatch/Job.printJob ()V
L3
LINENUMBER 9 L3
ALOAD 2
INVOKEVIRTUAL com/whiteship/white_ship_study/week6/Dispatch/Job.printJob ()V
아래의 Job.printJob 이 같은걸로 알 수 있듯이,
바이트코드상에서는 Job 타입의 printJob 을 실행함을 알 뿐, 어떤 클래스의 메서드인지는 모른다.
어떤 메서드를 실행시킬까는 런타임에 결정하게 되는 것이다.
이를 동적 디스패치라 한다.
그럼 어떻게 결정할까.
Job teacher = new Teacher(); 라고 객체를 생성하면 클래스에서 this 라는 자기자신 객체의 정보를 담은 참조자를 가지는 것 처럼 reciver parameter 라는 자기 객체의 정보를 담은 인자를 같이가지고 Job이라는 타입에 저장하게 된다.
런타임에 메서드를 호출할 때 이 reciver parameter 를 보고 어떤 객체인지 판단 후 그 클래스의 메서드를 호출한다.
더블 디스패치
public class Double {
interface Post {
void postOn(SNS sns);
}
static class Text implements Post {
public void postOn(SNS sns) {
if (sns instanceof Facebook) {
System.out.println("facebook - text");
}
if (sns instanceof Twitter) {
System.out.println("twitter - text");
}
}
}
static class Picture implements Post {
public void postOn(SNS sns) {
if (sns instanceof Facebook) {
System.out.println("facebook - picture");
}
if (sns instanceof Twitter) {
System.out.println("twitter - picture");
}
}
}
interface SNS {}
static class Facebook implements SNS {
}
static class Twitter implements SNS {
}
public static void main(String[] args) {
List<Post> posts = Arrays.asList(new Text(), new Picture());
List<SNS> sns = Arrays.asList(new Twitter(), new Facebook());
posts.forEach(post -> sns.forEach(sn -> post.postOn(sn)));
}
}
Sns 인터페이스를 구현하는 Facebook, Twitter 이 있다.
Post 인터페이스를 구현하는 Text, Picture 이 있다.
2 x 2 의 동작에 대해 모든 로직을 따로 만들어보고 싶다는 생각이 들었다 치자.
SNS 객체를 인수로 가지고 Post 타입의 객체의 postOn() 으로 접근하면, 다이나믹 디스패치가 적용되어 Text 인지 Picture 인지 판단하고알맞은 클래스의 postOn() 를 실행할지 런타임에서 결정하게 된다.
이후 인수 SNS 객체타입을 instanceOf 를 가지고 로직을 분기한다.
하지만 이런 방법에는 치명적인 문제가 있다.
Post 의 하위 클래스 Text, Picture 은 SNS 를 사용하고 있다. 의존성이 있다는 것이다.
Post 가 SNS 를 의존하고 있다.
만약에 SNS 타입이 하나 더 생기면? 의존하고 있는 클래스에 변경이 있다면 ?
class Instagram implements SNS {} 가 새로 생긴다면 그에 따른 로직을 정의하기 위해 새로운 if() 를 추가해야 한다.
이것이 문제다.
의존하고 있는 클래스에 변경이 있다면, 클라이언트 코드도 바뀌어야 한다는 것.
하지만 추가하지 않아도 컴파일타임에 에러를 뱉지는 않는다.
개발자가 실수로 추가 if 를 추가하지 않고, 클라이언트가 instagram 객체를 준다면? Exception 이 발생하며 프로그램이 종료될 것이다.
public class DoubleDispatch {
interface Post {
void postOn(SNS sns);
}
static class Text implements Post {
public void postOn(SNS sns) {
sns.post(this);
}
}
static class Picture implements Post {
public void postOn(SNS sns) {
sns.post(this);
}
}
interface SNS {
void post(Text text);
void post(Picture picture);
}
static class Facebook implements SNS {
@Override
public void post(Text text) {
System.out.println("facebook - text");
}
@Override
public void post(Picture picture) {
System.out.println("facebook - picture");
}
}
static class Twitter implements SNS {
@Override
public void post(Text text) {
System.out.println("twitter - text");
}
@Override
public void post(Picture picture) {
System.out.println("twitter - picture");
}
}
public static void main(String[] args) {
List<Post> posts = Arrays.asList(new Text(), new Picture());
List<SNS> sns = Arrays.asList(new Twitter(), new Facebook());
posts.forEach(post -> sns.forEach(sn -> post.postOn(sn)));
}
}
그래서 나온 해결책이 이 방법이고 이 방법이 더블 디스패치이다.
먼저번의 코드와 다른점을 잘 확인하자.
SNS 타입 클래스들에 post 라는 메서드가 새로 생겼다.
그리고 출력을 SNS 에서 하게 바뀌었다.
말했듯이, Post 에서 SNS 을 사용하고 의존하는 구조다.
Post 타입에 저장된 post 객체의 reciver parameter 로 자신의 Post 타입이 Text 인지 Picture 인지 결정한다. -> 다이나믹 디스패치
이후 적절한 클래스의 postOn 메서드로 들어오면, 인자로 들어온 SNS의 메서드 post 를 호출한다. 자기자신의 객체정보를 담은 this 를 인자로 넘기면서. 여기서 sns.post() 이 부분이 또 다이나믹 디스패치인 것이다.
이후 적절한 SNS 타입의 하위 클래스를 찾아가 오버로딩된 post() 메서드들 중 알맞은 메서드를 실행한다.
그럼 두번에 걸쳐 다이나믹 디스패치가 일어나므로 이를 더블 디스패치라 한다.
더블 디스패치는 의존성을 담당하는 SNS 에서 새로운 Instagram 이 생겼다고 해도 클라이언트인 Post 하위 클래스들은 변경점이 없다.
public class DoubleDispatch {
interface Post {
void postOn(SNS sns);
}
static class Text implements Post {
public void postOn(SNS sns) {
sns.post(this);
}
}
static class Picture implements Post {
public void postOn(SNS sns) {
sns.post(this);
}
}
interface SNS {
void post(Text text);
void post(Picture picture);
}
static class Facebook implements SNS {
@Override
public void post(Text text) {
System.out.println("facebook - text");
}
@Override
public void post(Picture picture) {
System.out.println("facebook - picture");
}
}
static class Twitter implements SNS {
@Override
public void post(Text text) {
System.out.println("twitter - text");
}
@Override
public void post(Picture picture) {
System.out.println("twitter - picture");
}
}
// 새로운 Instagram
static class Instagram implements SNS {
@Override
public void post(Text text) {
System.out.println("instagram - text");
}
@Override
public void post(Picture picture) {
System.out.println("instagram - picture");
}
}
public static void main(String[] args) {
List<Post> posts = Arrays.asList(new Text(), new Picture());
List<SNS> sns = Arrays.asList(new Twitter(), new Facebook(), new Instagram()); // 인스타그램도 추가
posts.forEach(post -> sns.forEach(sn -> post.postOn(sn)));
}
}
이 더블 디스패치를 활용하여 범용적으로 쓰기 위한 패턴을 visitor 패턴이라 한다.
기회가 된다면 정리하여 포스팅하도록 하겠다..!
참고:
www.youtube.com/watch?v=s-tXAHub6vg
thecodinglog.github.io/design/2019/10/29/visitor-pattern.html