본문 바로가기
스터디/[white-ship] 자바 스터디(study hale)

6주차 과제: 상속

by doyoungKim 2020. 12. 26.

백기선님 ISSUE - 6주차 과제: 상속

 

 

6주차 과제: 상속 · Issue #6 · whiteship/live-study

목표 자바의 상속에 대해 학습하세요. 학습할 것 (필수) 자바 상속의 특징 super 키워드 메소드 오버라이딩 다이나믹 메소드 디스패치 (Dynamic Method Dispatch) 추상 클래스 final 키워드 Object 클래스 마

github.com

6 주차 학습할 것 (필수)

  • 자바 상속의 특징
  • super 키워드
  • 메서드 오버 라이딩
  • 다이내믹 메서드 디스패치 (Dynamic Method Dispatch)
  • 추상 클래스
  • final 키워드
  • Object 클래스

*추가: 더블 디스패치 와 visitor(방문자) 패턴

백기선님의 책추천

  1. 객체지향의 사실과 오해
  2. 오브젝트

 

사용한 코드 저장소

 

doyoung0205/live-study

온라인 스터디. Contribute to doyoung0205/live-study development by creating an account on GitHub.

github.com


상속이란 무엇인가?

윗분 들로 부터 전해 내려오는 것. 예를 들어, 돈 이 될 수도 있지만, 어떠한 물건이나 도구 일 수 도 있다.

굳이 새로 사지 않고 왜 상속을 받는 것인가?

만약 새로 산다고 한다면,

비용이 들고 기존에 있던 물건 이나 도구를 버리는 것도 비용이 들기 때문에 상속을 받는 다고 생각한다.

 

자바도 마찬가지라고 생각한다.

자바의 클래스는 데이터와 기능으로 이루어져 있고, 자바는 클래스가 다른 클래스로 부터 상속을 받는다. 

데이터와 기능을 상속을 받아서 코드를 작성하게 되면,

새로 코드를 작성하는 비용이나 비슷한 기존의 코드와 새로운 코드 모두를 관리해야 하는데 있어서

비용적인 측면에서 이점이 든다고 생각한다.

 

상속의 예시

예를들어, 자바를 통해서 친구들의 정보를 프로그램을 만든다고자 한다.

친구는 어디서 사귀었느냐에 따라서, 대학 친구이 될 수도 있고, 사회 친구가 될 수 도 있다.

 

프로그램안에서 대학 친구과 사회 친구의 정보(데이터) 는 다음과 같고

각각 자신의 정보를 알려주는 기능을 하도록 설계 해서

MyFriends 라는 클래스에 main 메소드를 통해서 친구들을 불러와서 정보를 보여주는 프로그램을

단순한 버전과 상속 버전의 각각 예시를 보자.

 

대학 친구 이름  전공 전화번호
사회 친구 이름 직업 전화번호

 

 

단순한 친구 정보 출력 프로그램

public class CompFriend { // 사회 친구
    private String name;
    private String job;
    private String phone;

    public CompFriend(String name, String job, String phone) {
        this.name = name;
        this.job = job;
        this.phone = phone;
    }

    public void showInfo() {
        System.out.println(this.toString());
    }

    @Override
    public String toString() {
        return "CompFriend{" +
                "name='" + name + '\'' +
                ", job='" + job + '\'' +
                ", phone='" + phone + '\'' +
                '}';
    }
}
public class UnivFriend { // 대학 친구
    private String name;
    private String major; //전공
    private String phone;

    public UnivFriend(String name, String major, String phone) {
        this.name = name;
        this.major = major;
        this.phone = phone;
    }

    public void showInfo() {
        System.out.println(this.toString());
    }

    @Override
    public String toString() {
        return "UnivFriend{" +
                "name='" + name + '\'' +
                ", major='" + major + '\'' +
                ", phone='" + phone + '\'' +
                '}';
    }
}

 

public class MyFriends {
    public static void main(String[] args) {

        // 대학 친구들
        List<UnivFriend> univFriends = getUnivFiends();

        for (UnivFriend univFriend : univFriends) {
            univFriend.showInfo();
        }

        // 사회 친구들
        List<CompFriend> compFriends = getCompFriends();

        for (CompFriend compFriend : compFriends) {
            compFriend.showInfo();
        }

    }

