[C++ to Java] 3. 상속, 다형성, 인터페이스
Java에서의 상속, 다형성에 대해 알아 보자. 2021-12-25

Inheritance, Polymorphism

안녕하세요? Justkode 입니다. 오늘은 상속, 다형성, 인터페이스에 대해서 알아 보는 시간을 가져 보도록 하겠습니다.

상속 (Inheritance)

먼저 상속에 대해서 알아 보겠습니다. 상속은 기존의 클래스를 재사용하여 새로운 클래스를 작성 하는 것입니다. 이를 통해 우리는 적은 양의 코드를 이용하여, 새로운 클래스를 작성 할 수 있고, 공통의 수정 사항이 있을 때 편하게 수정 할 수 있다는 장점이 있습니다. 즉, 유지 보수 측면에서 이득을 볼 수 있다는 것입니다.

어떤 것들이 상속이 될 수 있을 까요? RPG 게임을 예시로 들어 보면, Player 클래스가 있다고 가정 하겠습니다. 하지만, Player 마다, 직업을 가질 수도 있겠죠? 그러면, Player 클래스를 상속 받는 Warrior 클래스, Magician 클래스 등등이 또 생길 수도 있겠네요.

자, 그러면 한 번 위의 사례를 예시로 들어 볼까요? 저번까지 사용 했던, Player 클래스를 사용 하겠습니다. 해당 클래스에 추상 메서드void attackPlayer(Player p) 를 추가 하겠습니다.

Player.java

abstract public class Player {
    private String userName;
    private int healthPoint;
    private int attackPoint;
    private final int USER_ID;
    static int objectNum;

    static {
        objectNum = 0;
    }

    {
        USER_ID = objectNum++;
    }

    Player(String userName, int healthPoint, int attackPoint) {
        setUserName(userName);
        setHealthPoint(healthPoint);
        setAttackPoint(attackPoint);
        printUserInfo();
    }

    Player(Player p) {
        this(p.userName, p.healthPoint, p.attackPoint);
    }

    public String getUserName() {return this.userName;};
    public int getHealthPoint() {return this.healthPoint;};
    public int getAttackPoint() {return this.attackPoint;};

    public void setAttackPoint(int attackPoint) {
        if (attackPoint < 0)
            return;
        this.attackPoint = attackPoint;
    }

    public void setHealthPoint(int healthPoint) {
        if (healthPoint < 0)
            return;
        this.healthPoint = healthPoint;
    }

    public void setUserName(String userName) {
        if (userName.length() < 2)
            return;
        this.userName = userName;
    }

    public void printUserInfo() {
        System.out.println("========================");
        System.out.println("플레이어 이름: " + this.userName);
        System.out.println("체력: " + this.healthPoint);
        System.out.println("공격력: " + this.attackPoint);
        System.out.println("유저 아이디: " + this.USER_ID);
        System.out.println("========================");
    }

    abstract public void attackPlayer(Player p);

    public static void printCurrentObjectNum() {
        System.out.println("현재 생성된 객체의 갯수는 " + objectNum + "개 입니다.");
    }
}

그 다음으로 Player를 상속 받은, Magician, Warrior 클래스를 만들어 보겠습니다. 클래스는 class 클래스명 extends 상속받을클래스명 을 이용하여, 부모 클래스를 상속 받을 수 있습니다.

우리는 자식 클래스에서, 생성자추상 메서드오버라이딩하여 구현 한 다음에 자식 클래스 객체를 만들 수 있습니다.

super()

super()를 이용 하여, 부모 클래스의 생성자를 사용 할 수 있습니다. 그 전에 super에 대해서 알아 놓을 필요가 있는데, super자식 클래스에서 부모 클래스로 부터 상속받은 멤버를 참조하는데 사용되는 참조 변수 입니다. 부모 클래스의 멤버 변수도 this를 이용하여 접근 가능 하지만, 부모 클래스자식 클래스멤버 변수 이름이 같으면, thissuper를 이용해 구분 합니다.

Warrior(String userName, int healthPoint, int attackPoint) {
    super(userName, healthPoint, attackPoint);
    System.out.println("직업 전사");
    System.out.println("========================");
}

오버라이딩 (Overriding)

오버라이딩부모 클래스로부터 상속받은 메서드의 내용을 자식 클래스에서 용도에 맞게 변경하는 것 입니다.

오버라이딩이 성립하기 위해서는 다음과 같은 조건들을 만족 하여야 합니다.

  • 메서드의 이름 일치
  • 메서드의 매개변수 일치
  • 메서드의 반환 타입 일치
  • 조상 클래스의 메서드보다 더 좁은 범위의 접근 제어자를 사용하지 않음.
  • 조상 클래스의 메서드보다 더 많은 예외를 선언 할 수 없음.
  • 인스턴스 메서드를 static 메서드로 변경 할 수 없음.

