[Java/객체지향] 추상 클래스와 인터페이스

양성욱·2023년 9월 18일
0
post-thumbnail

이 글은 프로그래머스 - 실무 자바 개발을 위한 OOP와 핵심 디자인 패턴 강의를 정리한 내용입니다.

객체지향의 추상화를 제공해주는 문법인 추상 클래스와 인터페이스에 대해 알아보겠습니다.

추상화(Abstract)

우선 추상화 그 자체에 대해 생각해보겠습니다. 추상화는 객체지향의 4대 특성 중 하나입니다.

추상화를 활용하지 않았을 경우

MessageSenderV1

public class RealMessageSender {
    public void send() {
        // 실제로 메시지 보내기
        System.out.println("RealMessageSender, 실제로 메시지 전송");
    }
}

public class FakeMessageSender {
    public void send() {
        // 메시지는 안보내고 메시지를 보냈다는 로그만 찍기
        System.out.println("FakeMessageSender, 실제로 메시지 전송되지 않음");
    }
}

public class Client {
    public void someMethod() {
        // 메시지 보내기 전 실행되는 어떤 작업
        
        RealMessageSender messageSender = new RealMessageSender();
        messageSender.send();
    }
}


현재 위 코드에서는 따로 추상화를 진행하지 않고 구현 클래스인 FakeMessageSender를 직접 의존하고 있습니다.

따라서 추상화를 적절하게 적용하지 않은 이 코드는 의존하는 MessageSender의 구현체가 변경될 때 마다 Client의 코드가 함께 변경될것입니다.

추상화를 활용했을 경우

MessageSender

public interface MessageSender {
    void send();
}

public class RealMessageSender implements MessageSender {
    public void send() {
        // 실제로 메시지 보내기
        System.out.println("RealMessageSender, 실제로 메시지 전송");
    }
}

public class FakeMessageSender implements MessageSender {
    public void send() {
        // 메시지는 안보내고 메시지를 보냈다는 로그만 찍기
        System.out.println("FakeMessageSender, 실제로 메시지 전송되지 않음");
    }
}

public class Client {
    private MessageSender messageSender;

    Client(MessageSender messageSender) {
        this.messageSender = messageSender;
    }

    public void someMethod() {
        // 메시지 보내기 전 실행되는 어떤 작업

        messageSender.send();
    }
}

MessageSender라는 인터페이스를 정의 후 이를 구현하는 각각의 구현체를 Client에 주입해주는 로직의 코드입니다.


이렇게 인터페이스를 적극적으로 활용해서 코드를 추상화하면, Client는 구현체를 직접적으로 참조할 필요가 없어지고, 구현체가 변경된다고 해도 외부에서 의존성을 주입해주므로 Client코드는 변경되지 않습니다.

여기서 추상화를 제공해주는것은 MessageSender 인터페이스입니다.

인터페이스는 부모 클래스 & 자식 클래스와 마찬가지로 다형성을 제공해주는 추상적인 존재이고, 우리는 가급적 코드를 작성할 때 인터페이스와 같은 추상적 존재에 의존하도록 코드를 작성해야합니다.

추상 클래스(Abstract Class)


추상 클래스는 다음과 같은 특징을 가지고 있습니다.

  • 추상 클래스는 인스턴스를 생성할 수 없습니다.
  • 일반적으로 하나 이상의 추상 메서드를 포함합니다.

AbstractClass

public abstract class AbstractClass {
    public void implementedMethod() {
        System.out.println("AbstractClass implementedMethod");
        this.abstractMethod();
    }

    abstract public void abstractMethod();
}

추상 클래스는 문법적으로 abstract 키워드를 붙여 선언합니다.

그리고 implementedMethod같이 일반 클래스와 마찬가지로 메서드를 정의해주는 것도 가능합니다.

