티스토리 뷰

728x90
반응형

서론


  • 상속의 목적은 코드를 재사용하기 위함이 아닙니다. 상속은 타입 계층을 구조화하기 위해서 사용해야하며 조금 더 쉽게 이해하자면 
    같은 범주로 묶기 위해서 사용해야 합니다.

 

다형성


  • 다형성이란 그리스어에서 "많은"을 의미하는 poly와 "형태"를 의미하는 morph의 합성어로 "많은 형태를 가질 수 있는 능력"을 의미합니다.
  • 컴퓨터 과학에서는 다형성을 하나의 추상 인터페이스에 의해 코드를 작성하고 이 추상 인터페이스를 활용하여 서로 다른 구현을 연결할 수 있는 능력으로 정의하고 있습니다.

 

💡 다형성의 분류

  • 다형성은 아래와 같은 사진처럼 분류할 수 있습니다.

매개변수 다형성이란?

  • 매개변수 다형성이란 제네릭 프로그래밍과 관련이 높습니다. 클래스의 인스턴스 변수나 메서드의 매개변수 타입을 임의의 타입으로 선언한 후 사용하는 시점에 구체적인 타입으로 지정하는 방식입니다. 예로는 List, Set, Map, Optional 등이 있습니다.
public final class Optional<T> {
  
    private final T value;
    // ... 생략
}

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

        Optional<String> text = Optional.of("text");
    }
}

 

포함 다형성이란?

  • 포함 다형성이란 객체지향 프로그래밍에서 널리 알려진 형태의 다형성입니다. 추상화를 통해 메시지가 동일하더라도 수신한 객체의 타입에 따라 실제로 수행되는 행동이 달라지는 능력을 의미합니다.
  • 포함 다형성을 서브타입 다형성이라고도 말합니다. 그 이유는 포함 다형성을 위한 전제조건은 자식 클래스가 부모 클래스의 서브 타입이어야 한다는 것입니다. 그리고 상속의 진정한 목적은 코드 재사용이 아닌 다형성을 위한 서브타입 계층을 구축하는 것입니다.
  • 아래 Movie 클래스는 생성자를 통해 어떤 클래스를 주입 받느냐에 따라 다양한 정책을 적용할 수 있습니다.
public abstract class DiscountPolicy {

}

public class PercentDiscountPolicy extends DiscountPolicy{

}

public class PeriodDiscountPolicy extends DiscountPolicy {

}

public class Movie {
    
    private String name;
    private DiscountPolicy discountPolicy; // 추상화에 의존

    public Movie(String name, DiscountPolicy discountPolicy) {
        this.name = name;
        this.discountPolicy = discountPolicy;
    }
}

 

오버로딩 다형성이란?

  • 오버로딩 다형성이란 하나의 클래스안에 동일한 이름의 메서드가 존재하는 경우를 말합니다. 대표적으로 List.of가 있습니다.

 

강제 다형성이란?

  • 언어가 지원하는 자동적인 타입 변환이나 사용자가 직접 구현한 타입 변환을 이용해 동일한 연산자를 다양한 타입에 사용할 수 있는 방식을 말합니다.
public class Main {

    public static void main(String[] args) {

        String text = "Hello World" + 123; // 연결 연산자
        int num = 10 + 123; // 덧셈 연산자
    }
}

 

상속의 양면성


  • 객체지향 패러다임의 근간은 데이터(상태)와 행동을 객체라고 불리는 하나의 실행 단위안으로 통합하는 것입니다. 따라서 객체지향
    프로그래밍을 작성하기 위해서는 항상 데이터와 행동이라는 두 가지 관점을 고려해야하는데 상속도 예외는 아닙니다.
  • 상속을 이용하면 부모 클래스에서 정의한 모든 데이터를 자식 클래스에 포함시킬 수 있는데 이를 데이터 관점에서의 상속입니다. 
    데이터 뿐만 아니라 부모 클래스의 메서드를 자식 클래스에게 포함시킬 수 있는데 이를 행동 관점의 상속입니다.

 

예제 코드

  • Human과 Dog 클래스는 Animal 클래스의 move 메서드를 재정의하고 있습니다. 또한 super 예약어를 사용하여 부모 메서드에 접근하고 있습니다.
@Getter
@AllArgsConstructor
public class Animal {

    private String name;
    private int age;

    public String move() {
        return "움직인다.";
    }
}

