본문 바로가기
JAVA

[Java] 인터페이스와 추상 클래스 어떻게 구분해서 쓸까?

by solar_youn 2022. 3. 31.

오늘은 추상 클래스(Abstract Class)인터페이스(Interface)에 대해 알아보도록 하겠습니다. 다만, 추상 클래스와 인터페이스 각각에 대한 상세한 내용은 다루지 않고, 어떤 상황에서 특정 추상화를 사용해야 할 지에 초점을 맞춰보겠습니다. 글을 읽기 전, 우선 추상 클래스와 인터페이스는 존재 목적이 다르다는 것을 명심해주세요!

추상화

추상 클래스와 인터페이스에 대해 알아보기 전에, 추상화에 대해 알아볼 필요가 있습니다.

추상화란 무엇일까요? 추상화는 객체 지향 프로그래밍의 핵심 기능 중 하나입니다. 추상화는 기능의 내부 구현을 숨기고 사용자에게 기능만 보여주는 것을 의미합니다. 추상화를 위해 공통의 속성이나 기능을 묶어 클래스로 만듦으로써 불필요한 부분을 생략할 수 있으며, 각 객체의 주요한 속성에 집중할 수 있습니다.

 

추상화에서는 함수를 작게 만드는 것이 핵심이며 함수가 하는 일이 하나여야 합니다. 이때 하나의 역할은 이름을 가지고 무슨 역할을 하는지 명확하게 파악되어야 합니다. 만약 너무 복잡해져서 함수가 커진다면 추상화를 제대로 하지 않았다는 뜻입니다.

 

반복문을 예시로 전체를 묶어서 하나의 함수로 만드는 것도 반복문 내에서 무슨 일이 벌어지는지 쉽게 나타내므로 추상화 작업의 일종입니다. 조금 더 큰 구조로 바라본다면 비행기, 기차, 자동차는 모두 이동 수단으로 추상화할 수 있으며, 이 때 공통적으로 move() 와 같은 메서드를 추상화할 수 있습니다.

 

이러한 추상화는 코드의 재사용성, 가독성 향상, 생산성 증가, 에러 감소로 귀결될 수 있으며 이는 결국 추후 유지보수 시간 단축에 용이합니다.

 

추상화에 대한 기본 개념을 알았으니, 이제 추상 클래스와 인터페이스에 대해 알아보도록 하겠습니다.

추상 클래스(Abstract Class)란?

기본적으로 추상클래스는 일반(concrete) 클래스와 비슷합니다. 단지, 추상 메서드를 선언해 상속을 통해서 자손 클래스에서 완성하도록 유도하는 클래스입니다. 기본적인 클래스를 설계도에 비유하면 추상 클래스는 특정 부분이 구현되지 않은 미완성 설계도로 볼 수 있습니다. 미완성 설계도만으로는 온전한 상품을 만들 수 없습니다. 이는 즉, 상속을 위한 클래스이기 때문에 추상 클래스만으로는 객체 생성이 불가능함을 뜻합니다.

인터페이스(Interface)란?

좀 전의 추상 클래스가 미완성 설계도였다면, 인터페이스는 가장 기본이 되는 틀만 제공하는 즉, 밑그림만 있는 기본 설계도로 볼 수 있습니다. 기본적으로 메서드는 선언부(declaration)와 구현부(definition)으로 구분되는데, 인터페이스는 선언부’만’ 작성한 클래스입니다. 인터페이스를 활용해 추상화를 하는 이유는 소프트웨어에 변경이 발생할 경우 소스 코드에 변경을 최소화함으로써 유지보수 비용을 줄이고, 변화에 빠르게 대응하기 위함입니다.

추상 클래스와 인터페이스에 대한 이론적인 내용보다는 언제 무엇을 사용할 지에 초점을 맞추도록 하겠습니다.

그래서 어떻게 구분하는데?

추상 클래스나 인터페이스 둘 다 공통적으로 추상 메서드를 사용해서 구현이 가능합니다. 그렇다면 왜 Java에서는 굳이 2가지로 나누어서 사용할까요? 추상 클래스는 인터페이스의 역할을 할 수도 있으며, 인터페이스 또한 자바 8에서 Default Method 가 추가되어 마치 추상 클래스처럼 작성될 수 있는데...?

지금부터 두 추상화를 구분하는 방법에 대해 말씀드리도록 하겠습니다.

1. 사용 용도

가장 중요하게 고려해야할 부분이 사용 용도입니다.

간단하게 말씀드리면,

추상클래스는 어떠한 것을 추상화 해둔 것으로 내부의 기능을 확장하기 위해 존재합니다.

