이 글은 프로그래머스 - 실무 자바 개발을 위한 OOP와 핵심 디자인 패턴 강의를 정리한 내용입니다.
객체지향의 추상화를 제공해주는 문법인 추상 클래스와 인터페이스에 대해 알아보겠습니다.
우선 추상화 그 자체에 대해 생각해보겠습니다. 추상화는 객체지향의 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
인터페이스입니다.
인터페이스는 부모 클래스 & 자식 클래스와 마찬가지로 다형성을 제공해주는 추상적인 존재이고, 우리는 가급적 코드를 작성할 때 인터페이스와 같은 추상적 존재에 의존하도록 코드를 작성해야합니다.
추상 클래스는 다음과 같은 특징을 가지고 있습니다.
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");
}
}
ExtendedClass
는 AbstractClass
를 상속받은 후 추상 메서드인 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();
}
}
위 코드와 같이 AbstractClass
를 new
키워드로 인스턴스를 생성해주면서, 추상 메서드에 대한 메서드 구현을 함께 넣어주면 즉석에서 추상 메서드가 오버라이딩 된 것과 같은 효과를 줄 수 있습니다.
이렇게 생성한 추상 클래스의 인스턴스는 implementedMethod
메서드는 물론, 오버라이딩 한 abstractMethod
메서드 역시 호출할 수 있습니다.
그리고 다형성이 적용되어 AbstractClass
타입의 레퍼런스 변수에 상속한 ExtendedClass
타입의 인스턴스를 넣을 수 있습니다.
위 코드를 실행해보면 다음과 같은 결과를 확인할 수 있습니다.
AbstractClass implementedMethod
AbstractClass abstractMethod
AbstractClass abstractMethod
AbstractClass implementedMethod
ExtendedClass abstractMethod
ExtendedClass abstractMethod
의도했던대로 잘 출력이됩니다.
하나만 상속받을 수 있는 클래스와 달리, 인터페이스는 여러 개를 구현할 수 있습니다.
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 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
를 호출하는 로직을 확인할 수 있습니다.
이 부분은 추상 클래스에 이미 정의된 메서드가 추상 메서드를 호출하는 것과 동일한 문법입니다.
🤔 아니 그럼... 추상 클래스랑 인터페이스는 똑같은 문법을 제공하는건데... 다중 구현 가능 여부 말고는 뭔 차이야...?
위 특징만 놓고 보면 인터페이스와 추상 클래스가 별 차이가 없다고 느낄 수 있지만, 엄연히 문법적으로 서로 다른 기능을 하고있습니다.
일반적으로 추상 클래스를 사용해야할 상황이 아니라면 인터페이스를 사용해서 추상화 하는 것을 권장합니다.
추상 클래스를 사용해야 하는 상황은 다음과 같습니다.
Object
클래스의 메서드를 오버라이딩 하고 싶은 경우이런 경우를 제외하면 대부분의 경우에서는 인터페이스를 사용하는게 더 적절합니다.
추상 클래스보다 인터페이스가 더 추상적인 존재이기 때문입니다. 추상적인것과 덜 추상적인 것 중 선택할 수 있다면 더 추상적인걸 사용하는게 좋습니다.