자바를 공부해본 경험이 있는 사람이라면 객체지향 프로그래밍에서 인터페이스가 얼마나 큰 비중을 차지하는지 알고 계실 거라 생각합니다. 이번 포스팅에서는 객체지향의 핵심 개념인 인터페이스의 문제점과 이를 해결하기 위해 자바에서 제공하는 기능에 대해 이야기해보고자 합니다.
인터페이스의 문제점
인터페이스는 동일한 목적의 동작을 수행하도록 구격을 정의하는 명세서의 역할을 하며, 이를 구현한 클래스에서는 구격에 맞춰 세부적인 동작을 작성합니다. 좀더 구체적으로 표현하면, 자바는 다형성을 사용하여 코드의 유연성을 확보하기 위해 인터페이스에서는 추상 메서드를 선언하고 implements 하는 구현체에서는 override 하여 구격에 맞춰 코드를 작성합니다.
객체지향 개발을 가능하게 해주는 인터페이스는 장점도 많았지만 큰 단점이 존재했습니다.
바로, 수정이 어렵다는 점입니다.
예를 들어 어느 쇼핑몰에서 다양한 상품들에 대해 판매와 환불 기능만을 제공한다고 가정해보겠습니다. 상품마다 제공하는 판매와 환불의 세부적인 기능이 다르기 때문에 표준 인터페이스를 두기로 하였고, ProductService.interface를 만들어 기능 구현을 강제하였습니다.
ProductService.interface
public interface ProductService {
public String buy(String productId);
public String refund(String productId);
}
쇼핑몰은 성공적인 성과를 거두어 더욱 다양한 상품들을 제공하였고, 그 결과 100개의 구현체가 프로젝트 내에 생성되어있었습니다. 그러던 어느 날, 건의사항 게시판을 보니 상품의 사이즈가 맞지 않은 사용자들이 환불 후 재구매하는 과정이 불편하다며 교환 기능을 추가해달라는 요청이 많았고, ProductService 인터페이스에 다음과 같이 교환 기능을 추가하기로 하였습니다.
public interface ProductService {
public String buy(String productId);
public String refund(String productId);
public String exchange(String oldProductId, String newProductId);
}
그러자, 인터페이스 배포 후 해당 인터페이스를 구현한 모든 100개의 클래스에서 컴파일 에러가 발생하였습니다.
이러한 문제를 해결하기 위해 자바 8에서 인터페이스의 기능을 대대적으로 개선하였습니다. 자바 8버전에서는 인터페이스의 기능 개선뿐만이 아니라 람다 표현식, 함수형 인터페이스, 메서드 참조, 스트림 API 등 가장 혁신적인 변화가 이루어졌습니다.
인터페이스의 개선
자바 8버전 이전에는 인터페이스에 구현 내용이 없는 public 추상 메서드의 선언만 작성해야 했고 이를 구현한 클래스에서 상세 내용을 정의해야 했습니다. 따라서 인터페이스에 메서드를 추가하면 모든 구현체에서 컴파일 에러가 발생하였습니다.
default 메서드
자바 8버전 부터는 default 키워드를 이용하여 인터페이스에 메서드를 추가하고 직접 내용을 정의할 수 있어서 인터페이스의 메서드 추가로 인한 문제를 해결할 수 있습니다.(default 메서드는 모두 public 메서드로 인식하며 public의 선언은 생략할 수 있습니다.)
개선된 ProductService.interface
public interface ProductService {
public String buy(String productId);
public String refund(String productId);
default public String exchange(String oldProductId, String newProductId){
Strinf oldProduct = refund(oldProductId);
Strinf newProduct = refund(newProductId);
return "exchanged : " + oldProduct + " -> " + newProduct;
}
}
default 메서드를 사용하면 구현체에서 이를 오버라이딩하여 사용할 수 있으며, 오버라이드 하지 않은 경우에도 인터페이스의 default 메서드가 호출되기 때문에 NoSuchMethodError가 발생하지 않고, 컴파일 에러도 발생하지 않습니다.
static 메서드와 private 메서드
자바 8버전에서 default 메서드가 추가되면서 한 가지 메서드가 더 추가되었습니다. 바로, static 메서드입니다. default 메서드와 동일하게 인터페이스에서 메서드 구현이 가능하며, 반드시 인터페이스명으로 접근해야 하고, 구현체에서 재정의가 불가능합니다.
이처럼 default와 static 메서드를 사용하여 인터페이스에서도 메서드 내부를 구현할 수 있게 되면서, 공통의 로직을 반복해야하는 일이 빈번히 일어났습니다. 공통의 로직에 사용되는 중복된 코드들을 메서드 추출하여 내부적으로 재사용할 수 있는 private 메서드의 필요성이 생겼고, 바로 다음 버전인 자바 9에서 인터페이스의 private 메서드가 추가되었습니다. 사용 예시는 다음과 같습니다.
public interface Test {
public static void staticMethod(){
reusableMethod();
}
default public void defaultMethod(){
reusableMethod();
}
public void reusableMethod(){
System.out.println("reusable private method");
}
}
추상 클래스? 인터페이스?
앞서 인터페이스는 동일한 목적의 동작을 수행하도록 구격을 정의하는 명세서의 역할을 한다고 설명했습니다. 그러나 자바8, 9 버전부터는 default, static과 private 메서드를 통해 인터페이스에서 메서드 내부 구현이 가능해졌습니다. 그렇다면 추상 클래스와 인터페이스의 역할에 차이가 없지 않을까?라고 생각할 수 있습니다.
하지만 추상 클래스와 인터페이스에는 큰 차이가 있습니다.
- 인터페이스는 멤버 변수를 가질 수 없습니다.
클래스는 흔히 '객체를 생성하기 위한 설계도이다.'라고 표현합니다. 객체는 속성을 가지고 있으며 다른 것과 식별이 가능한 것으로, 클래스는 객체의 속성을 유지하기 위해 멤버 변수를 선언하여 객체가 살아있는 동안 속성을 유지할 수 있도록 해줍니다. 반면, 인터페이스는 자기 자신을 객체화할 수 없습니다. 따라서 속성을 유지할 수 없으며, 멤버 변수를 선언할 수 없습니다.
- 클래스는 오직 하나의 클래스만을 상속받을 수 있지만, 인터페이스는 여러 개를 구현할 수 있습니다.
자바는 C, C++과 달리 다중 상속을 지원하지 않습니다. 다이아몬드 상속 문제처럼 자식 클래스가 두 부모 클래스로부터 동일한 이름의 메서드를 상속받았을 때 어떤 메서드가 호출되는지 파악하기 어렵다는 이유 때문입니다. 하지만 자바에서는 다중 인터페이스 구현을 통해 다중 상속 기능을 사용할 수 있습니다.(default 메서드가 생기면서 자바에서도 동일한 문제가 발생하기도 합니다.)
'[Back-end] > [Java]' 카테고리의 다른 글
람다식과 외부 변수 참조 (0) | 2021.12.18 |
---|