그리고 abstractMethod처럼 추상 메서드를 정의 후 메서드의 동작은 정의해주지 않을 수도 있습니다. 여기서 정의되지 않은 동작은 이 추상 클래스를 상속받은 클래스에서 구현해주면됩니다.

ExtendedClass

public class ExtendedClass extends AbstractClass {
    @Override
    public void abstractMethod() {
        System.out.println("ExtendedClass abstractMethod");
    }
}

ExtendedClassAbstractClass를 상속받은 후 추상 메서드인 abstractMethod를 구현하고있습니다.

여기서 눈여겨 볼 부분이 하나 있습니다. 추상 클래스의 implementedMethod에서 아직 구현되지 않은 abstractMethod를 호출하고 있는 부분입니다.

아직 구현되지 않은 메서드를 호출하는게 이상하게 느껴질 수 있겠지만, 이 부분이 추상 클래스에 정의된 추상 메서드가 제공하는 강력한 추상화 문법입니다.

이 문법을 활용해서 공통된 로직은 추상 클래스에 미리 정의해두고, 각각의 구현 클래스에서 달라져야할 부분만 추상 메서드를 오버라이딩하여 다르게 작성해줄 수 있습니다.

❗️ 이와 관련된 내용은 템플릿 메서드 패턴(Template Method Pattern)에서 더 자세하게 확인할 수 있습니다.

그리고 추상 메서드가 하나도 없다고 하더라도 해당 클래스 선언부에 abstract 키워드를 붙이면 해당 클래스는 추상 클래스가됩니다.

추상 클래스의 인스턴스 생성

추상 클래스는 인스턴스를 생성할 수 없다고 하였지만, 사실 방법이 하나 있기는 합니다.

Main

public class AbstractClassExampleMain {
    public static void main(String[] args) {
        AbstractClass abstractClass = new AbstractClass() {
            @Override
            public void abstractMethod() {
                // 이렇게 사용하려면 여기서 추상 메서드를 구현해야함
                System.out.println("AbstractClass abstractMethod");
            }
        };

        abstractClass.implementedMethod();
        abstractClass.abstractMethod();

        AbstractClass extendedClass = new ExtendedClass();
        extendedClass.implementedMethod();
        extendedClass.abstractMethod();
    }
}

위 코드와 같이 AbstractClassnew 키워드로 인스턴스를 생성해주면서, 추상 메서드에 대한 메서드 구현을 함께 넣어주면 즉석에서 추상 메서드가 오버라이딩 된 것과 같은 효과를 줄 수 있습니다.

이렇게 생성한 추상 클래스의 인스턴스는 implementedMethod 메서드는 물론, 오버라이딩 한 abstractMethod 메서드 역시 호출할 수 있습니다.

그리고 다형성이 적용되어 AbstractClass타입의 레퍼런스 변수에 상속한 ExtendedClass타입의 인스턴스를 넣을 수 있습니다.

위 코드를 실행해보면 다음과 같은 결과를 확인할 수 있습니다.

AbstractClass implementedMethod
AbstractClass abstractMethod
AbstractClass abstractMethod

AbstractClass implementedMethod
ExtendedClass abstractMethod
ExtendedClass abstractMethod

의도했던대로 잘 출력이됩니다.

인터페이스(Interface)

하나만 상속받을 수 있는 클래스와 달리, 인터페이스는 여러 개를 구현할 수 있습니다.

AnotherInterface

public interface AnotherInterface {
    void anotherMethod();
}

SomeInterface

public interface SomeInterface {
    void someMethod();

    default void defaultMethod() {
//      인터페이스에 메서드 정의 가능
        this.someMethod();  // 정의되지 않은 메서드도 호출 가능
    }
}

ImplementsClass

public class ImplementsClass implements SomeInterface, AnotherInterface {
    @Override
    public void someMethod() {
        System.out.println("ImplementsClass someMethod");
    }

    @Override
    public void anotherMethod() {
        System.out.println("ImplementsClass anotherMethod");
    }
}