오버 라이딩은 그저 자식 메서드에서 작성만 해 주면 됩니다.

@Override
public void attackPlayer(Player p) {
    System.out.println("========================");
    System.out.printf("Player %s가 Player %s에 강력한 물리 공격%n", this.getUserName(), p.getUserName());
    System.out.printf("Player %s는 %d의 피해를 입음");
    p.setHealthPoint(Math.max(0, p.getHealthPoint() - this.getAttackPoint()));
    System.out.printf("Player %s의 남은 체력: %d", p.getUserName(), p.getHealthPoint());
    System.out.println("========================");
}

@Override 같은 건 뭔가요?

@Java Annotation으로, 자바에서 사용하는 주석 입니다. 일반 주석과 다르게, 특별한 의미 및 기능을 Java 내에서 수행 할 수 있습니다. 더 정확하게 말하면, 메타 데이터의 역할을 한다고 볼 수 있습니다. 이게 언제 컴파일이 될 지.. 서버 개발 시 이 함수를 어떤 URI에 넣을지 등.. 명시해 주는 역할도 합니다. 대부분 함수 및 클래스 앞에 작성 합니다.

다형성 (Polymorphism)

다형성에 대해서 비유를 해보면, 사실 전사도, 마법사도 플레이어라는 개념으로 묶일 수 있습니다. 우리가 위에서 상속을 통해서 실제로 구현 한 것 처럼요. 다형성을 조금 더 기술적으로 풀어서 이야기 하자면, 부모 클래스 타입의 참조변수로 자식 클래스의 인스턴스를 참조 할 수 있게 하는 것입니다. 사실 위에서 public void attackPlayer(Player p) 함수도 다형성을 이용 한 것 입니다. Player 타입의 참조 번수로 Warrior, Magician을 참조할 수 있기 때문입니다. 다음과 같이 말이죠.

Warrior warrior = new Warrior("S2지존전사S2", 100, 10);
Magician magician = new Magician("S2최강법사S2", 50, 20);
warrior.attackPlayer(magician);
magician.attackPlayer(warrior);

코드 정리

위 코드를 모아서, 두 Player가 전투를 하는 코드를 만들어 보겠습니다.

Warrior.java

public class Warrior extends Player {
    Warrior(String userName, int healthPoint, int attackPoint) {
        super(userName, healthPoint, attackPoint);
        System.out.println("직업 전사");
        System.out.println("========================");
    }

    Warrior(Warrior p) {
        super(p);
        System.out.println("직업 전사");
        System.out.println("========================");
    }

    @Override
    public void attackPlayer(Player p) {
        System.out.println("========================");
        System.out.printf("Player %s가 Player %s에 강력한 물리 공격%n", this.getUserName(), p.getUserName());
        System.out.printf("Player %s는 %d의 피해를 입음%n", this.getUserName(), this.getAttackPoint());
        p.setHealthPoint(Math.max(0, p.getHealthPoint() - this.getAttackPoint()));
        System.out.printf("Player %s의 남은 체력: %d%n", p.getUserName(), p.getHealthPoint());
        System.out.println("========================");
    }
}

Magician.java

public class Magician extends Player {
    Magician(String userName, int healthPoint, int attackPoint) {
        super(userName, healthPoint, attackPoint);
        System.out.println("직업 마법사");
        System.out.println("========================");
    }

    Magician(Magician p) {
        super(p);
        System.out.println("직업 마법사");
        System.out.println("========================");
    }

    @Override
    public void attackPlayer(Player p) {
        System.out.println("========================");
        System.out.printf("Player %s가 Player %s에 치명적인 마법 공격%n", this.getUserName(), p.getUserName());
        System.out.printf("Player %s는 %d의 피해를 입음%n", this.getUserName(), this.getAttackPoint());
        p.setHealthPoint(Math.max(0, p.getHealthPoint() - this.getAttackPoint()));
        System.out.printf("Player %s의 남은 체력: %d%n", p.getUserName(), p.getHealthPoint());
        System.out.println("========================");
    }
}

Main.java

public class Main {
    static public void main(String[] args) {
        Warrior warrior = new Warrior("S2지존전사S2", 100, 10);
        Magician magician = new Magician("S2최강법사S2", 50, 20);
        warrior.attackPlayer(magician);
        magician.attackPlayer(warrior);
    }
}
========================
플레이어 이름: S2지존전사S2
체력: 100
공격력: 10
유저 아이디: 0
========================
직업 전사
========================
========================
플레이어 이름: S2최강법사S2
체력: 50
공격력: 20
유저 아이디: 1
========================
직업 마법사
========================
========================
Player S2지존전사S2가 Player S2최강법사S2에 강력한 물리 공격
Player S2지존전사S2는 10의 피해를 입음
Player S2최강법사S2의 남은 체력: 40
========================
========================
Player S2최강법사S2가 Player S2지존전사S2에 치명적인 마법 공격
Player S2최강법사S2는 20의 피해를 입음
Player S2지존전사S2의 남은 체력: 80
========================

