ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 14. 자바 인터페이스와 다형성 (2)
    자바(Java) 강의 2019. 4. 22. 04:15



    13. 자바 추상클래스와 추상메서드 에서 마지막에 우리가 만들고있는 동물친구 키우기 디자인에는 오류가 있다고 했다. 바로 모든 동물이 말을 하지 않는다는 것이다. 또 로봇과 같은 동물이 아닌 객체도 말을 할 수 있다. 이렇게 어떤 '기능'이 한 클래스에 귀속되지 않는경우 '인터페이스'를 활용할 수 있다.

    이전 포스트

    목표

    • 프로젝트 구조

    • 인터페이스 (Interface)

    • 유저 메뉴 만들기

    • 팩토리 메서드 (Factory Method)

    • 추상클래스와 인터페이스

    프로젝트 구조

    JavaTutorial
    ├── JavaTutorial.iml
    └── src
        ├── Main.java
        └── animal
            ├── Animal.java
            ├── AnimalFactory.java
            ├── Cat.java
            ├── Dog.java
            └── behavior
                ├── Behave.java
                ├── Feed.java
                └── Talk.java

    3 directories, 9 files

    인터페이스 (Interface)

    인터페이스는 추상클래스와 비슷하다. 단 변수를 가질 수 없으며 추상메서드만 있어야 한다. (자바 8에서는 default메서드를 제공하지만 이는 자바8에서 다루도록 한다.) 위에서 로봇을 예로 들며 이렇게 다른 종류의 클래스도 Talk을 사용할 수 있을지 모른다고 했다. 이는 꽤 극단적인 이야기이고 더 큰 디자인이 요구되기 때문에 이 포스트에서는 Animal을 중심으로 인터페이스에 대해 설명하도록 하겠다.
    우리는 지금까지 eat, talk과 같은 행동이 Animal클래스 안에 구현되어 있어야한다고 생각했다. 하지만 꼭 그럴필요가 있는가? 객체지향 프로그래밍에서는 클래스를 최대한 분리하는것(de-couple)을 지향한다. 그리고 이를 인터페이스를 이용해 달성할 수 있다.
    Animal은 모두 행동(behavior)를 가진다. 우리는 이 behavior를 하나하나 정의할 생각이다. 따라서 animal패키지 안에 behavior라는 패키지를 새로 만든다. 그리고 그 안에 behavior.java라는 '인터페이스'를 만든다.
    package animal.behavior;

    import animal.Animal;

    public interface Behave {
    void act(Animal animal);
    }

    인터페이스는 위처럼 <접근제어자> interface <인터페이스이름>으로 선언한다. 인터페이스는 멤버 '변수'를 가질 수 없고 추상 클래스만 정의할 수 있다. (멤버 '상수'는 가능하다. 이는 final을 다룰때 설명하겠다.) 이제 이 인터페이스를 구현할 Talk이라는 클래스를 만들어보겠다.

    package animal.behavior;

    import animal.Animal;

    public class Talk implements Behave {
    @Override
    public void act(Animal animal) {
    System.out.println(animal.name + " 이는 말한다!");
    }
    }

    인터페이스는 위처럼 'implements' 키워드를 이용해 구현한다. 또한 인터페이스는 여러개 implement가 가능하다. 

    public class Talk implements Behave, OtherInterface {

    이렇게 콤마를 이용해 여러가 인터페이스를 동시에 구현할 수 있다. 여기서는 Behavior만 구현 해 보도록 하자.

    이렇게 하면 Talk을 어떻게 사용할것인가? 메인메서드에서 아래처럼 구현할 수 있다.

    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);
    }
    int portion = getPortion();
    while(portion != 0) {
    animal.eat(portion);
    Behave behave = new Talk(); // <- 여기랑
    behave.act(animal); // <- 여기
    portion = getPortion();
    }
    System.out.println(animal.name + "을 놔두고 가지마세요...!!");
    System.out.println("종료됨.");
    }

    이게 무슨뜻인가? 바로 talk, eat과 같은기능들이 더 이상 Animal안에 존재하지 않아도 된다는 뜻이다. 그리고 필요할 때마다 선언해서 사용할 수 있다는 뜻이다. 예를들어 eat을 구현하기위해선 어떻게 해야겠는가? 일단 밥그릇에 밥이 필요하다. 위에서는 이를 portion으로 표시했다. 이를 Animal클래스로 옮겨보자.

    package animal;

    public abstract class Animal {
    public String name;
    public int food = 0; // 밥그릇에 있는 밥
    public int portion = 10; // 한번에 먹을수 있는 양
    public int hunger = 30; // 얼마나 배고픈지

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

    요구사항은 다음과 같다. 

    이 동물은 밥을 food에서 portion에 정의된 만큼 빼서 먹을것이다. 만약 food가 부족하다면 그때 유저에게 밥을 달라고 한다.

    위를 충족하기위해 새 Behavior은 Feed클래스를 만들어보자. (이전의 eat과 같은 기능) 먼저 어떻게 짜야할지 생각해보고 다음으로 넘어가길 바란다.

    package animal.behavior;

    import animal.Animal;

    public class Feed implements Behave {
    @Override
    public void act(Animal animal) {
    if(animal.hunger == 0) {
    System.out.println(animal.name + "(이)는 배가 안고프다.");
    return;
    }
    if(animal.food == 0) {
    System.out.println("먹을게 없다.. 밥을 줘야 할 것 같다.");
    return;
    }
    int portion = animal.portion;
    if(animal.portion > animal.food) {
    portion = animal.food;
    }
    animal.food = animal.food - portion;
    animal.hunger = animal.hunger - portion;
    System.out.println(animal.name + "(이)가 " + animal.portion + "만큼 밥을 먹었다.");
    System.out.println(animal.name + "의 배고픔 지수:" + animal.hunger);
    }
    }

    다양한 구현방법이 있겠지만 나는 간단히 하기 위해 위처럼 구현했다. (위의 코드에는 버그가 있다. 찾을 수 있는가? ) 그리고 메인 메서드를 아래처럼 수정했다.

    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);
    }
    int food = getPortion();
    while(food != -1) {
    animal.food += food;
    Behave feed = new Feed();
    feed.act(animal);
    food = getPortion();
    }
    System.out.println(animal.name + "을 놔두고 가지마세요...!!");
    System.out.println("종료됨.");
    }

    실행결과


    새 동물 친구의 이름: 토미
    토미은 어떤 동물인가요?
    1. 고양이
    2. 강아지
    1
    밥을 얼마나 줄까요? 30
    토미()10만큼 밥을 먹었다.
    토미의 배고픔 지수:20
    밥을 얼마나 줄까요? 0
    토미()10만큼 밥을 먹었다.
    토미의 배고픔 지수:10
    밥을 얼마나 줄까요? 0
    토미()10만큼 밥을 먹었다.
    토미의 배고픔 지수:0
    밥을 얼마나 줄까요? 30
    토미()는 배가 안고프다.
    밥을 얼마나 줄까요? 0
    토미()는 배가 안고프다.
    밥을 얼마나 줄까요? -1
    토미을 놔두고 가지마세요...!!
    종료됨.

    유저 메뉴 만들기

    이제 우리는 유저에게 원하는것을 입력받을 것이다. 즉 실행시 무조건 getPortion이 아닌 메뉴를 뿌리고 유저의 입력에따라 다른 행동을 할것이라는 뜻이다. 예를들어 1을 누르면 밥을 주고 2를 누르면 대화를하고 0을 누르면 종료한다. 이를 위해 메인메서드를 아래처럼 수정했다.

    import animal.Animal;
    import animal.Cat;
    import animal.Dog;
    import animal.behavior.Behave;
    import animal.behavior.Feed;
    import animal.behavior.Talk;

    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);
    }
    int selection = selectBehavior(name);
    while(selection != 0) {
    Behave behave = null;
    if(selection == 1) {
    int food = getPortion();
    animal.food += food;
    behave = new Feed();
    } else if(selection == 2) {
    behave = new Talk();
    }
    if(behave == null) {
    System.out.println("잘못된 명령입니다.");
    } else {
    behave.act(animal);
    }
    selection = selectBehavior(name);
    }
    System.out.println(animal.name + "을 놔두고 가지마세요...!!");
    System.out.println("종료됨.");
    }

    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();
    }

    private static String getAnimalName() {
    Scanner scan = new Scanner(System.in);
    System.out.print("새 동물 친구의 이름: ");
    return scan.next();
    }

    private static int getPortion() {
    Scanner scan = new Scanner(System.in);
    System.out.print("밥을 얼마나 줄까요? ");
    return scan.nextInt();
    }

    private static int selectBehavior(String name) {
    System.out.println(name + "에게 뭘 해줄까요?");
    System.out.println("# 0 종료");
    System.out.println("# 1 밥주기");
    System.out.println("# 2 대화하기");
    Scanner scan = new Scanner(System.in);
    return scan.nextInt();
    }
    }

    이렇게 하고나니 코드가 너무 지저분하고 길다. 그래서 animal생성부분을 따로 클래스로 떼어내려고 한다.

    package animal;

    public class AnimalFactory {
    public static Animal create(int choice, String name) {
    Animal animal;
    if (choice == 1) {
    animal = new Cat(name);
    } else {
    animal = new Dog(name);
    }
    return animal;
    }
    }

    이렇게 떼어낸 부분을 메인메서드에서 사용하면 다음과 같다.

    public static void main(String[] args) {
    String name = getAnimalName();
    int animalChoice = getUserChoice(name);
    Animal animal = AnimalFactory.create(animalChoice, name); // 팩토리 메서드
    int selection = selectBehavior(name);
    while(selection != 0) {
    Behave behave = null;
    if(selection == 1) {
    int food = getPortion();
    animal.food += food;
    behave = new Feed();
    } else if(selection == 2) {
    behave = new Talk();
    }
    if(behave == null) {
    System.out.println("잘못된 명령입니다.");
    } else {
    behave.act(animal);
    }
    selection = selectBehavior(name);
    }
    System.out.println(animal.name + "을 놔두고 가지마세요...!!");
    System.out.println("종료됨.");
    }

    이렇게 어떤 클래스를 생성하기 위한 메서드를 따로 떼어내는 것을 디자인패턴에서는 '팩토리 메서드(Factory Method)'라고 부른다. 지금은 동물이 2개밖에 없지만 만약 동물이 100개라면 메인에서 if-else를 100번하는것보다는 다른 클래스에서 하는것이 덜 헷갈릴 것이다. Behave도 마찬가지로 팩토리 메서드를 이용할 수 있을까? 생각해보아라. 힘들다면 왜 힘든가? 가능하게 하려면 어떻게 해야하는가? 우리가 가정했던 것 중 틀린것이 있었나? 이 디자인이 무조건 맞는 디자인이 아니다. 따라서 다양한 각도에서 또 요구사항을 보며 어떤 가정이 모듈화를 힘들게하는지 생각해보면 좋을 것이다. 

    추상클래스와 인터페이스

    그렇다면 추상클래스와 인터페이스의 차이는 무엇인가?

    1. 추상클래스는 상속해서 사용한다. 모든 클래스는 1개의 클래스만 상속이 가능하다. 인터페이스는 여러개 구현 가능하다.

    2. 추상클래스는 멤버 변수를 가질 수 있다. 인터페이스는 멤버 변수를 가질 수 없다. 상수만 가질수 있다. (상수는 이후 설명.)

    3. 추상클래스는 보통 상속가능한 계층이 있는 관계인 경우 사용한다. 인터페이스는 기능이다. 이 기능을 가지고 있어야한다면 누구나 구현할 수 있다. 이에대한 내용은 다음 포스트에서 더 설명하도록 하겠다.

    이번 포스트를 이용해 인터페이스를 사용하는 법과 인터페이스를 왜 사용하는지에 대해 알아보았다. 다음 포스트에서는 인터페이스를 활용하는 예인 Validator와 Iterator에 대해 설명하도록 하겠다.


    연습문제:

     getUserChoice, getAnimalName, getPortion, selectBehavior <-얘네를 하나의 클래스로 독립시켜 (예를들어 UserMenu.java) Main클래스와 decouple시킬 수 있는가?


    참고! 코드 전체는 https://gist.github.com/fsoftwareengineer/512f6664afc07d334bdc28a06854832d 에서 확인 가능합니다.

    다음 포스트:15. 자바 final 키워드

    댓글

f.software engineer @ All Right Reserved