ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 11. 자바 상속(Inheritance) - this와 super
    자바(Java) 강의 2019. 4. 9. 14:08

    지난 포스트에서 했던 '상속'을 이용해 코드를 재사용할 수 있었다. 그런데 만약 수퍼 클래스와 서브 클래스에 같은 이름의 변수나 메서드가 있다면 어떡할까? 두 클래스에 있는 멤버 변수와 메서드를 어떻게 구분할까? 또, 오브젝트에 파라미터로 넘어온 변수의 이름과 같은 이름의 멤버 변수가 있다면 어떡할까?

    이전 포스트

    목표

    • 프로젝트 구조
    • this
    • super
    • super()
    • super의 메서드

    프로젝트 구조

    이 포스트의 내용을 실습하기 위해 아래와 같은 자바 프로젝트 구조를 따랐다.

    JavaTutorial
    ├── JavaTutorial.iml
    └── src
        ├── Main.java
        └── animal
            ├── Animal.java
            ├── Cat.java
            └── Dog.java
    
    2 directories, 5 files

    패키지 - animal

    this

    아래와 같은 코드를 보자.

    package animal;
    
    public class Cat {
        public String name;
        public Cat(String name) {
           name = name;
        }
    }

    name이 너무 많다. Cat 클래스 안에 name이 있고, 생성자에 파라미터로 들어오는 name이 있다. 그리고 생성자 안에서는 name에 name을 할당해 주고 싶다. 따라서 위처럼 적어주고 실행해 본다.

    import animal.Cat;
    
    public class Main {
        public static void main(String[] args) {
            Cat cat = new Cat("Tom");
            System.out.println(cat.name);
        }
    }

    위의 코드를 실행해 보자. name이 Tom으로 나오는가? 안나올것이다. 생성자에서 name = name을 했을 때, 두 name모두 파라미터에서 넘어온 name으로 인식되기 때문이다. 어떻게 해야 오브젝트의 name과 파라미터로 넘어온 name을 구분 할 수 있을까? 지금 이 상태로는 이 오브젝트의 name에 파라미터로 넘어온 name을 넣을 수 없다. 따라서 위의 코드를 실행하면 null이 출력된다.

    이런 문제를 해결하기 위해 자바에서는 this라는 키워드를 제공한다. this는 아래처럼 사용할 수 있다.

    package animal;
    
    public class Cat {
        public String name;
        public Cat(String name) {
           this.name = name;
        }
    }

    어떤 클래스 내에서 this란 이 클래스의 오브젝트가 생길 당시의 자기 자신인 오브젝트를 의미한다. 오브젝트로 이해하기 힘들다면 지금은 현재 클래스에 있는 멤버 변수와 메서드를 this로 접근할 수 있다고 알아두면 된다. 따라서 위의 코드에서 왼쪽 this.name은 Cat클래스의 멤버 변수 name을 뜻하고 오른쪽 name은 파라미터로 넘어온 name을 뜻하는 것이다. 이렇게 하고 다시 메인 메서드를 실행해 보면 이젠 제대로 cat의 이름이 나올 것이다.

    super

    이번엔 상속에서 구현했던 Animal과 Animal을 상속받는 Cat 클래스를 보자.

    package animal;
    
    public class Animal {
        public String name;
        public int weight;
        public String gender;
        public String color;
    
        public void eat(int portion) {
            System.out.println(name + "(이)는 " + portion + " 만큼의 밥을 먹었다.");
        }
    }

    Animal.java의 클래스는 위와 같다.

    package animal;
    
    public class Cat extends Animal{
        public String name;
        public Cat(String name) {
           this.name = name;
        }
    }

    Cat.java의 클래스는 위와 같다. 이제 아래처럼 cat 오브젝트의 eat 메서드를 불러보자.

    import animal.Cat;
    
    public class Main {
        public static void main(String[] args) {
            Cat cat = new Cat("Tom");
            cat.eat(10);
            System.out.println(cat.name);
        }
    }
    
    //null(이)는 10 만큼의 밥을 먹었다.
    //Tom

    결과가 어떻게 나오는가? 'null(이)는 10 만큼의 밥을 먹었다.'아래에 'Tom'이 나올 것이다. 왜인가? 첫 번째 eat 메서드는 Animal에 있는 name을 사용(참조)하고, cat.name은 Cat에 있는 name을 참조하기 때문이다. 그렇다면 위와 같은 코드에서 eat이 제대로 name을 출력하게 하려면, 즉 Animal에 있는 name에 이름을 넣으려면 어떻게 해야 할까? 현재 오브젝트를 지칭하기 위해 this를 사용했던 것처럼, 슈퍼 클래스를 지칭하기 위해서 super키워드를 이용하면 된다.

    package animal;
    
    public class Cat extends Animal{
        public String name;
        public Cat(String name) {
           this.name = name;
           super.name = "Super Cat " + name;
        }
    }

    super는 this(Cat)가 상속하는 Animal을 지칭한다. Animal내부의 name을 접근하려면 super.name으로 사용(참조)하면 된다.

    import animal.Cat;
    
    public class Main {
        public static void main(String[] args) {
            Cat cat = new Cat("Tom");
            cat.eat(10);
            System.out.println(cat.name);
        }
    }
    
    //Super Cat Tom(이)는 10 만큼의 밥을 먹었다.
    //Tom

    수퍼클래스와 서브클래스에 같은 이름의 멤버 변수를 놓는 경우는 매우 드물다. 아주 헷갈리고 유지보수가 힘들기 때문에 지양하는 것이 좋다. 따라서 우리는 위에서 Cat의 name은 지우고 Animal의 name만 사용하고 싶다.

    package animal;
    
    public class Cat extends Animal{
        public Cat(String name) {
           super.name = "Super Cat " + name;
        }
    }

    이렇게 한 후 다시 실행하면 이제 첫 번째 줄과 두 번째 줄의 출력물이 모두 Super Cat Tom인 것을 확인할 수 있다.

    super()

    위의 코드에는 한 가지 문제가 있다. 만약 내가 Cat이나 Dog이 아닌 Animal 오브젝트 자체를 선언한다면 어떻게 되는가?

    import animal.Animal;
    
    public class Main {
        public static void main(String[] args) {
            Animal animal = new Animal();
            animal.eat(10);
        }
    }
    
    //null(이)는 10 만큼의 밥을 먹었다.

    왜 또 null인가? Cat의 경우 생성자를 통해 name을 강제(?)로 초기화시켜줬지만 Animal에는 생성자가 없기 때문이다. Animal에도 생성자를 만들어주자.

    package animal;
    
    public class Animal {
        public String name;
        public int weight;
        public String gender;
        public String color;
    
        public Animal(String name) {
            this.name = name;
        }
    
        public void eat(int portion) {
            System.out.println(name + "(이)는 " + portion + " 만큼의 밥을 먹었다.");
        }
    }

    위와 같이 Animal을 수정하면 에러가 발생한다.

    Error:(4, 29) java: constructor Animal in class animal.Animal cannot be applied to given types;
      required: java.lang.String
      found: no arguments
      reason: actual and formal argument lists differ in length

    왜인가? 자바에서 상속을 할 때 몇 가지 룰이 존재한다. 그중 하나는 바로 슈퍼 클래스의 생성자를 서브 클래스에서 반드시 불러줘야 된다는 것이다. 

    package animal;
    
    public class Cat extends Animal{
        public Cat(String name) {
            super(name);
        }
    }

    따라서 Animal생성자를 작성했다면 위처럼 Cat생성자 안에 Animal생성자를 불러줘야 한다. 어떻게? super를 이용해서. super가 바로 자신의 부모(수퍼)클래스의 생성자를 부르는 콜이다.

    아니 지금까지 수퍼 클래스의 생성자를 한 번도 부른 적이 없는데 무슨 소리인가? 옛날 옛날 클래스를 다루던 포스트에서 클래스에 생성자를 적어주지 않으면 자바가 아무 파라미터도 받지 않는 기본 생성자를 만들어 준다는 것을 기억하는가? 수퍼클래스도 마찬가지이다 수퍼클래스에 아무 생성자도 없는 경우 기본 생성자를 만들어준다. 그리고 서브클래스의 생성자가 불리면 가장 처음으로 수퍼클래스의 생성자를 불러 수퍼클래스의 오브젝트를 초기화해준다. 

    package animal;
    
    public class Cat extends Animal{
        public Cat(String name) {
            System.out.println("컴파일 에러가 날것이다.");
            super(name);
        }
    }

    가장 처음으로 수퍼클래스의 생성자를 불러 수퍼클래스의 오브젝트를 초기화해야 한다는 건 바로 위와 같이 super클래스를 부르기 전에 다른 문법을 사용할 수 없다는 뜻이다. 위와 같은 코드는 아래와 같은 에러를 야기할 것이다.

    Error:(6, 14) java: call to super must be first statement in constructor

    무슨 뜻인가? super이 반드시 생성자의 첫 번째 구문이어야 한다는 뜻이다. 우리가 Animal에 생성자를 만들지 않으면 자바가 알아서 디폴트 생성자를 만들고 Cat생성자가 불릴 때 실행해준다. 하지만 우리가 우리 자신만의 Animal생성자를 만든다면 반드시! Cat생성자안에서 가장 처음으로 불러야 한다는 뜻이다.

    super의 메서드

    이전 포스트에서 eat에서 마지막에 '야옹야옹' 또는 '멍뭉멍뭉'을 나오게 하려면 어떻게 해야 할지 생각해보라고 했다. 여러 가지 방법이 있지만 지금까지 배운 기술로는 아마 아래와 같은 방법이 최선일 것이다.

    package animal;
    
    public class Animal {
        public String name;
        public int weight;
        public String gender;
        public String color;
    
        public Animal(String name) {
            this.name = name;
        }
    
        public void eat(int portion, String message) {
            System.out.println(name + "(이)는 " + portion + " 만큼의 밥을 먹었다. " + message);
        }
    }

    이렇게 하고 Main에서 다음과 같이 메서드를 부른다.

    import animal.Cat;
    
    public class Main {
        public static void main(String[] args) {
            Cat cat = new Cat("토미");
            cat.eat(10, "야옹 야옹");
        }
    }

    또는 super를 이용해 상위 메서드를 부를 수도 있다.

    package animal;
    
    public class Cat extends Animal{
        public Cat(String name) {
           // System.out.println("컴파일 에러가 날것이다.");
            super(name);
        }
        
        public void meow(int portion) {
            super.eat(portion);
            System.out.println("야옹! 야옹!");
        }
    }

    메인 메서드

    import animal.Cat;
    
    public class Main {
        public static void main(String[] args) {
            Cat cat = new Cat("토미");
            cat.meow(10);
        }
    }
    
    //토미(이)는 10 만큼의 밥을 먹었다. 
    //야옹! 야옹!

    비록 한 줄에 나오진 않지만 얼추 비슷한 모양이다. 하지만 만약 meow처럼 새 메서드를 이용하고 싶지 않다면 어떻게 할까? 예를 들어 cat.eat(10)이라는 메서드가 1000군데에서 사용되고 있다면 1000군데를 돌아다니며 meow로 메서드 이름을 고쳐줘야 할 것이다. 또 아래와 같은 경우를 보자.

    import animal.Animal;
    import animal.Cat;
    import animal.Dog;
    
    public class Main {
        public static void main(String[] args) {
            Animal animal;
            int animalChoice = 1;
            if (animalChoice == 1) {
                animal = new Cat("토미");
            } else {
                animal = new Dog("하이디");
            }
            animal.eat(10);
        }
    }

    위의 코드는 animalChoice에 따라 다른 종류의 동물을 생성해 준다고 하자. (예를 들어 animalChoice = 1 부분을 new Scanner를 이용해 사용자에게 입력받는다고 하자.) 이런 경우 meow를 새로 만들면 기존의 animal을 사용하는 로직을 전부 바꿔야 한다! animal이 Cat일 경우에는 eat대신 meow를 사용해야 하므로.

    참고! Dog.java

    package animal;
    
    public class Dog extends Animal{
    
        public Dog(String name) {
            super(name);
        }
    }

    eat이라는 메서드를 그대로 사용하면서도 새로운 메시지(토미(이)는 10 만큼의 밥을 먹었다. 야옹야옹!)로 고칠순 없을까? 당연히 있다. 다음 포스트에서는 이렇게 수퍼클래스의 메서드를 오버 라이딩(overriding)하는 법에 대해서 알아보겠다.

    이번 포스트에서는 this와 super에 대해 알아보았다. 위에서도 말했듯 다음 포스트에서는 조금씩 객체지향 프로그래밍(Object Oriented Programming)에 대해 조금씩 소개하면서 자바자 객체지향 프로그래밍을 지원하기 위해 제공하는 문법 중 하나인 오버 라이딩에 대해 알아보도록 하겠다. 

    다음 포스트:12. 자바 메서드 오버라이딩과 다형성

    댓글

f.software engineer @ All Right Reserved