Interface

인터페이스추상 클래스 종류의 하나 입니다. 추상 클래스 중, 가장 추상화 정도가 높다 라고 이야기 할 수 있습니다. 왜냐하면, 인터페이스는 오로지 추상 메서드상수만을 멤버로 가질 수 있기 때문 입니다.

이를 통해 우리는 다음과 같은 장점을 꾀할 수 있습니다.

  • 개발시간을 단축할 수 있습니다.
  • 표준화가 가능합니다.
  • 서로 관계 없는 클래스들 끼리의 관계를 맺어 줄 수 있습니다.
  • 독립적인 프로그래밍이 가능 합니다.
  • 인터페이스 타입의 참조변수로 인스턴스를 참고 할 수 있습니다. 다형성이 존재 합니다.

작성은 다음과 같은 형태로 작성 할 수 있습니다. 단, 다음과 같은 조건을 만족 하면서 이루어 져야 합니다.

  • 모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있음.
  • 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다. 단, JDK 1.8 이상에서는 staticdefault는 예외로 둔다.
interface PlayerHealthPointAct {
    public static final int HEALING_HEALTH_POINT = 10;
    public abstract void healing();
}

또한, 인터페이스상속도 가능 합니다. 클래스와 달리 다중 상속이 가능 합니다.

interface PlayerHealthPointAct {
    public static final int HEALING_HEALTH_POINT = 10;
    public abstract void healing();
}

interface PlayerBuffAct {
    public static final int BUFF_ATTACK_POINT = 10;
    public abstract void buff();
}

public interface PlayerAct extends PlayerHealthPointAct, PlayerBuffAct {
    public void useAllSkill();
}

이는, 클래스의 상속과 별개로, implements를 이용하여, 서로 관계가 없는, 다른 클래스간에 표준화를 시켜 줄 수 있습니다.

public class Warrior extends Player implements PlayerAct {
    Warrior(String userName, int healthPoint, int attackPoint) {
        super(userName, healthPoint, attackPoint);
        System.out.println("직업 전사");
        System.out.println("========================");
    }

    Warrior(Warrior p) {
        super(p);
        System.out.println("직업 전사");
        System.out.println("========================");
    }

    @Override
    public void attackPlayer(Player p) {
        System.out.println("========================");
        System.out.printf("Player %s가 Player %s에 강력한 물리 공격%n", this.getUserName(), p.getUserName());
        System.out.printf("Player %s는 %d의 피해를 입음%n", this.getUserName(), this.getAttackPoint());
        p.setHealthPoint(Math.max(0, p.getHealthPoint() - this.getAttackPoint()));
        System.out.printf("Player %s의 남은 체력: %d%n", p.getUserName(), p.getHealthPoint());
        System.out.println("========================");
    }

    @Override
    public void healing() {
        System.out.println("========================");
        System.out.printf("Player %s가 %d 포인트 만큼 회복을 시도 합니다.%n", this.getUserName(), this.HEALING_HEALTH_POINT);
        this.setHealthPoint(this.getHealthPoint() + this.HEALING_HEALTH_POINT);
        System.out.printf("체력이 %d가 되었습니다.%n", this.getHealthPoint());
        System.out.println("========================");
    }

    @Override
    public void buff() {
        System.out.println("========================");
        System.out.printf("Player %s가 %d 포인트 만큼 공격력을 증가 시킵니다.%n", this.getUserName(), this.BUFF_ATTACK_POINT);
        this.setAttackPoint(this.getAttackPoint() + this.BUFF_ATTACK_POINT);
        System.out.printf("공격력이 %d가 되었습니다.%n", this.getAttackPoint());
        System.out.println("========================");
    }

    @Override
    public void useAllSkill() {
        this.healing();
        this.buff();
    }
}

1회성 인터페이스 구현 하기

특정 인터페이스를 구현 하는 객체를, 단 한번만 사용 하는 경우가 생길 수 있습니다. 나중에 Thread에 대해서 배울 텐데, Thread을 생성할 때 사용 하는 Runnable 인터페이스가 대표적 입니다. 우리는 이를 익명 객체를 생성 하여, 단 한 번만 인터페이스 구현체를 사용 하는 경우를 커버 할 수 있습니다. 예를 들어 보여드리자면 다음과 같습니다.

interface TestInterface {
    public abstract void printSomething();
}

public class Main {
    public static void main(String[] args) {
        TestInterface testInterface = new TestInterface() {
            @Override
            public void printSomething() {
                System.out.println("printSomething");
            }
        };
        
        testInterface.printSomething();
    }
}
printSomething

마치며

다음 시간에는 Package, Import에 대해서 알아 보는 시간을 가져 보겠습니다.