자바 인터페이스(Java Interface)는 무엇인가?
개요
"자바 인터페이스(Java Interface)는 무엇인가?"
이런 궁금점을 가지고 있는 Java Programmer가 많습니다.
저는 "객체 지향 개발 5대 원칙 - SOLID"을 만족시켜줄 수 있어서라고 생각합니다.
그러면 "왜 SOLID를 만족시켜야 하는가?"라는 질문을 가지게 됩니다.
서비스 애플리케이션의 라이프사이클을 "설계 / 개발 / 유지보수" 세 단계로 나누면,
유지보수가 소프트웨어 라이프사이클에서 가장 큰 부분을 차지합니다.
유지보수에서 인터페이스(interface)는 SOLID를 구현하고, 객체 지향 개발을 하는데 큰 도움을 줍니다.
"설계 / 개발 / 유지보수" 관점에서 객체 지향을 설명하면, 다음과 같은 장점이 있습니다.
- 객체 지향 사용하여, 대상을 추상화를 하고, 추상화된 대상의 행동을 묘사하면서, 설계를 쉽게 할 수 있게 합니다.
- 설계가 끝나고, 개발하게 되는데, 개발자는 설계된 추상화 객체 단위로 쉽게 대상을 이해할 수 있습니다.
- 개발이 완료되고 유지보수를 하면, 기존 코드와 로직을 그대로 두고, 새로운 기능을 유연하게 추가할 수 있습니다.
그럼 다시 "자바 인터페이스(Java interface)는 무엇인가?"로 돌아와서,
자바 인터페이스(Java interface)가 "객체 지향"에 어떤 도움을 주는지 알아보도록 하겠습니다.
목차
- 인터페이스(interface) 역할
- 인터페이스(interface) 선언
- 인터페이스(interface) 구현
- 인터페이스(interface) 사용
- 인터페이스(interface) 타입 변환과 다형성
- 인터페이스(interface) 상속
- 인터페이스(interface) 디폴트 메서드와 확장
1. 인터페이스(interface) 역할
인터페이스는 어떤 역할을 할까요?
- 인터페이스는 객체를 어떻게 구성해야 하는지 정리한 설계도입니다.
- 인터페이스는 객체의 교환성(또는 다형성)을 높여줍니다.
- 인터페이스 변수에 인터페이스가 구현된 서로 다른 구현 객체를 할당해서 사용이 가능합니다.
- 구현 객체를 직접 몰라도 인터페이스 메서드만 알아도 객체 호출이 가능하게 합니다.
- 객체가 인터페이스를 사용하면, 인터페이스 메서드를 반드시 구현해야 하는 제약을 합니다.
위 특징을 이용해서 얻고자 하는 인터페이스(interface) 역할은 다음과 같습니다.
"interface를 이용하여, 개발 코드를 직접 수정하지 않고도, 사용하고 있는 객체만 변경할 수 있도록 하기 위함입니다."
2. 인터페이스(interface) 선언
인터페이스(Interface) 선언 형식은 다음과 같이합니다.
인터페이스(Interface)
[public] interface 인터페이스이름 { ... }
// 예시
public interface User { ... }
- "인터페이스이름"은 Upper CamelCase로 작성되어야 합니다.
- interface도 Class, Enum, Annotation처럼 "~.java" 파일로 작성되고, 컴파일러(javac.exe)를 통해서 바이트코드 형태의 "~.class" 파일로 컴파일됩니다.
- interface는 접근 지정자로 public을 사용하면, 다른 패키지에서도 사용이 가능합니다. public을 사용하지 않으면, interface가 위치한 해당 패키지 내에서만 사용이 가능합니다.
- interface의 접근 지정자는 public만 가능합니다. 이유는 interface는 class 설계도 이기 때문에 애초에 존재 목적이 공개이기 때문입니다.
- interface는 객체로 생성할 수 없기 때문에, 생성자를 가질 수 없습니다.
- class는 상수 필드, 정적 필드, 인스턴스 필드, 생성자, 인스턴스 메서드, 정적 메서드를 구성 멤버로 가지는데, interface는 상수 필드, 추상 메서드, 디폴트 메서드, 정적 메서드를 구성 멤버로 가집니다.
- Java 7까지는 실행 블록이 없는 추상 메서드로만 선언이 가능했습니다. Java 8부터는 디폴트 메서드와 정적 메서드도 선언이 가능합니다.
인터페이스(Interface) 구성요소는 다음과 같습니다.
2-1. 상수 필드(Constant Field)
2-2. 추상 메서드(Abstract Method)
2-3. 디폴트 메서드(Default Method)
2-4. 정적 메서드(Static Method)
2-1. 상수 필드(Constant Field)
public interface User {
// 상수 필드(Constant Field)
[public static final] 필드타입 상수명 = 값;
// 예시
String FIRST_NAME = "Ryan"; // 또는
public static final String FIRST_NAME = "Ryan"; // 는 같다.
}
- 인터페이스는 객체가 될 수 없기 때문에, 런타임에 필드 데이터를 저장할 수 없습니다. 그래서 인스턴스 필드/ 정적 필드는 선언이 불가능합니다.
- 상수 필드는 Compile Time에 선언되고 Run Time에 변경되지 않으므로 인터페이스에 선언이 가능합니다.
- [public static final]는 명시적으로 사용하지 않아도, Compile Time에 자동으로 선언되어 상수로 만듭니다.
- 네이밍은 모두 대문자로 구성되고 구분자는 "_"(언더바)로 표현됩니다. (Java Convention)
2-2. 추상 메서드(Abstract Method)
public interface User {
// 추상 메서드(Abstract Method)
[public abstract] 리턴타입 메서드이름(매개변수, ...);
// 예시
String sendMoney(Money money); // 와
public abstract String sendMoney(Money money); // 는 같다.
}
- 인터페이스 변수로 호출된 메서드는 최종적으로 구현 객체에서 실행됩니다. 그래서 실체는 인터페이스에 없고, 구현 클래스에 있습니다.
- 추상 메서드는 리턴 타입 / 메서드 이름 / 매개변수 가 기술되는 클래스 설계 메서드입니다.
- [public abstract]은 명시적으로 선언하지 않아도, Compile Time에 자동으로 선언됩니다.
2-3. 디폴트 메서드(Default Method)
public interface User {
// 디폴트 메서드(Default Method)
[public] default 리턴타입 메서드이름(매개변수, ...) { ... }
// 예시
public default void setStatus(Status status) {
if(status == Status.ACTIVE) {
System.out.println("사용자가 활성화 되었습니다");
return;
}
System.out.println("사용자가 비활성화 되었습니다");
}
}
- Java 8에서 추가된 인터페이스의 멤버입니다.
- 클래스의 인스턴스 메서드와 동일합니다. 즉, 인스턴스 메서드입니다. 다만 인터페이스에서 선언할 때, 리턴 타입 앞에 default 키워드가 붙습니다.
- [public]은 명시적으로 사용하지 않아도, Compile Time에 자동 선언됩니다.
- 디폴트 메서드는 나중에 인터페이스를 구현한 구현 클래스에 인스턴스 메서드로 추가됩니다.
- 재정의(Override)를 통해서 구현 클래스에서 재정의된 인스턴스 메서드로 사용할 수 있습니다.
2-4. 정적 메서드(Static Method)
public interface User {
// 정적 메서드(Static Method)
[public] static 리턴타입 메서드이름(매개변수, ...) { ... }
// 예시
public static void printFirstName() {
System.out.println("나의 이름은 " + firstName + "입니다.");
}
}
- Java 8에서 추가된 인터페이스의 멤버입니다.
- 선언 형식은 클래스 정적 메서드와 완전 동일합니다.
- [public]은 명시적으로 사용하지 않아도, Compile Time에 자동으로 선언됩니다.
- 인터페이스의 정적 메서드도 클래스의 정적 메서드와 똑같은 방식으로 사용이 가능합니다.
3. 인터페이스(interface) 구현
개발 코드에서 인터페이스의 메서드를 호출하면, 인터페이스는 구현 객체의 메서드를 찾아서 호출합니다.
객체는 인터페이스에 있는 추상 메서드를 구현한 실체 메서드를 가지고 있어야 합니다.
인터페이스를 구현한 객체를 구현 객체(구현체)라고 합니다.
구현 객체를 생성하는 클래스를 구현 클래스라고 합니다.
인터페이스 구현 방식은 세 가지 방식이 있습니다.
3-1. 단일 인터페이스 구현 클래스(Single Interface Implement Class)
3-2. 다중 인터페이스 구현 클래스(Multiple Interface Implement Class)
3-3. 익명 구현 객체(Anonymous Implement Object)
세 가지 방식에 대해서 설명하겠습니다.
3-1. 단일 인터페이스 구현 클래스(Implement Class)
public class 구현클래스이름 implements 인터페이스이름 {
// 인터페이스의 추상 메서드를 구현한 실체 메서드를 선언하는 부분
}
인터페이스 예시를 먼저 제시하겠습니다.
public interface User {
public static final String FIRST_NAME = "Ryan";
public abstract String sendMoney(Money money);
public default void setStatus(Status status) {
if(status == Status.ACTIVE) {
System.out.println("사용자가 활성화 되었습니다");
return;
}
System.out.println("사용자가 비활성화 되었습니다");
}
public static void printFirstName() {
System.out.println("나의 이름은 " + firstName + "입니다.");
}
}
인터페이스 예시를 구현한 구현 클래스 예시입니다.
public class Recipient implements User {
// 추상 메서드는 다음처럼 실체 메서드를 정의해야합니다.
public String sendMoney(Money money) {
thirdpartyApi.send(money.getType(), money.getAmount());
return Status.SUCCESS.name();
}
// 디폴트 메서드는 재정의가 가능합니다.
// 재정의 하지 않으면, 인터페이스에 정의된 내용 그대로 사용됩니다.
@Override
public default void setStatus(Status status) {
if(status == Status.ACTIVE) {
System.out.println("수취인이 활성화 되었습니다");
return;
}
System.out.println("수취인이 비활성화 되었습니다");
}
}
만약 인터페이스를 구현한다고 하고, 추상 메서드를 구현 클래스에서 실체 메서드를 모두 작성하지 않으면 해당 구현 클래스는 추상 클래스로 선언되어야 합니다.
public abstract class Recipient implements User {
}
인터페이스 변수에 구현 객체 대입 예시
// User 인터페이스를 구현한 구현 클래스 Recipient
public class Recipient implements User { ... }
// User 인터페이스를 구현한 구현 클래스 Sender
public class Sender implements User { ... }
User user = new Recipient();
user = new Sender();
3-2. 다중 인터페이스 구현 클래스(Multiple Interface Implement Class)
public class 구현클래스이름 implements 인터페이스이름1, 인터페이스이름2 {
// 인터페이스의 추상 메서드를 구현한 실체 메서드를 선언하는 부분
}
- 인터페이스를 구현한 구현 클래스는 다중 인터페이스를 구현 가능합니다.
- 다중 인터페이스를 구현한 구현 클래스는 반드시 모든 인터페이스의 추상 메서드를 실체 메서드로 구현해야 합니다.
- 하나라도 추상 메서드가 구현되지 않으면, 구현 클래스는 추상 클래스로 선언되어야 합니다.
3-3. 익명 구현 객체(Anonymous Implement Object)
- 구현 클래스를 만들어서 사용하는 것이 일반적이고, 재사용이 가능하기에 편리합니다. 하지만 일회성으로 사용하는 구현 클래스는 클래스로 만들어서 선언해서 쓰는 것이 비효율적입니다.
- 비효율을 개선하기 위해서 만들어진 것이 익명 구현 객체입니다. 익명 구현 객체는 임시 작업 스레드를 만들기 위해 많이 활용됩니다.
- 특징 중에 하나는 new 키워드 뒤에 원래는 인터페이스 구현 클래스 이름이 와야 하는데, 익명 구현 객체의 경우에는 참조할 구현 클래스가 없기 때문에 User 인터페이스 이름을 그대로 사용합니다.
- 다만, 익명 구현 객체의 구현 부에는 인터페이스의 추상 메서드가 아닌 실체 메서드를 선언해야 합니다.
익명 구현 객체 예시
// 인터페이스에 선언된 추상 메서드의 실체 메서드 선언
User user = new User() {
public String sendMoney(Money money) {
thirdpartyApi.send(money.getType(), money.getAmount());
return Status.SUCCESS.name();
}
@Override
public default void setStatus(Status status) {
if(status == Status.ACTIVE) {
System.out.println("수취인이 활성화 되었습니다");
return;
}
System.out.println("수취인이 비활성화 되었습니다");
}
};
그렇다고 익명 구현 객체를 사용한다고 해서, 클래스가 생성되지 않는 것은 아닙니다.
익명 구현 객체가 사용된 자바 파일을 컴파일을 하게 되면 자동으로 익명 구현 객체의 클래스 파일이 생성됩니다.
생성된 익명 구현 클래스 파일은 이름은 다음과 같습니다.
[익명 구현 객체가 사용된 자바 파일]$[번호].class ([번호]는 1부터 시작되어, 증가합니다.)
ex) TestExample$1.class
4. 인터페이스(interface) 사용
인터페이스 변수는 참조 타입이기 때문에 구현 객체가 대입될 경우 구현 객체의 번지가 저장됩니다.
User sender = new Sender(); // User 인터페이스 참조변수 sender에 Sender 객체의 번지 저장
User recipient = new Recipient(); // User 인터페이스 참조변수 recipient에 Recipient 객체의 번지 저장
인터페이스 변수는 다음 5개의 부분에서 구현 객체의 참조 용도로 사용될 수 있습니다.
- 클래스의 필드
- 생성자의 파라미터
- 생성자의 로컬 변수
- 메서드의 파라미터
- 메서드의 로컬 변수
public class TestClass {
// 1.클래스의 필드
User user = new Recipient();
// 2.생성자의 파라미터
TestClass(User user) {
this.user = user;
// 3.생성자의 로컬변수
User recipient = new Recipient();
}
// 4.메서드의 파라미터
void methodA(User user) {
...
}
void methodB() {
// 5.메서드의 로컬변수
User user = new Recipient();
}
}
이 부분부터는 인터페이스의 구성요소를 어떻게 사용하는지 살펴보겠습니다.
4-1. 상수 필드(Constant Field) 사용
4-2. 추상 메서드(Abstract Method) 사용
4-3. 디폴트 메서드(Default Method) 사용
4-4. 정적 메서드(Static Method) 사용
설명을 위해서 User 인터페이스를 예시 용도로 가져왔습니다.
public interface User {
public static final String FIRST_NAME = "Ryan";
public abstract String sendMoney(Money money);
public default void setStatus(Status status) {
if(status == Status.ACTIVE) {
System.out.println("사용자가 활성화 되었습니다.");
return;
}
System.out.println("사용자가 비활성화 되었습니다.");
}
public static void printFirstName() {
System.out.println("나의 이름은 " + firstName + "입니다.");
}
}
4-1. 상수 필드(Constant Field) 사용
User.FIRST_NAME
위 방식으로 상수는 클래스의 상수와 같은 방식으로 사용할 수 있습니다.
4-2. 추상 메서드(Abstract Method) 사용
public class Example {
public static void main(String[] args) {
User user = null;
user = new Recipient();
user.sendMoney(new Money(1l));// Recipient가 1원을 보냈다.
user = new Sender();
user.sendMoney(new Money(2l));// Sender가 2원을 보냈다.
}
}
user 인터페이스의 추상 메서드 sendMoney를 호출하면, 인터페이스 변수에 대입되었던 구현 객체의 주소를 판단해서 해당하는 실체 메서드를 호출합니다.
4-3. 디폴트 메서드(Default Method) 사용
public class Example {
public static void main(String[] args) {
User user = null;
user = new Recipient();
user.setStatus(Status.ACTIVE); // 사용자가 활성화 되었습니다.
user = new Sender();
user.setStatus(Status.INACTIVE); // 사용자가 비활성화 되었습니다.
}
}
구현 클래스에 실체 메서드를 작성하지 않아도 구현 객체에서 호출이 가능합니다.
물론, 구현 클래스에서 디폴트 메서드가 변경이 필요한 경우, 재정의(Override)가 가능합니다.
추상 메서드가 아니고 인스턴스 메서드이기 때문에 생성한 구현 객체가 있어야 사용할 수 있습니다.
디폴트 메서드를 재정의한 예시를 보겠습니다.
public class Recipient implements User {
@Override
public default void setStatus(Status status) {
if(status == Status.ACTIVE) {
System.out.println("수취인이 활성화 되었습니다.");
return;
}
System.out.println("수취인이 비활성화 되었습니다.");
}
}
public class Sender implements User {
@Override
public default void setStatus(Status status) {
if(status == Status.ACTIVE) {
System.out.println("송신자가 활성화 되었습니다.");
return;
}
System.out.println("송신자가 비활성화 되었습니다.");
}
}
public class Example {
public static void main(String[] args) {
User user = null;
user = new Recipient();
user.setStatus(Status.ACTIVE); // 수취인이 활성화 되었습니다.
user = new Sender();
user.setStatus(Status.INACTIVE); // 송신자가 비활성화 되었습니다.
}
}
구현 클래스에서 재정의된 디폴트 메서드가 사용되어 호출됩니다.
4-4. 정적 메서드(Static Method) 사용
public class Example {
public static void main(String[] args) {
User.printFirstName(); // 나의 이름은 Ryan입니다.
}
}
인터페이스의 정적 메서드도 클래스의 정적 메서드와 같은 방식으로 사용합니다.
정적 메서드이기 때문에 재정의(Override)는 불가능합니다.
5. 인터페이스(interface) 타입 변환과 다형성
인터페이스는 다형성을 이용해서 타입 변환을 합니다. 타입 변환에 대한 이야기 하기에 앞서서 다형성에 대해서 알아보겠습니다.
다형성(Polymorphism) 이란?
: 하나의 타입 변수에 대입되는 객체에 따라서 실행 결과가 다양한 타입의 형태로 나오는 성질을 이야기합니다. 즉, 하나의 타입 변수를 동일한 메서드로 동작시키지만 실제 동작은 다를 수 있는 성질입니다.
인터페이스를 사용해서 다형성을 사용하면, 사용할 구현 객체만 바꿔주면 나머지 소스코드는 변경할 필요가 없습니다.
Interface i = new ImplementObjectA();
// i를 수정해도
i = new ImplementObjectB();
// 변경이 없습니다.
i.method1();
i.method2();
상속에서도 부모 클래스 타입의 변수에 어떤 자식 객체를 대입하냐에 따라서 객체의 메서드 실행 결과는 달라질 수 있습니다.
결국, 상속에서도 인터페이스와 마찬가지로 다형성을 구현하고 있습니다.
ParentClass p = new ChildClassA();
// 자식 객체로 대입이 가능하다. (다형성)
p = new ChildClassB();
// 다만, ParentClass안에 구현된 메서드만 호출이 가능
p.methodAInParent();
p.methodBInParent();
인터페이스가 매개변수로 쓰이는 경우
public class UserService {
// User 인터페이스를 매개변수로 사용.
public void printUserType(User user) {
user.printType();
}
}
// User 인터페이스를 구현한 구현 객체 Recipient, Sender를 매개변수로 사용
userService.printUserType(new Recipient()); // 수취인입니다.
userService.printUserType(new Sender()); // 송신자입니다.
간단하게 인터페이스와 상속 관계에서 타입 변환과 다형성에 대해서 살펴보았습니다.
여기서 조금 더 자세하게 "타입 변환"과 "다형성"에 대해서 알아보겠습니다.
5-1. 자동 타입 변환(Promotion)
5-2. 매개 변수의 다형성
5-3. 강제 타입 변환(Casting)
5-4. 객체 타입 확인(instanceof)
위에 나열한 목록 순서로 설명하겠습니다.
5-1. 자동 타입 변환(Promotion)
Runtime에 구현 객체가 인터페이스 타입의 참조 변수로 자동 변환하는 것을 "자동 타입 변환(Promotion)"이라고 합니다.
자동 타입 변환은 인터페이스를 구현한 구현 클래스의 자식 클래스를 만들고, 자식 객체를 인터페이스의 변수에 담아도 다형성이 적용됩니다.
그러면 왜 인터페이스를 구현한 클래스의 자식 클래스도 다형성이 적용되고 자동 타입 변환이 가능할까요?
부모 클래스인 구현 클래스는 자식 클래스에게 상속을 하고 나면,
인터페이스와 마찬가지로 부모 클래스에 대한 멤버를 자식 클래스에게 전달하게 됩니다.
결국 부모 클래스는 이미 먼저 인터페이스를 통해서 추상 메서드를 실체 메서드로 구현하였기 때문에,
결국 구현된 실체 메서드가 자식 클래스에게 전달되는 것이 확실해집니다.
그렇기 때문에 인터페이스를 구현한 클래스의 자식 클래스도 다형성이 적용되고 자동 타입 변환이 가능합니다.
예시)
// 인터페이스 A
// 인터페이스 A를 구현한 클래스 B
// 인터페이스 A를 구현한 클래스 C
// 클래스 B를 상속한 자식 클래스 D
// 클래스 C를 상속한 자식 클래스 E
// B, C, D, E에는 printClassName를 자기 클래스 이름을 출력하는 로직이 재정의 되어있다고 가정합니다.
A a1 = new B(); // OK
a1.printClassName(); // B 클래스입니다.
A a2 = new C(); // OK
a2.printClassName(); // C 클래스입니다.
A a3 = new D(); // OK
a3.printClassName();
// 기본은 상속된 printClassName가 호출됩니다. "B 클래스입니다."
// 만약 클래스 D의 printClassName를 재정의하면 "D 클래스입니다."가 됩니다.
A a4 = new E(); // OK
a4.printClassName();
// 기본은 상속된 printClassName가 호출됩니다. "C 클래스입니다."
// 만약 클래스 E의 printClassName를 재정의하면 "E 클래스입니다."가 됩니다.
5-2. 매개 변수의 다형성
매개 변수를 인터페이스 변수로 두고, 구현 객체를 대입해주면 다형성을 적용해서 사용할 수 있습니다.
예시)
// 인터페이스
public interface User {
void printType();
}
// 구현 클래스 "Recipient"
public class Recipient implements User {
@Override
public void printType() {
System.out.println("수취인입니다.");
}
}
// 구현 클래스 "Sender"
public class Sender implements User {
@Override
public void printType() {
System.out.println("송신자입니다.");
}
}
public class Mail {
public void printUserType(User user) {
user.printType();
}
}
public class Main {
public static void main(String[] args) {
Mail mail = new Mail();
mail.printUserType(new Recipient()); // User user = new Recipient(); "수취인입니다."
mail.printUserType(new Sender()); // User user = new Sender(); "송신자입니다."
}
}
5-3. 강제 타입 변환(Casting)
인터페이스의 변수를 구현 객체와 사용하면, 인터페이스에서 정의된 추상 메서드의 실체 메서드 이외에는 호출할 수 없습니다.
즉,
구현 클래스에 추가로 메서드를 구현하고,
나중에 구현 객체를 사용할 때, 인터페이스 변수가 사용된다면,
추가로 구현된 메서드는 호출이 되지 않습니다.
그래서 다음 솔루션을 사용합니다.
5-3-1. 직접 구현 클래스 변수에 구현 객체를 대입하는 방법
Recipient recipient = new Recipient();
5-3-2. 인터페이스 변수를 구현 객체로 강제 타입 변환을 사용하면 됩니다.
User user = new Recipient();
Recipient recipient = (Recipient) user;
예시)
// 인터페이스
public interface User {
void printType();
}
// 구현 클래스 "Recipient"
public class Recipient implements User {
private static final String COUNTRY = "KOREA";
@Override
public void printType() {
System.out.println("수취인입니다.");
}
// 추가로 메서드 구현
public void printCountry() {
System.out.println("국가는 " + COUNTRY + "입니다.");
}
}
public class Main {
public static void main(String[] args) {
User user = new Recipient();
user.printType(); // "수취인입니다."
user.printCountry(); // 호출이 불가능합니다.
//
Recipient recipient = (Recipient) user;
recipient.printType(); // "수취인입니다."
recipient.printCountry(); // "국가는 KOREA입니다."
}
}
5-4. 객체 타입 확인(instanceof)
위 강제 타입 변환(Casting)은 구현 객체가 인터페이스 타입으로 변환되어 있는 상태에서 가능합니다.
User user = new Recipient();
그런데 어떤 구현 객체가 대입되는지 모르는 상태에서 강제 타입 변환을 하게 되면 ClassCastException이 발생할 수 있습니다.
User user = new Recipient();
Sender sender = (Sender) user; // ClassCastException 발생
ClassCastException를 발생시키지 않기 위해서 "instanceof" 를 사용합니다.
User user = new Recipient();
if( user instanceof Recipient ) { // user 변수에 담긴 객체가 Recipient 타입이면,
Recipient recipient = (Recipient) user;
}
"instanceof"를 사용하면, 강제 타입 변환에 앞서서 ClassCastException을 방지할 수 있습니다.
6. 인터페이스(interface) 상속
인터페이스도 다른 인터페이스를 상속할 수 있습니다.
클래스는 다중 상속을 허용하지 않지만, 인터페이스는 다중 상속을 허용합니다.
다음 방식으로 선언되어 인터페이스 상속이 가능합니다.
public interface 하위인터페이스 extends 상위인터페이스1, 상위인터페이스2, ... { ... }
하위 인터페이스를 구현하는 구현 클래스에서는 "하위 인터페이스의 추상 메서드"와 "상속하는 모든 상위 인터페이스의 추상 메서드"의 실체 메서드로 구현해야 합니다.
하위인터페이스 변수 = new 구현클래스();
상위인터페이스1 변수 = new 구현클래스();
상위인터페이스2 변수 = new 구현클래스();
"하위 인터페이스 변수"는 하위인터페이스, 상위 인터페이스1, 상위 인터페이스2의 실체 메서드를 모두 사용할 수 있습니다.
"상위 인터페이스1 변수"는 상위인터페이스1의 실체 메서드를 사용할 수 있습니다.
"상위 인터페이스2 변수"는 상위인터페이스2의 실체 메서드를 사용할 수 있습니다.
예시)
public interface InterfaceA {
public void methodA();
}
public interface InterfaceB {
public void methodB();
}
public interface InterfaceC extends InterfaceA, InterfaceB {
public void methodC();
}
// InterfaceC를 구현했기 때문에 methodA(), methodB(), methodC() 실체 메서드를 모두 가져야합니다.
public class ImplementationClassC implements InterfaceC {
public void methodA() {
System.out.println("ImplementationClassC-methodA()");
}
public void methodB() {
System.out.println("ImplementationClassC-methodB()");
}
public void methodC() {
System.out.println("ImplementationClassC-methodC()");
}
}
public class Main {
public static void main(String[] args) {
ImplementationC implC = new ImplementationC();
InterfaceA iA = implC;
iC.methodA(); // OK
iC.methodB(); // NO
iC.methodC(); // NO
InterfaceB iB = implC;
iC.methodA(); // NO
iC.methodB(); // OK
iC.methodC(); // NO
InterfaceC iC = implC;
iC.methodA(); // OK
iC.methodB(); // OK
iC.methodC(); // OK
}
}
7. 인터페이스(interface) 디폴트 메서드와 확장
7-1. 디폴트 메서드는 왜 만들어졌을까?
디폴트 메서드는 인스턴스 메서드입니다. 즉, 구현 객체가 있어야 사용할 수 있습니다.
인스턴스 메서드인데 왜 인터페이스에 있을까요?
Java8에서 인터페이스에서 디폴트 메서드를 허용하는 이유는 기존 인터페이스를 확장해서 새로운 기능을 추가하기 위해서입니다.
만약 기존 인터페이스에 새로운 기능을 위해서 추상 메서드를 추가하면 어떤 상황이 발생할까요?
추상 메서드를 새롭게 선언하면, 해당 인터페이스를 구현하는 구현 클래스를 찾아다니면서,
새로 선언된 추상 메서드에 대한 실체 메서드를 선언해주어야 합니다. 아니면 Compile Error가 발생합니다.
하지만 인터페이스에 디폴트 메서드를 선언하게 되면,
이미 선언된 인스턴스 메서드이기에 실체 메서드를 선언하지 않아도 문제없이 필요한 곳에서 디폴트 메서드를 사용할 수 있습니다.
디폴트 메서드는 추상 메서드가 구현 클래스에서 실체 메서드로 구현되어야 한다는 제약을 없애준 편의 메서드로 볼 수 있습니다.
public interface SampleInterface {
// 무조건 구현 클래스에서 실체 메서드로 정의해야하는 추상 메서드
public void abstractMethod();
// 구현 객체에서 사용할 수 있는 인스턴스인 디폴트 메서드
// 구현 클래스에서 재정의도 가능합니다.
public default void defaultMethod() {
System.out.println("(1) SampleInterface-defaultMethod()");
}
}
public class ImplementationClassA implements SampleInterface {
@Override
public void abstractMethod() {
System.out.println("(2) ImplementationClassA-abstractMethod()");
}
}
public class ImplementationClassB implements SampleInterface {
@Override
public void abstractMethod() {
System.out.println("(3) ImplementationClassB-abstractMethod()");
}
// 디폴트 메서드 재정의 가능
@Override
public void defaultMethod() {
System.out.println("(4) ImplementationClassB-defaultMethod()");
}
}
public class Main {
public static void main(String[] args) {
SampleInterface sI1 = new ImplementationClassA();
sI1.abstractMethod(); // (2)
sI1.defaultMethod(); // (1)
SampleInterface sI2 = new ImplementationClassB();
sI2.abstractMethod(); // (3)
sI2.defaultMethod(); // (4)
}
}
7-2. 디폴트 메서드가 있는 인터페이스 상속
디폴트 메서드를 가진 부모 인터페이스를 상속한 자식 인터페이스에서 디폴트 메서드를 활용하는 방법에 대해서 알아보겠습니다.
자식 인터페이스에서는 디폴트 메서드를 다음 방식으로 사용할 수 있습니다.
7-2-1. 부모 인터페이스의 디폴트 메서드를 상속받아서 그대로 사용
7-2-2. 부모 인터페이스의 디폴트 메서드를 자식 인터페이스에서 재정의하여 사용
7-2-3. 부모 인터페이스의 디폴트 메서드를 추상 메서드로 재선언하여, 구현 클래스에서 실체 메서드로 구현하여 사용
위 세 가지 방식에 대해서 예시를 보이겠습니다.
7-2-1. 부모 인터페이스의 디폴트 메서드를 상속받아서 그대로 사용
public interface ParentInterface {
public default void defaultMethod() {
System.out.println("(1) ParentInterface-defaultMethod()");
}
}
public interface ChildInterface extends ParentInterface {
}
public class ImplementationClass implements ChildInterface {
}
public class Main {
public static void main(String[] args) {
ChildInterface iC = new ImplementationClass();
iC.defaultMethod(); // (1) ParentInterface-defaultMethod()
}
}
7-2-2. 부모 인터페이스의 디폴트 메서드를 자식 인터페이스에서 재정의하여 사용
자식 인터페이스에서 디폴트 메서드를 재정의한 경우
public interface ParentInterface {
public default void defaultMethod() {
System.out.println("(1) ParentInterface-defaultMethod()");
}
}
public interface ChildInterface extends ParentInterface {
@Override
public default void defaultMethod() {
System.out.println("(2) ChildInterface-defaultMethod()");
}
}
public class ImplementationClass implements ChildInterface {
}
public class Main {
public static void main(String[] args) {
ChildInterface iC = new ImplementationClass();
iC.defaultMethod(); // (2) ChildInterface-defaultMethod()
}
}
구현 클래스에서 디폴트 메서드를 재정의한 경우
public interface ParentInterface {
public default void defaultMethod() {
System.out.println("(1) ParentInterface-defaultMethod()");
}
}
public interface ChildInterface extends ParentInterface {
@Override
public default void defaultMethod() {
System.out.println("(2) ChildInterface-defaultMethod()");
}
}
public class ImplementationClass implements ChildInterface {
@Override
public void defaultMethod() {
System.out.println("(3) ImplementationClass-defaultMethod()");
}
}
public class Main {
public static void main(String[] args) {
ChildInterface iC = new ImplementationClass();
iC.defaultMethod(); // (3) ImplementationClass-defaultMethod()
}
}
7-2-3. 부모 인터페이스의 디폴트 메서드를 추상 메서드로 재선언하여, 구현 클래스에서 실체 메서드로 구현하여 사용
public interface ParentInterface {
public default void defaultMethod() {
System.out.println("(1) ParentInterface-defaultMethod()");
}
}
public interface ChildInterface extends ParentInterface {
// 추상 메서드로 재선언
void defaultMethod();
}
public class ImplementationClass implements ChildInterface {
// 추상 메서드의 실체 메서드 구현
@Override
public void defaultMethod() {
System.out.println("(2) ImplementationClass-defaultMethod()");
}
}
public class Main {
public static void main(String[] args) {
ChildInterface iC = new ImplementationClass();
iC.defaultMethod(); // (2) ImplementationClass-defaultMethod()
}
}
결론
지금까지 인터페이스는 무엇이고, 어떻게 사용하는지에 대해서 알아봤습니다.
하지만 여전히 인터페이스는 왜 사용해야 하는지에 대해서 의문일 수 있습니다.
저는 "객체 지향 프로그래밍을 돕는 디자인 패턴이니까 사용해서 구현하자."라고 말할 수 있습니다.
인터페이스를 구현하고, 상속하고, 확장하면서 얻을 수 있는 프로그래밍의 장점을 다형성과 사용하면서,
유지보수에 있어서 기존의 코드를 변경 없이 해당 구현 객체를 변경하기만 해도 호출되는 소스의 내용을 바꿔줄 수 있습니다.
저는 보통 클래스의 상속보다 추상 클래스와 인터페이스를 이용해서 공통된 메서드를 구현하고, 다형성을 이용합니다.
이유는 클래스 간 강한 결합도를 낮추기 위함입니다.
응집도를 높이고, 결합도를 낮추는 게 객체지향이 추구하는 기본 원칙이기 때문입니다.
인터페이스를 잘 사용하는 방법은 많이 사용해보고, 장단점을 파악해서 필요한 곳에 사용할 수 있도록 하는 것이라고 생각합니다.
글을 마치겠습니다.