인터페이스 정의
객체 지향 프로그래밍의 추상화를 배우고 추상 클래스를 접해봤다면 그 다음 접해보는 것이 인터페이스(Interface) 일 것이다.
인터페이스는 간단히 말하자면 프로그램을 설계하고 조금 더 유연한 프로그램을 만드는 기법을 말한다. 인터페이스는 추상화와 상속과 더불어 다형성이라는 객체 지향의 특징을 구현하는 핵심이다.
실생활의 인터페이스
인터페이스(Interface) 라는 용어는 꼭 자바(java)에만 있는 것이 아니다.
위키 백과에 따르면, 인터페이스는 서로 다른 두 개의 시스템, 장치 사이에서 정보나 신호를 주고받는 경우의 접점이나 경계면이라고 한다. 즉, 사용자가 기기를 쉽게 동작시키는데 도움을 주는 상호작용 시스템을 의미한다.
이러한 정의를 자바 프로그래밍에 접목해보면, 객체의 인스턴스 메소드를 이용하는 사용자 입장에서 '그 객체의 내부 구현이 어떻든 깊이 학습할 필요없이 원하는 메소드만 호출하고 결과 값을 제대로 받게 해주는 간편한 상호작용 기능이다' 라고 말 할 수 있다.
이처럼 상호작용을 통해 사용자가 메소드를 편하게 이용하게 해주듯이, 개발자가 프레임워크를 이용하여 웹서비스를 개발하는데 있어 프레임워크의 내부 구성 학습 없이, 그저 지원해주는 메서드를 이용하여 간편하게 프로젝트를 개발 할 수 있게 해주는 것이 인터페이스의 첫번째 역할이라고 할 수 있다.
두번째 인터페이스의 역할은 일종의 '스펙' 을 지정하게 하여 소프트웨어 확장에 유리하다는 것이다.
실생활로 예를 들어보자면, 우리는 삼성 키보드든, 애플 키보드든, 로지텍 키보드든 제조사 상관없이 컴퓨터에 연결만하면 설치없이 바로 이용할수 있다. 이러한 간편함은 모두 OS에서 미리 I/O 처리에 대해 인터페이스로 추상화 하였기 때문에, 각 키보드 제조 회사들이 인터페이스의 구현 스펙에 따라 동작 메서드를 구현했기 때문이다.
그래서 키보드 제조사 상관없이 우리는 간편하게 키보드를 연결만 하면 이용할 수 있는 것이다.
알게 모르게 우리는 실생활에서 인터페이스로 단단하게 구조적으로 짜여져 나온 제품들을 이용해왔던 것이다.
자바의 인터페이스
보통 학부생들은 인터페이스를 처음 배울 때 추상 메서드 집합이며 다중 상속이 되는 기능 정도로만 암기한다. 하지만 위에서 인터페이스에 대한 예제를 들며 소개한 이유는, 자바의 인터페이스 자체가 지닌 고유한 기능과 시스템 상호작용 적인 요소로 정말 다양한 분야에 이용될 수 있기 때문이다.
상속 및 추상 메서드 강제 구현 외에도, 다양한 프레임워크에서 클래스끼리 통신하는데 자주 사용되며, 객체 지향(OOP) 프로그래밍의 전략에서 결합도(Coupling)을 낮춰 유지보수성을 늘리는 디자인 패턴으로서의 역할도 병행하기 때문이다.
우리가 반복되는 코드들을 줄이기 위해 for문 이나 while문을 사용하듯이, 인터페이스를 사용하는 목적은 설계상 이점을 위해 사용하는 클래스라고 말할수 있다.
설계 관점에서 프로그래밍을 할때는 나무도 보지만 숲을 볼 수 있어야 한다. '여기 왜 이런 나무가 심어져 있지?' 가 아닌, '전체 숲을 위해서 여기 나무를 심었구나' 식으로 접근해야 된다.
인터페이스가 무엇인지 친숙하게 다가 가는데 성공했다면, 이제 자바에서의 인터페이스의 여러가지 활용도에 대해 학습해보자.
인터페이스 기본 문법
인터페이스 정의
- 인터페이스를 작성하는 것은 추상 클래스를 작성하는 것과 같다고 보면 된다. (추상 메서드 집합)
- 인터페이스도 필드를 선언할 수 있지만 변수가 아닌 상수(final)로서만 정의할 수 있다.
- public static final 과 public abstract 제어자는 생략이 가능하다.
인터페이스에 정의된 모든 멤버에 적용되는 사항이기 때문에 편의상 생략 가능하게 지원하는 것이다.
생략된 제어자는 컴파일 시에 컴파일러가 자동으로 추가해 준다.
interface 인터페이스이름{
public static final 타입 상수이름 = 값;
public abstract 타입 메서드이름(매개변수목록);
}
// --------------------------------------------------------
interface TV {
int MAX_VOLUME = 10; // public static final 생략 가능
int MIN_VOLUME = 10;
void turnOn(); // public abstract 생략 가능
void turnOff();
void changeVolume(int volume);
void changeChannel(int channel);
}
인터페이스 구현
- 인터페이스도 추상 클래스처럼 그 자체로는 인스턴스를 생성할 수 없으며, 추상 클래스가 상속을 통해 완성되는 것처럼 인터페이스도 구현부를 만들어주는 클래스에 구현(상속) 되어야 한다.
- 해당 클래스에 인터페이스를 구현하고 싶다면, implements 키워드를 쓴 후에 인터페이스를 나열하며 된다.
- 인터페이스를 상속 받았으면, 자식 클래스에서 인터페이스가 포함하고 있는 추상 메소드를 구체적으로 구현해준다.
- 인터페이스의 가장 큰 특징은 여러개를 다중 구현(다중 상속)이 가능하다는 것이다.
- 자식 클래스에 클래스 상속(extends)와 인터페이스 구현(implements)는 동시에 가능하다.
interface Animal {
public abstract void cry();
}
interface Pet {
public abstract void play();
}
class Tail {
// ...
}
class Cat extends Tail implements Animal, Pet { // 클래스와 인터페이스를 동시에 상속
public void cry() {
System.out.println("냐옹냐옹!");
}
public void play() {
System.out.println("쥐 잡기 놀이하자~!");
}
}
※ 참고
인터페이스도 따지고 보면 상속이지만 extends 키워드 대신 implements 라는 '구현' 이라는 키워드를 사용하는 이유는, 상속은 클래스간의 부모 - 자식 관계를 연관 시키는데 의미가 중점 된다면, 구현은 클래스를 확장 시켜 다양하게 이용하는데 중점이 되기 때문이다.
※ 참고
인터페이스를 구현받고 추상 메서드를 구체적으로 구현할때 접근제어자 설정에 주의해야 한다.
기본적으로 메서드를 오버라이딩(overriding) 할때는 부모의 메서드 보다 넓은 범위의 접근제어자를 지정해야 한다는 규칙이 존재한다. 따라서 인터페이스의 추상 메소드는 기본적으로 pubic abstract 가 생략된 상태이기 때문에 반드시 자식클래스의 메서드 구현부에서는 제어자를 public으로 설정해 주어야 한다.
인터페이스 일부 구현 (추상 클래스)
- 만일 클래스가 구현하는 인터페이스의 메서드 중 일부만 구현한다면 abstract 를 붙여서 추상 클래스로 선언해야 한다.
- 어찌 보면 당연한게 인터페이스의 추상 메서드 멤버를 그대로 상속받기 때문에, 인터페이스를 상속한 클래스에서 메서드 구현을 안한다면, 곧 추상 메서드를 가진 추상 클래스가 되기 때문이다.
interface Animal {
void walk();
void run();
void breed();
}
// Animal 인터페이스를 일부만 구현하는 포유류 추상 클래스
abstract class Mammalia implements Animal {
public void walk() { ... }
public void run() { ... }
// public void breed() 는 자식 클래스에서 구체적으로 구현하도록 일부로 구현하지 않음 (추상 메서드로 처리)
}
class Lion extends Mammalia {
@Override
public void breed() { ... }
}
인터페이스 자체 상속
- 클래스 끼리 상속을 통해 확장을 하듯이, 인터페이스 자체를 확장 시키고 싶다면 extends를 통해 인터페이스를 상속하면 된다.
- 클래스와 달리 인터페이스 끼리의 상속은 다중 상속이 가능하다. (메소드 구현부가 없으니 충돌 가능성이 없음)
- 클래스의 상속과 마찬가지로 자손 인터페이스는 조상 인터페이스에 정의된 멤버를 모두 상속받는다.
- 그러나 필드의 경우 기본적으로 static 이기 때문에 구현체를 따라가지 않게 된다. (독립 상수)
- 참고로 인터페이스에 클래스를 상속하는 행위는 불가능한데, 왜냐하면 인터페이스는 클래스와는 달리 Object 클래스가 최고 조상이 아니기 때문이다.
※ 참고
자바는 클래스 상속에 대해서 다중 상속을 허용하지 않지만, 인터페이스에 한해서는 다중상속을 허용한다.
interface Changeable{
/* 채널을 바꾸는 기능의 메서드 */
void change();
}
interface Powerable{
/* 전원을 껐다 켰다 하는 메서드 */
void power(boolean b);
}
// 채널 기능과 전원 기능을 가진 인터페이스들을 하나의 인터페이스로 통합 상속
interface Controlable extends Changeable, Powerable {
// 인터페이스끼리 다중 상속하면 그대로 추상 멤버들을 물려 받음
}
// 클래스에 통합된 인터페이스를 그대로 상속
class MyObject implements Controlable {
public void change() {
System.out.println("채널을 바꾸는 기능의 메서드");
}
public void power(boolean b) {
System.out.println("전원을 껐다 켰다 하는 메서드");
}
}
public class Main {
public static void main(String[] args) {
// 인터페이스 다형성 (인터페이스를 타입으로 취급해서 업캐스팅 가능)
Controlable[] o = { new MyObject(), new MyObject() };
o[0].change();
o[0].power(true);
// 각각 단일 인터페이스로도 타입으로 사용이 가능하다. (그러나 지니고 있는 추상 메서드만 사용이 가능하다)
Changeable inter1 = new Changeable();
inter1.change();
Powerable inter2 = new Powerable();
inter2.power(true);
}
}
※ 참고
[ 인터페이스의 extends는 상속이 아니다 ]
인터페이스는 하나의 타입이나 규격일 뿐이지 그 자체가 하나의 객체가 되는 것이 아니다.
따라서 엄밀히 말하자면, 인터페이스의 상속은 클래스의 상속처럼 부모의 속성과 동작을 물려받는 것이 아니다.
정확히 말하면, 인터페이스의 상속은 규격이나 스펙 자체 혹은 기능 자체의 선언을 물려받은 것이다.
규격이나 스펙을 물려받아서 새로운 스펙을 만든다면 기존 여러 개의 스펙을 조합해서 하나로 묶거나 기존의 스펙을 고스란히 물려받은 후에 다시 추가적인 기능을 가지게 하는 것이다.
인터페이스 상수 필드 상속 관계
- 클래스의 상속일 경우 클래스 필드 멤버끼리 상속되어 덮어 씌워지지만, 인터페이스의 필드들은 모두 public static final 이기에, 서로 상속을 해도 독립적으로 운용된다.
interface Iflower {
int ex = 10; // 각각 public static final
}
interface IPlant extends Iflower {
int ex = 20; // 각각 public static final
}
class Tulip implements IPlant {
int ex = 30; // 그냥 인스턴스 변수
}
public class Main {
public static void main(String[] args) {
// 클래스 타입 객체로 ex 멤버에 접근하면, 클래스 인스턴스 변수로 접근
Tulip t = new Tulip();
System.out.println(t.ex); // 30
// 인터페이스 타입 객체로 멤버에 접근하면, 인터페이스 static 상수로 접근
Iflower a = new Tulip();
System.out.println(a.ex); // 10 - 좋지않은 방법
System.out.println(Iflower.ex); // 10 - 클래스 static 처럼 '인터페이스.멤버' 로 접근
IPlant b = new Tulip();
System.out.println(b.ex); // 20 - 좋지않은 방법
System.out.println(IPlant.ex); // 20 - 클래스 static 처럼 '인터페이스.멤버' 로 접근
}
}
인터페이스 독립 파일
- 자바에서 .class 파일을 만들 수 있는 것에는 꼭 클래스만 있는 것이 아니다.
- 인터페이스 자체만으로도 클래스명으로 소스 파일을 만들 수 있다.
자바8 인터페이스 구현 메소드
본래 인터페이스의 메서드는 몸통(구현체)을 가질 수 없지만, java8 에 이르러서 디폴트 메서드와 스태틱 메소드를 통해 추상 클래스처럼 구현 메소드를 정의 할 수 있게 되었다.
그래서 이러한 점 때문에 오히려 추상 클래스와의 차이점이 거의 사라졌다고 말하기도 한다.
이 기능은 이전 인터페이스를 사용하여 java8의 람다 표현식 기능을 활용할 수 있도록 이전 버전과의 호환성을 위해 추가되었다.
예를 들어 java8 버전부터 새롭게 추가된 스트림이나 람다와 같은 함수형 프로그래밍을 컬렉션(Collection) 클래스에서 사용하기 위해, 기존에 만들어놓았던 인터페이스들을 구현하고 있는 컬렉션 클래스들의 구조에서 특정한 기능을 추가해야 되는 상황이 오게 되었다.
그런데 만일 기존 인터페이스에 추상 메서드를 추가 해버리면, 이 인터페이스를 구현하고 있는 모든 구현 클래스도 변경이 필요해지기 때문에 추상 메서드 대신 디폴트 메서드를 새롭게 추가하여 해결했다고 보면 된다.
또한 앞으로 외부 자바 라이브러리를 이용할때, 라이브러리의 각종 인터페이스에 디폴트 메서드들이 정의되어 있을텐데 이를 익명클래스로 메서드를 호출하거나 오버라이딩 해서 재정의하거나 할때 자주 애용된다.
그리고 static 메서드는 인스턴스와 관계없는 독립적인 메서드이기 때문에 인터페이스에 추가해도 상관없지만, 규칙을 단순히 할 필요가 있어서 자바에서는 허용되지 않았었다.
이 때문에 인터페이스와 관련된 static 메서드는 별도의 클래스에 따로 두어야 했다. 대표적으로 java.util.Collection 인터페이스가 있는데, 이 인터페이스와 관련된 static 메서드들이 인터페이스에는 추상 메서드만 선언할 수 있다는 원칙 때문에 별도의 클래스인 Collections 라는 클래스에 들어가게 되었다.
그렇지만 역시 자바8에 와서 위의 제약은 없어지게 되었다.
default 메소드
- 디폴트 메서드는 앞에 키워드 default 를 붙이며 일반 메서드처럼 구현부 { ... } 가 있어야 한다
- 디폴트 메서드 역시 접근제어자가 public 이며 생략 가능하다.
- 자식 클래스(구현체)에서 default 메소드를 오버라이딩 하여 재정의 가능하다.
- 보통 인터페이스를 구현한 이후, 수정과정에서 인터페이스 모든 구현체에게 수정 없이 전체적으로 함수를 만들어주고 싶을 때 사용된다. (대신 모든 구현체가 원하는 값을 return 하게 보장하기 위해 @implSpec 자바 doc 태그를 사용해 문서화 해줘야 한다)
- 주의 해야할점은 인터페이스는 Object 클래스를 상속받지 않기 때문에, Object 클래스가 제공하는 기능(equals, hasCode)는 기본 메소드로 제공할 수 없다. 따라서 구현체가 직접 재정의를 해 주어야 한다.
interface Calculator {
int plus(int i, int j);
int multiple(int i, int j);
// default로 선언함으로 메소드를 구현할 수 있다.
default int sub(int i, int j){
return i - j;
}
}
// Calculator인터페이스를 구현한 MyCalculator클래스
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 Main {
public static void main(String[] args){
MyCalculator mycal = new MyCalculator();
// 인터페이스 타입으로 업캐스팅
Calculator cal = (Calculator) mycal; // 괄호 생략해도 됨
// 인스턴스의 인터페이스 디폴트 메서드 호출
int value = cal.sub(5, 10);
System.out.println(value); // -5
}
}
※ 참고
인터페이스의 디폴트 메서드를 호출하기 위해선, 객체의 타입을 반드시 인터페이스 타입으로 업캐스팅 해주어야 한다.
@implSpec 주석 문서
디폴트 메서드는 보통 인터페이스를 구현한 이후, 수정과정에서 인터페이스 모든 구현체에게 수정 없이 전체적으로 함수를 만들어주고 싶을 때 사용된다.
예를 들어 A 와 B 업체가 협업을 해서 어떤 인터페이스를 구현하고 있는데, 개발자가 해당 인터페이스에 기능을 추가하기 위해 추상 메서드를 추가하면, 이 인터페이스를 구현하고 있는 모든 클래스에도 수정을 가해야 한다.
그럴때 인터페이스에 디폴트 메서드를 추가하면, 추가된 메서드를 구현체 클래스에서 굳이 구현하지 않아도 에러 없이 사용할 수 있고, 나중에 필요하면 재정의를 통해 업그레이드된 메서드 구현도 가능해진다.
대신 모든 구현체가 원하는 값을 return 하게 보장하기 위해 @implSpec 자바 doc 태그를 사용해 문서화 해줘야 한다
interface IJson {
String printJson(); // 추상 메서드
/**
* @impspec
* printJson()의 결과를 대문자 변환한다.
*/
default void uppperString() { // default 메서드
// 구현 로직상, 추상 메서드인 printJson()의 반환 값이 정상적인 값이 될수도 있고 null이되서 예외 오류가 발생할 수 있으니 @impspec 문서화를 한다.
String text = printJson().toUpperCase();
System.out.println(text);
}
}
default 메소드 다중 상속 문제
클래스의 다중 상속 문제점에 대해서, 동일한 메서드명이 겹치는 경우 죽음의 다이아몬드 현상에 대해 배웠을 것이다.
그래서 다중 상속을 금지 시켰더니, 이번엔 인터페이스에 디폴트 메서드라는것이 추가되면서 인터페이스를 다중 구현할때 클래스 다중 상속 문제와 똑같은 문제가 발생되게 되었다. 따라서 인터페이스 다중 구현에 한해서 자바에서는 다음과 같은 규칙을 정하였다.
1. 다중 인터페이스들 간의 디폴트 메서드 충돌
- 애초에 똑같은 디폴트 메서드를 가진 두 인터페이스를 하나의 클래스에 구현하고 아무런 조치를 취하지 않으면 컴파일 자체가 되지 않는다.
- 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩 하여 하나로 통합한다.
interface A1{
public void styleA();
// 메소드 시그니처가 같은 디폴트 메서드
default public void styleSame(){
System.out.println("A1 인터페이스의 디폴트 메서드 입니다.");
}
}
interface B1{
public void styleB();
// 메소드 시그니처가 같은 디폴트 메서드
default public void styleSame(){
System.out.println("B1 인터페이스의 디폴트 메서드 입니다.");
}
}
class MultiInterface implements A1, B1 {
@Override
public void styleA() {}
@Override
public void styleB() {}
// 두 인터페이스 디폴트 메서드중 A1 인터페이스의 디폴트 메서드를 오버라이딩 하여 구현
default public void styleSame(){
System.out.println("A1 인터페이스의 디폴트 메서드 입니다.");
}
}
public class Main {
public static void main(String[] args) {
MultiInterface m1 = new MultiInterface();
m1.styleSame(); // "A1 인터페이스의 디폴트 메서드 입니다."
}
}
2. 인터페이스의 디폴트 메서드와 부모 클래스 메서드 간의 충돌
- 위의 상황은 자식 클래스에서 인터페이스와 부모 클래스를 동시에 extends / implement 하였을때 디폴트 메서드와 인스턴스 메서드 간의 충돌을 가정한 상황이다.
- 이때는 부모 클래스의 메서드가 상속되고 디폴트 메서드는 무시된다.
- 만일 인터페이스 쪽의 디폴트 메서드를 사용할 필요가 있다면, 필요한 쪽의 메서드와 같은 내용으로 그냥 오버라이딩 해버리면 된다.
interface A1{
public void styleA();
// C1 클래스와 메소드 시그니처가 같은 디폴트 메서드
default public void styleSame() {
System.out.println("A1 인터페이스의 디폴트 메서드 입니다.");
}
}
abstract class C1 {
// A1 인터페이스와 메소드 시그니처가 같은 인스턴스 메서드
public void styleSame() {
System.out.println("C1 클래스의 인스턴스 메서드 입니다.");
}
}
// 메서드 시그니처가 같은 두 추상화들을 동시에 상속
class MultiClassInterface extends C1 implements A1 {
@Override
public void styleA() {}
}
public class Main {
public static void main(String[] args) {
MultiClassInterface m1 = new MultiClassInterface();
m1.styleSame(); // "C1 클래스의 인스턴스 메서드 입니다." - 클래스의 메서드 시그니처가 우선되어 적용됨
// 마찬가지로 인터페이스 타입으로 다운캐스팅 해도 클래스 인스턴스 메서드로 호출 됨
((A1) m1).styleSame(); // "C1 클래스의 인스턴스 메서드 입니다."
}
}
// 메서드 시그니처가 같은 두 추상화들을 동시에 상속
class MultiClassInterface extends C1 implements A1 {
@Override
public void styleA() {}
// 클래스의 인스턴스 메서드를 무시하고 인터페이스의 디폴트 메서드를 사용하기 위해 그대로 오버라이딩
public void styleSame() {
System.out.println("A1 인터페이스의 디폴트 메서드 입니다.");
}
}
public class Main {
public static void main(String[] args) {
MultiClassInterface m1 = new MultiClassInterface();
m1.styleSame(); // "A1 인터페이스의 디폴트 메서드 입니다."
}
}
default 메소드의 super
상위 클래스를 상속하고 상위의 메소드를 오버라이딩하여 재정의하였을때, 만일 부모 메서드를 호출할 일이 생긴하면 super 키워드를 통해 부모 메서드를 호출할 수 있었다.
이와 같이 인터페이스도 디폴트 메서드를 구현한 클래스에서 오버라이딩 하였을때, super 키워드를 통해 인터페이스의 원래의 디폴트 메서드를 호출이 가능하다.
다만 문법이 클래스 방식과 약간 차이가 있다.
인터페이스의 super는 다음과 같은 구성으로 호출된다. 인터페이스명.super.디폴트메서드
interface IPrint{
default void print(){
System.out.println("인터페이스의 디폴트 메서드 입니다.");
}
}
class MyClass implements IPrint {
@Override
public void print() {
IPrint.super.print(); // 인터페이스의 super 메서드를 호출
System.out.println("인터페이스의 디폴트 메서드를 오버라이딩한 메서드 입니다.");
}
}
public class Main {
public static void main(String[] args) {
MyClass cls = new MyClass();
cls.print();
}
}
/*
인터페이스의 디폴트 메서드 입니다.
인터페이스의 디폴트 메서드를 오버라이딩한 메서드 입니다.
*/
static 메소드
- 인스턴스 생성과 상관없이 인터페이스 타입으로 접근해 사용할 수 있는 메서드
- 인터페이스 전용 static 메소드라 해서 특별한 것은 없다.
일반 클래스의 static 메소드와 다를 바 없다. (똑같이 취급 하면 된다) - 해당 타입 관련 헬퍼 또는 유틸리티 메소드를 제공할 때, 인터페이스에 static메소드로 제공하기도 한다.
interface Calculator {
public int plus(int i, int j);
public int multiple(int i, int j);
// 디폴트 메서드
default int sub(int i, int j){
return i - j;
}
// 스태틱 메서드
public static void explain(){
System.out.println("interface static 메서드 입니다. 이 인터페이스는 pluc, multipe, sub 기능을 제공하는 메서드를 지니고 있습니다. (설명)");
}
}
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 Main {
public static void main(String[] args){
// 클래스 처럼 static 메소드 호출 하면 된다.
Calculator.explain(); // "interface static 메서드 입니다. 이 인터페이스는 pluc, multipe, sub 기능을 제공하는 메서드를 지니고 있습니다. (설명)"
}
}
private 메소드
- 자바9 버전에 추가된 메서드
- 인터페이스에 default, static 메소드가 생긴 이후, 이러한 메소드들의 로직을 공통화하고 재사용하기 위해 생긴 메소드
- private 메소드도 구현부를 가져야한다.
- 단, private 메소드는 인터페이스 내부에서만 돌아가는 코드이다. (인터페이스를 구현한 클래스에서 사용하거나 재정의 할 수 없음)
- 따라서 인터페이스 내부에서 private 메소드를 호출할때, default 메소드 내부에서 호출해야 하며,
만일 private static 키워드를 붙인 메소드는 static 메소드에서만 호출이 가능하다.
※ 참고
어렵게 생각할 필요없이, 클래스에서도 private 접근제어자를 가진 메서드를 정의하였을때, 호출 메서드에서 private 내부 메서드를 호출하여 사용하는 식 이었던 것 처럼, 인터페이스도 어렵지 않게 똑같이 생각하면 된다.
단, 인터페이스는 클래스가 아니기 때문에 this 키워드를 사용할 수 없다.
interface Calculator {
public int plus(int i, int j);
public int multiple(int i, int j);
// private 메서드
private void printf() {
System.out.println("private 메서드는 default 내부에서만 호출이 가능합니다.");
}
// private 스태틱 메서드
private static void printfStatic() {
System.out.println("private static 메서드는 static 메서드 내부에서만 호출이 가능합니다.");
}
// 디폴트 메서드
default void callPrivate() {
printf(); // private 메서드 호출
}
// 스태틱 메서드
static void callPrivateStatic() {
printfStatic(); // private 스태틱 메서드 호출
}
}
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 Main {
public static void main(String[] args){
// 인터페이스 디폴트 메서드를 통한 private 메서드 호출
Calculator c = new MyCalculator(); // 인터페이스 타입으로 업캐스팅
c.callPrivate(); // "private 메서드는 default 내부에서만 호출이 가능합니다."
// 인터페이스 스태틱 메서드를 통한 private static 메서드 호출
Calculator.callPrivateStatic(); // "private static 메서드는 static 메서드 내부에서만 호출이 가능합니다."
}
}
※ 참고
인터페이스의 상수는 private으로 만들 수 없다.
인터페이스는 실제 객체는 아니지만 서로 간의 약속으로 사용된다. 정해진 약속을 한 쪽에서 일방적으로 수정하게 되면 문제가 발생할 수 있다. 따라서 인터페이스에 선언하는 필드들은 자동으로 public static final 상수가 된다.
인터페이스의 다양한 활용도
지금까지 인터페이스의 다양한 문법에 대해 알아보았다.
하지만 위 부분은 그저 인터페이스 사용법만 열거해놓은 수준이고, 자바에서의 진정한 인터페이스의 활용성에 대해서는 살펴보지 않았다.
인터페이스에 대한 이해를 이렇게 강조하는 이유는, 대부분 자바 언어를 배우는 주된 목적이 스프링 프레임워크(Spring Framework)를 사용하기 위해서 인데, 스프링에서 이 인터페이스를 정말 많이 사용한다. 인터페이스의 사용 집합체가 프레임워크이다 라고 말해도 될정도로 말이다.
지금부터 자바에서 인터페이스 라는 기법을 이용해 어떠한 장점을 얻을수 있고, 자바 프로그래밍에서 어떤식으로 다채롭게 활용 할 수 있는지 정리해자.
인터페이스 다형성
부모클래스 타입으로 자식 클래스 타입을 포함 시킬수 있다는 다형성의 법칙도 인터페이스에 그대로 적용이 가능하다.
클래스가 여러 개의 인터페이스를 구현하게 되면, 결과적으로 변수의 타입으로도 다양하게 쓰일 수 있다는 것을 의미하게 된다. 인터페이스 타입으로 변수를 선언하게 되면 사용하는 입장에서는 뒤에 오는 모든 객체는 간단히 인터페이스만 구현한 객체면되기 때문에 좀 더 시스템이 유연해지는 계기를 마련하게 된다.
interface Keyboard { }
class Logitec_Keyboard implements Keyboard { }
class Samsung_Keyboard implements Keyboard { }
class Apple_Keyboard implements Keyboard { }
public class Main {
public static void main(String[] args) {
// 인터페이스 타입 배열로 여러가지 클래스들을 한번에 타입 묶음을 할 수 있다.
Keyboard[] k = {
new Logitec_Keyboard(),
new Samsung_Keyboard(),
new Apple_Keyboard(),
};
}
}
또한 인터페이스를 자료형으로 쓰는 습관을 들이면 프로그램 훨씬 유용해진다고 이야기한다.
이 말의 의미는 객체는 클래스가 아닌 인터페이스로 참조하라 라는 의미로 확장할 수 있다. 적합한 인터페이스만 있다면 매개변수뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하면 좋다.
- 객체는 인터페이스를 사용해 참조하라.
- 적당한 인터페이스가 있다면 매개변수뿐만 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하라.
- 객체의 실제 클래스를 사용할 상황은 '오직' 생성자로 생성할 때 뿐이다.
- 매개변수 타입으로는 클래스 보다는 인터페이스를 활용하라.
예를들어 중복이 없는 집합 자료형을 사용하기 위해 LinkedHashSet 클래스를 초기화 하려할때, 이때 객체의 타입을 똑같이 LinkedHashSet 으로 하지말고 인터페이스인 Set 타입으로 설정하여 선언하는 것이다.
// 나쁜 예) 클래스를 바로 타입으로 사용했다.
LinkedHashSet<Object> s = new LinkedHashSet<>();
// 좋은 예) 인터페이스를 타입으로 사용했다.
Set<Object> s = new LinkedHashSet<>();
이런식으로 코드 구현을 해놓는다면, 나중에 변수에 담긴 구현 클래스를 다른 Set 자료형 클래스로 교체하고자 할때 그저 새 클래스의 생성자를 다시 호출해주기만 하면 되어 간편해진다.
// 본래 LinkedHashSet으로 구현하였다가 사정이 생겨 TreeSet클래스로 변경해야한다고 하면, 그냥 인터페이스 타입의 변수에 재할당만 하면 된다
Set<Object> s = new LinkedHashSet<>();
s = new TreeSet<>();
하지만 이런식으로 인터페이스 타입으로 선언하는 습관은 꼭 좋은 것만 있는 것도 아닌데, 나쁜 예의 경우 LinkedHashSet을 HashSet으로 변환하면, LinkedHashSet 과 달리 HashSet은 반복자의 순회 순서를 보장하지 않기 떄문에 나중에 로직상 문제가 될 수 있기도 하다.
이밖에도 인터페이스는 변수 선언 타입을 넘어서, 메소드의 파라미터나 메소드의 리턴 타입으로도 사용된다.
이 역시 추상 클래스나 상속을 이용하는 것과 차이가 없지만 역시 인터페이스에 선언된 기능을 가진 객체이면 되기 때문에 좀 더 확장성 있는 구조가 되고 상속의 단점을 보완하는 방식의 설계가 가능해진다.
그외에도 자바의 다형성 활용법에 대해서는 다음 포스팅을 참고.
마지막으로 무엇보다 인터페이스의 다형성의 가장 큰 특징은 바로 다중 구현을 통한 자유로운 상속 관계를 만들어 클래스의 다형성보다 더욱 다양하게 그리고 자유롭게 사용이 가능하다는 것이다.
아무래도 클래스의 다형성은 기본적으로 클래스의 상속에 기반을 두기 때문에, 클래스 상속은 한번밖에 안되며 또한 클래스끼리 의미있는 연관 관계가 있어야 되어 여러모로 제약이 많다. 그러나 인터페이스는 언제어디서나 마음대로 implements를 여러개 추가하거나 빼거나 할 수 있으니 제약으로부터 많이 자유로워 진다.
이를 이용한 예시가 바로 다음에 소개할 클래스끼리 형제 관계를 맺게 해주거나 상속을 넘어선 타입 제한도 가능하게 된다.
형제 관계를 맺어줌
기본적으로 자바의 클래스 상속 구조는 부모 - 자식 관계로만 가능하게 되어 있다.
만일 여러 자식들끼리 의미있는 관계를 맺고싶다면 또다른 부모를 두어 다시 부모 - 자식 관계를 계층형으로 만들어 주어야 한다. 그러나 자바의 클래스 상속 구조는 단일 상속의 원칙을 갖기 때문에, 만일 사용하는 클래스들이 각기 다른 부모 클래스를 상속하고 있는 상황이면 위의 자료형 타입 통합을 할수가 없게 된다.
하지만 인터페이스는 클래스 상속 관계와는 달리, 하나의 기능에 대한 약속이기 때문에 어떤 객체이든 간에 그 약속을 지키기만 한다면 필요한 곳에서 사용할 수 있게 한다는 것을 의미한다.
그리고 인터페이스는 다중 상속에 대해 제약을 받지 않기 때문에, 일부만 묶고 싶은 클래스들을 implements 키워드로 등록시키면, 각기 다른 부모클래스를 상속하고 있는 자식 클래스에 인터페이스를 구현(상속) 시켜줌으로써 형제 클래스 끼리 묶는 타입 통합을 이루어 낼수 있는 것이다.
즉, 아무 관계도 없는 클래스들에게 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어줄 수 있다.
예를 들어 각기 다른 부모 클래스를 상속하고 있는 Soccer 클래스와 BassGuitar 클래스를 하나의 타입으로 묶어서 사용할 필요가 있을때, 인터페이스를 implements 함으로써 마치 Soccer 와 BassGuitar 클래스를 묶은 형제 클래스 타입 IBehavior 를 만든것과 같다.
이렇게 형제 타입을 구성해놓고 아래의 코드와 같이, 인터페이스를 메소드의 매개변수 타입으로 받음으로써 인터페이스에 등록된 추상메소드를 실행하면 실제 구현 클래스의 오버라이딩한 구현 메서드를 실행하게 된다.
즉, 자유로운 인터페이스 다형성을 통해 코드 중복을 없애고 간편화를 시켜 설계상 이점을 얻을 수 있게 되는 것이다.
타입 접근 제한
이외에도 인터페이스는 일종의 접근 제한 역할도 할 수 있다.
이는 위의 형제 관계 역할과 매우 비슷한 개념인데, 만일 똑같은 부모를 상속하고 있는 3개의 자식들중, 2개의 자식 클래스 타입만 받을 수 있는 메서드를 구현한다고 했을때 이용된다.
다형성이랍시고 부모 클래스 타입으로 파라미터를 받아버리면, 모든 자식 클래스가 접근이 가능하지 때문에 제한이 되지 않기 때문이다.
이 부분은 바로 코드 예시를 보는 것이 훨씬 이해하기 쉬울 것이다.
다음과 같이 스타크래프트를 구성하기 위해 Marine, SCV, Tank 클래스를 만들고 이들을 공통으로 묶을 부모 클래스 GroundUnit 클래스로 상속 관계를 맺어 주었다.
그리고 repair() 메서드에서 중복을 줄이기 위한 다형성 기법으로 매개변수 타입을 GroundUnit 부모 클래스 타입으로 받도록 설정 하였다.
class GroundUnit { }
class Marine extends GroundUnit{ }
class SCV extends GroundUnit{ }
class Tank extends GroundUnit{ }
public class Main {
public static void main(String[] args) {
repair(new Marine());
}
static void repair(GroundUnit gu) {
// 마린은 기계가 아니기 때문에 수리는 불가능 하다. 하지만 상속 관계상 마린 클래스 타입이 들어와 실행될 수 있는 위험성이 존재한다.
}
}
하지만 위의 코드의 문제점은 기본적으로 repair 기능은 기계 유닛만 가능하여 SCV와 Tank 클래스 타입만 들어와야 되는데 생물 유닛인 Marine 클래스 타입도 상속 관계에 의해 들어 올수 있다는 것이다.
아무리 타이트하게 코딩을 해도 개발자도 결국은 사람이고, 사람은 결국은 실수를 범할 수 있기 때문에 아예 접근하지 못하도록 원천 차단하는 것이 중요하다.
따라서 별도의 Machine 이라는 인터페이스를 선언하고 SCV, Tank 클래스에 implements 시킨다.
그렇게 3개의 자식중 2개의 자식만 머신 이라는 타입으로 형제 타입 관계를 맺어주면서 동시에 다른 타입의 접근 제한 역할도 해낸 것이다.
interface Machine { } // SCV, Tank 클래스를 통합한 타입으로 이용하는 인터페이스
class GroundUnit { }
class Marine extends GroundUnit{ }
class SCV extends GroundUnit implements Machine{ }
class Tank extends GroundUnit implements Machine{ }
public class Main {
public static void main(String[] args) {
repair(new Marine()); // ! ERROR
}
static void repair(Machine gu) {
// SVG와 탱크 타입만 받을 수 있게 인터페이스를 타입으로 하여 다형성을 적용
}
}
메서드 접근 제한
이밖에도 객체에서 사용할 수 있는 메서드를 제한 하는 효과도 있는데, 예를들어 A, B, C라는 인터페이스를 구현한 클래스를 반환할 때 A 타입으로 변환하게 되면 외부에서는 A 인터페이스의 메소드만 보이게 된다. 따라서 별도의 접근 제한을 이용하지 않고도 사용할 수 있는 메서드 접근 제한과 마찬가지 효과를 보게 하는 방법이다.
이런 이유로 오히려 거꾸로 클래스에 여러 가지 메소드를 만들어 둔 다음 인터페이스로 분리하는 작업을 진행하는 경우가 가끔 있다.
interface PlayMovie {
void play();
}
interface ViewImage {
void view();
}
interface VolumeUpDown {
void volume();
}
class MP3 implements PlayMovie, ViewImage, VolumeUpDown {
public void play() {}
public void view() {}
public void volume() {}
}
public class Main {
public static void main(String[] args) {
PlayMovie mp3 = new MP3(); // 3개의 구현한 인터페이스중 하나로 객체 선언
mp3.play(); // play() 이외의 메소드는 제한된다.
}
}
의존성을 제거 (decoupling)
의존성이란 객체 지향 설계에 아주 중요한 개념으로 객체가 어떻게 소통하는지 나타내는 것이다.
만일 코드 설계를 구체 클래스로 하게 되면 객체간의 강한 의존성이 생기게 되어 만일 코드 변경이 필요하게 되면 이것저것 여러가지 변경이 필요해질 수 도 있다. 따라서 클래스 간의 관계를 구성할때 그 관계를 느슨하게 하는 것이 중요하다.
그래서 클래스의 관계를 상속(extends)이 아닌 구현(implements)으로 인터페이스로 확장시킨다면, 반환 타입이나 매개변수 타입으로 다른 객체와 소통하는 구간에 인터페이스 타입으로 사용함으로써, 객체간 의존성이 줄어들어 자신과 소통하는 객체의 변화에 강한 클래스를 만들 수 있게 된다.
이런 의존성은 과거부터 있었던 많은 Framework에서 발전해 MVC, MVVM 패턴등으로도 활용되었고 depedecny injection등의 기술로도 사용된다.
인터페이스 타입으로 통신
예를들어 다음과 같은 코드가 있다고 하자.
ServiceLogic 클래스의 메소드 printInt() 를 보면 파라미터로 MapStore 클래스 타입을 받아 MapStore 클래스의 메서드를 실행해 값을 얻고 출력하는 로직으로 구성되어 있다.
이러한 형태를 ServiceLogic 클래스는 MapStore 클래스에 의존적이다 라고 말한다. 왜냐하면 MapStore 클래스가 잘못되면 ServiceLogic 클래스의 메서드는 동작하지 않을 것이기 때문이다.
class ServiceLogic {
// ServiceLogic 클래스의 메소드는 MapStore 클래스를 사용하기 때문에, ServiceLogic 는 MapStore 에 의존적이다.
public void printInt(MapStore cls) { // 지정한 클래스 타입만 받음
int num = cls.getNum() * 2;
System.out.println(num);
}
}
class MapStore {
private int num = 10;
public int getNum() {
return this.num;
}
}
따라서 이러한 의존성 관계를 없애기 위해 ClubStore 인터페이스를 만들고 MapStore 클래스에 implements 하여 구현한다. 그러면 ServiceLogic 클래스에서 만일 MapStore 객체 데이터를 사용할 일이 생길경우, 직접 MapStore 객체를 사용하는게 아닌 오로지 ClubStore 인터페이스를 이용해 통신함으로써 클래스 간의 의존성을 없앨 수 있는 것이다.
이것을 변경에 유리한 유연한 설계라고도 한다.
interface ClubStore {
int getNum(); // 인터페이스 타입 상태의 객체에서 메서드를 사용하기 위해선 인터페이스에도 추상 메서드 형태로 넣어져 이썽야 한다.
}
class ServiceLogic {
// ServiceLogic 클래스의 메소드는 ClubStore 인터페이스를 사용함으로써 클래스 간의 의존성을 없앴다.
public void printInt(ClubStore cls) {
int num = cls.getNum() * 2;
System.out.println(num);
}
}
class MapStore implements ClubStore{
private int num = 10;
public int getNum() {
return this.num;
}
}
표준화
개발 시간을 단축
인터페이스를 사용하면 클래스의 선언과 구현을 분리시킬 수 있기 때문에 실제 구현에 독립적인 프로그램을 작성하는 것이 가능하다.
예를 들어 기존의 클래스와 클래스 간의 직접적인 관계를, 인터페이스를 이용해서 간접적인 관계로 변경하면, 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않는 독립적인 프로그래밍이 가능하다.
따라서 메서드를 호출하는 쪽에서는 선언 부만 알면 되기 때문에 인터페이스만 가지고도 프로그램을 작성할 수 있으며, 동시에 다른 한쪽에서는 인터페이스를 구현하는 클래스를 작성하면 인터페이스의 구현을 기다리지 않고 작업이 가능하다.
즉, A가 B클래스의 구성 완성을 기다리지 않고, 설계도인 인터페이스를 보고 동시에 개발을 함으로써 결과적으로 개발 시간을 단축 시킬 수 있는 것이다.
사실 이는 추상 클래스 특징과 겹치는 부분기이도 한데, 인터페이스도 결국 추상화 클래스의 일종이기 때문에 가능한 것이다.
나중에 변경이 용이함
또한 프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음 개발자들에게 인터페이스를 구현하여 프로그램을 짜도록 하면 보다 일관되고 정형화된 프로그램의 개발이 가능하다.
자바의 데이터베이스 인터페이스인 JDBC가 대표적인데, 이 추상화된 인터페이스 규칙에 따라 다양한 종류의 데이터베이스를 사용하더라도 인터페이스 구현만 잘하면 동일하게 접속할 수 있는 이유이기도 하다.
예를들어 어느 자바 어플리케이션이 있고 데이터베이스를 MySQL을 사용하고 있었는데, 만일 데이터베이스를 MySQL에서 Oracle로 변경한다고 한다고 가정하자.
본래라면 자바 어플리케이션의 DB 커넥션 코드를 변경해야 되는데, 이를 자바에서는 JDBC 라는 인터페이스 그룹을 통해 데이터베이스와 관계 중개를 통해 통신한다. 그래서 자바 어플리케이션 입장에선 데이터베이스 종류는 모르고 오로지 인터페이스 기준으로 데이터를 읽고 쓰고 작업을 하게 된다.그리고 각각의 데이터베이스 제조 밴더사들은 위의 인터페이스를 구현한 구현 클래스들을 따로 만들어 자바 언어와 잘 연결되게 지원한다.
따라서 MySQL을 쓰다가 Oracle을 쓴다고 한다면, 기존에 쓰고 있는 오라클에서 제공하는 클래스 라이브러리들을 MySQL 클래스 라이브러리 파일로 바꾸고 약간 설정만 해주면 복잡한 수정 없이 변경이 완료되는 것이다.
만일 인터페이스라는 관계 중개 가 없다면 하드 코딩한 것을 통째로 바꿔야 할지도 모른다.
이것이 자바 데이터베이스 인터페이스라는 '표준화' 가 있기 때문에 가능한 것이다.
마커 인터페이스
자바의 마커 인터페이스는 일반적인 인터페이스와 동일하지만 사실상 아무 메소드도 선언하지 않은 빈 껍데기 인터페이스를 말한다.
예를 들면 아래와 같다.
interface XXXable{ // 아무런 내부 내용이 없는 빈 껍데기 인터페이스
}
얼핏 보기엔 인터페이스의 존재 원리와 반하는 형태이다.
다만 인터페이스를 자유롭게 다중 상속이 가능하다는 점에서 착안하여 이러한 형태로도 사용이 가능한 것이다.
아무 내용도 없어서 쓸모가 없어 보이지만, 마커 인터페이스의 역할은 객체의 타입과 관련된 정보만을 제공해주는 것이다.
따라서 컴파일러와 JVM은 이 마커 인터페이스를 통해 객체에 대한 추가적인 정보를 얻을 수 있다.
다음 예시 코드를 봐보자.
상위 클래스 Animal을 만들고 그 하위들로 Lion, Chicken, Snake ...등 여러가지 동물 클래스들을 만들어 상속 관계를 맺었다. 이때 born 이라는 메서드에서 Animal 타입의 매개변수를 받고 새끼를 낳는 동물인지 알을 낳는 동물인지 구분하기 위해 일일히 instanceof 연산자로 클래스 타입을 구분하였다.
class Animal {
public static void born(Animal a) {
if(a instanceof Lion) {
System.out.println("새끼를 낳았습니다.");
} else if(a instanceof Chicken) {
System.out.println("알을 낳았습니다.");
} else if(a instanceof Snake) {
System.out.println("알을 낳았습니다.");
}
// ...
}
}
class Lion extends Animal { }
class Chicken extends Animal { }
class Snake extends Animal { }
하지만 이러한 방식은 자식 클래스 갯수가 많으면 많을 수록 코드가 난잡해지고 길어진다는 단점이 있다.
따라서 아무런 내용이 없는 빈 껍데기 인터페이스를 선언하고 적절한 클래스에 implements 시킴으로써, 추상화, 다형성 이런걸 떠나서 그냥 단순한 타입 체크용으로 사용하는 것이다.
그러면 조건문 코드도 다음과 같이 심플해질 수 있다.
// 새끼를 낮을 수 있다는 표식 역할을 해주는 마커 인터페이스
interface Breedable {}
class Animal {
public static void born(Animal a) {
if(a instanceof Breedable) {
System.out.println("새끼를 낳았습니다.");
} else {
System.out.println("알을 낳았습니다.");
}
}
}
class Lion extends Animal implements Breedable { }
class Chicken extends Animal { }
class Snake extends Animal { }
※ 참고
이러한 마커 인터페이스의 대표적인 자바 인터페이스로는 Serializable, Cloneable 정도 있다.
참고
- https://www.youtube.com/watch?v=teABcIl_GBM
- https://www.youtube.com/watch?v=VuJHRyIq-w0&list=WL&index=14
- https://stackoverflow.com/questions/28520164/why-are-interfaces-helpful
- https://smoothiecoding.kr/자바-인터페이스-다중-구현/?_ga=2.226971741.1970575914.1663379482-1400978808.1663379482
- https://www.geeksforgeeks.org/interfaces-and-inheritance-in-java/