    private static List<CompFriend> getCompFriends() {
        return Arrays.asList(
                new CompFriend("ILL", "computer", "010-1111-1111"),
                new CompFriend("LEE", "Electronics", "010-2222-2222")
        );
    }

    private static List<UnivFriend> getUnivFiends() {
        return Arrays.asList(
                new UnivFriend("SAM", "computer", "010-3333-3333"),
                new UnivFriend("SA", "Electronics", "010-4444-4444")
        );
    }
}

 

출력 결과

내 친구들의 정보를 출력하는 프로그램 까지 만들었다. 잘 출력이되어 기쁘지만, 이 코드에는 단점이 있다.

 

단순한 친구 정보 출력 프로그램의 단점

 

main 메소드를 잘 관찰하면 다음과 같은 사실을 확인할 수 있다.

  • 인스턴스를 저장하는 배열이 두 개이다.
  • 대학 친구의 정보를 저장하는 과정과 사회 친구의 정보를 저장하는 과정이 나뉜다.
    • 저장에 필요한 배열과 변수가 다르기 때문.
  • 저장된 정보를 모두 출력할 때 두 개의 for문을 작성해야 한다.
    • 출력에 사용되는 배열과 변수가 다르기 때문.

 

즉, 배열이 두 개 이므로 무엇을 하건 그 과정이 둘로 나뉜다.

 

만약에, 특정 이름의 정보를 탐색하는 기능을 추가한다면, 이 경우에도 두 배열을 모두 검색해야 하는 비용이 발생한다.

여기서는 배열이 2개이지만, 나중에 추가적으로 고등학교 친구같은 다른 저장 대상이 생긴다면, 생긴 수만큼 배열을 추가하고 추가된 배열만큼 프로그램이 복잡해 진다.

 

어느순간, 배열이 수십개가 된 상황에서 name 이라는 변수를 firstName 과 lastName으로 나눠야 하는 상황을 생각해본다면,

비용은 막대할 것이고 관리하기 힘들 것 이다.

 

즉, 배열의 수가 얼마나 더 늘어날지 모르고 복잡해진다

 

상속을 이용해서

이런 배열의 수를 항상 하나로 줄여줄 수 있고, 나중에 공통된 name 이라는 변수가 firstName 과 lastName 으로 나뉘어도 적은 비용으로 수정할 수 있게 해결 해줄 수 있다.

 

 

상속으로 단점을 해결한 친구 정보 출력 프로그램

 

연관된 일련의 클래스들에 대해 공통적인 규약을 정의할 수 있다.

 

 

사회 친구와 대학 친구는

이름과 핸드폰 번호라는 공통적인 데이터와 정보를 보여주는 기능을 공통적으로 가지고 있다.

이런 공통적인 특성 들만 뽑아서 최초의 클래스를 만들고 사회 친구와 대학 친구에게 각각 상속 시킬 수 있다.

 

public class Friend {
    protected String name;
    protected String phone;

    public Friend(String name, String phone) {
        this.name = name;
        this.phone = phone;
    }

    public void showInfo() {
        this.toString();
    }

    @Override
    public String toString() {
        return "name : " + name + "\n" + "phone : " + phone;
    }
}

 

이제 상속을 받아 사회친구를 만들어보겠다.

 

public class CompFriend extends Friend { // 사회 친구
    private String job;
    
}

 

오류메시지

 

super

조상 클래스 Friend 에서의 기본 생성자가 없다는 에러 메시지가 나온다.

여기서 알게되는 점은 조상클래스로 부터 상속을 받는 클래스는 무조건 조상클래스의 생성자를 호출 해야한다.

호출하는 방법은 super라는 예약어를 사용하여 조상 클래스의 생성자를 호출한다.

 

지금 같은 경우는 클래스에서 기본생성자가 생략되듯이 상속을 받게되면 매개변수 없는 super 가 생략되어 있다.

이를 해결 해주려면 name 과 phone 을 초기화 해주는 super를 만들어주면 된다.

 

public class CompFriend extends Friend { // 사회 친구
    protected String job;

    public CompFriend(String name, String phone, String job) {
        super(name, phone); // new Friend(name, phone);
        this.job = job;
    }
    
}

 

