자바에서 클래스 형변환을 하면 상위 클래스의 메소드만 사용 가능한 이유가 뭔가요?

조회수 5086회

형변환을 하면 상위 클래스(부모 클래스)의 메소드만 사용 가능하다는 부분이 가장 이해가 가지 않습니다.

일단 형변환을 하는 궁극적인 목적이 메서드의 매개변수를 모두 통일할 수있는 장점을 가지고 있기에 그렇다라고 이해할 수 잇지만.

"부모 클래스 = 자식 클래스"의 상황에서 왜 "부모 클래스"의 메서드들만 사용이 가능한지 전혀 이해가 가지않습니다.


출처: http://tryhelloworld.co.kr/courses/자바-입문/lessons/클래스-형변환 의 질문에 답변하려다가 코드를 바로 실행해 보면 좋을것 같아서 여기에 질문을 옮겼습니다.

3 답변

  • 질문하신 것은 비단 자바에 한정되는 사항은 아닙니다.

    객체지향설계(객체지향프로그래밍)에 대한 이해가 필요로 합니다.

    개념적으로는 추상화와 구체화에 관련한 내용이고, 컴파일러 관점에서 보면 타입 추론과 관련있습니다.

    정보 처리 관점에서 객체지향설계에서 추상화를 한 이유는 동일한 정보를 다루기 위합니다. 동일한 정보를 공통적인 방법으로 다루기 위한 것이 목적입니다. 반면에 분류(하위 클래스 생성)를 하는 이유는 동일하지 않은 정보를 다루기 위함입니다. 즉 각각이 하위 클래스에서만 처리할 수 있는 방법들을 지원하는 것입니다.

    먼저 개념적으로 이해할 때, 추상화에 관한 예를 들어 보겠습니다.

    가솔린 자동차와 디젤 자동차, 전기 자동차는 자동차입니다.

    이 문장을 설계를 하게 되면, 자동차라는 상위 클래스를 만들고, 하위 클래스로 가솔린 자동차, 디젤 자동차, 전기 자동차와 같이 만들 수 있습니다.

    // 예시
    public class Car {
        public void run();
    }
    
    class GasolinCar extends Car {
        public void BurnGasolin();
    }
    
    class DiselCar extends Car {
        public void BurnDisel();
    }
    
    class ElectorincCar extends Car {
        public void Charge();
    }
    

    그럼 위의 예에서 개념적으로 다음과 같은 문장들은 참이 됩니다.

    1. 가솔린 자동차는 자동차이다. Car car = new GasolinCar()
    2. 디젤 자동차는 자동차이다. Car car = new DiselCar()
    3. 전기 자동차는 자동차이다. Car car = new ElectorincCar()

    그러나 위 1,2,3에 대한 역은 성립할까요?

    • 자동차는 가솔린 자동차이다 (성립안함)
    • 자동차는 디젤 자동차이다 (성립안함)
    • 자동차는 전기 자동차이다 (성립안함)

    성립하지 않습니다.

    이와 같은 이론으로 설명하면, Car car = new GasolinCar() 라고 할 때, 이 문장이 참인것을 이해할 수 있습니다. 그러나 car 는 자동차일 뿐, 이 것이 가솔린 차나, 디젤, 전기 자동차를 구분할 수 있는 것이 아닙니다.

    여기까지는 이해하셨는데, 아마도 아직 그래도 new GasolinCar() 를 했는 것을 알지 않느냐? 라는 의문이 남아 있을 겁니다.

    이 문제는 2번째 문제인, 컴파일러 관점에서 타입 추론과 관련있습니다.

    다음의 상황을 생각해보고, 타입을 추론할 수 있을지 고민해보면 됩니다.

    // case 1
    public void case1(Car car) {
        car.run();
    }
    // case 2
    public void case2(Car car) {
        car.BurnGasolin();
    }
    // case 3
    public void case3(Car car) {
        car.BurnDisel();
    }
    // case 4
    public void case4(Car car) {
        car.Charge();
    }
    
    • 위의 4가지 경우에서 항상 참이 될 수 있는 것은 어떤 경우일까요? 우리가 아는 정보로는 case 1만 확실하게 참이라고 말할 수 있습니다. case 2,3,4가 될 수 없는 또 다른 이유는 Car를 상속한 클래스 어떤 것이 있는 지, 언제 알 수 있을 지를 생각해보면 됩니다.

    우리는 언제든지 자동차(Car)를 상속한 수소 자동차도 만들 수 있고, 수륙양륙 자동차 등등등을 만들 수 있습니다.

    즉, 컴파일 하는 시점에서는 어떤 클래스가 추가로 발생할지(가까운 혹은 먼 미래의 일)까지 예측할 수 없습니다.

    그래서 우리는 컴파일러에게 명시적으로 강제하는 Casting을 사용합니다. 컴파일러는 모르겠지만, 개발하는 사람은 알고있다는 것을 컴파일러에게 알려줘서 컴파일할 때, 에러를 발생하지 않게 하는 겁니다.

    Car car = ...
    GasolinCar gcar = (GasolinCar)gcar;
    

    위의 예에서 우리는 car가 무엇으로 생성되는지 알 수 없지만, 마지막에 GasolinCar로 변경을 시도하는 것을 알 수 있습니다. 컴파일러는 딱 이정도만 이해하고 넘어갑니다. 위 문장에 실제로 에러가 나는 시점은 실행할 때(런타임에러)입니다. car에 실제 GasolinCar 혹은 GasolinCar를 상속한 무엇인가가 아닐 때 에러가 발생합니다.

    하지만 Casting이 없이 GasolinCar의 함수를 호출한다면?

    Car car = ...
    car.BurnGasolin();
    

    컴파일러 입장에서 car는 BurnGasolin 함수가 없습니다. 앞에서도 말했듯이 하위클래스는 이 시점(컴파일 시점)에서 무엇이 있는지 알수가 없습니다.(특히 미래에 생길 클래스까지 생각할 수 도 없으므로) 즉 car가 실제 무엇으로 구현되었는지에 대해 관심이 가지기 어렵죠.

    학술적이고 알고리즘 적으로 설명하는 것보다는 위의 얘기가 쉽지 않을 까 싶어 풀어서 설명했습니다.

    이런 내용은 글로 설명하는 게 쉽지 않습니다. 주변의 잘 설명해줄 수 있는 선배, 교수님 등에게 조언을 구해보는 게 좋을 것 같습니다.

    • 답변 감사합니다. 좀 더 고민하는 시간이 필요할 것 같습니다. 알 수 없는 사용자 2016.5.13 23:52
  • 예를들어서 설명 드릴게요.

    기본리모컨(BasicRemote)이라는 클래스가 있습니다. 볼륨업/다운, 채널업/다운 기능을 가지고 있습니다. 기본리모컨 클래스를 상속 받은 음소거리모컨(MuteRemote)이라는 클래스가 있습니다. 추가로 음소거기능을 가지고 있습니다.

    이 때 다음 코드를 한 번 실행해 보세요.(코드 아래에 실행 버튼 누르면 됩니다.)

    class CodeRunner{
        public static void main(String[] args){
    
            /* remote에는 사실 MuteRemote가 들어 있습니다.
             * 하지만 BasicRemote타입이기 때문에 
             * BasicRemote의 메소드만 사용하기로 약속됩니다. */
            BasicRemote remote = new MuteRemote();
    
            //volumnUp과 volumnDown을 부르는건 아무 문제가 없습니다.
            remote.volumnUp();
            remote.volumnDown();
    
            /* 이 아래 줄은 애러가 발생합니다.
             * 왜냐하면 remote는 
             * BasicRemote의 기능만 사용하기로 약속되었기 떄문입니다 */
            //remote.mute();
    
            /* 하지만 remote에 들어있는 알맹이는 실재로는 MuteRemote입니다.
             * 그래서 다시 그 알맹이를 MuteRemote에 담아서 사용할 수도 있습니다.*/
            MuteRemote muteRemote = (MuteRemote)remote;
            muteRemote.mute();
        }
    }
    
    
    class BasicRemote{
        public void volumnUp(){
            System.out.println("VolumnUp");
        }
        public void volumnDown(){
            System.out.println("VolumnDown");
        }
        public void channelUp(){
            System.out.println("ChannelUp");
        }
        public void channelDown(){
            System.out.println("ChannelDown");
        }   
    }
    
    class MuteRemote extends BasicRemote{
        public void mute(){
            System.out.println("Mute");
        }
    }
    

    위의 코드대로 실행해 보고 나서 BasicRemote remote = new MuteRemote();BasicRemote remote = new BasicRemote();로 변경해 보세요. 그러면 muteRemote.mute부분에서 에러가 발생할겁니다. 알맹이가 BasicRemote이니까요.

  • 정두식선생님 일단 답변해주셔서 감사합니다.

    제 질문을 더 상세하게 설명해드리자면.. 위 코드에서

    BasicRemote remote = new MuteRemote();

    /* 이 아래 줄은 애러가 발생합니다. * 왜냐하면 remote는 * BasicRemote의 기능만 사용하기로 약속되었기 떄문입니다 */ //remote.mute();

    remote의 실제 알맹이는 MuteRemote인데, 실제 사용할 수 있는 변수와 메소드는 BasicRemote클래스의 것들만 사용하기로 약속했냐는 것입니다.

    만약 이 약속이 어쩔 수 없는것이라면 /* 하지만 remote에 들어있는 알맹이는 실재로는 MuteRemote입니다. * 그래서 다시 그 알맹이를 MuteRemote에 담아서 사용할 수도 있습니다.*/ MuteRemote muteRemote = (MuteRemote)remote; muteRemote.mute(); 와 같이 형변환을 강제하는 것인지 궁금합니다.

    • (•́ ✖ •̀)
      알 수 없는 사용자

답변을 하려면 로그인이 필요합니다.

프로그래머스 커뮤니티는 개발자들을 위한 Q&A 서비스입니다. 로그인해야 답변을 작성하실 수 있습니다.

(ಠ_ಠ)
(ಠ‿ಠ)