Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
JAVA/Java Intermediate(OOP)

11. 추상 클래스와 인터페이스

by Toughie 2024. 3. 19.

 

 

추상클래스와 인터페이스를 다루기 전에, 지난 시간에 배운 다형성을 고려한 간단한 예시를 하나 보자.

이 세상은 대부분 객체로 표현할 수 있다. 

나는 동물을 좋아하기 때문에 자바로 동물을 만들려고 한다.

고양이도 만들고.. 강아지도 만들고.. 

class Cat {
    public void sound() {
        System.out.println("냥");
    }
}

class Dog {
    public void sound() {
        System.out.println("멍");
    }
}

 

지금은 울음소리라는 메서드 하나뿐이지만, 실제로는 더 다양한 메서드가 존재할 것이다. (달린다, 먹는다 등)

그럼 새로운 동물을 만들 때마다 함수를 일일이 다 만들어주기는 너무 귀찮지 않을까..?

 

여기서 추상화의 개념을 떠올려 보자.

고양이와 강아지의 공통점은 무엇일까? 둘 다 동물이고, 둘 다 울음소리를 낸다. (공통되는 특징, 기능이 있다)

이러한 특성을 고려해서 '동물'이라는 더 상위 개념의, 추상화 된 클래스를 만들어 보자. 

class Animal {
    public void sound() {
        System.out.println("동물은 울어요");
    }
}

 

굳이 이렇게 추상화를 해서 상위 클래스를 만든 이유가 뭘까?

강아지나 고양이 객체를 찍어내서 관리를 한다고 가정하면, 둘은 현재 '타입이 다르기 때문'에 여러 제약이 생긴다.

 

같은 타입의 연속적 메모리 공간을 의미하는 배열도 사용하기 까다롭고, 지난 시간에 배운 다형적 참조도 못한다.

따라서 고양이와 개가 Animal이라는 부모 클래스를 상속하도록 하면 다형적 참조 및 같은 타입 취급이 용이해진다!

class Cat extends Animal{
    @Override
    public void sound() {
        System.out.println("냥");
    }
}

class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("멍");
    }
}

public static void main(String[] args) {
    Animal tom = new Cat();
    Animal jerry = new Dog();
    Animal[] animals = {tom,jerry};
}

 

메서드 오버라이딩을 통해 각 동물에 맞게 동작도 잘 수행한다. 

Animal 타입으로 고양이와 강아지를 참조해도 sound()를 호출하면 각 동물의 울음소리가 잘 출력될 것이다.

오버라이딩 된 메서드가 최우선 순위이기 때문이다! 

(Animal 객체로 가서 sound메서드를 확인한 뒤 오버라이딩이 되어 있다면 오버라이딩 된 메서드를 호출함)

 

그런데 현재는 당연히 Animal 클래스의 객체도 생성할 수 있다.

근데 강아지, 고양이, 소와 같은 객체들은 생성하는 게 이상하지 않지만 동물 객체를 생성한다는 게 좀 어색하다. 

이 클래스는 동물을 찍어내는 설계도 입니다! 라고 하면 무슨 동물..?이라는 생각이 들 것이다.

 

또한 Animal 클래스의 메소드를 바로 활용하는 것도 어색하다. '동물은 울어요'를 출력하기보다는

각 동물에 맞게 오버라이딩 하는 것이 바람직하기 때문이다.

 

Animal 클래스는 추상화가 많이 되어 객체를 생성하고 기능을 물려주는 목적보다는,

'하위 클래스에 적절한 기능을 제공하는 가이드' 역할을 수행하기 때문이다. 

 

이러한 상황에서 추상 클래스가 등장했다.


추상 클래스 (abstract Class)

기본적으로 클래스는 다음 3가지를 할 수 있다.

1. 상속 2. 참조변수 선언 3. 객체 생성

하지만 추상 클래스는 1번과 2번은 되지만 객체 생성은 불가능하다. 

 

추상 클래스는 추상화가 많이 되어 실제로 생성되면 안 되는 클래스를 의미한다. 

이름처럼 '추상적인 개념을 제공'하는 클래스로 실체인 인스턴스가 존재하지 않는다.(객체 생성 불가)

 

abstract 키워드를 사용해서 추상 클래스를 선언할 수 있다.

abstract class AbstractAnimal {
	public abstract void sound(); //바디 구현은 상속에서
    
        //구현된 일반 메서드도 포함할 수 있다.
        public void eat() {
            System.out.println("먹이를 먹어요");
        }
}

 

추상 메서드

추상 메서드가 하나라도 있으면 무조건 추상 클래스로 선언해야 한다.

(완성된 설계도가 아니니!)

마찬가지로 반환타입 앞에 abstract를 붙여주면 된다.

 

추상적 개념을 제공하는 메서드이기에, 시그니처만 있고 바디 부분이 없어서 상속부에서 구현을 꼭 해줘야 한다.

(Abstract methods cannot have a body)

