C++에서 배우는 자바-클래스(2)

추상 클래스

abstract를 사용하여 추상 클래스를 선언할 수 있다. 프로그램을 하나라도 개발해봤다면 어떤 메서드가 어떤 기능을 구현할지 상당히 모호하다는 것을 느껴봤을 것이다. java에서는 이러한 프로그램의 설계를 돕고 클래스 간의 관계를 맺어줄 수 있는 추상클래스, 인터페이스 등이 존재한다.
추상 클래스의 선언은 간단하다. class 앞에 abstract를 붙이면 된다. 추상 클래스는 그 자체로 인스턴스의 선언이 불가능하고 대신 상속을 통해 선언부를 오버라이딩하여 사용함으로써 어떠한 기능을 구현해야 하는지에 대한 가이드라인이 되어줄 수 있다.
예를 들어

abstract class Player {
    abstract void play(int pos);
    abstract void stop();
}  

class AudioPlayer extends Player {
    void play(int pos) {
        //implements...
    }
    void stop() {
        //implements...
    }
}

위의 코드를 살펴보면 2개의 추상 메서드를 가진 추상 클래스 Player를 선언했다.
Player 종류의 클래스는 play와 stop의 기능을 할 것이라고 선언하는 것과 마찬가지이다.
아래에서 Player 클래스를 상속받은 AudioPlayer 클래스가 두 추상 메서드를 오버라이딩하여 사용한다. 이처럼 추상 클래스를 상속받았다면 반드시 추상 클래스 내의 추상 메서드를 모두 구현해주어야 일반 클래스처럼 사용할 수 있다.
추상 메서드를 하나라도 구현하지 않는다면 자식 또한 추상 클래스가 되어야 한다.
추상 클래스는 위와 같이 Top-down 방식으로 공통된 부분을 정해놓고 이후 클래스를 작성해도 되지만, Bottom-up 방식으로 여러 클래스의 공통된 부분을 추출하여 추상 클래스를 작성할 수도 있다. 예를 들어

class Marine {
    int x,y;
    void move(int x, int y) {}
    void stop() {}
    void stimpack() {}
}  

class Tank {
    int x,y;
    void move(int x, int y) {}
    void stop() {}
    void changemod() {}
}  

class Dropship {
    int x,y;
    void move(int x, int y) {}
    void stop() {}
    void load() {}
    void unload() {}
}

매우 익숙한 게임 내의 유닛들이다. 코드를 살펴보면 각 클래스가 공통적으로 현재 위치인 x, y와 move() stop() 메서드를 가지고 있는 것을 알 수 있다. 따라서 공통된 부분을 추출하여 추상 클래스로 작성해보면

abstract class Unit {
    int x,y;
    abstract void move(int x, int y);
    void stop() {}
}

class Marine extends Unit {
    void move(int x, int y) {}
    void stimpack() {}
}  

class Tank extends Unit {
    void move(int x, int y) {}
    void changemod() {}
}  

class Dropship extends Unit {
    void move(int x, int y) {}
    void load() {}
    void unload() {}
}

위와 같은 코드가 완성이 되었다. stop()은 모든 클래스가 공통이니 추상 메서드가 아닌 일반 메서드로 빼두었고, 지상 유닛과 공중 유닛의 move()는 다르므로 move()는 각자 클래스 내에서 구현하도록 추상 메서드로 작성하였다.
앞으로 이 Unit 추상 클래스는 여러 유닛들을 만들 때 재활용할 수 있고, 유닛들의 클래스를 만들 때 move() 메서드를 빼먹지 않고 구현하도록 강제하는 효과도 있을 것이다.
추상 클래스보다 더 추상화 정도가 높은 인터페이스에 대해서도 알아보자.

인터페이스(Interface)

인터페이스는 일종의 추상 클래스이지만 추상 클래스보다 추상화 정도가 더 높아서 일반 메서드나 멤버 변수를 가질 수 없다. 오직 추상 메서드와 상수만을 가질 수 있는 클래스이다. 인터페이스의 선언은 간단하다. class 대신 interface를 사용하면 된다. 예를 들어

interface ec {
    public static final int cons=1;
    public abstract void tmp();
}

인터페이스의 법칙은 다음과 같다.

  • 모든 멤버 변수는 public static final이다.
  • 모든 메서드는 public abstract이다.
  • 이는 생략 가능하다.
    인터페이스는 인터페이스끼리만 상속받을 수 있으며 다중 상속을 허용한다.
interface Movable {
    void move(int x, int y);
}
interface Attackable {
    void attack(Unit u);
}
interface Fightable extends Movable, Attackable {}

위에서 작성했던 추상클래스 코드의 연장선이다. 두 인터페이스로부터 다중상속 받음으로써 move(), attack() 메서드를 가지게 된다.
인터페이스도 추상 클래스의 일종이기 때문에 인스턴스를 가질 수 없고 다른 일반 클래스들이 상속받음으로써 사용이 가능하다. 인터페이스는 클래스와 달리 extends 대신 implements를 사용한다.

class Fighter implements Fightable {
    public void move(int x, int y) {}
    public void attack(Unit u) {}
}