이제 정보를 출력하는 기능을 만들어보자.

이미 조상 클래스에서 showInfo 라는 기능을 만들었지만, 해당 기능에는 job 이라는 데이터가 빠져있으니 job 만 추가로 출력하도록 만들어보자.

 

public class CompFriend extends Friend { // 사회 친구
    protected String job;

    public CompFriend(String name, String phone, String job) {
        super(name, phone);
        this.job = job;
    }

    @Override
    public void showInfo() {
        super.showInfo();
        System.out.println(this.toString());
    }

    @Override
    public String toString() {
        return "\n" + "job : " + job;
    }
}

 

오버라이딩

super 라는 예약어를 통해 조상 클래스의 메소드에 접근이 가능하다.

이미 조상클래스에서 name과 phone 의 정보를 출력하는 것이 있으니 job 만 출력하는 것을 밑에 추가해 보았다.

 

이런 조상 클래스의 메소드와 같은 시그니처를 가지고 구현부를 변형하는 것을 오버라이딩 이라고 한다.

그리고 오버라이딩 된 메소드는 @Override 라는 어노테이션이을 붙일 수 있다.

 

    public static void main(String[] args) {
        // 사회 친구들
        List<CompFriend> compFriends = getCompFriends();

        for (CompFriend compFriend : compFriends) {
            compFriend.showInfo();
        }
    }
    private static List<CompFriend> getCompFriends() {
        return Arrays.asList(
                new CompFriend("ILL", "computer", "010-1111-1111"),
                new CompFriend("LEE", "Electronics", "010-2222-2222")
        );
    }

 

출력 결과

이상하게 job 만 2번씩 나왔다...

 

처음에는 조상 클래스쪽 메소드의 있는 this 가 문제인줄 알았지만,

문제는 오버라이딩 이였다.

 

현 상황에서는 showInfo 라는 메소드 뿐만 아니라 toString 메소드 또한 계속해서 오버라이딩 되고 있었다.

따라서 super.showInfo 로 인해 Friend의 showInfo가 실행되고 showInfo 는 this.toString 메소드를 실행 하게 되는데, 여기서 this의 유무 상관 없이 super 예약어가 없이 호출하는 오버라이딩 된 메소드는 무조건 자식 쪽 메소드를 호출 하기 때문에 job 의 데이터만 출력이 된 것이다.

따라서 CompFriend의 코드를 다음과 같이 변경해줘야 한다.

 

    @Override
    public void showInfo() {
        super.showInfo(); //job 출력
        System.out.println(super.toString()); // name, phone 출력
    }

 

super.showInfo가 job 을 출력한다니, 믿겨지지 않지만 그 내부의 오버라이딩 된 메소드가 있기 때문에 job 만을 출력하는 것이 맞다.

 

이제 나머지 대학친구도 리팩토링을 진행해보자.

 

public class UnivFriend extends Friend { // 대학 친구
    protected String major; //전공

    public UnivFriend(String name, String phone, String major) {
        super(name, phone);
        this.major = major;
    }
    
    @Override
    public void showInfo() {
        super.showInfo();
        System.out.println(super.toString());
    }

    @Override
    public String toString() {
        return "major : " + major;
    }
}

 

public class MyFriends {
    public static void main(String[] args) {

        // 모든 친구들
        List<Friend> univFriends = getFiends();
        for (Friend univFriend : univFriends) {
            univFriend.showInfo();
        }
        
    }

    private static List<Friend> getFiends() {
        return Arrays.asList(
                new CompFriend("ILL", "computer", "010-1111-1111"),
                new CompFriend("LEE", "Electronics", "010-2222-2222"),
                new UnivFriend("SAM", "computer", "010-3333-3333"),
                new UnivFriend("SA", "Electronics", "010-4444-4444")
        );
    }

}

 

이렇게 상속을 하면서 다음과 같은 효과를 얻게 되었다.

  • 인스턴스를 저장하는 배열이 하나이다.
    • Friend 클래스를 상속하는 클래스가 더 추가되어도 이 사실은 변함이 없다.
  • 정보를 저장하는 과정이 나뉘지 않는다.
    • 하나의 배열에 모든 인스턴스를 저장할 수 있다.
  • 저장된 정보를 모두 출력할 때 하나의 for문으로 충분하다.
    • 하나의 배열이 사용되었고 또 메소드 오버라이딩이 도움이 되었다.
  • 추후 어떤 변수가 변경이 일어나도 대응하기 쉽다.

 