즉 추상 클래스를 상속받을 자식 클래스가 반드시 오버라이딩 해야 함을 의미한다. 

 

class Dog extends AbstractAnimal {
    @Override
    public void sound() {
        System.out.println("바우와우!");
    }
}

 

이렇게 추상 클래스를 활용하면 아래와 같은 효과가 있다.

 

1. 추상적 개념을 전달하는 상위 클래스의 인스턴스 생성 방지

2. 필수로 각 클래스에 맞게 오버라이딩 강제


순수 추상 클래스

만약 추상 클래스 내부에 추상 메서드만 존재한다면?

이 클래스에는 실행할 로직이 전혀 없고 다형성을 위한 제약 껍데기일 뿐이다.

(부모의 기능을 물려받으려는 목적의 상속이 아니다.)

 

순수 추상 클래스를 상속할 자식 클래스는 반드시 추상 메서드를 오버라이딩 해야 한다.

 

이는 실생활에서 보면 휴대폰의 충전 단자와 비슷한 맥락으로 볼 수 있다.

휴대폰의 기종에 따라 8 Pin, C type 등 규격이 맞는 단자 케이블을 사용해야 하고

USB 단자 같은 경우에도 USB라는 동일한 규격의 단자를 이용한다.

이를 좀 더 전문적인 용어로 인터페이스라고 한다.

 

인터페이스는 서로 다른 두 개의 시스템이나 장치 사이에서 정보나 신호를 주고받는 경우의 접점이나 경계면을 의미한다.

둘 사이에는 꼭 지켜야 하는 것(제약, 약속)이 있는 것이다.

 

자바에서는 순수 추상 클래스의 개념에다 편리함을 더해서 '인터페이스' 자체를 제공한다.


인터페이스(Interface)

인터페이스는 순수 추상 클래스에 편의성이 더해진 개념이라고 이해할 수 있다.

인스턴스 생성이 불가능하며, 상속 시 모든 메서드를 오버라이딩 해야 한다.

인터페이스의 추상 메서드는 전부 *public abstract이기 때문에 해당 키워드를 생략할 수 있다. 

(*다른 클래스에서 구현해서 사용할 목적이기에)

 

또한 클래스와 달리  

인터페이스는 다중 구현(다중 상속)이 가능하다!

 

인터페이스에는 멤버변수를 포함시킬 수 있는데, 이때 멤버변수는 

public static final이 모두 포함되었다고 간주된다. 즉 상수라는 것이다.

(인터페이스는 인스턴스 생성이 불가하니 인스턴스 멤버 변수를 포함시키지 못하는건 당연하다.)

 

상속의 경우 자식 클래스가 부모 클래스의 변수를 상속받아 사용할 수 있겠지만,

인터페이스는 말 그대로 제약을 거는 껍데기이다. 따라서 멤버 변수가 상속되도록 하는 것이 아닌

필요하다면 바로 상수를 꺼내서 쓰도록 하는 것이다. (상수는 주로 대문자 스네이크 케이스를 사용한다)

 

멤버변수처럼 선언해도 앞에 public static final이 자동으로 붙기 때문에 혼돈하지 말자.

 

public interface InterfaceCar {
    void openDoor(); //public abstract
    void drive();
}

class ElectricCar implements InterfaceCar {
    @Override
    public void openDoor() {
        System.out.println("앱을 통해 문이 열렸습니다.");
    }

    @Override
    public void drive() {
        System.out.println("윙~");
    }
}

public class CarMain {
    public static void main(String[] args) {
        InterfaceCar car1 = new ElectricCar();
        car1.drive();
        car1.openDoor();
    }
}

 

인터페이스는 추상클래스와 달리, 실행 가능한 메서드가 절대 들어갈 수 없다. 

정말 규격과 제약을 제공하는 목적에 충실한 것이다. 

 

default 메서드

*JAVA8부터는 default 메서드가 생겨서 인터페이스 내부에 추상 메서드가 아닌 일반 메서드를 구현할 수 있다.

(오버라이딩도 가능)

하지만 이것은 꼭 필요한 경우에만 사용하도록 하자. (인터페이스의 제약을 완화하기 위해 등장한 개념)

 

public interface InterfaceCar {
    void openDoor(); //public abstract
    void drive();

    default void defaultMethod() {
        System.out.println("디폴트 메서드");
    }
}


인터페이스를 구현한 클래스가 굉장히 많은 상황에서, 인터페이스에 기능을 추가하면

각 클래스별로 구현을 전부 해줘야 한다. 이때 default 메서드를 사용하면 더 효율적으로 문제를 해결할 수 있다.

하지만 default메서드를 통해 다이아몬드 문제가 발생할 수 있다.

 

다이아몬드 문제

다이아몬드 문제는 여러 개의 인터페이스를 상속한 클래스에서 특정 메서드를 호출했을 때 

어떤 메서드를 호출해야 하는지 결정하지 못하고 충돌하는 문제를 의미한다.

 

