자바(10) - 인터페이스
06 Aug 2021 | java 인터페이스인터페이스
인터페이스(interface)는 클래스 혹은 프로그램이 제공하는 기능을 명시적으로 선언하는 역할을 한다. 인터페이스는 추상 메서드와 상수로만 이루어져 있다. 구현된 코드가 없기에 인터페이스로 인스턴스를 생성할 수 없다. 인터페이스 선언은 다음과 같다.
public interface Calc {
double PI = 3.14;
int ERROR = -9999999;
int add(int num1, int num2);
int substract(int num1, int num2);
}
이런식으로 만들면 컴파일 과정에서 변수들은 상수로(public static final) 변환되고, 메서드들은 추상 메서드(public abstract)로 변환이 된다.
기능을 명시했으니 실제 클래스로 구현을 해야 한다. 인터페이스를 구현한다는 의미이기에 예약어로 implemnets
를 사용한다.
public abstract class Calculator implements Calc {
@Override
public int add(int num1, int num2) {
return num1 + num2;
}
}
public class CompleteCalc extends Calculator {
@Override
public int substract(int num1, int num2) {
return num1 - num2;
}
public void showInfo() {
System.out.println("인터페이스 구현");
}
}
인터페이스를 implementation하려면 추상클래스와 마찬가지로 오버라이딩을 해줘서 메서드를 구현해주어야 한다. 위 예제 Calculator 클래스처럼 전부 구현하지 않으면 추상 클래스로 선언해주어야 하고, CompleteCalc는 추상 클래스를 상속받았으니 나머지 남은 메서드를 구현해주면 된다. 인터페이스도 상속처럼 마찬가지로 하위 클래스는 상위 클래스 자료형으로 묵시적 형 변환을 할 수 있다.
Calc calc = new CompleteCalc();
이러면 상속때처럼 동일하게 Calc형의 변수와 메서드들만 사용할 수 있게 된다.
인터페이스와 다형성
근데 추상 클래스랑 비슷해 보이는 이 인터페이스를 왜 사용할까. 인터페이스는 클라이언트 프로그램에 어떤 메서드를 제공하는지 미리 알려주는 명세(specification) 또는 약속의 역할을 한다. 예를 들어 Abc 인터페이스를 구현한 A클래스가 있다. 이 클래스를 사용하는 Z 프로그램이 있다 치자. Abc인터페이스는 구현할 추상 메서드가 모두 선언되어 있고, 어떤 매개 변수가 사용되는지, 어떤 자료형 값이 반환되는지 선언되어있다. 즉 Z 프로그램은 A 클래스의 구현 코드를 보지 않아도 Abc 인터페이스의 선언부를 보고 A클래스의 사용법을 알 수 있다.
우리가 나중에 다룰 Spring에서도 다양한 데이터베이스를 사용할 것이다. mysql, postgresql 등 서로 다른 종류들이다. 하지만 이 DB들에 대한 접근들도 인터페이스로 명세되어있기에 우리는 인터페이스의 내용만 살표보면 이용할 수 있게 된다. 이런것들이 가능한 것들이 결국엔 다형성 덕분이다. 다형성을 활용해보도록 하자.
시나리오
콜센터가 있는데 전화가 오면 대기열에 저장이 되고, 상담원이 지정되기 전까지 대기 상태가 된다. 전화를 상담원에게 배분하는 정책은 여러가지 방식이 있다.
- 순서대로 배분하기: 모든 상담원이 동일한 상담 건수를 처리하도록 들어오는 전화 순서대로 상담원에게 하나씩 배분.
- 짧은 대기열 찾아 배분하기: 고객 대기 시간을 줄이기 위해 상담을 하지 않는 상담원이나 가장 짧은 대기열을 보유한 상담원에게 배분.
- 우선순위에 따라 배분하기: 고객 등급에 따라 등급이 높은 고객의 전화를 우선 가져와서 업무 능력이 좋은 상담원에게 우선 배분.
먼저 Scheduler 인터페이슬 만들어 보자. 해당 인터페이스에는 다음 전화를 가져오는 getNextCall()과 상담원에 전화를 배분하는 sendCallToAgent() 메서드를 선언한다.
public interface Scheduler {
public void getNextCall();
public void sendCallToAgent();
}
이후 방식에 맞는 클래스들을 구현한다.
// 1
public class RoundRobin implements Sceduler {
@Override
public void getNextCall() {
System.out.println("상담 전화를 순서대로 대기열에서 가져온다.");
}
@Override
public void sendCallToAgent() {
System.out.println("다음 순서 상담원에게 배분");
}
}
// 2
public class LeastJob implements Sceduler {
@Override
public void getNextCall() {
System.out.println("상담 전화를 순서대로 대기열에서 가져온다.");
}
@Override
public void sendCallToAgent() {
System.out.println("현재 상담 업무가 없거나 대기가 적은 상담원에게 할당.");
}
}
// 3
public class PriorityAllocation implements Sceduler {
@Override
public void getNextCall() {
System.out.println("고객 등급이 높은 고객의 전화를 먼저 가져온다.");
}
@Override
public void sendCallToAgent() {
System.out.println("업무 능력이 좋은 상담원에게 우선적으로 배분");
}
}
테스트
public class SchedulerTest {
public static void main(String[] args) throws IOException {
System.out.println("R: 한명씩 차례로");
System.out.println("L: 쉬고 있거나 대기가 가장 적은 상담원에게 할당");
System.out.println("P: 우선순위가 높은 고객 먼저 할당");
int ch = System.in.read();
Scheduler sc = null;
if(ch == 'R' || ch == 'r') {
sc = new RoundRobin();
} else if(ch == 'L' || ch == 'l') {
sc = new RoundRobin();
} else if(ch == 'P' || ch == 'p') {
sc = new RoundRobin();
} else {
System.out.println("지원되지 않는 기능");
return;
}
// 어떤 정책인가와 상관없이 인터페이스에 선언한 메서드 호출
sc.getNextCall();
sc.sendCallToAgent();
}
}
인터페이스 요소
인터페이스는 추상 메서드로 이루어지므로 인스턴스를 생성할 수 없고 멤버 변수도 사용할 수 없다.
하지만 아래처럼 구성해도 오류가 나지 않는다. 이것은 컴파일하면서 public static final double PI = 3.14
이런식으로 상수로 바꾸어주기 때문이다.
public interface Calc {
double PI = 3.14;
int ERROR = -9999999;
}
디폴트 메서드와 정적 메서드
자바 7까지는 인터페이스에서 추상 메서드와 상수, 이 두가지 요소만 선언해서 사용할 수 있었다. 그런데 어떤 인터페이스를 구현한 여러 클래스에서 사용할 메서드가 클래스마다 같은 기능을 제공하는 경우가 있다. 자바 7은 이런 문제를 해결하기 어려워 클래스마다 일일히 구현을 해주어야 하고, 클래스를 생성하지 않아도 사용할 수 있는 메서드가 필요한 경우도 있는데, 인터페이스만으로는 메서드를 호출할 수 없어 불편했다.
이런 문제점을 해결하기 위해서 자바 8부터 인터페이스에 디폴트 메서드와 정적 메서드 기능을 제공한다. 디폴트 메서드는 인터페이스에서 구현 코드까지 작성한 메서드이다. 즉 인터페이스를 구현한 클래스에서 기본적으로 제공할 메서드인 것이다. 정적 메서드는 인스턴스 생성과 상관없이 사용할 수 있는 메서드이다. 그렇다고 인터페이스로 인스턴스를 만들 수 있는 것은 아니니 주의하자.
public interface Calc {
// 디폴트 메서드 default 예약어 필요
default void description() {
System.out.println("정수 계산기를 구현");
}
// 정적 메서드
static int total(int[] arr) {
int total = 0;
for(int i: arr) {
total += i;
}
return total;
}
}
// 디폴트 메서드도 오버라이딩 할 수 있다. 인터페이스 본래 메서드를 부르려면 super를 쓴다.
public class CompleteCalc extends Calculator {
@Override
public void description() {
super.description();
}
}
public class CalculatorTest {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
// 인터페이스 정적 메서드 활용
System.out.print(Calc.total(arr));
}
}
private 메서드
자바 9부터 인터페이스에 private 메서드를 구현할 수 있다. private 메서드는 인터페이스를 구현한 클래스에서 사용하거나 재정의할 수 없다. 즉 기존에 구현된 코드를 변경하지 않고 인터페이스를 구현한 클래스에서 공통으로 사용하는 경우에 private 메서드로 구현하면 코드 재사용성을 높일 수 있다. private 메서드는 코드를 모두 구현해야 하므로 추상 메서드에 private예약어를 사용할 수 없고 정적 메서드에는 사용 가능하다.
public interface Calc {
default void description() {
myMethod();
}
static int total(int[] arr) {
int total = 0;
for(int i: arr) {
total += i;
}
myStaticMethod();
return total;
}
private void myMethod() {
System.out.print("private method");
}
private static void myStaticMethod() {
System.out.println("private static method");
}
}
인터페이스 활용하기
한 클래스가 여러 클래스를 상속받으면 메서드 호출이 모호해지는 문제가 발생할 수 있다. 하지만 인터페이스는 한 클래스가 여러 인터페이스를 구현할 수 있다. 예를들어 고객 클래스가 있는데 고객은 물건을 살 수도 팔 수도 있다. 클래스를 나누어 구현해보자.
public interface Buy {
void buy();
}
public interface Sell {
void sell();
}
public class Customer implements Buy, Sell {
@Override
public void sell() {
System.out.println("sell");
}
@Override
public void buy() {
System.out.println("buy");
}
}
public class CustomerTest {
public static void main(String[] args) {
Customer customer = new Customer();
Buy buyer = customer;
buyer.buy();
Sell seller = customer;
seller.sell();
if(seller isinstanceof Custoer) {
Customer customer2 = (Customer)seller;
customer2.buy();
customer2.sell();
}
}
}
두 인터페이스의 디폴트 메서드가 중복되는 경우
정적 메서드는 인스턴스 생성과 상관없이 사용할 수 있다. Customer 클래스가 Buy, Sell 두 인터페이스를 구현하고 Buy 인터페이스와 Sell 인터페이스에 똑같은 pay() 정적 메서드가 있다 보자. 이 경우엔 Buy.pay()와 Sell.pay()로 호출하여 문제가 되지 않는데 디폴트 메서드는 문제가 된다. 두 인터페이스에 order()라는 메서드가 있으면 Customer 클래스에서 중복 에러가 나온다. 이런 경우 구현 클래스에서 오버라이딩 해주어야 한다.
인터페이스 상속하기
인터페이스 간에도 상속이 가능하다. 인터페이스 간 상속은 구현 코드를 통해 기능을 상속하는 것이 아니기에 형 상속(type inheritaance)라고 부른다. 클래스의 경우에는 하나의 클래스만 상속받을 수 있지만, 인터페이스는 여러 개 동시에 상속받을 수 있다. 한 인터페이스가 여러 인터페이스를 상속받으면, 상속받은 인터페이스는 상위 인터페이스에 선언한 추상 메서드를 모두 가지게 된다.
public interface X {
void x();
}
public interface Y {
void y();
}
public interface MyInterface extends X, Y {
void myMethod();
}
public class MyClass implements MyInterface {
@Overrid
public void x() {
System.out.println("X");
}
@Overrid
public void y() {
System.out.println("Y");
}
@Overrid
public void myMethod() {
System.out.println("myMethod");
}
}
public class MyClassTest {
public static void main(String[] args) {
MyClass mClass = new MyClass();
// X에 선언한 메서드만 호출 가능
X xClass = mClass;
xClass.x();
// Y에 선언한 메서드만 호출 가능
Y yClass = mClass;
yClass.y();
// 모든 메서드 호출 가능
MyInterface iClass = mClass;
iClass.myMethod();
iClass.x();
iClass.y();
}
}
인터페이스 구현과 클래스 상속 함께 쓰기
한 클래스에서 클래스 상속과 인터페이스 구현을 모두 할 수도 있다.
public class Shelf {
protected ArrayList<String> shelf;
public Shelf() {
shelf = new ArrayList<String>)();
}
public ArrayList<String> getShelf() {
return shelf;
}
public int getCount() {
return shelf.size();
}
}
public interface Queue {
void enQueue(String title);
String deQueue();
int getSize();
}
public class BookShelf extends Shelf implements Queue {
@Override
public void enQueue(String title) {
shelf.add(title);
}
@Overrid
public String deQueue() {
return shelf.remove(0);
}
@Override
public int getSize() {
return getCount();
}
}
public class BookShelfTest {
public static void main(String[] args) {
Queue shelfQueue = new BookShlef();
shelfQueue.enQueue("book1");
shelfQueue.enQueue("book2");
shelfQueue.enQueue("book3");
System.out.println(shelfQueue.deQueue());
System.out.println(shelfQueue.deQueue());
System.out.println(shelfQueue.deQueue());
}
}