Object

이번에는 Friend의 toString 이 왜 @Override 표시가 있는지, System.out.println의 매개변수는 어떤 것들이 들어갈 수 있는지 알아보자.

 

먼저, System.out.println의 내부를 확인해보면 다음과 같다.

 

    /**
     * Prints an Object and then terminate the line.  This method calls
     * at first String.valueOf(x) to get the printed object's string value,
     * then behaves as
     * though it invokes <code>{@link #print(String)}</code> and then
     * <code>{@link #println()}</code>.
     *
     * @param x  The <code>Object</code> to be printed.
     */
    public void println(Object x) {
        String s = String.valueOf(x);
        synchronized (this) {
            print(s);
            newLine();
        }
    }

 

먼저 매개변수로 쓰인 Object 는 무엇일까? 내부속으로 들어가니 다음과 같이 설명하고 있었다.

가장 최상위 조상 클래스이며 어떤 클래스이든 상속을 받고 있다고하고, toString 메소드 또한 구현하고 있다.

 

/**
 * Class {@code Object} is the root of the class hierarchy.
 * Every class has {@code Object} as a superclass. All objects,
 * including arrays, implement the methods of this class.
 *
 * @author  unascribed
 * @see     java.lang.Class
 * @since   JDK1.0
 */
public class Object {
      public final native Class<?> getClass();

      public native int hashCode();
      public boolean equals(Object obj) {
          return (this == obj);
      }
      protected native Object clone() throws CloneNotSupportedException;
      public String toString() {
          return getClass().getName() + "@" + Integer.toHexString(hashCode());
      }
      public final native void notify();
      public final native void notifyAll();
      public final native void wait(long timeout) throws InterruptedException;
          public final void wait(long timeout, int nanos) throws InterruptedException {
          if (timeout < 0) {
              throw new IllegalArgumentException("timeout value is negative");
          }

          if (nanos < 0 || nanos > 999999) {
              throw new IllegalArgumentException(
                                  "nanosecond timeout value out of range");
          }

          if (nanos > 0) {
              timeout++;
          }

          wait(timeout);
      }
      public final void wait() throws InterruptedException {
          wait(0);
      }
	  protected void finalize() throws Throwable { }
} 

 

그렇다면 정말 모든 테스트가 Object 상속받고 있는지 확인해보자.

 

public class AnyClass{

}

 

public class AnyClassTest {
    @Test
    @DisplayName("정말 모든 클래스가 Object 를 상속 받고 있을까?")
    void anyClass() {

        AnyClass anyClass = new AnyClass();

        assertTrue(anyClass instanceof Object);
    }
}

 

출력결과

instanceof 를 사용하여 Object 타입이거나 Object 타입을 상속받는지 확인 해보았더니 진실인 결과를 얻었다.

백기선님 왈: 

객체지향은 Object 로 코딩을 할 수 있어야 한다.

First-class citizon

함수가 함수 자체를 매개변수로 넘기거나 변수에 할당하거나 리턴 할 수 있어야 한다.

자바도 정확히 함수는 아니지만 functional 한 무언가를 통해서 할 수 있다..

하지만 functional 한 무언가가 나오기 전까지는 Object 였다. 

모든 것들을 , Object 객체를 매개변수로 넘기거나 변수에 할당하거나 리턴 할 수 있어야 했다.

따라서 최상위에 Object를 둔 것이다.

 

또한 우리가 작성했던 클래스들을 보면 toString 위의 Override라는 어노테이션이 있었던 것으로 확인할 수 있다. 

 

public class Friend {

	... 
    
    @Override
    public String toString() {
        return "name : " + name + "\n" + "phone : " + phone;
    }
}

 

왜 모든 클래스가 Object 를 상속받도록 설계가 되어있을까?

이는 자바의 모든 인스턴스에 공통된 기준 및 규약을 적용하기 위함이라고 한다.

 

우리가 사용한 System.out.println 은 매개변수가 Object 타입 이기 때문에, 어떤 객체든 들어갈 수 있다.

