본문 바로가기
Study/오브젝트

[오브젝트] 12장. 다형성

by Nahwasa 2023. 1. 14.

스터디 메인 페이지

목차

    - ☆ 표시가 붙은 부분은 스터디 중 나온 얘기 혹은 제 개인적인 생각이나 제가 이해한 방식을 적어놓은 것으로, 책에 나오지 않는 내용입니다. 따라서 책에서 말하고자 하는 바와 다를 수 있습니다.

    - 모든 이미지의 출처는 오브젝트(조용호 저) 책 입니다.

     


     

     

    CHAPTER 12. 다형성

     

    ⚈ 코드 재사용을 목적으로 상속을 사용하면 변경하기 어렵고 유연하지 못한 설계에 이를 확률이 높아진다. 상속의 목적은 코드 재사용이 아니다. 상속은 타입 계층을 구조화하기 위해 사용해야 한다.

     

    12장 : 상속의 일차적인 목적이 코드 재사용이 아니라 서브타입의 구현이라는 사실을 이해하기 위한 챕터

     


    01 다형성

    ⚈ 다형성(Polymorphism) : 하나의 추상 인터페이스에 대해 코드를 작성하고 이 추상 인터페이스에 대해 서로 다른 구현을 연결할 수 있는 능력. 즉, 여러 타입을 대상으로 동작할 수 있는 코드를 작성할 수 있는 방법

     

    ⚈ 다형성의 분류

    • 매개변수 다형성 : 클래스의 인스턴스 변수나 메서드의 배개변수 타입을 임의의 타입으로 선언한 후 사용하는 시점에 구체적인 타입으로 지정하는 방식. 예를들어 자바에서 제너릭 타입으로 List에서 임의의 타입 T로 보관할 요소를 지정하고 있다. 실제 인스턴스를 생성하는 시점에 T를 구체적인 타입으로 지정할 수 있다.
    • 포함 다형성 : 특별한 언급 없이 다형성이라고 할 때는 포함 다형성을 의미한다. 메시지가 동일하더라도 수신할 객체의 타입에 따라 실제로 수행되는 행동이 달라지는 능력 (=서브타입(subtype) 다형성)
    • 오버로딩 다형성 : 클래스 안에 동일한 이름의 메서드가 존재하는 경우 (메서드 오버로딩)
    • 강제 다형성 : 언어가 지원하는 자동적인 타입 변환이나 사용자가 직접 구현한 타입 변환을 이용해 동일한 연산자를 다양한 타입에 상용할 수 있는 방식. 예를들어 "ABC" + 3 = "ABC3"

     


    02 상속의 양면성

    데이터 관점의 상속 : 상속을 이용하면 부모 클래스에서 정의한 모든 데이터를 자식 클래스의 인스턴스에 자동으로 포함시킬 수 있다.

     

    행동 관점의 상속 : 부모 클래스에서 정의한 일부 메서드 역시 자동으로 자식 클래스에 포함시킬 수 있다.

     

    타입 계층에 대한 고민 없이 코드를 재사용하기 위해 상속을 사용하면 이해하기 어렵고 유지보수하기 버거운 코드가 만들어질 확률이 높다..

     

    상속을 이용한 강의 평가

    • 코드는 아래와 같다.
    public class Lecture {
        private int pass;
        private String title;
        private List<Integer> scores = new ArrayList<>();
    
        public Lecture(String title, int pass, List<Integer> scores) {
            this.title = title;
            this.pass = pass;
            this.scores = scores;
        }
    
        public double average() {
            return scores.stream().mapToInt(Integer::intValue).average().orElse(0);
        }
    
        public List<Integer> getScores() {
            return Collections.unmodifiableList(scores);
        }
    
        public String evaluate() {
            return String.format("Pass:%d Fail:%d", passCount(), failCount());
        }
    
        private long passCount() {
            return scores.stream().filter(score -> score >= pass).count();
        }
    
        private long failCount() {
            return scores.size() - passCount();
        }
    }
    
    ----
    
    public class Grade {
        private String name;
        private int upper,lower;
    
        private Grade(String name, int upper, int lower) {
            this.name = name;
            this.upper = upper;
            this.lower = lower;
        }
    
        public String getName() {
            return name;
        }
    
        public boolean isName(String name) {
            return this.name.equals(name);
        }
    
        public boolean include(int score) {
            return score >= lower && score <= upper;
        }
    }
    
    ----
    
    public class GradeLecture extends Lecture {
        private List<Grade> grades;
    
        public GradeLecture(String name, int pass, List<Grade> grades, List<Integer> scores) {
            super(name, pass, scores);
            this.grades = grades;
        }
    
        @Override
        public String evaluate() {
            return super.evaluate() + ", " + gradesStatistics();
        }
    
        private String gradesStatistics() {
            return grades.stream().map(grade -> format(grade)).collect(joining(" "));
        }
    
        private String format(Grade grade) {
            return String.format("%s:%d", grade.getName(), gradeCount(grade));
        }
    
        private long gradeCount(Grade grade) {
            return getScores().stream().filter(grade::include).count();
        }
    
        public double average(String gradeName) {
            return grades.stream()
                    .filter(each -> each.isName(gradeName))
                    .findFirst()
                    .map(this::gradeAverage)
                    .orElse(0d);
        }
    
        private double gradeAverage(Grade grade) {
            return getScores().stream()
                    .filter(grade::include)
                    .mapToInt(Integer::intValue)
                    .average()
                    .orElse(0);
        }
    }

     

    • 데이터 관점의 상속 : 자식 클래스의 인스턴스 안에 부모 클래스의 인스턴스를 포함하는 것으로 볼 수 있다. 따라서 자식 클래스의 인스턴스는 자동으로 부모 클래스에서 정의한 모든 인스턴스 변수를 내부에 포함하게 된다.

     

    • 행동 관점의 상속 : 부모 클래스의 모든 퍼블릭 메서드는 자식 클래스의 퍼블릭 인터페이스에 포함된다. 따라서 외부의 의 객체가 부모 클래스의 인스턴스에게 전송할 수 있는 모든 메시지는 자식 클래스의 인스턴스에게도 전송할 수 있다. 이 때 런타임에 시스템이 자식 클래스에 정의되지 않은 메서드가 있을 경우 이 메서드를 부모 클래스 안에서 탐색한다. 자바에서는 모든 클래스의 부모 클래스인 Object까지 상속 계층을 탐색한다.

     


    03 업캐스팅과 동적 바인딩

    코드 안에서 선언된 참조 타입과 무관하게 실제로 메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 달라질 수 있는 것은 업캐스팅과 동적 바인딩이라는 메커니즘이 작용하기 때문이다.

    • 업캐스팅 : 부모 클래스 타입으로 선언된 변수에 자식 클래스의 인스턴스를 할당하는 것이 가능
    • 동적 바인딩 : 선언된 변수의 타입이 아니라 메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 결정

     

    업캐스팅, 다운캐스팅

     

    동적 바인딩

    • 전통적인 언어들은 호출될 함수를 컴파일타임에 결정한다. -> 정적 바인딩(static binding), 초기 바인딩(early biding), 또는 컴파일타임 바인딩(compile-time binding)
    • 실행될 메서드를 런타임에 결정하는 방식 -> 동적 바인딩(dynamic binding) 또는 지연 바인딩(late binding)

     


    04 동적 메서드 탐색과 다형성

    객체지향 시스템은 다음 규칙에 따라 실행할 메서드를 선택한다.

    • 메시지를 수신한 객체는 먼저 자신을 생성한 클래스에 적합한 메서드가 존재하는지 검사한다. 존재하면 메서드를 실행하고 탐색을 종료한다.
    • 메서드를 찾지 못했다면 부모 클래스에서 메서드 탐색을 계속한다. 이 과정은 적합한 메서드를 찾을 때까지 상속 계층을 따라 올라가며 계속된다.
    • 상속 계층의 가장 최상위 클래스에 이르렀지만 메서드를 발견하지 못한 경우 예외를 발생시키며 탐색을 중단한다.

     

     

    self 참조(self reference) : 객체가 메시지를 수신하면 컴파일러는 self 참조라는 임시 변수를 자동으로 생성한 후 메시지를 수신한 객체를 가리키도록 설정한다. (자바에서는 self 참조를 this라고 부른다.)

     

    동적 메서드 탐색의 입장에서 상속 계층은 메시지를 수신한 객체가 자신이 이해할 수 없는 메시지를 부모 클래스에게 전달하기 위한 물리적인 경로를 정의한 것으로 볼 수 있다. 적절한 메서드를 찾을 때까지 상속 계층을 따라 부모 클래스로 처리가 위임된다.

     

    ☆ 정적 타입 언어 : 컴파일 시에 변수의 타입이 결정되는 언어 - 자바, C, C++, C# 등

     

    ☆ 동적 타입 언어 : 런타임 시 자료형이 결정 - Python, js, Ruby 등

     

    super 참조(super reference) : '지금 이 클래스의 부모 클래스에서부터 메서드 탐색을 시작하세요' 

     

     

     

    댓글