(인터페이스 1에도 동일한 이름의 default 메서드가 있고 인터페이스 2에도 동일한 이름의 default 메서드가 있는데 인터페이스 1과 2를 구현한 클래스 객체에서 default 메서드를 호출하면 뭘 호출해야 하는가? )

 

자바에서 다중 상속을 지원하지 않는 것도 위와 같은 다이아몬드 문제 때문이다.

(여러 부모 클래스에서 동일한 메서드가 있다면 오버라이딩 하지 않는 이상 어떤 메서드를 호출할지 판단할 수가 없음)


인터페이스의 다중 구현

하지만 인터페이스는 상속과 달리 다중 구현이 가능하다. 

왜? 애초에 인터페이스의 추상 메서드는 무조건 구현(오버라이딩)을 해줘야 하기 때문이다.

따라서 무조건 구현된 함수를 호출하면 되는 것이다.

 

public interface interfaceA {
    void methodA();
    void methodCommon();
}

public interface interfaceB {
    void methodB();
    void methodCommon();
}

public class Child implements interfaceA, interfaceB {
    @Override
    public void methodA() {
        System.out.println("Child.methodA");
    }

    @Override
    public void methodB() {
        System.out.println("Child.methodB");
    }


    @Override
    public void methodCommon() {
        System.out.println("헷갈릴 일이 없지요");
    }
}

 

아래와 같이 다형적 참조와 함께 다이아몬드 문제없이 메서드 호출이 가능하다.

 

    public static void main(String[] args) {
        Child child1 = new Child();
        child1.methodA();
        child1.methodB();
        child1.methodCommon();

        interfaceA child2 = new Child();
        child2.methodA();
        child2.methodCommon();

        interfaceB child3 = new Child();
        child3.methodB();
        child3.methodCommon();

        Child child4 = (Child) child3;
        child4.methodA();
        child4.methodB();
        child4.methodCommon();
    }

 

Child 객체를 만들면 메모리에 interfaceA, interfaceB, Child 총 세 개의 객체가 함께 생성된다. 

각 타입에 맞게 메서드를 찾고 오버라이딩 된 것을 호출하는 순서!


자 이제 클래스 상속과 인터페이스 다중구현을 배웠으니 아래와 같이 간단히 활용을 할 수 있게 되었다.

 

public abstract class AbstractAnimal {
    public void eat() {
        System.out.println("먹이를 먹어요");
    };
    public abstract void sound();
}

public interface Fly {
    void fly();
}

public interface Jump {
    void jump();
}

public class Penguin extends AbstractAnimal implements Fly, Jump {
    @Override
    public void sound() {
        System.out.println("펭귄은 어떻게 울지");
    }

    @Override
    public void fly() {
        System.out.println("펭귄도 날 수 있어 펄럭펄럭");
    }

    @Override
    public void jump() {
        System.out.println("펭귄도 뛸 수 있어 폴 짝");
    }
}

 

public class PenguinMain {
    public static void main(String[] args) {
        AbstractAnimal peng1 = new Penguin();
        Fly peng2 = new Penguin();
        Jump peng3 = new Penguin();
        Penguin penguin = new Penguin();

        peng1.eat();
        peng1.sound();

        peng2.fly();

        peng3.jump();

        penguin.eat();
        penguin.sound();
        penguin.fly();
        penguin.jump();
    }
}

 

클래스 상속을 통해 필요한 기능은 물려받거나 오버라이딩 해서 사용하고,

추가적으로 필요한 기능은 인터페이스를 통해 직접 구현해서 사용할 수 있다.

 

이러한 측면에서 인터페이스는 구현만 하면 해당 기능을 수행할 수 있는 '자격증'으로 이해할 수도 있다.

위 예시에서는 펭귄이 추상메서드 오버라이딩을 통해서 비행자격을 획득한 것이다. 

 

new Penguin()을 하면 AbstractAnimal 인스턴스, Fly 인스턴스, Jump 인스턴스, Penguin 인스턴스가 함께 생기고

참조 타입에 따라 접근 범위가 달라진다고 이해할 수 있다. 

 

메서드 호출은 항상 오버라이딩 된 메서드가 우선순위라는 것, 필요한 경우 다운캐스팅을 할 수 있지만 

런타임 에러가 날 수 있기 때문에 instanceof 등을 활용해서 안전하게 수행할 것 정도를 짚고 넘어가자.

 

 

이렇게 추상 클래스와 인터페이스의 등장 배경,

이들을 활용한 다형적 참조와 메서드 오버라이딩에 대해 알아보았다.


학습 출처

김영한의 실전 자바 - 기본편

'JAVA > Java Intermediate(OOP)' 카테고리의 다른 글

10. 다형성 - 캐스팅과 오버라이딩  (2) 2024.03.17
8. 상속, 오버라이딩, super  (0) 2024.03.11
7. final  (0) 2024.03.07
6. static 정적  (3) 2024.03.07
5. 접근 제어자와 캡슐화  (0) 2024.03.03