Programming/Java

자바 기초부터 모던 자바까지 - 인터페이스 편

KOOCCI 2022. 7. 3. 15:04

목표 : 자바의 인터페이스에 대해 알아본다.


인터페이스

인터페이스가 무엇인지 그에 대한 개념을 설명하지는 않겠다.

자바는 다중상속을 지원하지 않는다거나, 추상화에 대한 내용을 모른다면 잘 정리된 다른 포스팅을 찾아 보도록 하자.

 

자바8부터 람다와 함수형 프로그래밍을 적용하기 위해 인터페이스를 활용하기 시작한다.

그럼 기존의 인터페이스에는 무슨 문제점이 있었고, 어떻게 진화해 온 것인지 먼저 알아보려 한다.

 

인터페이스의 문제점

인터페이스는 주로 여러 개의 구현체를 통일화한 명세서로 정의하기 위해 사용한다.

그러면 인터페이스를 구현한 클래스들은 동일한 메소드 명으로 통일성을 확보하고, 구현 방법에 상관없이 자신이 원하는 메서드를 호출해 목적을 이룰 수 있다.

 

그러나, 가장 치명적인 단점은  명세서기 때문에 수정이 어렵다는 것이다.

 

딱 메소드 2개의 아래 인터페이스를 보자.

public interface EncryptionIF {
    public byte[] encrypt(byte[] bytes) throws Exception;
    public byte[] decrypt(byte[] bytes) throws Exception;
}

암호화 알고리즘 인터페이스로, 위 2가지 메소드 명세서를 만들어 배포했다.

수십개의 팀이 이 인터페이스를 활용해 잘 사용하고 있었는데, 기능을 추가해야하는 경우가 생겼다.

몇몇 팀이 파라미터로 들어온 값이 암호화 알고리즘으로 암호화된 것인지 확인하는 기능이었다.

public interface EncryptionIF {
    public byte[] encrypt(byte[] bytes) throws Exception;
    public byte[] decrypt(byte[] bytes) throws Exception;
    public boolean isEncoded(byte[] bytes) throws Exception;
}

이렇게 수정/배포하면 다음과 같은 문제점들이 속출한다.

 

  1. 인터페이스 배포 후 해당 인터페이스를 구현한 모든 클래스에서 컴파일 에러가 발생한다.
  2. 구현한 클래스가 너무 많아 이를 모두 한번에 수정하는 것이 어렵다.
  3. 내부에서 사용하는 인터페이스가 아니라 외부 노출되는 오픈된 규격이였다.
  4. 클래스를 수정하지 않고 인터페이스를 컴파일하여 배포하면 NoSuchMethod 에러가 발생한다.

그럼 그 다음으로 고민하는 과정은 버져닝이다.

업그레이드가 아닌 새로운 규격을 내는 것이다.

public interface EncryptionChecker {
    public boolean isEncoded(byte[] bytes) throws Exception;
}

임시 방편은 될 수 있지만, 매번 수정이 일어날 때마다 인터페이스를 새롭게 제공할 수는 없었다.

 

이런 사고가 자바에서 제공하는 API 중 컬렉션 프레임워크에서 발생했다.

자바의 버전이 올라갈 수록 새로운 환경(멀티 쓰레드 등)에 대응하고자 하는 자료 구조는 계속 생겨나는데, 컬렉션 프레임워크의 핵심 인터페이스는 수정을 못하는 것이다.

 

인터페이스의 진화

결국 인터페이스의 기능을 대대적으로 변경하게 된다.

그 핵심이 디폴트(default) 메서드이다.

 

최초의 자바 버전(1.x)에서는 인터페이스 제약이 다음과 같았다.

 

  1. 상수를 선언할 수 있다. 해당 상수는 반드시 값이 할당되어 있어야 하며 변경하지 못한다. 명시적으로 final처리를 안해도 final로 인식한다.
  2. 메서드는 반드시 추상(abstract)메서드여야 한다. 즉, 구현체가 아니라 메서드 명세만 정의되어야 한다.
  3. 인터페이스를 구현한 클래스는 인터페이스에서 정의한 메서드를 구현하지 않았다면 반드시 추상 클래스로 선언되어야 한다.
  4. 인터페이스에 선언된 상수와 메서드에 public을 선언하지 않더라도 public으로 인식한다.

