ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 13. 자바 추상 클래스와 추상 메서드
    자바(Java) 강의 2019. 4. 15. 09:33


    12. 자바 메서드 오버라이딩과 다형성에서 우리는 Animal클래스를 상속하고 eat 메서드를 오버라이딩 해 각 서브클래스가 같은 이름의 메서드로 다른 기능을 구현할 수 있도록 코드를 짜 보았다. 그리고 마지막에 talk 메서드를 언급하며, 반드시 기능이 달라야 하는 수퍼 클래스의 메서드를 어떻게 개발자들에게 구현하도록 강요할 수 있을까라는 질문을 남기며 포스트를 마쳤다. 이 포스트에서는 자바가 이를 해결하기 위해 제공하는 추상클래스와 추상메서드에 대해 이야기 하도록 한다.

    이전 포스트

    목표

    • 프로젝트 구조

    • 문제 확인 (Problem Statement)

    • 현실 세계의 추상적 개념들

    • 추상클래스 (Abstract Class)

    • 추상메서드 (Abstract Method)

    • 이 디자인의 심각한 오류

    프로젝트 구조

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

    문제 확인 (Problem Statement)

    Animal 클래스는 다음과 같다

    package animal;

    public class Animal {
    public String name;

    public Animal(String name) {
    this.name = name;
    }

    public void eat(int portion) {
    System.out.println(name + "(이)는 " + portion + " 만큼의 밥을 먹었다. ");
    }
    }

    Cat 클래스는 다음과 같다.

    package animal;

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

    @Override
    public void eat(int portion) {
    System.out.println(name + "(이)는 " + portion + " 만큼의 밥을 먹었다. 야옹야옹!");
    }
    }

    Dog 클래스는 다음과 같다.

    package animal;

    public class Dog extends Animal{

    public Dog(String name) {
    super(name);
    }

    @Override
    public void eat(int portion) {
    System.out.println(name + "(이)는 " + portion + " 만큼의 밥을 먹었다. 멍뭉멍뭉!");
    }
    }

    이제 이 클래스들에 talk이라는 메서드를 추가하고싶다. talk 메서드는 고양이도, 강아지도, 새도 모두 가지고 있는 기능이다. 따라서 talk 메서드를 수퍼 클래스인 Animal에 정의하고 싶다. 따라서 아래처럼 Animal 클래스를 수정했다. (eat 메서드는 생략했다.)

    package animal;

    public class Animal {
    public String name;

    public Animal(String name) {
    this.name = name;
    }

    public void talk() {
    // ?????????
    }
    }

    talk을 Animal에서 어떻게 구현해야 할까? 인간의 기준에서 각 동물이 우는 소리는 다 다르게 들린다. 강아지는 멍멍 또는 왈왈, 고양이는 야옹, 새는 짹짹으로 들린다. 그렇다면 이 동물들을 서브 클래스로 구현할 때 talk 메서드를 오버라이딩 하지 않으면 어떻게 되는가? Animal 클래스에 있는 talk 메서드를 사용하게 될 것이다. 즉 Animal 메서드에 구현된 talk 메서드가 모든 동물의 디폴트(Default) 메서드가 되는 것이다. 이러한 가정이 타당한가? 타당하지 않다. 만약 talk 메서드를 아래처럼 구현했다고 해보자.

    public void talk() {
    System.out.println("블라블라..");
    }

    예를들어서 삐멜 엔터테이먼트라는 회사에서 동물친구 키우기라는 게임을 만드는 중 이었고, 여러분은 talk이라는 메서드를 만들고 위처럼 "블라블라.."하고 talk 메서드를 만들어놨다. 여러분은 talk 메서드가 서브 클래스인 Cat과 Dog에서 오버라이딩을 반드시 해야 한다는 것을 안다. 그런데 여러분이 사정이 생겨 오버라이딩 부분을 구현하기 전에 회사를  그만두게 되었다. 삐멜은 새 개발자를 고용 했고 새 개발자는 코드가 돌아가니 talk 메서드를 오버라이딩 해야한다는 사실을 알지 못했다. 개발이 다 된줄 알고 게임을 돌려보니 어떻게 되는가?

    import animal.Animal;
    import animal.Cat;

    public class Main {
    public static void main(String[] args) {
    Animal animal = new Cat("토미");
    animal.talk();
    }
    } 실행 결과: 블라블라..

    고양이 토미가 난데없이 "블라블라.." 하고 사람말을 하기 시작했다. 이제 새 개발자는 모든  Animal 클래스의 서브 클래스를 돌아다니며 talk 메서드를 오버라이딩 해 주어야 한다. 

    현실 세계의 추상적 개념들

    원론으로 돌아가보자. 

    import animal.Animal;

    public class Main {
    public static void main(String[] args) {
    Animal animal = new Animal("토미");
    animal.talk();
    }
    }

    new Animal("토미"); 이 오브젝트는 무슨 오브젝트인가? Animal형의 오브젝트이다. 무슨 Animal인가? 모른다. 이름에 '토미'인 어떤 Animal이다. 실제 세계에서 이와 같은 상황이 말이 되는가? 동물이라는 것은 인간이 카테고리화 하기 위해 만든 범주이다. 현실 세계에서 동물이라는 단어 자체에는 형체가 없다. 이 동물이 강아지이거나, 고양이이거나, 새이거나, 개구리일 때, 형체가 생긴다. 즉, 동물은 현실 세계에서 인간이 카테고리화 하기 위해 만든 추상적인 개념이라는 것이다. 현실 세계에서 Animal이라는 오브젝트(형체/객체)는 존재하지 않는다. 따라서 우리(개발자)도 프로그램 내에서 Animal 오브젝트를 생성할 수 없어야 한다. 이렇게 현실세계의 추상적인 개념과 기능을 표현하기 위해 자바에서는 추상 클래스/추상 메서드를 제공한다. 

    추상클래스

    위에서 말했듯이 Animal이라는 오브젝트는 논리상 말이 안된다. 이를 어떻게 강제화 할까? 바로 Animal 클래스를 추상 클래스로 만들면 된다. class 앞에 abstract 키워드를 붙여주자.

    package animal;

    public abstract class Animal {
    public String name;

    public Animal(String name) {
    this.name = name;
    }

    public void talk() {
    System.out.println("블라블라..");
    }
    }

    이렇게 하고 메인 메서드의 부분으로 돌아가보자.

    import animal.Animal;

    public class Main {
    public static void main(String[] args) {
    Animal animal = new Animal("토미");
    }
    }
    실행 결과: Error:(5, 25) java: animal.Animal is abstract; cannot be instantiated

    실행시 위와 같은 에러가 난다. Animal is abstract; 즉 이 클래스는 추상적이므로 이 클래스의 오브젝트를 만들 수 없다는 뜻이다. Animal 클래스를 추상 클래스로 만들어 이제 누구도 Animal이라는 오브젝트를 만들지 못하게 했다. Animal의 형을 가진 오브젝트는 반드시 Animal을 상속해 구현한 클래스의 형 중 하나여야 한다는 뜻이다.

    추상 클래스를 이용하는 이유는 다음과 같다. 추상 클래스로 클래스의 내부를 전부 구현하지 않아도 서브클래스들이 공유하는 기능을 정의할 수 있다(추상메서드 참고). 이를 통해 서브 클래스들의 베이스를 제공하는 역할을 한다. 동시에 추상 클래스 자체가 오브젝트화(Instantiation)되지 못하도록 막는다.

    추상메서드

    이제 다시 문제 상황으로 돌아가보자. Animal 클래스의 talk 메서드는 반드시 이를 상속하는 클래스마다 기능이 달라야 한다. 고양이는 야옹야옹 하고 울어야 하고 강아지는 멍멍멍하고 울어야 한다. 이를 어떻게 강제화 할 것인가? 추상 클래스를 이용해 해당 클래스를 반드시 상속해 사용하도록 강제화 했던 것 처럼, 추상 메서드를 이용해 서브 클래스에서 이 메서드를 반드시 오버라이드 하도록 강제화 할 수 있다.

    package animal;

    public abstract class Animal {
    public String name;

    public Animal(String name) {
    this.name = name;
    }

    public abstract void talk();
    }

    추상 메서드는 추상 클래스와 마찬가지로 리턴 타입 앞에 abstract를 붙여준다. 그리고 메서드의 정의 부분만 선언하고 바디(body)는 만들지 않는다. 메서드 전체를 구현하지 않는다. 왜? 서브 클래스에서 반드시 오버라이드 할 것이기 때문에. 추상 메서드는 메서드의 이름, 파라미터, 리턴타입만 정의하고 그 구현은 서브클래스가 하도록 강제화 하는 것이다. 이렇게 하면 Animal을 상속받는 모든 클래스가 talk 메서드를 오버라이딩 해야 한다.  메인 메서드로 가 아래의 코드를 실행 해보자.

    import animal.Cat;

    public class Main {
    public static void main(String[] args) {
    Animal animal = new Cat("토미");
    animal.talk();
    }
    } 실행 결과: Error:(3, 8) java: animal.Dog is not abstract and does not override abstract method talk() in animal.Animal Error:(3, 8) java: animal.Cat is not abstract and does not override abstract method talk() in animal.Animal

    무슨 뜻인가? Dog와 Cat 클래스가 반드시 talk 메서드를 오버라이딩 해야 한다는 뜻이다. 에러가 시키는 대로 메서드를 오버라이드 해보자.

    package animal;

    public class Dog extends Animal{

    public Dog(String name) {
    super(name);
    }

    @Override
    public void talk() {
    System.out.println(name + "(이)는 멍뭉! 멍뭉! 왈왈! 하고 울었다.");
    }
    }

    Dog는 위와같이 수정 할 수 있다. 

    package animal;

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

    @Override
    public void talk() {
    System.out.println(name + "(이)는 야옹! 야옹! 미야오! 꿍냥! 하고 울었다.");
    }
    }

    Cat 클래스에서는 talk을 위처럼 오버라이드 할 수 있다.

    팁(Tip)

    IntelliJ에서 메서드를 정의하려는 자리에 커서를 놓고 Ctrl + n (맥에서는 Command + n)을 누르면 위처럼 Generate 탭이 나온다. 여기서 Implement Methods를 누르면 우리가 구현할 수 있는 메서드들이 나온다.

    그러면 각 수퍼 클래스마다 오버라이드 할 수 있는 메서드가 나온다. (Object라는 클래스는 무엇이고 왜 여기있을까..? 생각해보라 :) ! ) 여기서 talk()을 누르고 ok를 누르면 메서드가 바로 생성된다.

    package animal;

    public class Dog extends Animal{

    public Dog(String name) {
    super(name);
    }

    @Override
    public void talk() {

    }
    }

    이제 바디의 부분만 구현하면 된다.

    이 디자인의 심각한 오류

    고백 할 것이 있다. 이 디자인에는 심각한 오류가 있다. 무엇인지 짐작 할 수 있는가? 우리는 지금까지 모든 동물이 talk, 즉 말한다는 가정을 하고 Animal 클래스에 추상 메서드인 talk 메서드를 만들었다. 하지만 세상에는 말하지 않는 동물도 많이 있다. 파충류, 곤충, 어류등 구강기관과 공기의 진동을 이용해 의사소통하지 않는 동물을 포함하려면 어떻게 해야할까? '운다/말하다(talk)'라는건 모든 동물의 특성이 아니라는 뜻이다. 하지만 어떤 동물은 분명히 말을 한다. 또, 만약 이 게임에서 우리가 로봇도 키우고 싶다고 하자. 로봇은 Animal이 아니다. 하지만 로봇도 말을 할 수 있다. 즉 talk이라는 것은 특정 동물만의 전유물이 아닐 수도 있다는 것이다. 이렇게 어떤 기능(메서드)이 반드시 구현되어야 하지만 모든 서브 클래스의 공통적인 기능이 아닐 경우, 어떻게 하면 특정 클래스만 이 기능을 반드시 구현(오버라이드)하도록 할 수 있을까? 이는 바로 '인터페이스(Interface)'라는 것을 이용한다. 인터페이스(Interface)'는 다음 포스트에서 더 자세히 다루도록 하겠다.

    이번 포스트에서는 추상 클래스와 추상 메서드를 이용해 개발자들에게 일종의 제약을 거는 방법에 대해 이야기 해 보았다. 실제 개발에서는 추상메서드를 많이 사용하지 않는다. 실제 개발시 구현하는 기능들이 보통 한 추상클래스가 반드시 가져야할 특성이 아닐 때가 많기 때문이다. 따라서 프로그램을 디자인 할 때 추상메서드를 사용한다면 그 메서드가 그 메서드를 오버라이드하는 모든 서브클래스에 공통적으로 해당되는 기능인지 잘 생각해 봐야 한다. 그렇지 않으면 이후에 구현부분이 없는 빈 메서드를 만들어야하는 불상사가 생길 수 있다.

    연습하기

    이전 포스트에서 새 동물친구를 만들기 위해 코드를 수정 해 보았는가? 나는 아래처럼 코드를 수정했다.

    import animal.Animal;
    import animal.Cat;
    import animal.Dog;

    import java.util.Scanner;

    public class Main {
    public static void main(String[] args) {
    Animal animal;
    String name = getAnimalName();
    int animalChoice = getUserChoice(name);
    if (animalChoice == 1) {
    animal = new Cat(name);
    } else {
    animal = new Dog(name);
    }
    animal.eat(getPortion());
    animal.talk();
    }

    // 터미널 또는 cmd로부터 정수를 입력받는 메서드
    private static int getUserChoice(String name) {
    Scanner scan = new Scanner(System.in);
    System.out.println(name + "은 어떤 동물인가요?");
    System.out.println("1. 고양이");
    System.out.println("2. 강아지");
    return scan.nextInt();
    }

    // 터미널 또는 cmd로 부터 스트링을 입력받는 메서드
    private static String getAnimalName() {
    Scanner scan = new Scanner(System.in);
    System.out.print("새 동물 친구의 이름: ");
    return scan.next();
    }

    // 터미널 또는 cmd로 부터 정수를 입력받는 메서드
    private static int getPortion() {
    Scanner scan = new Scanner(System.in);
    System.out.print("밥을 얼마나 줄까요? ");
    return scan.nextInt();
    }
    } 실행 결과: 새 동물 친구의 이름: tommy tommy은 어떤 동물인가요? 1. 고양이 2. 강아지 1 밥을 얼마나 줄까요? 10 tommy(이)는 10 만큼의 밥을 먹었다. 야옹야옹! tommy(이)는 야옹! 야옹! 미야오! 꿍냥! 하고 울었다.

     이제 반복문을 이용해 연속해서 밥을 줄 수 있도록 해보자. 0을 누르면 종료한다고 하자. 

    실행 결과 예:

    새 동물 친구의 이름: tommy tommy은 어떤 동물인가요? 1. 고양이 2. 강아지 1 밥을 얼마나 줄까요? 10 tommy(이)는 10 만큼의 밥을 먹었다. 야옹야옹! tommy(이)는 야옹! 야옹! 미야오! 꿍냥! 하고 울었다. 밥을 얼마나 줄까요? 20 tommy(이)는 20 만큼의 밥을 먹었다. 야옹야옹! tommy(이)는 야옹! 야옹! 미야오! 꿍냥! 하고 울었다. 밥을 얼마나 줄까요? 0 tommy을 놔두고 가지마세요...!! 종료됨.


    댓글

f.software engineer @ All Right Reserved