public class Human extends Animal {

    public Human(String name, int age) {
        super(name, age);
    }

    @Override
    public String move() {
        return "두 발을 사용하여 "+ super.move();
    }
}

public class Dog extends Animal {

    public Dog(String name, int age) {
        super(name, age);
    }

    @Override
    public String move() {
        return "네발을 사용하여 "+ super.move();
    }
}

 

💡 데이터 관점의 상속

  • 데이터 관점의 상속은 자식 클래스의 인스턴스 변수 내부에 부모 클래스의 인스턴스를 포함하는 것을 볼 수 있습니다. 따라서 자식 클래스의 인스턴스는 자동으로 부모 클래스에서 정의한 모든 인스턴스 변수를 내부에 포함하게 됩니다.
public class Main {

    public static void main(String[] args) {
        Animal animalA = new Animal("골든 리트리버", 3);
        Animal animalB = new Human("홍길동", 25);
    }
}

 

💡 행동 관점의 상속

  • 부모 클래스에서 정의한 일부 메서드를 자식 클래스의 메서드로 포함시키는 것을 의미합니다.
  • 부모 클래스의 퍼블릭 인터페이스가 자식 클래스로 합쳐진다라고 생각할 수 있지만 실제로 합쳐지는게 아닙니다. 자식 클래스의 인스턴스에서 부모 클래스의 메서드를 호출할 수 있는 이유는 런타임 시점에 자식 클래스에서 정의되지 않은 메서드가 있는 경우 해당 메서드를 부모 클래스 안에서 탐색하기 때문입니다.
  • 객체의 경우 서로 다른 힙 메모리에 영역을 할당받지만 메서드의 경우 동일한 클래스의 인스턴스끼리 공유가 가능하므로 클래스는 한번만 메모리에 로드하고 각 인스턴스별로 클래스를 가리키는 포인터를 갖게합니다.
  • 자식 클래스에서 부모 클래스로의 메서드 탐색이 가능하기 때문에 자식 클래스는 마치 부모 클래스에 구현된 메서드의 복사본을 가지고 있는것처럼 보이게 됩니다.

 

업캐스팅과 동적 바인딩


💡 같은 메시지, 다른 메서드

  • Creator 클래스의 생성자에서 어떤 Animal 타입 클래스를 주입 받느냐에 따라 howToMove 메서드가 반환하는 내용이 달라집니다.
public class Creator {

    private Animal animal;

    public Creator(Animal animal) {
        this.animal = animal;
    }

    public void howToMove() {
        System.out.println(animal.move());
    }
}

public class Main {

    public static void main(String[] args) {
        Animal animalA = new Dog("골든 리트리버", 3);
        Animal animalB = new Human("홍길동", 25);

        Creator creatorA = new Creator(animalA);
        Creator creatorB = new Creator(animalB);

        creatorA.howToMove(); // 네발을 사용하여 움직인다.
        creatorB.howToMove(); // 두 발을 사용하여 움직인다.
    }
}

 

업캐스팅

  • 부모 클래스 타입으로 선언된 변수에 자식 클래스의 인스턴스를 할당하는 것을 의미합니다.
  • 업캐스팅은 서로 다른 클래스의 인스턴스를 동일한 타입에 할당하는 것을 가능하게 해줍니다.
  • 상속을 이용하면 부모 클래스의 퍼블릭 인터페이스가 자식 클래스의 퍼블릭 인터페이스에 합쳐지기 때문에 부모 클래스의 인스턴스에게 전송할 수 있는 메시지를 자식 클래스의 인스턴스에게 전송할 수 있습니다.
  • 부모 클래스의 인스턴스 대신 자식 클래스의 인스턴스를 사용하더라도 메시지를 처리하는데는 아무런 문제가 없으며, 컴파일러는 명시적인 타입 변환 없이도 자식 클래스가 부모 클래스를 대체할 수 있게 해줍니다.

 

동적 바인딩

  • 선언된 변수의 타입이 아니라 메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 결정됩니다.
  • 동적 메서드 탐색은 부모 클래스의 타입에 대해 메시지를 전송하더라도 런타임 시점에는 실제 클래스를 기반으로 실행될 메서드를 선택할 수 있도록 해줍니다.
  • 컴파일타임에 호출될 메서드를 결정하는 방식을 정적 바인딩, 초기 바인딩 또는 컴파일타임 바인딩이라 하고 런타임 시점에 메서드를 결정하는 방식을 동적 바인딩 또는 지연 바인딩이라 합니다.

 