위의 Fightable 인터페이스를 상속받은 Fighter 클래스이다. 만약 둘 중 하나라도 구현하지 않는다면 Fighter는 추상 클래스로 선언되어야 한다.
상속을 받아 메서드를 오버라이딩 할 때엔 접근 제어자를 같거나 더 넓게 지정해야 한다는 것을 기억할 것이다. 따라서 public abstract가 생략된 것이므로 move와 attack 모두 접근 제어자를 public으로 해주어야 한다.

상속을 받았을 때 부모 클래스의 참조변수로 자식 클래스의 인스턴스를 참조할 수 있었던 것을 기억할 것이다. 인터페이스 또한 인터페이스의 참조 변수로 인터페이스를 상속받은 클래스의 인스턴스를 참조할 수 있다. 예를 들어

Fightable f=new Fighter();

위와 같이 사용 가능하고 이러한 특성을 이용하여 서로 관계가 없는 클래스간의 관계를 만들어주는 것이 가능하다. 예를 들어

class Marine extends GroundUnit {
    void move(int x, int y) {}
    void stimpack() {}
}  

class SCV extends GroundUnit {

}

class Tank extends GroundUnit {
    void move(int x, int y) {}
    void changemod() {}
}  

class Dropship extends AirUnit {
    void move(int x, int y) {}
    void load() {}
    void unload() {}
}

위와 같은 상황에서 기계 유닛을 수리하는 repair() 메소드를 만들려 한다. 하지만 매개변수로 GroundUnit을 받자니 기계가 아닌 marine도 수리를 받게 되고, 아니면 매개변수로 각 클래스들을 받는 메서드를 여러 개 만들어야 한다.
위와 같은 상황은 다음과 같이 해결 가능하다.

interface repairable {} 
class Marine extends GroundUnit {
    void move(int x, int y) {}
    void stimpack() {}
}  

class SCV extends GroundUnit implements repairable {

}

class Tank extends GroundUnit implements repairable {
    void move(int x, int y) {}
    void changemod() {}
}  

class Dropship extends AirUnit implements repairable {
    void move(int x, int y) {}
    void load() {}
    void unload() {}
}

void repair(repairable r);

빈 인터페이스인 repairable을 선언하여 수리 가능한 유닛마다 상속시켜 주었다. 이처럼 인터페이스와 상속을 통해 다형성을 구현함으로써 코드를 간결하게 하면서 여러 상황에 대응할 수 있다.
또한 클래스 대신 클래스에 상속된 인터페이스 참조변수를 사용함으로써 클래스 간의 직접적인 관계를 간접적인 관계로 바꾸어 클래스 내부가 변경되더라도 다른 클래스에서는 기존 코드를 그대로 사용할 수 있도록 구현할 수도 있다.
인터페이스는 여러모로 ‘외부에서는 내부를 알 필요가 없고, 내부가 변경되어도 외부에는 영향이 없는’ 객체지향의 추상화 원리를 그대로 구현한 듯한 기능이란 생각이 든다.

디폴트 메서드

인터페이스에 새 메서드를 추가한다는 것은, 즉 새로운 추상 메서드를 추가한다는 것이고, 이 인터페이스를 상속받는 모든 클래스가 새로운 추상 메서드를 구현하도록 변경되어야 한다는 것이다.
보통 일이 아니기 때문에 이 디폴트 메서드(default method)가 추가되었다.
디폴트 메서드는 메서드 앞에 default를 붙이는 것으로 선언되고, 추상 메서드가 아니기 때문에 몸통 부분이 있다. 추상 메서드가 아니기 때문에 부모 클래스에 새로운 메서드가 추가된 것과 동일한 효과를 낸다. 예를 들어

interface Movable {
    void move(int x, int y);
    default void f_move(int x, int y) {
        //implements...
    }
}
interface Attackable {
    void attack(Unit u);
}
interface Fightable extends Movable, Attackable {}

와 같이 사용 가능하다.

내부 클래스

클래스의 내부에서도 클래스를 선언할 수가 있다. 이를 내부 클래스라고 하는데 두 클래스가 매우 긴밀한 관계일 때, 내부에 선언될 클래스가 다른 클래스에선 사용되지 않을 때에 한해서 유용하게 사용이 가능하다. 내부 클래스를 선언하면 두 클래스간의 접근이 쉬워지고 외부에는 불필요한 클래스를 노출하지 않을 수 있는 장점이 있다. 예를 들어

class Outer {
    class inner {
        int iv=0;
        //static int cv=1 은 인스턴스 클래스이기에 불가능
    }
    static class sinner {
        int iv=2;
        static int cv=2;
    }
    void method() {
        class local_inner {
            int iv=3;
            //static int cv=2는 지역 클래스므로 불가
        }
    }
}

위와 같이 사용이 가능하다.

익명 클래스

정말 한 번만 사용하고 다시 사용하지 않을 클래스라면 부모 클래스, 또는 인터페이스를 이용해 일회용 클래스인 익명 클래스를 선언할 수 있다. 만약 위의 Fightable 인터페이스를 매개변수로 받는 메소드 match(Fightable f)가 있고 일회용 클래스가 필요하다면

void match(new Fightable() {
    void move(int x, int y) {

    }
    void attack(Unit u) {

    }
})

위와 같이 작성하여 일회용 익명 클래스를 생성할 수 있다.