인터페이스는 implements 키워드를 붙여서, 클래스가 인터페이스에 선언되어 있는 메서드를 구현해서 사용합니다.

코드에서 확인할 수 있듯이 하나의 클래스는 여러개의 인터페이스를 구현할 수 있고, 인터페이스에 선언된 메서드를 반드시 전부 구현해야합니다.

Main

public class InterfaceExampleMain {
    public static void main(String[] args) {
        SomeInterface someInterface = new ImplementsClass();
        AnotherInterface anotherInterface = new ImplementsClass();

        someInterface.someMethod();
        anotherInterface.anotherMethod();
        
//        아래 코드는 실행불가
//        someInterface.anotherMethod();
//        anotherInterface.someMethod();
    }
}

추상 클래스와 마찬가지로 다형성에 의해 각각의 구현한 인터페이스 타입의 레퍼런스 변수에 인스턴스를 생성해서 넣을 수 있습니다. 그리고 당연하지만 레퍼런스 변수의 타입에 따라 호출할 수 있는 메서드는 제한됩니다.

Default Method

다음 코드를 살펴봅시다.

default void defaultMethod() {
//        인터페이스에 메서드 정의 가능
        this.someMethod();  // 정의되지 않은 메서드도 호출 가능
}

위에서 예제 코드로 사용한 SomeInterface에는 default 키워드가 붙은 메서드가 존재합니다. 심지어 인터페이스 내부에 선언된 메서드인데 구현 부분이 존재합니다.

Default Method는 인터페이스에서도 메서드를 정의할 수 있게 해주는 문법입니다. default method문법은 Java8부터 도입되었습니다.

InterfaceExampleMain

package study.ooppractice.part01.abstractclass_and_interface.interfaceex;

public class InterfaceExampleMain {
    public static void main(String[] args) {
        SomeInterface someInterface = new ImplementsClass();
        AnotherInterface anotherInterface = new ImplementsClass();

        someInterface.someMethod();
        anotherInterface.anotherMethod();

        // 아래 코드는 실행 불가!
//        someInterface.anotherMethod();
//        anotherInterface.someMethod();

        someInterface.defaultMethod();
    }
}

InterfaceExampleMain 실행 결과

ImplementsClass someMethod
ImplementsClass anotherMethod
ImplementsClass someMethod

실제로 잘 실행되는 것을 확인할 수 있습니다.

그리고 defaultMethod 내부를 살펴보면 아직 구현되지 않은 someMethod를 호출하는 로직을 확인할 수 있습니다.
이 부분은 추상 클래스에 이미 정의된 메서드가 추상 메서드를 호출하는 것과 동일한 문법입니다.

🤔 아니 그럼... 추상 클래스랑 인터페이스는 똑같은 문법을 제공하는건데... 다중 구현 가능 여부 말고는 뭔 차이야...?

위 특징만 놓고 보면 인터페이스와 추상 클래스가 별 차이가 없다고 느낄 수 있지만, 엄연히 문법적으로 서로 다른 기능을 하고있습니다.

추상 클래스 VS 인터페이스

일반적으로 추상 클래스를 사용해야할 상황이 아니라면 인터페이스를 사용해서 추상화 하는 것을 권장합니다.

추상 클래스를 사용해야 하는 상황은 다음과 같습니다.

  • 인스턴스 변수가 필요한 경우
  • 생성자가 필요한 경우
  • Object 클래스의 메서드를 오버라이딩 하고 싶은 경우

이런 경우를 제외하면 대부분의 경우에서는 인터페이스를 사용하는게 더 적절합니다.

추상 클래스보다 인터페이스가 더 추상적인 존재이기 때문입니다. 추상적인것과 덜 추상적인 것 중 선택할 수 있다면 더 추상적인걸 사용하는게 좋습니다.

profile
개발의 신이시여... 제게 집중할 수 있는 ㅎ... 네? 맥주요?

0개의 댓글