동적 메서드 탐색과 다형성


💡 실행될 메서드를 선택하는 규칙

  1. 메시지를 수신한 객체는 먼저 자신을 생성한 클래스에 적합한 메서드가 존재하는지 검사합니다. 존재한다면 실행하고 탐색을 종료합니다.
  2. 메서드를 찾지 못했다면 부모 클래스에서 메서드 탐색을 이어갑니다. 이 과정은 적합한 메서드를 찾을 때까지 상속 계층을 올라갑니다.
  3. 상속 계층의 최상위 클래스에 도착했지만 메서드를 발견하지 못했다면 예외를 발생시키며 탐색을 종료합니다.

 

self(this) 참조란?

  • 메서드 탐색에서 중요한 변수가 있는데 이를 self 또는 this라 합니다.
  • 객체가 메시지를 수신하면 컴파일러는 self(this) 참조라는 임시 변수를 자동으로 생성한 후 메시지를 수신한 객체를 가리키도록 설정합니다. 동적 메서드 탐색은 self가 가리키는 객체의 클래스에서 시작해서 상속 계층의 역방향으로 탐색이 이루어지며 메서드 탐색이 종료되는 순간 self 참조는 자동으로 소멸됩니다.
  • 시스템은 메시지를 처리할 메서드를 탐색하기 위해 self 참조가 가리키는 메모리로 이동합니다. 이 메모리에는 객체의 현재 상태를 표현하는 데이터와 객체의 클래스를 가리키는 class 포인터가 존재합니다.
  • 메서드 탐색은 자식 클래스에서 부모 클래스의 방향으로 진행됩니다. 그렇기 때문에 항상 자식 클래스의 메서드가 부모 클래스의 메서드보다 먼저 탐색되기 때문에 자식 클래스에 선언된 메서드가 부모 클래스의 메서드보다 더 높은 우선순위를 가지게 됩니다.
public class Main {

    public static void main(String[] args) {
        Animal animalA = new Dog("진돗개", 3);
        animalA.move();
    }
}

 

💡 자동적인 메시지 위임

  • 자식 클래스는 자신이 이해할 수 없는 메시지를 전송받은 경우 상속 계층을 따라 부모 클래스에게 처리를 위임합니다. 이과정은 프로그래머의 개입없이 자동으로 이루어집니다. 핵심은 적절한 메서드를 찾을 때까지 상속 계층을 따라 부모 클래스로 처리가 위임됩니다.
  • 동일한 시그니처를 가지는 자식 클래스의 메서드는 부모 클래스의 메서드를 감추지만 이름만 같고 시그니처가 다른 경우 해당 메서드들은 상속 계층에 걸쳐 공존하게 됩니다. 이를 메서드 오버로딩이라 힙니다.

오버라이딩

  • 자식 클래스가 부모 클래스의 메서드를 오버라이딩하면 자식 클래스에서 부모 클래스로 향하는 메서드 탐색 순서 때문에 자식 클래스의 메서드가 부모 클래스의 메서드를 감추게됩니다.

오버로딩

  • 메서드 오버로딩은 상속 계층에 걸쳐 서로 공존하게 됩니다.
@Getter
@AllArgsConstructor
public class Animal {

    private String name;
    private int age;

    public String move() {
        return "움직인다.";
    }
}

public class Dog extends Animal {

    public Dog(String name, int age) {
        super(name, age);
    }

    // 오버로딩
    public String move(String how) {
        return how;
    }
}

public class Main {

    public static void main(String[] args) {
        Dog animalA = new Dog("진돗개", 3);
        animalA.move("열심히 달립니다."); // Dog에 선언된 메서드 실행
        animalA.move();               // Animal에 선언된 메서드 실행
    }
}

 