따라서 아까 배열을 하나로 만든 것 처럼 매개변수 또한 하나로 유지 관리 할 수 있다.

 

어떤 객체든 들어와서 String.valueOf 를 이용해서 문자열로 만들어 출력하는 것이다. 

    /**
     * Returns the string representation of the {@code Object} argument.
     *
     * @param   obj   an {@code Object}.
     * @return  if the argument is {@code null}, then a string equal to
     *          {@code "null"}; otherwise, the value of
     *          {@code obj.toString()} is returned.
     * @see     java.lang.Object#toString()
     */
    public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString();
    }

 

valueOf는 toString 을 이용해서 문자열을 만들기 때문에 위의 예제 에서는 밑에 있는 코드에서 toString을 지워버려도 된다.

기본적으로 toString 은 객체의 주소를 문자열로 나타내지만, 오버라이딩 된 메소드가 실행될테니 출력결과는 기존과 같을 것이다.

 

   public void showInfo() {
        System.out.println(this.toString());
    }

 

    public void showInfo() {
        System.out.println(this);
    }

 

출력결과

 

만약 오버라이딩 하지 않았더라면, 객체를 toString 하면 다음과 비슷한 문자열을 얻을 수 있을 것이다.

 

com.company.inheritance.good.CompFriend@d716361

 

자바 진영에서는 클래스를 정의 할 때,

간결하고 일기 쉬우면서도 인스턴스 구분에 도움이 되는 문자열을 구성하여 반환하도록 오버라이딩 하라고 조언하고 있다.

 

확실히 위의 내용보다 아래 내용이 인스턴스 구분에 훨씬 도움이 된다.

 

job : 010-1111-1111
name : ILL
phone : computer

 

클래스와 메소드의 final 선언

하지만 경우에 따라 상속을 원하지 않을 때 가 있다. 예를 들어 String 을 보자

 

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0
	
    ...

 

클래스의 final 이 붙어있다. 이는 상속을 막는 표시인데, 메소드에도 추가할 수 있다.

이렇게 final 이 붙은 이유는 다음과 같다.

 

개발 의도 때문에 수정이 불가능하게 할 메소드나 변수가 있을 때, 

 

String 이 final 이 붙여진 이유는 다음과 같다.

 

  • 스레드 안전
  • 보안
  • Java 자체에서 관리되는 힙 (다른 방식으로 가비지 수집되는 일반 힙과는 다름)
  • 메모리 관리

 

문자열이란 모든 곳에서 쓰이기 때문에, 위와 같은 이슈가 있다. String 같은 경우는 hashCode를 통해 캐시 등의 많은 성능 최적화를 할 수 있도록 구현되어 있는데, 만약 누군가 상속을 받아서 오버라이딩을 하게 된다면, 최초의 String 의 개발 전략이 쉽게 바뀔 수 있기 때문이다.

 

추상 클래스:Abstract Class

final과 반대로 상속을 유도하는 클래스가 있는데, 이것을 추상 클래스라고 부른다. 

 

이 클래스는 하나의 추상 메소드를 갖고 있는 추상클래스이다.

추상 클래스를 대상으로 인스턴스 생성이 불가능하며 다른 클래스에 의해서 추상 메소드가 구현 되어야 한다.

public abstract class House {
    public abstract void methodOne(); // 추상 메소드
}

 

public class MyHouse extends House{
    @Override
    public void methodOne() {
        System.out.println("methodOne in my house");
    }
}

 

추상 클래스 대상으로 인스턴스 생성 불가능 예시

 

정리하면, 여느 클래스들과 마찬가지로 인스턴스 변수와 인스턴스 메소드를 갖지만, 이를 상속하는 하위 클래스에 의해서 구현되어야 할 메소드가 하나 이상 있는 경우 이를 '추상 클래스' 라고 한다.

 

다이내믹 메서드 디스패치 (Dynamic Method Dispatch)

추상 클래스의 추상 메소드로 인해 구체화 되는 메소드, 상속으로 오버라이딩된 메소드, 나중에 배울 인터페이스로 인해 구현되는 메소드

등은 실제 코드만 봐서는 어떤 메소드가 실행될지 예측하기 어려울 수 있다.

아래의 코드만 해도 showInfo 메소드가 총 3개나 있었다.

 

        for (Friend univFriend : univFriends) {
            univFriend.showInfo();
        }

 

