[JAVA] 인터페이스(Interface)의 기본 개념과 Java 8 이후의 변화
kindof
·2021. 6. 19. 23:49
0. 인터페이스(Interface)
인터페이스는 자바의 다형성(Polymorphism)을 극대화하여 객체지향프로그래밍을 더 수월하게 해주는 역할을 합니다.
인터페이스를 통해 객체는 추상화에 더 의존하게 되고, 이에 따라 프로그램의 유지 보수가 용이해집니다.
1. 인터페이스 정의하기
인터페이스의 구성요소는 크게 상수 필드, Abstract 메서드, Default 메서드, Static 메서드, Private Method가 있습니다.
상수 필드는 인터페이스에서 정의한 상수를 클래스에서 그대로 값만 참조하여 사용할 수 있게 하고,
Abstract 메서드는 인터페이스 구현 클래스에서 직접 오버라이딩해서 구현해야 하는 메서드(빈 껍데기)를 말합니다.
Default 메서드는 인터페이스에서 기본적인 메서드 내용을 정의해주지만, 구현 클래스마다 이를 오버라이딩해서 재정의할 수 있죠.
Static 메서드는 구현 클래스에서 변경할 수 없는(오버라이딩 할 수 없는) 인터페이스가 정한 메서드를 말합니다.
마지막으로 Private 메서드 역시 인터페이스 안에 정의될 수 있는데, 이 녀석의 역할은 아래에서 살펴보도록 하겠습니다.
2. 인터페이스 구현 클래스
인터페이스를 구현하는 방식에는 크게 세 가지가 있습니다.
1) 단일 인터페이스 구현 클래스(Single Interface Implement Class)
단일 인터페이스 구현 클래스는 말그대로 인터페이스 하나를 구현한 클래스를 말합니다.
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 + "입니다.");
}
}
User라는 인터페이스 구조는 다음과 같습니다.
- 상수 필드: String FIRST_NAME;
- 추상 메서드: snedMoney()
- 디폴트 메서드: setStatus()
- 정적 메서드: printFirstName()
그리고 이 인터페이스를 구현한 단일 인터페이스 구현 클래스는 아래와 같습니다.
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("수취인이 비활성화 되었습니다");
}
}
만약 인터페이스를 구현한 클래스가 실체 메서드를 모두 작성하지 않으면 해당 구현 클래스는 추상 클래스로 선언되어야 합니다.
2) 다중 인터페이스 구현 클래스(Multiple Interface Implement Class)
다중 인터페이스 구현 클래스는 아래와 같이 정의하며 인터페이스 여러 개를 구현할 수 있습니다.
public class 클래스이름 implements 인터페이스이름1, 인터페이스이름2{
// 인터페이스의 추상 메서드를 구현한 실체 메서드를 선언하는 부분
}
다중 인터페이스를 구현한 구현 클래스는 반드시 모든 인터페이스의 추상 메서드를 실체 메서드로 구현해야 합니다.
하나라도 추상 메서드가 구현되지 않으면, 구현 클래스는 추상 클래스로 선언되어야 하죠.
나머지 내용도 모두 단일 인터페이스에서의 규칙과 동일합니다. 두 개의 인터페이스의 필드와 메서드를 모두 처리(?)해야 한다는 것만 다를 뿐이죠.
3) 익명 구현 객체(Anonymous Implement Object)
일회성으로 사용하는 구현 클래스는 클래스로 만들어서 쓰는 것이 비효율적입니다. 그래서 이런 클래스는 특별히 인터페이스를 바로 구현 객체로 만들어서 사용하는 것이 가능합니다.
아래 예시를 보겠습니다.
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("수취인이 비활성화 되었습니다");
}
}
분명히 User는 인터페이스인데 인터페이스를 객체처럼 초기화하고 사용하고 있습니다. 단지, new User(){...}처럼 대괄호 안에서 인터페이스를 구현한 객체처럼 내용을 써주면 됩니다.
3. 인터페이스 상속
인터페이스도 다른 인터페이스를 상속할 수 있습니다. 클래스는 다중 상속을 허용하지 않지만, 인터페이스는 아래처럼 다중 상속을 허용하죠.
public interface 하위인터페이스 extends 상위인터페이스1, 상위인터페이스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
}
}
4. 인터페이스의 디폴트 메소드 (Default Method), 자바 8
인터페이스의 디폴트 메서드(Default method)는 구현 객체가 있어야 사용할 수 있습니다. 그런데, 왜 굳이 객체에서 만들지 않고 인터페이스에서 디폴트 메서드를 만들어두는 것일까요?
Java8에서 인터페이스에서 디폴트 메서드를 허용하는 이유는 기존 인터페이스를 확장해서 새로운 기능을 추가하기 위함입니다.
만약 기존 인터페이스에 새로운 기능을 위해서 추상 메서드를 추가하면 어떻게 될까요?
추상 메서드를 새롭게 선언하려면, 해당 인터페이스를 구현하는 모든 구현 클래스를 찾아다니면서 새로 선언된 추상 메서드에 대한 실체 메서드를 선언해야 합니다.
쉽게 말해서 수정이나 유지 보수가 쉽지 않다는 겁니다.
하지만, 인터페이스에 디폴트 메서드를 선언하게 되면 구현 클래스들에서는 아무런 행위를 하지 않아도 자동적으로 해당 메서드를 가지게 됩니다.
따라서, 디폴트 메서드는 추상 메서드가 구현 클래스에서 실체 메서드로 구현되어야 한다는 제약을 없애주는 것이죠.
예시 하나를 보겠습니다.
public interface Calculator {
public int plus(int i, int j);
public int multiple(int i, int j);
default int exec(int i, int j){ //default로 선언함으로 메소드를 구현할 수 있다.
return i + j;
}
}
//Calculator인터페이스를 구현한 MyCalculator클래스
public class MyCalculator implements Calculator {
@Override
public int plus(int i, int j) {
return i + j;
}
@Override
public int multiple(int i, int j) {
return i * j;
}
}
public class MyCalculatorExam {
public static void main(String[] args){
Calculator cal = new MyCalculator();
int value = cal.exec(5, 10);
System.out.println(value);
}
}
위의 예시의 인터페이스에서는 int exec() 메서드를 디폴트 메서드로 선언했습니다. 만약 엄청 나게 많은 클래스들이 Calculator 인터페이스를 구현하고 있을 때 int exec() 메서드를 abstract 메서드로 추가하게 되면 모든 클래스들을 찾아다니면서 오버라이딩을 해줘야겠죠?
이 맥락에서 디폴트 메서드라는 이름을 잘 생각해보면, 해당 인터페이스를 구현하는 클래스들이 모두 공통으로 가지고 있는(가지고 있어야 하는) 메서드를 정의할 때 유용함을 알 수 있습니다. 그리고 추가적으로 각 클래스에서는 디폴트 메서드를 다시 오버라이딩할 수도 있죠.
5. 인터페이스의 정적(static) 메소드, 자바 8
인터페이스의 정적 메서드(static method) 역시 자바8부터 사용이 가능해졌습니다.
예전에 클래스를 공부할 때, 클래스에서 static method를 선언하면 클래스 이름을 통해 해당 메서드를 호출할 수 있다고 했죠.
그런데 이 static method가 인터페이스에 들어가게 되면 클래스의 static method와 마찬가지로 interface를 통해 해당 메서드를 호출할 수 있게 됩니다. 컴파일 시점에 인터페이스가 메모리에 이미 생성되기 때문이겠죠?
한편, 인터페이스에 정의된 static method는 각 클래스에서 오버라이딩할 수 없다고 했습니다.
// Calculator.java
public interface Calculator{
...
public static int exec2(int i, int j){
return i * j;
}
}
// MyCal.java
public class MyCal implements Calculator{
public int plus(int i,int j){
return i+j;
}
public int multiple(int i,int j){
return i*j;
}
// interface의 default, static 메서드는 구현하지 않아도 됌
// default method의 경우에는 오버라이딩은 할 수 있음
}
// MyCalTest.java
public class MyCalTest{
public static void main(String[] args){
Calculator cal = new MyCal();
int exec2Result = Calculator.exec2(3,4); // 인터페이스의 static method를 인터페이스명으로 호출해서 사용 가능
System.out.println(exec2Result); // = 12
6. 인터페이스의 private 메소드, 자바 9
일반적으로 인터페이스는 public하게 만들어지고, 그 안에 있는 method들도 모두 public한데요.
자바9부터는 private method를 사용할 수 있게 되어서 외부에서 접근할 수 없도록 만드는 게 가능해졌습니다. 그런데 인터페이스에서 private 메서드가 왜 필요할까요?
아래 예시를 보겠습니다.
public interface Boo{
private String printBye(){
return "Bye";
}
default void knockDoor(){
System.out.println("OK... " +printBye());
}
}
public class Main implements Boo{
@Override
public String printBye(){ // Error! cannot access interface's private method
return "Hello!";
}
}
'Boo' 인터페이스에서 printBye()는 private 메서드입니다. 따라서 구현 클래스가 printBye()를 오버라이딩하는 것 자체가 불가능하죠.
하지만, 인터페이스에서 정의한 printBye()는 디폴트 메서드인 knockDoor()에서 사용됨을 볼 수 있습니다.
즉, private 메서드를 인터페이스 안에 생성해두고, 디폴트 메서드나 정적 메서드 안에서 이를 사용하면 코드의 수정과 유지보수가 쉬워지고 재사용성이 올라간다는 뜻입니다.
만약 private 메서드가 없는 상태에서 디폴트 메서드가 5개, 정적 메서드가 5개라고 해봅시다. 그런데 이 10개의 메서드에서는 동일한 작업을 해야 하는 로직이 포함됩니다. 그러면 이 10개의 메서드에 이 로직을 다 일일이 쳐야할까요? 매우 고통스러울 것 같지 않나요? 이게 바로 private 메서드의 필요성입니다.
자바의 인터페이스의 필요성과 개념, 선언, 구현까지 들여다봤습니다. 인터페이스는 OOP에서 매우 중요한 역할을 한다고 생각합니다.
이번 글에서 정리한 것들은 반드시 알아두면 좋을 것 같습니다. 그리고 제일 중요한 것은 이것들을 머릿속으로 외우지 말고 이해하면 좋겠습니다.
감사합니다.
'Java & Kotlin' 카테고리의 다른 글
[JAVA] Enum 클래스에 대한 이해 (0) | 2021.06.20 |
---|---|
[JAVA] 예외 처리를 어떻게 해야할까? - (2) (0) | 2021.06.20 |
[JAVA] 다형성과 상속, 그리고 객체지향 (0) | 2021.06.19 |
[JAVA] 자바 데이터 타입과 변수 (0) | 2021.06.19 |
[JAVA] 예외 처리를 어떻게 해야할까? - (1) (0) | 2021.06.17 |