💡 동적인 문맥

  • status 메서드는 현제 객체가 어떤 객체인지에 따라 호출되는 맥락이 달라집니다. 여기서 현재 객체란 self 참조가 가리키는 객체입니다. 이 객체는 처음에 status 메시지를 수신했던 객체입니다.
  • 이처럼 self 참조가 가리키는 자기 자신에게 메시지를 전송하는 것을 self 전송이라 부릅니다. self 전송을 이해하기 위해서는 self 
    참조가 가리키는 그 객체에서부터 메시지 탐색을 다시 시작한다는 사실을 알아야합니다.
  • self 전송은 자식 클래스에서 부모 클래스 방향으로 진행되는 동적 메서드 탐색 경로를 다시 self 참조가 가리키는 원래의 자식 클래스로 이동시킵니다. 이로인해 최악의 경우 실제로 실행될 메서드를 이해하기 위해서는 상속 계충 전체를 훓어가면서 코드를 이해해햐 하는 상황이 발생할 수도 있습니다.
@Getter
@AllArgsConstructor
public class Animal {

    private String name;
    private int age;

    public String move() {
        status();
        return "움직인다.";
    }

    public void status() {

    }
}

public class Dog extends Animal {

    public Dog(String name, int age) {
        super(name, age);
    }

    @Override
    public void status() {
        System.out.println("힘차게");
    }
}

public class Main {

    public static void main(String[] args) {
        Animal animalA = new Dog("진돗개", 3);
        System.out.println(animalA.move());
    }
}

 

💡 super

  • super 참조의 용도는 부모 클래스에 정의된 메서드를 실행하기 위한 것이 아닙니다. super 참조의 정확한 의도는 "지금 이 클래스의 부모 클래스에서부터 메서드 탐색을 시작하세요" 입니다. 만약 부모 클래스에서 원하는 메서드를 찾지 못했다면 더 상위의 부모 클래스로 이동하면서 메서드가 존재하는지 검사합니다.
  • 바로 부모 클래스의 메서드를 호출하는 것과 부모 클래스에서부터 메서드를 탐색하는 것은 의미가 다릅니다. 부모 클래스의 메서드를 호출한다는 것은 해당 메서드가 반드시 부모 클래스에 정의되어 있어야한다는 것을 의미합니다. 반면 부모 클래스에서부터 메서드를 탐색한다는것은 그 클래스의 조상 어딘가에 메서드가 정의되어 있기만 하면 됩니다.
  • super 참조를 통해 메시지를 전송하는 것은 마치 부모 클래스의 인스턴스에게 메시지를 전송하는 것처럼 보이기 때문에 이를 super 전송이라 부릅니다.

 

상속 대 위임


  • 다형성은 self 참조가 가리키는 현재 객체에게 메시지를 전달하는 특성을 기반으로 합니다. 동일한 타입의 객체 참조에게 동일한 메시지를 전송하더라도 self 참조가 가리키는 객체의 클래스가 무엇이냐에 따라 메서드 탐색을 위한 문맥이 달라집니다.

💡 위임과 self 참조

  • animalA의 입장에서 self 참조는 당연히 Dog 인스턴스 자신입니다. 그렇다면 Dog 인스턴스에 포함된 Animal 인스턴스의 입장에서 self 참조는 무엇을 가리킬까요? 의외로 Dog 인스턴스를 가리키고 있습니다. 그 이유는 self 참조는 항상 메시지를 수신한 객체를 가리키기 때문입니다.
public class Main {

    public static void main(String[] args) {
        Animal animalA = new Dog("진돗개", 3);
    }
}

위임

  • 자신이 수신한 메시지를 다른 객체에게 동일하게 전달해서 처리를 요청하는 것을 위임이라 합니다.
  • 위임은 본질적으로는 자신이 정의하지 않거나 처리할 수 없는 속성 또는 메서드의 탐색 과정을 다른 객체로 이동시키기 위해 사용합니다. 이를 위해 위임은 항상 현재의 실행 문맥을 가리키는 self 참조를 인자로 전달합니다.

 

예제 코드

  • Animal 클래스의 status 메서드는 어떤 인스턴스를 주입 받느냐에 따라 self 참조를 다르게 가져갈 수 있습니다.
@Getter
@AllArgsConstructor
public class Animal {

    public void status(Animal animal) {
        animal.methodA();
    }

    public void methodA() {
        System.out.println("methodA of Animal");
    }
}

public class Dog extends Animal {

    @Override
    public void status(Animal animal) {
        super.status(animal); // 위임
    }

    @Override
    public void methodA() {
        System.out.println("methodA of Dog");
    }
}

public class Main {

    public static void main(String[] args) {
        Animal animalA = new Dog();
        animalA.status(animalA); // methodA of Dog
    }
}

 

 

 

 

 

 

 

 

728x90
반응형