실제도 실행되는, for문이 도는 과정속에서 메소드가 호출 되는 순간에

해당 객체가 어떤 객체인지 사회 친구 인지 대학 친구인지 에 따라서 showInfo가 결정 되었다.

객체를 찾는 과정이 다이나믹 하다.

 

showInfo 로 메소드를 호출 하는 것을 디스 패치라고 하고

이렇게 실행 환경(run time) 에서 메소드가 결정되는 것을 다이나믹 메서드 디스패치라고 한다.

 

cf). 객체를 찾지 않아도 되는 디스패치는 정적 디스패치라고 한다.

 

더블 디스패치 (Double Dispatch)

디스패치가 두번 일어나면 더블 디스패치라고 한다.

 

오늘은 추상 클래스를 배웠으니 인터페이스 말고 추상 메서드로 예제를 구현해보자.

사회 친구와 대학 친구에게 페이스북과 트위터로 연락하는 프로그램을 만들어보자.

 

SNS와 친구 클래스를 다음과 같이 작성할 수 있다.

 

public abstract class Friend {
    protected String name;
    protected String phone;

    public Friend(String name, String phone) {
        this.name = name;
        this.phone = phone;
    }

    public abstract void writeLetter(SNS sns);
}

class CompFriend extends Friend { // 사회 친구
    protected String job;

    public CompFriend(String name, String phone, String job) {
        super(name, phone);
        this.job = job;
    }

    @Override
    public void writeLetter(SNS sns) {
        sns.contact(this);
    }
}

class UnivFriend extends Friend { // 대학 친구
    protected String major; //전공

    public UnivFriend(String name, String phone, String major) {
        super(name, phone);
        this.major = major;
    }

    @Override
    public void writeLetter(SNS sns) {
        sns.contact(this);
    }
}

 

 

public abstract class SNS {
    public abstract void contact(UnivFriend friend);

    public abstract void contact(CompFriend friend);
}

class Facebook extends SNS {
    public void contact(UnivFriend friend) {
        System.out.println("facebook 으로 " + friend.major + " 전공인 " + friend.name + "에게 연락하기");
    }

    public void contact(CompFriend friend) {
        System.out.println("facebook 으로 " + friend.job + " 직업인 " + friend.name + "에게 연락하기");
    }
}

class Twitter extends SNS {
    public void contact(UnivFriend friend) {
        System.out.println("Twitter 로 " + friend.major + " 전공인 " + friend.name + "에게 연락하기");
    }

    public void contact(CompFriend friend) {
        System.out.println("Twitter 로 " + friend.job + " 직업인 " + friend.name + "에게 연락하기");
    }
}

 

그리고 이 둘을 바탕으로 연락하는 프로그램을 만들어보자.

 

import java.util.Arrays;
import java.util.List;

public class MyFriends {
    public static void main(String[] args) {

        // 모든 친구들
        List<Friend> univFriends = getFiends();
        List<SNS> snsGroup = getSnsGroup();
        for (Friend friend : univFriends) {
            for (SNS sns : snsGroup) {
                friend.writeLetter(sns);
            }
        }

    }
    private static List<SNS> getSnsGroup() {
        return Arrays.asList(
                new Facebook(),
                new Twitter()
        );
    }

    private static List<Friend> getFiends() {
        return Arrays.asList(
                new CompFriend("ILL", "010-1111-1111", "computer"),
                new CompFriend("LEE", "010-2222-2222", "Electronics"),
                new UnivFriend("SAM", "010-3333-3333", "computer"),
                new UnivFriend("SA", "010-4444-4444", "Electronics")
        );
    }
    
}

 

먼저, friend 가 어떤 타입인지 찾아 writeLetter 를 디스패치 하고

writerLetter 속에sns 가 어떤 객체인지에 따라 각각 다른 contact 메소드를 디스패치한다.

이렇게 총 디스패치가 두번인 것을 더블 디스패치라고 한다.

 

 

실행 결과 화면

 

친구 클래스들에서는 어떤 SNS 에 따라 어떻게 구현할 것인지 분기해서 작성하지 않고, SNS 라는 조상 타입하나로 해결이 가능하다.

 

 

분기문 예시