인터페이스는 어떠한 약속을 정의해둔 것으로 정의된 함수들의 구현을 강제하기 위해 존재합니다. 구현을 강제하여 인터페이스를 구현한 객체의 같은 동작을 보장할 수 있습니다.

이는 아래 예제를 통해서 더 살펴보도록 하겠습니다.

2. 공통된 기능 사용

모든 클래스를 인터페이스를 통해 기본 틀을 구성한다면 공통적인 기능들도 모든 클래스에서 Override를 통해 재정의 해야하는 번거로움이 있습니다. 하여 이러한 공통된 기능이 필요하다면 추상 클래스에서 일반 메서드를 사용할 수 있다는 것을 이용해 자식 클래스에서 사용하도록 하면 될까요? 아닙니다. 자바는 기본적으로 하나의 상속만을 허용합니다. 즉 각각 다른 추상 클래스를 상속하는데 공통된 기능이 필요하다면 해당 기능을 인터페이스로 작성해서 구현해야 합니다.

추상 클래스의 다중 상속 (불가능)

무슨 의미인지 예시를 통해 알아보도록 하겠습니다.

class Vehicle extends Car, Plane {
	
    @Override
    public void go() {
    	super.drive();
    }
}

자바는 다중 상속을 지원하지 않기 때문에 위와 같은 코드는 compile이 불가능합니다. 만일 두 클래스 Car, Plane 모두 drive() 메서드를 가지고 있다면 어느 클래스로부터 해당 메서드를 호출해야할 지 알 수 없으므로, Java에서는 다중 상속이 금지되어 있습니다.

인터페이스의 다중 상속 (가능)

class Car implements Vehicle, Engine {
	
    @Override
    public void drive() {
    	...
    }
}

추상 클래스와 다르게 인터페이스에서는 위와 같이 마치 여러 개의 인터페이스를 받아 구현할 수 있습니다. 인터페이스의 구현 클래스에서는 재정의가 필수로 요구되기 때문에 충돌이 없으므로 정상적으로 작동 가능합니다.

인터페이스는 다중 상속이 가능하고 default 메서드를 정의할 수 있으니 인터페이스를 우선시 할까?

우선 정답부터 말하자면 반은 맞고 반은 틀렸습니다. 인터페이스를 우선적으로 고려하는 것은 좋은 방법이나 default 메서드를 이런 경우를 위해 사용해서는 안됩니다.

디폴트 메서드(Default Method)

Java 8에서 default 메서드가 추가된 이유는 바이너리 호환성, 즉 기존에 존재하던 코드(legacy)들이 잘 돌아갈 수 있도록 만들어진 것이지 다중 상속을 허용하기 위해 만들어진 것이 아닙니다.

 

예를 들어, 자바 8 이전에 작성된 SomeInterface() 가 있다고 칩시다. 해당 인터페이스는 자바 8 이전부터 존재하던 코드들이라 기능적으로 부족한 부분이 많아 자바가 새롭게 변화할 때마다 (ex. stream) 불가피하게 새로운 메서드들이 필요할 상황이 생깁니다. 하여, 모든 구현체에 새로운 메서드에 대한 재정의가 필요해지는데, 이때 default 메서드를 이용한다면 사용자의 코드를 깨뜨리지 않고 이미 사용 중인 기존 인터페이스에 메서드를 추가할 수 있습니다. 이에 대한 대표적인 예가 java.util.Collection<T>.stream() 입니다.

 

즉, default 메서드를 통해 예전 코드들의 안정적인 하위 호환성을 위해 추가된 것이지 새로운 코드를 작성하는데 default 메서드를 사용하는 것은 바람직한 작업이 아닙니다.

결론

추상 클래스는 메서드 구현을 수직적으로 공유하기 위해 존재하는 반면, 인터페이스는 메서드 구현을 수평적으로 공유하기 위해 존재합니다. 하여, 같은 동작을 공유해야 하는 경우에는 인터페이스와 디폴트 메서드로 구현하지 않을 것을 권장하며 추상 클래스를 사용해주세요. 인터페이스는 클래스간의 의사 소통을 위해 미리 특정 기능을 구현하라고 알려주기 위해 사용하는 것을 권장드립니다.

 

인터페이스로 추상화를 하였는데, 구현체들에서 중복이 많다면 부가적으로 추상 클래스를 사용하는 방법 또한 존재하는데, 이를 골격 구현(Skeletal Implementation) 클래스라고 부릅니다. 이에 대해서는 추후에 다시 다뤄보도록 하겠습니다.

'JAVA' 카테고리의 다른 글

[Java] DTO (Data Transfer Object)  (5) 2022.05.03
[Java] VO(Value Object)란?  (2) 2022.03.15
[Java] 불변 객체(Immutable Object)와 final  (1) 2022.03.03

댓글