--> 개발 코드가 직접 객체의 메소드를 호출하면 간단한데 왜 중간에 인터페이스를 두는지 의문점이 생기는데 그 이유는 개발 코드를 수정하지 않고 사용하는 객체를 변경할 수 있도록 하기 위해서이다. 인터페이스는 하나의 객체가 아니라 여러 객체들과 사용이 가능하므로 어떤 객체를 사용하느냐에 따라서 실행 내용과 리턴값이 다를 수 있다. 따라서 개발 코드 측면에서는 코드 변경없이 실행 내용과 리턴값을 다양화할 수 있다는 장점을 가지게 된다.
인터페이스는 "~.java" 형태의 소스 파일로 작성되고 컴파일러(javac.exe)를 통해 "~.class"형태로 컴파일되기 때문에 물리적 형태는 클래스와 동일하다. 차이점은 소스를 작성할 때 선언하는 방법이 다르다.
인터페이스는 일종의 추상클래스 입니다. 클래스가 객채(Instance)의 명세서 혹은 설계서라고 한다면 추상클래스는 미완성 설계도 입니다. 추상 클래스는 멤버(변수, 메서드)를 가진다는 점은 일반 클래스와 다를바 없지만 추상 클래스의 메서드는 구현부는 작성하지 않고 선언부만 작성하는 '추상 메서드'를 가진다는 점이 다릅니다.
인터페이스를 일종의 추상클래스라고 하였지만 추상화 정도가 더 높아서 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없습니다. 인터페이스는 오직 추상메서드와 상수만을 가집니다.
인터페이스 선언은 class키워드 대신에 interface키워드를 사용한다.
[public] interface 인터페이스명 {...}
인터페이스의 작성은 클래스의 작성과 비슷합니다. 단지, 선언부의 class 키워드 대신 interface를 작성한다는 것이 다릅니다. 인터페이스에 사용 할 수 있는 접근 제어자는 public 도는 default가 있습니다. 구현부에서는 상수와 추상메서드만을 사용 할 수 있는데 상수는 'public static final' 키워드를 추상 메서드는 'public abstract' 키워드를 사용하여 선언합니다. 해당 키워드들은 인터페이스에 모든 멤버에 예외없이 적용되는 사항이기 때문에 생략할 수도 있습니다. (생략된 키워드들은 컴파일시 컴파일러가 자동으로 추가합니다.)
일반 클래스
class 클래스 이름{
// 멤버변수
변수타입 변수명;
// 메서드
반환타입 메서드명(매개변수타입 매개변수명){
// 구현부
return 반환타입 값;
}
}
추상 클래스
interface 인터페이스명{
// 멤버변수
public static final 변수타입 변수명 = 값;
// 메서드
public abstract 반환타입 메서드명(매개변수타입 매개변수명);
}
인터페이스 자체만으로는 인스턴스를 생성 할 수 없습니다. 인터페이스는 상속을 통해 자신의 추상메서드 구현부를 완성 해줄 클래스를 정의 함으로써 사용이 가능합니다. 단, 일반적으로 상속을 사용하기위해 클래스에 'extends'를 흔히 사용하였지만 인터페이스에서의 상속은 구현을 한다는 의미에서 'implements'를 사용합니다.
인터페이스를 구현하는 class는 반드시 인터페이스의 추상메서드를 모두 구현해야만합니다. 만약 인터페이스의 일부 추상 메서드만 구현을 하고시팓면 class 구현부 앞에 'abstract'를 붙여 주어야만 합니다.
인터페이스
public interface TestInterface {
public static final int var1 = 1;
public abstract void testMethod1();
public abstract void testMethod2();
public abstract void testMethod3();
}
인터페이스 구현 클래스(추상 메소드 모두 구현)
public class TestImplements implements TestInterface{
@Override
public void testMethod1() {
System.out.println("인터페이스의 추상 메서드 구현1");
}
@Override
public void testMethod2() {
System.out.println("인터페이스의 추상 메서드 구현2");
}
@Override
public void testMethod3() {
System.out.println("인터페이스의 추상 메서드 구현3");
}
}
인터페이스 구현 클래스(추상 메소드 일부 구현)
public abstract class TestImplements2 implements TestInterface {
@Override
public void testMethod2() {
System.out.println("인터페이스의 추상 메서드 구현2");
}
}
상수 필드(Constant Field)
인터페이스는 객체 사용 설명서이므로 런타임 시 데이터를 저장할 수 있는 필드를 선언할 수 없다.
그러나 상수 필드는 선언이 가능하다. 상수는 인터페이스에 고정된 값으로 런타임 시에 데이터를 바꿀 수 없다. 상수를 선언할 때에는 반드시 초기값을 대입해야 한다.
상수는 public static final로 선언하는데 인터페이스에서 선언된 필드는 이를 생략하더라도 컴파일 과정에서 자동을 붙게 된다.
상수명은 대문자로 작성하되, 서로 다른 단어로 구성되어 있을 경우에는 언더바(_)로 연결하는 것이 관례이다.
public interface RemoteControl{
public int MAX_VOLUME = 10;
public int MIN_VOLUME = 0;
}
추상 메소드(Abstract Method)
인터페이스를 통해 호출된 메소드는 최종적으로 객체에서 실행된다. 그렇기 때문에 인터페이스의 메소드는 실행 블록이 필요 없는 추상 메소드로 선언한다. 추상 메소드는 리턴 타입, 메소드명, 매개 변수만 기술되고 중괄호 {}를 붙이지 않는 메소드를 말한다. 인터페이스에 선언도니 추상 메소드는 모두 public abstract의 특성을 갖기 때문에 이를 생략하더라도 컴파일 과정에서 자동적으로 붙게 된다.
public interface RemoteControl{
public int MAX_VOLUME = 10;
public int MIN_VOLUME = 0;
public void turnOn();
public void turnOff();
public void setVolume(int volume);
//메소드 선언부만 작성(추상 메소드)
}
디폴트 메소드(Default Method)
형태는 클래스의 인스턴스 메소드와 동일한다, default 키워드가 리턴 타입 앞에 붙는다. 디폴트 메소드는 public 특성을 갖기 때문에 public을 생략하더라도 자동적으로 컴파일 과정에서 붙게 된다.
[public] default 리턴타입 메소드명(매개변수, ...) {...}
인터페이스는 기능에 대한 선언만 가능하기 때문에, 실제 코드를 구현한 로직은 포함될 수 없습니다. 하지만 자바8에서 이러한 룰을 깨트리는 기능이 나오게 되었는 데 그것이 Default Method입니다. 메소드 선언 시에 default를 명시하게 되면 인터페이스 내부에서도 로직이 포함된 메소드를 선언할 수 있습니다.
(접근제어자에서 사용하는 default와 같은 키워드이지만, 접근제어자는 아무것도 명시하지 않은 접근 제어자를 default라고 하며 인터페이스의 default method는 'default'라는 키워드를 명시해야 합니다.)
interface MyInterface {
default void printHello() {
System.out.println("Hello World");
}
}
default라는 키워드를 메소드에 명시하게 되면 인터페이스 내부라도 코드를 작성할 수 있습니다.
디폴트 메소드는 인터페이스에 선언되지만, 인터페이스에서 바로 사용할 수 없다. 디폴트 메소드는 추상 메소드가 아닌 인스턴스 메소드이므로 구현 객체가 있어야 사용할 수 있다. 예를 들어 RemoteControl 인터페이스는 setMut()라는 디폴트 메소드를 가지고 있지만, 이 메소드를 다음 과 같이 호출 할 수는 없다.
RemoteControl.setMute(true);
setMute() 메소드를 호출하려면 RemoteControl의 구현 객체가 필요한데, 다음과 같이 Television 객체를 인터페이스 변수에 대입하고 나서 setMute()를 호출할 수 있다. 비록 setMute()가 Television에 선언되지는 않았지만 Television 객체가 없다면 setMute()도 호출할 수 없다.
RemoteControl rc = new Television();
rc.setMute(true);
디폴트 메소드는 인터페이스의 모든 구현 객체가 가지고 있는 기본 메소드라고 생각하면 된다.
그러나 어떤 구현 객체는 디폴트 메소드의 내용이 맞지 않아 수정이 필요할 수도 있다. 구현 클래스를 작성할 때 디폴트 메소드를 재정의(오버라이딩)해서 자신에게 맞게 수정하면 디폴트 메소드가 호출될 때 자신을 재정의한 메소드가 호출된다.
왜 사용할까?
사실 인터페이스는 기능에 대한 구현보다는, 기능에 대한 '선언'에 초점을 맞추어서 사용 하는데, 디펄트 메소드는 왜 등장했을까요? 자바 기본서 '자바의 신'에서는 디펄트 메소드에 대한 존재 이유를 아래와 같이 설명했습니다.
...(중략) ... 바로 "하위 호환성"때문이다. 예를 들어 설명하자면, 여러분들이 만약 오픈 소스코드를 만들었다고 가정하자. 그 오픈소스가 엄청 유명해져서 전 세계 사람들이 다 사용하고 있는데, 인터페이스에 새로운 메소드를 만들어야 하는 상황이 발생했다. 자칫 잘못하면 내가 만든 오픈소스를 사용한 사람들은 전부 오류가 발생하고 수정을 해야 하는 일이 발생할 수도 있다. 이럴 때 사용하는 것이 바로 default 메소드다. (자바의 신 2권)
기존에 존재하던 인터페이스를 이용하여서 구현된 클래스를 만들고 사용하고 있는데 인터페이스를 보완하는 과정에서 추가적으로 구현해야 할 혹은 필수적으로 존재해야 할 메소드가 있다면, 이미 이 인터페이스를 구현한 클래스와의 호환성이 떨어 지게 됩니다. 이러한 경우 default 메소드를 추가하게 된다면 하위 호환성은 유지되고 인터페이스의 보완을 진행 할 수 있습니다.
(실제로 스프링 프레임 워크 버전 4에서 WebMvcConfigure라는 인터페이스를 구현한 클래스 WebMvcConfigurerAdapter를 사용하였지만 자바8에서 default 메소드가 등장하고 WebMvcConfigurerAdapter클래스는 더 이상 사용되지 않습니다.)
예제 코드
interface MyInterface {
default void printHello() {
System.out.println("Hello World");
}
}
class MyClass implements MyInterface {}
public class DefaultMethod {
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.printHello(); //실행결과 Hello World 출력
}
}
// Test 인터페이스
public interface Test {
void testMethod();
}
// Test 구현한 TestImipl 클래스
public class TestImpl implements Test {
@Override
public void testMethod() {
System.out.println("=== testMethod1 ===");
}
}
위는 기본적인 형태의 인터페이스와 구현한 클래스입니다.
당연히 TestImpl 클래스는 testMethod()의 오버라이딩이 강제됩니다.
이때 Test 인터페이스에 새로운 추상 메서드가 들어오게 된다면? 당연히 오버라이딩이 강제되며, 사용하지 않더라도 일단 구현해야 합니다. 지금은 출력만 하는 간단한 구현부지만, 복잡한 API의 경우에는 불필요한 시간낭비와 노력이 필요합니다.
// Test 인터페이스
public interface Test {
void testMethod();
// 추가된 default 메서드
default void defaultMethod() {
System.out.println("defaultMethod");
}
}
// Test 구현한 TestImipl 클래스
public class TestImpl implements Test {
@Override
public void testMethod() {
System.out.println("=== testMethod1 ===");
}
// default 메소드, 강제 구현 X
@Override
public void defaultMethod() {
Test.super.defaultMethod();
}
}
그래서 Test 인터페이스에 default 메서드를 구현합니다.
특징은 추상메서드가 아니기 때문에 인터페이스에서 본문이 구현되어야 합니다. 그리고 이를 구현한 TestImpl 클래스에서는 강제로 defaultMethod()를 구현하지 않아도 됩니다.
정적 메소드(static method)
오라클 공식문서에서는 static 메서드를 인터페이스에서 helper 메서드로 사용하는 예시를 들고 있습니다. 여기서 말하는 helper 메서드는 주목적으로서 사용되는 것이 아닌, 다른 객체를 돕기 위해서 특정 목적으로 만들어진 메서드입니다.
이러한 이유는 클래스의 static 메서드처럼 사용하기보다는, 인터페이스 내부적으로 필요한 것을 정의해두고 사용하는데 목적이 있다고 추측됩니다.
마찬가지로 구현이 강제되지 않습니다.
// Test 인터페이스
public interface Test {
static String staticMethod() {
return "hello ";
}
default String defaultMethod(String param) {
return staticMethod() + param;
}
}
// Test 인터페이스 구현한 TestImpl
public class TestImpl implements Test {
@Override
public String defaultMethod(String param) {
return Test.super.defaultMethod(param);
}
public static void main(String[] args) {
TestImpl testImpl = new TestImpl();
System.out.println(testImpl.defaultMethod("world"));
}
}
Test 인터페이스에서 static 메서드는 default와 마찬가지고 구현 클래스에서 강제로 구현할 필요 없습니다.
staticMethod()는 defaualtMethod()의 철저한 helper class로 사용하고 있습니다. 이를 외부에서 사용하여 주목적으로 사용하는 것이 아닌, defaultMethod() 하나를 돕는 목적입니다.
정리
default, static 메서드 모두 인터페이스에서 구현되지만, 이를 구현한 클래스에서는 강제성이 없습니다. 그렇기 때문에 더 유연한 개발을 가능하게 하고, 하위 호환성의 문제도 피할 수 있다고 생각합니다.
익명 구현 객체 구현
인터페이스 변수 = new 인터페이스() {
// 인터페이스에 선언된 추상 메소드의 실체 메소드 선언;
};
하나의 실행문이므로 끝에는 세미콜론을 반드시 붙여야 합니다.
중괄호 {}에는 인터페이스에 선언되 모든 추상 메소드들의 실체 메소드를 작성해야 합니다.
추가적으로 필드와 메소드를 선언할 수 있지만, 익명 객체 안에서만 사용할 수 있고 인터페이스 변수로 접근할 수 없습니다.
예시
인터페이스 SwitchEvent
public interface TactSwitch {
void onClick();
}
인터페이스 SwitchEvent의 익명 구현 객체
TactSwitch tactSwitch = new TactSwitch() {
boolean check = false;
@Override
public void onClick() {
if (check) {
check = false;
System.out.println("OFF");
} else {
check = true;
System.out.println("ON");
}
}
};