@Override
public void writeLetter(SNS sns) {
        if (sns instanceof Facebook) {
            sns.contactFromFaceBook();
        } else if (sns instanceof Twitter) {
            sns.contactTwitterFaceBook();
        } else {
            throw new IllegalArgumentException();
        }
    }

 

 

더블 디스패치 예시

   @Override
    public void writeLetter(SNS sns) {
        sns.contact(this);
    }

 

따라서 추가적인 SNS 가 생긴다고 해도 다음과 같이만 추가해주면 된다.

 

class Instagram extends SNS {
    public void contact(UnivFriend friend) {
        System.out.println("Instagram 로 " + friend.major + " 전공인 " + friend.name + "에게 연락하기");
    }

    public void contact(CompFriend friend) {
        System.out.println("Instagram 로 " + friend.job + " 직업인 " + friend.name + "에게 연락하기");
    }
}

 

 

방문자 패턴 visitor pattern

다이나믹 메소드 디스패치는 방문자 패턴과 연관이 있다.

백기선님이 사용한 xml parser 를 예로 들어보겠다. 아래와 같은 xml 을 읽는 코드를 만든다고 하자.

 

<members>
    <member id="1">
        <username>게릴라</username>
        <license>라이센스 없죠? 스터디에 참여 안했으니까!!</license>
    </member>
    <member id="2">
        <username>whiteship</username>
        <license>123141-1231245345</license>
    </member>
</members>

 

읽어오는 방법은 크게 DomParser 와 SaxParser 로 나뉜다.

DomParser

public class DomParser {
    public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        final DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse(new ClassPathResource("members.xml").getInputStream());
        final NodeList members = document.getElementsByTagName("member");
        for (int i = 0; i < members.getLength(); i++) {
            Node node = members.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                Element element = (Element) node;
                String id = element.getAttribute("id");
                System.out.println(id);
                final NodeList childs = element.getChildNodes();
                for (int j = 0; j < childs.getLength(); j++) {
                    Node childNode = childs.item(j);
                    if (childNode.getNodeType() == Node.ELEMENT_NODE) {
                        Element childElement = (Element) childNode;
                        System.out.println(childElement.getNodeName() + "  " + childElement.getTextContent());
                    }
                }
            }
        }
    }
}

 

SaxParser

public class SaxParser {
    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        final SAXParser saxParser = factory.newSAXParser();
        saxParser.parse(new ClassPathResource("members.xml").getInputStream(), new MemberHandler());
    }


    // visitor 패턴
    static class MemberHandler extends DefaultHandler {

        private String text;

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            this.text = new String(ch, start, length);
        }

        @Override
        public void startDocument() throws SAXException {
            System.out.println("start parsing xml");
        }

        @Override
        public void endDocument() throws SAXException {
            System.out.println("end parsing xml");
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            System.out.println("element " + qName);
            if (qName.equals("member")) {
                System.out.println(attributes.getValue("id"));
            }
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if (qName.equals("username")) {
                System.out.println("username: " + text);
            }

            if (qName.equals("license")) {
                System.out.println("license: " + text);
            }
        }
    }
}

.

이 두 Parser 는 똑같이 xml 파일을 읽어오는 코드이다.

xml 태그를 찾는데 있어서 시간 복잡도를 따지면 Dom 은 O(n) Sax 는 O(1) 이다. 

Sax 는 이미 읽어드리는데, 필요한 부분을 DefaultHandler를 통해서 이미 parse 부분을 구현을 하였다.

우리는 이미 뼈대가 있는 DefaultHandler 에 부가적으로 기능을 추가하기 위해, 이미 돌아가고 있는 기능에 방문객 입장으로 부가적인 일을 하는 더 하는 것 처럼 DefaultHandler 상속을 통해 오버라이딩 하여 필요한 코드를 붙여 MemberHandler 를 구현할 수 있다.

 

728x90

'스터디 > [white-ship] 자바 스터디(study hale)' 카테고리의 다른 글

5주차 과제: 클래스  (0) 2021.01.01
5주차 과제: BinaryTree 이론  (0) 2021.01.01
7주차 과제: 패키지  (0) 2021.01.01
5주차 과제: BinaryTree 실습  (0) 2021.01.01
0주차 : kick off  (0) 2020.12.26

댓글