자바 버전 1.2부터는 몇가지 추가가 된다.

 

  1. 중첩(Nested) 클래스를 선언할 수 있다. 선언은 내부(Inner)클래스 같지만, 실제로는 중첩 클래스로 인식한다.
  2. 중첩(Nested)인터페이스를 선언할 수 있다.
  3. 위의 중첩 클래스와 중첩 인터페이스는 모두 public과 static이어야 하며 생략 가능하다.

이전 포스팅에서 내부 클래스를 설명할 때도 설명했지만, 현재 참고중인 자료에는 중첩 클래스와 내부 클래스를 구분하고 있다.

중첩 클래스는 클래스나 인터페이스 내부에 static으로 선언된 클래스임을 확인하자.

 

중첩클래스를 통해 인터페이스 내부에서 세부 동작에 대해 상세히 규정할 수 있고, 개발자가 구현할 필요 없이 인터페이스 차원에서 제어할 수 있게 되었다.

 

자바 버전 5부터는 새로운 기능인 제네릭, Enum, 어노테이션이 인터페이스에 영향을 주었다.

 

  1. 중첩(Nested) 열거형(Enum)을 선언할 수 있다.
  2. 중첩(Nested) 어노테이션을 선언할 수 있다 (위의 중첩 열거형과 중첩 어노테이션은 모두 public과 static이어야 하며 생략가능하다)

제네릭의 등장으로 인터페이스 선언문과 메서드 선언에 모두 타입 파라미터를 사용할 수 있게 되었다.

단, 타입파라미터로 변수나 상수를 선언할 수 없고, 오직 메서드의 리턴 타입과 인수 타입에만 사용할 수 있다.

 

