목표
자바의 인터페이스에 대해 학습하세요.
학습할 것 (필수)
- 인터페이스 정의하는 방법
- 인터페이스 구현하는 방법
- 인터페이스 레퍼런스를 통해 구현체를 사용하는 방법
- 인터페이스 상속
- 인터페이스의 기본 메소드 (Default Method), 자바 8
- 인터페이스의 static 메소드, 자바 8
- 인터페이스의 private 메소드, 자바 9
인터페이스
자바에서 인터페이스란 하나의 설계도 입니다. 설계도는 어떤것을 만들어야 하는지
알려주는 역할을 합니다. 인터페이스도 이처럼 추상 메서드를 만들어서 인터페이스를 구현하는
클래스가 인터페이스의 추상 메서드를 오버라이드하여 메서드를 완성시켜야 합니다.
설계도의 역할은 구현 대상과 만드는 사람의 중간 다리 역할을 합니다.
우리는 설계도를 보면서 어떤 기능이 필요한지 알 수 있습니다.
하지만 어떻게 구현할지는 만드는 사람의 마음대로 입니다.
인터페이스는 설계도와 같은 역할을 합니다.
코드와 구현 객체 사이의 중간 다리 역할을 하며
인터페이스를 통해 어떤 메서드를 정의해야 하는지 알 수 있고
메서드를 구현하는 것은 개발자의 마음대로 입니다.
따라서 인터페이스는 인터페이스를 구현한 클래스만 사용자의 필요에 따라 바꾸면 되기 때문에
사용자는 객체를 필요에 따라 수정할 수 있으며 인터페이스의 로직은 변경하지 않아도 됩니다.
인터페이스 정의하는 방법
- 예약어로 class 대신 interface 를 사용해 정의할 수 있습니다.
- 인터페이스는 접근지시자로 public 또는 (default) 만 선언 가능합니다.
[접근지시자] interface [인터페이스이름] {
}
인터페이스의 멤버 구성
- 인터페이스는 추상 메서드와 상수만 멤버로 가질 수 있습니다.
- 일반 메서드와 멤버 변수는 구성원으로 가질 수 없습니다.
- 모든 멤버 변수는 public static final 이어야 하며, 이를 생략할 수 있습니다.
- 멤버 변수에 public static final을 생략해도 컴파일러가 자동으로 추가해줍니다.
- 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있습니다.
- 메서드에 public 과 abstract 를 생략해도 컴파일러가 자동으로 추가해줍니다.
- 자바 8 부터는 default 메서드와 static 메서드를 선언할 수 있습니다.
public interface MyInterface {
// 인터페이스의 멤버변수
public static final int ONE = 1;
// 멤버 변수에 public을 생략해도 컴파일러가 추가해줍니다.
static final int TWO = 2;
// 멤버 변수에 static을 생략해도 컴파일러가 추가해줍니다.
final int THREE = 3;
// 멤버 변수에 final을 생략해도 컴파일러가 추가해줍니다.
int FOUR = 4;
// 추상 메서드
public abstract void method1();
// 메서드에 public을 생략해도 컴파일러가 추가해줍니다.
abstract void method2();
// 메서드에 abstract를 생략해도 컴파일러가 추가해줍니다.
void method3();
}
인터페이스 구현하는 방법
인터페이스는 생성자가 없습니다. 즉, 인터페이스 자체로는 인스턴스(객체)를 생성할 수 없습니다.
또한 인터페이스는 추상 메서드를 갖기 때문에 구현할 대상이 필요합니다.
그 대상은 클래스 이며 구체화 시키는 것을 구현한다고 합니다.
구현하는 방법은 구현할 클래스 뒤에 implements [인터페이스 이름] 을 명시하면 됩니다.
이때 구현할 클래스가 추상 클래스나 다른 클래스를 상속하는 경우에는 선 상속 후 구현을 해야 합니다.
public class implements [인터페이스 이름] {
}
// 클래스를 함께 상속받는 경우
public class extends Parent implements [인터페이스 이름] {
}
인터페이스 레퍼런스를 통해 구현체를 사용하는 방법
이전에 동적 바인딩(Dynamic Dispatch)으로 다형 참조를 생성할 수 있다는걸 배웠습니다.
인터페이스 또한 클래스처럼 변수의 타입으로 사용될 수 있습니다.
(다만 인스턴스는 생성할 수 없습니다.)
Animal 인터페이스의 구현체인 Dog와 Cat 클래스가 있습니다.
두 클래스는 다음과 같이 인스턴스화 하여 메서드를 사용할 수 있습니다.
위 코드는 당연히 아무 에러 없이 실행됩니다.
그렇다면 Animal 인터페이스 타입으로 변수를 만들어서 Cat과 Dog의 인스턴스로 초기화하면
어떻게 되는지 보겠습니다.
Animal 타입의 dog 변수에서 bark() 메서드를 찾을 수 없다는 에러가 발생했습니다.
Animal 타입의 변수를 사용했기 때문에 Animal 인터페이스에 정의된 move() 메서드와 cute() 메서드만 사용 할 수 있다는것을 보장하기 때문에 Dog 클래스의 bark() 메서드와 Cat 클래스의 grooming() 메서드는 컴파일 단계에서 오류가 발생합니다.
만약 cute() 메서드와 move() 메서드만 사용한다면 컴파일 단계에서 에러없이 실행이 됩니다.
여기서 Dog 와 Cat 의 참조변수로 Animal 타입을 지정할 수 있다는 것은 다형성의 큰 장점입니다.
메서드의 매개변수를 Animal 타입으로 사용한다면 Animal 인터페이스를 구현하는 어떤 클래스의 어떤 객체라도 그 메서드의 매개변수로 전달 될 수 있다는 것을 의미합니다.
다형 참조를 메소드의 형식 매개변수로 사용하는 것은 강력한 기법입니다.
메소드가 전달되는 매개변수의 타입을 제어하는 것을 허용하고, 메소드에게 다양한 타입의 인자들을 받아들일 수 있는 유연성을 제공합니다.
인터페이스 상속
- 인터페이스는 다른 인터페이스를 상속하여 확장할 수 있습니다.
- 이 때 상속받은 인터페이스를 구현하는 클래스는 구현하는 인터페이스의 추상 메서드와 부모 인터페이스의 추상 메서드 모두 구현해야 합니다.
- 인터페이스는 다중 상속을 받을 수 있습니다.
인터페이스의 기본 메소드 (Default Method), 자바 8
기본 메서드(Dafulat Method)는 자바 8에서 새롭게 추가된 것입니다.
기본 메서드가 정의된 인터페이스를 구현한 클래스는 기본 메서드도 함께 구현됩니다.
기본 메서드는 인터페이스가 로직을 가지고 있지만 실제로는 인터페이스를 구현한 모든 객체에 기본적으로 들어가는 메서드 라고 생각하면 됩니다. 또한 구현하는 측에서 재정의해서 사용할 수도 있습니다.
일반 클래스의 메서드와 작성 방식은 같지만, 앞에 default 키워드를 적어줍니다.
기본 접근지시자가 public이라서 public을 생략하면 자동으로 붙여줍니다.
클래스가 2개의 인터페이스를 implements 했을 때
만약 어떤 클래스가 같은 메서드 이름(method signature)의 기본 메서드를 가진 인터페이스 2개를 구현했다면
어떤 메서드가 실행 되는지 보겠습니다.
실행시 아래와 같은 에러가 발생합니다.
error: class User inherits unrelated defaults for move() from types FlyUnit and GroundUnit public class User implements FlyUnit, GroundUnit{
FlyUnit 과 GroundUnit 두 인터페이스 모두 move() 라는 메서드가 있기때문에 어떤 인터페이스의 move() 메서드를 싱행해야 할지 모르기 때문에 발생한 에러입니다.
이 에러를 해결하려면 두 인터페이스를 구현한 클래스에서 move() 메서드를 재정의(Override) 해줘야 합니다.
한 클래스가 클래스 상속과 인터페이스 구현을 함께 받을 때
클래스가 한개의 클래스를 상속 받고 한개의 인터페이스를 구현 했을 때 같은 메서드 이름이 존재한다면
어떤 메소드가 실행되는지 보겠습니다.
자바는 동일한 메소드 이름으로 충돌이 발생하면 컴파일러는 상속받은 클래스의 메서드를 우선시하기 때문에
extends 한 클래스의 메서드를 우선으로 상속 받습니다.
아래의 코드는 에러가 발생하지 않고 정상적으로 실행됩니다.
인터페이스의 static 메소드, 자바 8
- 인터페이스에서 static 메소드를 이용하면 메소드 몸통을 구현할 수 있습니다.
- 클래스 이름으로 메소드를 호출할 수 있습니다.
- public 을 생략해도 public 으로 사용됩니다.
- static 메서드 이므로 재정의가 불가능합니다.
인터페이스의 private 메소드, 자바 9
Java8 부터 default 및 static 메소드의 도입으로 인터페이스는 갑자기 메소드 이름(method signatures) 뿐만 아니라 구현을 포함 할 수 있게 됐습니다. 인터페이스에 구현을 작성할 때 몇가지 간단한 메소드 중에서 더 복잡한 메소드를 작성하는것이 좋습니다. 이러한 코드는 재사용, 유지 및 이해하기가 더 쉽기 때문입니다.
일반적으로 이러한 메소드는 구현 세부 사항이며 외부에서 볼 수 없고 사용할 수 없으므로 주로 private 메소드 를 사용합니다.
Java 8 까지는 인터페이스에 private 메소드를 사용할 수 없었습니다.
즉, 다음 중 하나를 수행할 수 밖에 없습니다.
1. 길고 복잡하며 어려운 method body를 사용할 것
2. 인터페이스의 일부인 helper 메소드를 사용할 것. 이렇게 하면 캡슐화가 중단되고 인터페이스 와 구현 class의 public API가 오염됩니다.
Java9 부터는 인터페이스에서 private methods를 사용할 수 있게 됐습니다.
인터페이스에서 사용하는 private methods 는 다음과 같은 특성을 가지고 있습니다.
- 메소드 body가 있고 abstract가 아닙니다.
- static 이거나 non-static 일 수 있습니다.
- 구현 클래스와 인터페이스가 상속되지 않습니다.
- 인터페이스에서 다른 메소드를 호출할 수 있습니다.
인터페이스의 private 메소드에서 호출할 수 있는 메소드는 다음과 같습니다.
- private는 private, abstract, default 또는 static 메소드를 호출 할 수 있습니다.
- private static은 static 및 static private 메소드만 호출할 수 있습니다.
추상 클래스 vs 인터페이스 (자바 8 기준)
디폴트 메소드의 도움으로 인터페이스는 추상 클래스(abstract)와 차이가 없어보입니다.
- 추상 클래스, 인터페이스는 인스턴스화 하는것이 불가능하며, 구현부가 있는 메소드와 없는 메소드 모두 가질 수 있다는 점에서 유사합니다.
- 인터페이스에서 모든 변수는 기본적으로 public static final 이며, 모든 메소드는 public abstract 인 반면 추상 클래스에서는 static 이나 final 이 아닌 필드를 지정할 수 있고, public, protected, private 메소드를 사질 수 있습니다.
- 인터페이스를 구현하는 어떤 클래스는, 다른 여러개의 인터페이스들을 함께 수현할 수 있습니다.
- 추상 클래스는 상속을 통해 구현되는데, 자바에서는 다중 상속을 지원하지 않으므로 추상클래스를 상속받은 서브클래스는 다른 클래스를 상속받을 수 없습니다.
추상클래스, 인터페이스의 적절한 사용 케이스
추상클래스
- 관련성이 높은 클래스 간에 코드를 공유하고 싶은 경우
- 추상클래스를 상속받은 클래스들이 공통으로 가지는 메소드와 필드가 많거나, public 이외의 접근제어자(protected, private) 사용이 필요한 경우
- non-static, non-final 필드 선언이 필요한 경우. 즉, 각 인스턴스에서 state 변경을 위한 메소드를 선언할 수 있다.
인터페이스
- 서로 관련성이 없는 클래스들이 인터페이스를 구현하게 되는 경우에 사용한다. 예를 들어, Comparable, Cloneable 인터페이스는 여러 클래스들에서 구현되는데, 구현클래스들 간에 관련성이 없는 경우가 대부분이다.
- 특정 데이터 타입의 행동을 명시하고 싶은데, 어디서 그 행동이 구현되는지는 신경쓰지 않는 경우.
- 다중상속을 허용하고 싶은 경우
JDK 에서 추상클래스와 인터페이스의 사용 예시
JDK 에서 추상클래스의 대표적인 예시는 Collections Framework 의 일부인 AbstractMap 이다. AbstractMap 의 서브클래스인 HashMap, TreeMap, ConcurrentHashMap 에서는 AbstractMap 에 정의되어 있는 get, put, isEmpty, containsKey, containsValue 등의 메소드를 공유한다.
여러개의 인터페이스를 구현하는 JDK 의 클래스 예시로는 HashMap 이 있다. HashMap 은 Serializable, Cloneable, Map<K,V> 를 구현한 클래스이다. 위 인터페이스를 통해 HashMap 의 인스턴스는 복제가능하며, 직렬화(byte stream 으로 컨버팅)가 가능하며, map 으로써의 기능을 가진다는 것을 추론할 수 있다.
템플릿 메소드 패턴에서 인터페이스가 아닌 추상 클래스를 사용하는 이유
템플릿 메소드 패턴에서는 템플릿 메소드와 hook 메소드를 분리하여 일정한 프로세스를 가진 기능을 hook 단위로 분리시킵니다. 이때 추상클래스를 하나 만들고, 템플릿 메소드에서는 hook 메소드를 호출하고, 추상클래스를 상속받아 구현한 클래스에서 hook 메소드들을 구현하는 방식입니다.
인터페이스를 사용해도 구현은 똑같이 할 수 있지만 추상클래스를 사용하여 hook 메소드들에 대한 엄격한 접근제어를 사용할 때, 템플릿 메소드 패턴을 강제할 수 있다는 결론을 내렸다.
아래는 Gof Design Patterns 의 템플릿 메소드 패턴에 대한 정의 입니다.
Defines the skeleton of an algorithm in a method, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithms structure.
– GoF Design Patterns
알고리즘의 구조를 메소드에 정의하고, 하위 클래스에서 알고리즘 구조의 변경없이 알고리즘을 재정의 하는 패턴입니다.알고리즘이 단계별로 나누어 지거나, 같은 역할을 하는 메소드이지만 여러곳에서 다른형태로 사용이 필요한 경우 유용한 패턴입니다.
토비의 스프링에는 템플릿 메소드 패턴에 대해 아래와 같이 정의합니다.
상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법. 변하지 않는 기능은 슈퍼클래스에 만들어두고 자주 변경되며 확장할 기능은 서브클래스에서 만들도록 한다. – 토비의 스프링 3.1
아래는 템플릿 메소드 패턴에 대한 코드 예제 입니다.
템플릿 메소드 패턴이라고 하면 뭔가 대단해 보이지만 구현해보면 간단한 패턴입니다.
추상 클래스인 AbstractClass 에는 실제로 실행을 위해 호출 될 public 메소드인 templateMethod가 정의되어 있고,
templateMethod 내부에는 hook1 -> hook2 의 단계를 가지는 추상 메소드가 호출됩니다.
이 추상 메소드들은 AbstractClass 를 상속받아 구현한 ConcreteClass 에서 구체적인 구현이 정의됩니다.
JDK8 에서의 사용예제
AbstractMap<K,V> 클래스를 보면 템플릿 메소드 패턴이 적용된 것을 볼 수 있습니다. 토비의 스프링에 나온 설명처럼 "변하지 않는 기능은 슈퍼클래스에 만들어두고 자주 변경되며 확장할 기능은 서브클래스에서 만들도록 한다" 는 관점에서 보면, hashCode() 메소드는 AbstractMap 추상클래스에 있는 것을 사용하고, get() 등 기타 많은 메소드들은 HashMap, TreeMap 등 서브클래스에서 오버라이드하여 재정의 하고 있는 것을 볼 수 있다.
AbstractMap<K,V> 추상 클래스의 선언 부분 Map<K, V> 인터페이스를 구현한걸 확인할 수 있습니다.
AbstractMap<K,V> 의 get() 메소드
HashMap<K, V>가 오버라이드한 get() 메소드
TreeMap<K, V> 가 오버라이드한 get() 메소드
HashMap 과 TreeMap 둘다 AbstractMap을 상속받아 구현했는데 AbstractMap 은 Map 인터페이스를 구현하기 때문에 Map 인터페이스의 추상 메소드 get() 메소드를 구현하도록 되어 있습니다.
하지만 AbstractMap 을 상속받은 HashMap, TreeMap 둘 다 get() 메소드를 가지지만 각자 자신만의 구현방법으로 서로 다른 방식의 get() 메소드를 재정의 한것을 볼 수 있습니다. 이것이 템플릿 메소드 패턴이 적용된 대표적인 예시라고 할 수 있습니다. 앞서 토비의 스프링에서 말한 "상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법. 변하지 않는 기능은 슈퍼클래스에 만들어두고 자주 변경되며 확장할 기능은 서브클래스에서 만들도록 한다. – 토비의 스프링 3.1" 이라는 템플릿 메소드 패턴에 맞는 구현방법입니다.
hook 메소드란?
슈퍼클래스에서 디폴트 기능을 정의해두거나 비워뒀다가 서브클래스에서 선택적으로 오버라이드할 수 있도록 만들어둔 메소드를 훅(hook) 메소드라고 합니다.
AstractMap 클래스에서 get() 메소드의 경우 hook 메소드라고 볼 수 있습니다.
hook 메소드가 꼭 추상 메소드 인것만은 아닙니다. 단지 선택적으로 재정의 할 수 있도록 만들어둔 메소드를 의미합니다.
인터페이스는 기본적으로 변수필드는 public static final 이며, 모든 메소드는 public abstract 이므로 인터페이스로 구현할 경우, 템플릿 메소드 내부에서만 호출되어야 할 메소드들이 public 제어자에 의해 의도치 않은 사용처에서 호출될 위험이 있다.
(자바 9부터는 private 메소드도 사용이 가능해졌으므로 이런 문제는 해결 됐을 지도?)
템플릿 메소드 내부에서 사용되는 메소드들에 대한 외부에서의 접근을 막고 싶다면 protected, private 접근 지시자를 사용가능한 추상클래스를 사용하면 됩니다.(추상클래스가 같은 패키지 내에 있다면 protected 라도 접근 가능하지만, 템플릿메소드패턴이 적용된 추상클래스는 라이브러리 형태로 외부패키지에서 제공되는 방식일 것이므로, 상속을 받지 않은 클래스에서는 호출하지 못하게 할 수 있다고 봐도 될 것 같습니다.)
정리하고 보면 AbstractMap, AbstractSet 등등은 단지 Java8 이전에 만들어졌기 때문에 인터페이스는 구현체를 가질 수 없어서 그리고 접근지시자의 제한때문에 추상클래스로 구현한 것이 아닌가 싶습니다.
참고 문헌
- honbabzone.com/java/java-interface/
- hyeonstorage.tistory.com/266
- codechacha.com/ko/java8-default-methods/
- yaboong.github.io/java/2018/09/25/interface-vs-abstract-in-java8/
- yaboong.github.io/design-pattern/2018/09/27/template-method-pattern/
'java-liveStudy' 카테고리의 다른 글
10주차 과제. 멀티쓰레드 프로그래밍 (0) | 2021.01.17 |
---|---|
9주차 과제. 예외 (0) | 2021.01.16 |
7주차. 패키지 (0) | 2021.01.01 |
6주차 과제. 상속 (0) | 2020.12.25 |
5주차. 과제 (0) | 2020.12.17 |
댓글