[JAVA 객체지향프로그래밍] 상속

    반응형

     

    이 글은 패스트 캠퍼스 한번에 끝내는 Java/Spring 웹 개발 마스터 초격차 패키지 강의를 듣고 공부한 내용을 바탕으로 정리한 글입니다.


     

    객체지향 프로그래밍의 핵심인 상속 개념에 대해서 살펴보도록 하겠습니다.

    1. 클래스 상속

    클래스 상속이란, 새로운 클래스를 정의 할 때, 이미 구혀노딘 클래스를 상속(inheritance)방아서 속성이나 기능을 확장하여 클래스를 구현하는 것을 말합니다. 이때, 새로 구현되는 상속받는 클래스는 이미 구현된 클래스보다 더 구체적인 기능을 가진 클래스이어야 합니다.

    이때에 상속하는 클래스를 상위 클래스, parent 클래스, base 클래스, super 클래스라고 부르며, 상속받는 클래스를 하위 클래스, child class, derived class, subclass라고 부릅니다.

    다이어그램으로 표현할 때에는 이와 같이 표현합니다. (하위 클래스가 상위 클래스를 바라봄)

    상위 클래스는 하위 클래스보다 더 일반적인 개념과 기능을 가지고, 하위 클래스는 상위 클래스보다 더 구체적인 개념과 기능을 가집니다. 따라서 상속은 하위 클래스가 상위 클래스의 속성과 기능을 확장(extends)하는 개념입니다.

    2. 상속 구현

    상속의 기본 문법은 다음과 같습니다.

    class child_class extends parent_class {}

    이때, extends 키워드 뒤에는 단 하나의 클래스만 올 수 있습니다. 왜냐하면 자바는 다중 상속을 지원하지 않고, 단일 상속(single inheritance)만을 지원하기 때문입니다.

    *자바가 단일 상속만을 지원하는 이유
    기존의 C/C++보다 안전하고 간단한 언어를 만드는 것이 자바가 초기부터 추구한 것이었습니다.(현재는 기능이 많이 추가되어 복잡해지긴 했습니다.) 따가서 자바는 모호성을 애초에 방지하고자 다중 상속을 지원하지 않습니다.

    예시로 상속을 구현해보도록 하겠습니다. 쇼핑몰에서 고객 정보를 일반고객과 우수고객으로 분류하여 적용되는 할인율과 보너스 포인트의 비율을 다르게 설정하는 예시를 구현해보도록 하겠습니다. (getter와 setter는 코드에서 생략하였습니다.)

    public class Customer {
    
        protected int customerId;
        protected String customerName;
        protected String customerGrade;
        protected int bonusPoint;
        protected double bonusRatio;
    
        public Customer() {
            this.customerGrade = "SILVER";
            this.bonusRatio = 0.01;
        }
    
        public int calcPrice(int price) {
            this.bonusPoint += price * bonusRatio;
            return price;
        }
    }

    5개의 멤버변수와 default constructor, calcPrice라는 메서드를 정의해주었습니다. 이때, 상속받을 하위 클래스가 Customer의 멤버 변수에 접근할 수 있도록 모든 멤버 변수를 protected로 선언하였습니다. (protected 접근 제어자는 외부 클래스에서는 접근할 수 없도록하지만 해당 클래스를 상속받는 하위 클래스는 접근할 수 있도록 합니다.) 그럼 이제, Customer 클래스를 상속받는 VIPCustomer 클래스를 만들어보겠습니다.

    public class VIPCustomer extends Customer {
        double salesRatio;
        String agentId;
    
        public VIPCustomer() {
            this.customerGrade = "VIP";
            this.salesRatio = 0.1;
        }
    }

    VIPCustomer에서 더 추가해주어야 하는 멤버 변수를 추가해주고, default constructor에서 초기화를 다르게 설정해주었습니다. 앞서 Customer의 멤버 변수를 protected로 선언해주었기 때문에 Customer를 상속받는 하위 클래스의 생성자가 상위 클래스의 멤버 변수에 접근 가능합니다.

    만약 상속을 구현하지 않고 Customer에서 조건문으로 일반 고객과 VIP 고객을 구분한다면 코드가 어떻게 될까요?

    package ch02;
    
    public class Customer {
    
        private int customerId;
        private String customerName;
        private String customerGrade;
        private int bonusPoint;
        private double bonusRatio;
    
        private agentId;
        private salesRatio;
    
        public Customer() {
            this.customerGrade = "SILVER";
            this.bonusRatio = 0.01;
        }
    
    
        public int calcPrice(int price) {
            if (customerGrade == "SILVER")
                ...
            else if (customerGrade == "VIP")
                ...
            return price;
        }
    }

    위와 같이 if 조건문으로 분기하여 수행할 명령어를 다르게 하기 때문에 코드가 복잡해지고, 일반 고객이 가지지 않아도 될 속성(멤버 변수)까지 모두가 가져야 합니다. 따라서 하나의 객체를 나타내는 클래스의 단일성이 망가지게 되지요. 따라서 객체지향 프로그래밍에서는 상속을 이용합니다.

    상위 클래스(Customer)와 하위 클래스(VIPCustomer)를 각각 이용해서 인스턴스를 하나씩 만들어보겠습니다.

    public class CustomerTest {
        public static void main(String[] args) {
            Customer customerLee = new Customer();
            customerLee.setCustomerName("Lee");
            customerLee.setCustomerId(1001);
            customerLee.setBonusPoint(1000);
            System.out.println(customerLee.toString());
    
            VIPCustomer customerKim = new VIPCustomer();
            customerKim.setCustomerName("Kim");
            customerKim.setCustomerId(1002);
            customerKim.setBonusPoint(50000);
            System.out.println(customerKim.toString());
        }
    }

    하위 클래스의 인스턴스는 상위 클래스를 상속받아 상위 클래스의 멤버 변수에 추가로 정의한 멤버 변수를 가지고 있음을 확인할 수 있습니다.

    3. 하위 클래스 생성 과정

    하위 클래스를 생성하면 상위 클래스가 먼저 생성됩니다. 즉, new 키워드로 하위 클래스의 생성자를 호출하면 하위 클래스의 생성자에서 반드시 상위 클래스의 생성자를 먼저 호출합니다.

    위와 같은 코드에서 로그만 찍어보면 다음과 같이 출력됩니다. %Customer%의 기본 생성자가 VIPCustomer 인스턴스를 만들 때에 호출되고 그리고 나서 VIPCustomer의 생성자가 호출됩니다.

    그럼 어떻게 하위 클래스의 생성자에서 자동으로 상위 클래스의 생성자가 호출되는 것일까요? 그것은 컴파일러가 자동으로 하위 클래스안에 super(); 코드를 넣어주기 때문입니다.

    클래스 안에 생성자가 전혀 구현되어있지 않을때에 default constructor를 자동으로 넣어주었던 것처럼 상위 클래스의 생성자를 부르는 코드가 없으면 자동으로 super();를 넣어줍니다.

    4. super 키워드

    • super 키워드는 하위 클래스에서 가지는 상위 클래스에 대한 참조 변수입니다. 즉, 부모의 객체를 가리킵니다. 그리고 super() 메서드는 상위 클래스의 기본 생성자를 호출합니다.
    • 자식 클래스에서 명시적으로 부모 클래스의 생성자를 호출하지 않으면 컴파일러가 super();를 자동으로 삽입하고 super() 메서드는 상위 클래스의 기본 생성자가 호출합니다. 이때 당연히 부모 클래스의 기본 생성자가 존재해야겠죠?
    • 부모 클래스의 기본 생성자가 없는 경우(다른 생성자가 있는 경우)에는 자식 클래스의 생성자에서는 super를 이용하여 명시적으로 부모 클래스의 생성자를 호출합니다.
    • super는 생성된 부모 클래스 인스턴스의 참조값을 가지므로 super를 이용하여 상위 클래스의 메서드나 멤버 변수에 접근할 수 있습니다.

    위의 코드 예시에서 부모 클래스의 디폴트 생성자를 없대고 매개 변수가 있는 생성자를 추가해보도록 하겠습니다.

    public class Customer {
        protected int customerId;
        protected String customerName;
        protected String customerGrade;
        protected int bonusPoint;
        protected double bonusRatio;
    
        public Customer(int customerId, String customerName) {
            this.customerId = customerId;
            this.customerName = customerName;
            this.customerGrade = "SILVER";
            this.bonusRatio = 0.01;
            System.out.println("Customer(int, String) called");
        }
    }

    이렇게 부모 클래스의 기본 생성자를 없애면 자식 클래스에서도 super를 이용해서 상위 클래스의 생성자를 명시적으로 호출해주어야 합니다.

    public class VIPCustomer extends Customer {
        double salesRatio;
        String agentId;
    
        public VIPCustomer() {
            super(0, "no-name");
            this.customerGrade = "VIP";
            this.salesRatio = 0.1;
            this.bonusRatio = 0.05;
            System.out.println("VIPCustomer() called");
        }
    
        public VIPCustomer(int customerId, String customerName) {
            super(customerId, customerName);
            this.customerGrade = "VIP";
            this.salesRatio = 0.1;
            this.bonusRatio = 0.05;
            System.out.println("VIPCustomer(int, String) called");
        }
    }

    자식 클래스의 생성자는 기본 생성자와 매개변수를 2개 가지고 있는 생성자 2개를 만들었습니다. 각 생성자 안에서는 customerIdcustomerName을 매개변수로 갖는 부모 클래스의 생성자를 super키워드로 명시적으로 호출해주고 있습니다.

    메인 함수로 어떤 생성자가 호출되었는지 확인하면 다음과 같습니다.

    public class CustomerTest {
        public static void main(String[] args) {
            Customer customerLee = new Customer(1001, "Lee");
            customerLee.setBonusPoint(1000);
            System.out.println(customerLee.toString());
    
            VIPCustomer customerNoName = new VIPCustomer();
            System.out.println(customerNoName.toString());
    
            VIPCustomer customerKim = new VIPCustomer(1002, "Kim");
            customerKim.setBonusPoint(50000);
            System.out.println(customerKim.toString());
        }
    }

    5. 상속에서 인스턴스 메모리

    항상 상위 클래스의 인스턴스가 먼저 생성되고, 하위 클래스의 인스턴스가 생성됩니다.

    Customer() 생성자 호출 → Customer 클래스의 멤버 변수가 메모리에 생성됨

    VIPCustomer() 생성자 호출 → VIPCustomer 클래스의 멤버 변수가 메모리에 생성됨

    6. 업캐스팅과 다운캐스팅

    부모 클래스로 변수를 선언하고 자식 클래스의 생성자로 인스턴스를 생성하면 상위 클래스 타입의 변수에 하위 클래스 타입의 변수가 대입되는 것을 업캐스팅이라고 합니다.

    이렇게 상위 클래스로의 묵시적 형 변환이 가능한 이유는 자식 클래스가 상위 클래스의 타입을 내포하고 있기 때문입니다. 사람이 상위 클래스이고, 학생이 하위 클래스일때, 학생은 모두 다 사람이기 때문의 사람의 특징과 기능을 모두 갖고 있기 때문에 학생에서 사람으로의 업캐스팅이 가능한 것이지요. 하지만, 모든 사람이 학생은 아니기 때문에 모든 상위 클래스가 하위 클래스로 다운캐스팅되지는 않습니다.

    즉, 상속 관계에서 모든 하위 클래스는 상위 클래스로 형변환(업캐스팅)되지만, 그 역(다운캐스팅)은 성립하지 않습니다.

    Customer customerLee = new VIPCustomer();

    따라서 위의 코드처럼 선언했을 때, customerLee 인스턴스는 VIPCustomer 타입처럼 메모리를 할당받습니다.

    하지만, 하위 클래스 생성자(VIPCustomer())에 의해 하위 클래스의 모든 멤버 변수에 대한 메모리는 생성되었지만, 변수 타입이 상위 클래스(Customer)이므로 상위 클래스(Customer)의 변수와 메서드에만 접근 가능합니다.

    업캐스팅되었지만, 부모 클래스의 변수의 메서드에만 접근 가능

    반응형

    댓글