그리고, 자바 8부터는 또다시 2가지가 추가된다.

 

  1. 실제 코드가 완성되어 있는 static 메서드를 선언할 수 있다.
  2. 실제 코드가 완성되어 있는 default 메서드를 선언할 수 있다. (위의 static 메서드와 default 메서드는 모두 public 메서드로 인식하며 public 선언은 생략할 수 있다.

가장 중요한 변화는 실제 구현된 코드를 정의할 수 있다는 것이다.

물론, static메서드나 default 둘 중 하나가 전제 조건이다.

물론 클래스에서도 별도로 정의할 필요가 없다. 이를 통해 굳이 메서드를 사용하는 클래스에서 구현하지 않아도 컴파일 에러가 나지 않는다.

 

이런 변화는 8버전부터 Collection 인터페이스에 메서드를 추가하게 되었다.

 

자바 9버전 부터는 또 한가지가 추가된다.

  1. private 메서드를 선언할 수 있다.

기존에는 public 메소드만 가능했고, 접근자를 선언하지 않고 메서드를 정의해도 public으로 인식했다.

이 변화는 클래스의 외부에는 공개되지 않더라도 인터페이스 내부의 static 메서드와 default 메서드의 로직을 공통화하고 재사용하는데 유용해졌다.

단, private 메서드는 실제 동작하는 소스 코드까지 구현해야한다는 제약이 있다. 즉, 자바 8의 static 혹은 default 메서드와 동일한 개념을 가진 기능의 발전이었다.

 

정리해보면 현재 인터페이스에는 총 9가지를 선언할 수 있다.

  1. 상수
  2. 추상 메서드
  3. 중첩 클래스
  4. 중첩 인터페이스
  5. 중첩 열거형
  6. 중첩 어노테이션
  7. static 메서드
  8. default 메서드
  9. private 메서드

클래스와의 차이점과 제약 조건

인터페이스가 구현이 되게 된다면, 클래스와의 차이는 어떻게 될 것인가?

추상클래스와 인터페이스의 차이점은 하기와 같다.

  1. 추상 클래스는 멤버 변수를 가질 수 있지만 인터페이스는 멤버 변수를 가질 수 없다. static으로 정의된 변수를 내부적으로 선언할 수 있지만 멤버 변수는 선언할 수 없다.
  2. 클래스를 구현할 때 오직 하나의 클래스만을 상속받을 수 있는 반면에 인터페이스는 여러 개를 상속받거나 구현할 수 있다.

즉, 가장 큰 차이점은 멤버 변수의 선언 유무다.

멤버변수는 그 객체의 속성을 담아두기 위한 용도인데, 인터페이스에서 멤버 변수가 없다는 것은 인터페이스 자체를 객체화 할 수 없다는 것이다.

 

자바는 다중 상속이 허용되지 않지만, 인터페이스를 통해 여러 인터페이스를 구현할 수는 있다.

마치 상속과 비슷하게 default 메서드, private, static 메서드의 기능을 이어 받을 수 있게 되었고 특히 default 메서드는 오버라이드도 가능하다.

 

public interface HouseAddress {
    public static final String DefaultCountry = "Korea";
    
    public String getPostCode();
    default public String getCountryCode() {
        return getDefaultCountryCode();
    }
    
    private String getDefaultCountryCode() {
        return HouseAddress.DefaultCountry;
    }
}

위 인터페이스를 구현한 클래스를 보자.

public class KoreaHouseAddress implements HouseAddress {
    private String postCode;

    @Override
    public String getPostCode() {
        return null;
    }

    @Override
    public String getCountryCode() {
        return HouseAddress.super.getCountryCode();
    }
}

대부분 super 키워드를 통해 상위 클래스의 메서드 혹은 속성에 접근하는데, 당연히 인터페이스다 보니 Object 클래스가 연결된다.

HouseAddress.super.getCountryCode();

따라서, 자바 8부터는 위와 같은 방법으로 상위 인터페이스의 메소드로 연결지을 수 있다.

 

다중 상속 관계

자바에서는 다중 상속을 지원하지 않는다.

복잡성이 높아져 버그가 많이 발생하고 유지보수가 어려워진다는 것이 그 이유다.

그러나 인터페이스의 default의 등장은 조금 난감한 이슈가 되었다.

 

우선 private 메서드는 자바의 접근 규칙에 따라, 하위 클래스로 상속되지 않는다. 즉, private 메서드가 인터페이스에 있더라도 문제가 되지 않는다.

static 메서드도 인터페이스 레벨 혹은 클래스 레벨로 정의되므로(컴파일 타임에 생성되는 사항이라 상속이 불가하다) 메서드 오버라이드의 범위에 속하지 않는다.

 

그러나 default 메소드는 가능하다.

따라서, 흔히 다중 상속의 문제점인 다이아몬드 상속과 같은 문제가 발생한다.

다이아몬드 상속

Worker 입장에서 default 메소드로 만들어진 성별을 가져오려고 하면, Duplicate 에러가 날 수 밖에 없다.

어떤 클래스에서 받아야 할지 모호하기 때문이다.

 

이를 피하기 위해서는 결국 Worker에서 오버라이드를 필수적으로 해야한다. (앞서 확인한 super 키워드를 통해서도 가능하다)

 

다만, 만약에 Man이 Abstract 클래스라면 어떻게 될 것인가?

성별을 가져오는 getSex() 메소드가 있을 때, Person에서는 default로 구현되었을 것이다.

Man이라는 클래스에서는 이를 Abstract 클래스로 가져왔고, Woman은 인터페이스로 가져왔다.

문제는 없다. 다만, Worker에서 오버라이드를 하지 않아도 에러가 나지 않는다는 것이다.

 

이때는 클래스가 우선적으로 적용된다. 즉, Man에 적용된 getSex()메소드가 호출되는 것이다.

 

요약하면 다음과 같다.

  1. 클래스가 인터페이스에 대해 우선순위를 가진다. 동일한 메서드가 인터페이스와 클래스에 둘 다 있다면 클래스가 우선이다.
  2. 위의 조건을 제외하고 상속관계에 있을 경우 하위 클래스/인터페이스가 상위 클래스/인터페이스보다 우선된다.
  3. 위의 두가지 조건을 제외하고 메서드 호출 시 어떤 메서드를 호출해야할지 모호할 경우, 컴파일 에러가 발생할 수 있으며, 명확한 지정이 필요하다.

하나 신경써야 할 것은 버전이 올라갈 수록 인터페이스 기능이 많아졌다고 하여, 남용하는 것은 좋지 않다.

본연의 인터페이스 역할에 충실하고 영향을 최소화하도록 구현하는 것이 가장 명확한 해답이다.