Live Study 8주차 - 인터페이스


인터페이스 정의하는 방법

인터페이스는 추상 클래스보다 추상화가 더 높은 클래스이다. 추상 클래스가 미완성 설계도라면, 인터페이스는 밑그림만 그려져있는 기본 설계도라고 할 수 있다.

인터페이스의 목적 : 느슨한 결합

  1. 클래스간 결합(느슨한 결합)을 낮춰준다. 인터페이스를 통해서 여러 클래스에서 각각 구현하여 사용할 수 있게된다.
  2. 표준화가 가능하다. 어떤 기능을 구현해야 하는지를 인터페이스를 통해 추론할 수 있다.

이 느슨한 결합을 설명하는 예제코드가 있어서 가져와봤다. (출처 : 남궁성 - Java의 정석)

강한 결합 관계

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A {
public void methodA(B b) { // A -> B
b.methodB();
}
}
class B {
public void methodB() {
System.out.println("methodB();");
}
}
class DevAndy {
public static void main(String[] args){
A a = new A();
a.methodA(new B());
}
}
1
methodB();

A가 B에 의존하는 관계이다. 이 경우에 methodA()의 인자를 새로운 클래스 C가 사용하도록 하려면 아래처럼 바꿔야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A {
public void methodA(C c) { // A -> C
b.methodB();
}
}
class C {
public void methodC() {
System.out.println("methodC();");
}
}
class DevAndy {
public static void main(String[] args){
A a = new A();
a.methodA(new C());
}
}
1
methodC();

Class A와 Class DevAndy 모두 변경을 해야했다. 그렇다면 느슨한 결합관계에선 어떨까?

느슨한 결합 관계

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class A {
public void methodA(I i) { // A -> I
i.methodB();
}
}
interface I {
void methodB();
}
class B implements I {
public void methodB() {
System.out.println("methodB();");
}
}
class C implements I {
public void methodB() {
System.out.println("methodC();");
}
}
class DevAndy {
public static void main(String[] args){
A a = new A();
I i = new C();
a.methodA(i); // a.methodA(new C()); 와 같다.
}
}

Class C가 있다는 가정하에, a.methodA() 의 인자값만 변경하면 되었다.

I i = new C(); 코드의 설명은 인터페이스 레퍼런스를 통한 구현체 사용에서 정리했다.

강한 결합관계에서는 Class A와 호출해서 사용하는 클래스인 Class DevAndy 모두 수정을 해야했다면, 느슨한 결합관계에서는 Class DevAndy에서만 변경하면 되기 때문에 코드 수정이 더 용이하다는 장점이 있었다.


인터페이스 정의는 다음과 같다.

1
2
3
4
5
6
7
interface BoardService {
/**
* 게시글 작성
* @author DevAndy
*/
public void createBoard(HttpSession session, String title, String contents);
}

인터페이스는 기본적으로 클래스지만, class 대신 interface 라고 작성한다. 추상 클래스가 abstract class 로 작성하는것과 다른 문법이다.

인터페이스의 접근제한자는 일반 클래스와 마찬가지로 public, default만 가질 수 있다.


인터페이스 구현하는 방법

인터페이스를 구현하는 방법은 2가지가 있다. 하나는 implements 키워드를 사용하여 구현체 클래스를 생성하는 방식과 하나는 익명클래스로 인터페이스를 구현하는 방법

implements

인터페이스를 구현한다는 이야기는 클래스간 상속과 같다. 그러나 추상클래스와 달리 인터페이스는 extends 키워드가 아닌 implements 키워드로 구현한다. (인터페이스와 추상클래스의 차이)

1
2
3
interface Player {
public void introduction(String name);
}
1
2
3
4
5
6
public class MyPlayer implements Player {
@Override
public void introduction(String name) {
System.out.println("Hello my name is "+name);
}
}

IDE에서 implements 키워드로 인터페이스 구현체를 생성하면, 자동으로 추상 메서드를 구현하도록 도와준다.

추상 메서드를 구현할 때에 @Override 어노테이션이 자동으로 붙는데, 사실 어노테이션이 없어도 오버라이딩 문법만 맞춰서 메서드를 작성한다면 적용이 된다. 하지만 @Override 어노테이션을 작성하면, 해당 메서드가 오버라이딩 문법에 맞춰서 작성되었는지를 체크해주므로 작성해주는 것이 좋다.

익명클래스

인스턴스를 생성할 때, 생성자를 이용하여 이름없는 클래스인 익명 클래스를 통해서 인터페이스를 구현하는 방식이다.

익명클래스의 구조는 다음과 같다.

1
2
3
인터페이스 인스턴스 = new 인터페이스(){
// 인터페이스 구현
}

익명클래스로 인터페이스 Player의 추상 메서드(introduction())를 구현한 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
public class DevAndy {
public static void main(String[] args){
Player player = new Player(){
@Override
public void introduction(String name) {
System.out.println("Hello my name is "+name);
}
};
player.introduction("Andy");
}
}
1
Hello my name is Andy

인터페이스 레퍼런스를 통해 구현체를 사용하는 방법

인터페이스 레퍼런스를 통해 구현체를 사용한다는 의미를 이번 스터디를 통해 처음 알게되었다. 위의 코드를 활용해서 인터페이스 레퍼런스를 통해서 구현체를 사용해보도록 하겠다.

1
2
3
interface Player {
public void introduction(String name);
}
1
2
3
4
5
6
public class MyPlayer implements Player {
@Override
public void introduction(String name) {
System.out.println("Hello my name is "+name);
}
}
1
2
3
4
5
6
7
public class DevAndy {
MyPlayer player = new MyPlayer();
player.introduction("Andy");

Player player02 = player;
player02.introduction("Steve Jobs");
}
1
2
Hello my name is Andy
Hello my name is Steve Jobs

인터페이스인 Player의 인스턴스 player02를 선언하면서 참조변수로 인터페이스 구현체인 MyPlayer의 인스턴스를 참조했다.

출처 : ssonsh - 인터페이스


인터페이스 상속

인터페이스도 상속이 된다. 자바에서는 인터페이스간 상속을 허용한다. 역시 인터페이스가 클래스이기 때문에 가능한 것으로 추론한다.

1
2
3
4
interface Athlete {
void preSeason();
void whileSeason();
}
1
2
3
interface FootballPlayer extends Athlete {
void postSeaon();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ProfessionalPlayer implements FootballPlayer{
@Override
public void postSeaon() {
System.out.println("post seaon");
}

@Override
public void preSeason() {
System.out.println("pre season");
}

@Override
public void whileSeason() {
System.out.println("시즌중!!");
}
}
1
2
3
4
5
6
7
8
public class PlayGround {
public static void main(String[] args) {
ProfessionalPlayer proFootballer = new ProfessionalPlayer();
proFootballer.preSeason();
proFootballer.whileSeason();
proFootballer.postSeaon();
}
}
1
2
3
pre season
시즌중!!
pre season

인터페이스의 디폴트 메서드(Default Method), 자바8

인터페이스는 원래 추상 메서드만 선언할 수 있었다고 한다. 그러나 Java 8부터 인터페이스에 디폴트 메서드static 메서드도 추가하는것이 가능해졌다.

Why?

Java 8 이전의 인터페이스의 단점은 인터페이스에 변경사항이 발생할 경우, 해당 인터페이스를 구현하고있는 모든 구현체를 수정해야 한다는 단점이 있었다. 예를들면 인터페이스에 추상 메서드를 추가해야 할일이 발생한다면, 인터페이스에도 메서드를 추가해야하지만, 해당 인터페이스를 구현하고 있는 모든 구현체에서 메서드를 추가하고, 구현해야 한다는 부담이 발생한다.

이 문제를 해결하기 위해 등장한 것이 디폴트 메서드이다. 디폴트 메서드는 추상메서드가 아니기 때문에 메서드에 변경사항이 발생하더라도 구현체를 변경할 필요가 없다.

default method 문법

앞서 디폴트 메서드는 추상 메서드가 아니라고 했다. 따라서 디폴트 메서드는 구현체(메서드 바디)가 필요하다.

메서드 앞에 default 키워드를 붙이고, 구현체를 작성하면 디폴트 메서드가 추가된다.

1
2
3
4
5
6
interface StudyHalle {
void method01(); // 추상 메서드
default void method02(){
// 디폴트 메서드
}
}

이렇게 method02()를 디폴트 메서드로 인터페이스에 추가하면, 모든 구현체에 method02()를 추가할 필요없이 인터페이스만 수정하고, 사용하는 클래스에서 메서드를 사용하면 된다.

예제 코드를 만들어보았다.

1
2
3
interface MyInterface {
void methodA();
}
1
2
3
4
5
6
7
8
9
10
public class MyInterfaceImpl implements MyInterface {
@Override
void methodA() { // overriding 메서드
System.out.println("method A by MyInterfaceImpl");
}

void methodB() { // 일반 메서드
System.out.println("method B by MyInterfaceImpl");
}
}

Class MyInterfaceImpl는 인터페이스 MyInterface의 구현체이다. methodA()를 오버라이딩하였으며, methodB()를 추가로 가지고 있다. 여기서 디폴트 메서드를 추가해보았다.

1
2
3
4
5
6
7
interface MyInterface {
void methodA();

default methodB() {
System.out.println("method B by MyInterface")
}
}

디폴트 메서드는 구현체를 건드릴 필요없이 인터페이스에만 추가하면 사용할 수 있다. 그런데 구현체에 있는 메서드와 이름이 똑같은 일이 발생했다. 이렇게 되면 구현체의 인스턴스에서 접근되는 methodB()는 어떤 메서드에 접근될까?

디폴트 메서드가 구현체의 메서드와 이름이 중복될 경우

1
2
3
4
5
6
7
public class StudyHalle {
public static void main(String[] args){
MyInterface myInterface = new MyInterfaceImpl();
myInterface.methodA();
myInterface.methodB();
}
}
1
2
method A by MyInterfaceImpl
method B by MyInterfaceImpl

이처럼 디폴트 메서드가 구현체의 메서드와 이름이 중복되어 충돌할 경우 디폴트 메서드가 무시되고, 구현체의 메서드가 우선된다. 상속 관계에서도 부모 클래스의 메서드가 상속되고, 인터페이스의 디폴트 메서드는 무시된다.


인터페이스의 static 메서드, 자바8

Java 8에 디폴트 메서드와 함께 추가된 static 메서드는 디폴트 메서드에 static 키워드를 선언하면 static 메서드가 된다.

구현체의 인스턴스를 생성하지 않고도 인터페이스만으로 바로 메서드를 호출할 수 있다.

1
2
3
4
5
6
interface MyInterface {
void methodA();
static void methodB() {
System.out.println("static method");
}
}
1
2
3
4
5
public class StudyHalle {
public static void main(String[] args){
MyInterface.methodB();
}
}
1
static method

인터페이스의 private 메서드, 자바 9

Java 9에서는 인터페이스에 private 메서드가 추가되었다. private 메서드가 추가되면서 인터페이스의 메서드를 외부에 공개하지 않을수 있게 되었따.

인터페이스의 private 메서드의 특징은 다음과 같다.

  • 디폴트 메서드처럼 구현체를 가져야 한다.
  • 인터페이스 내부에서만 사용되는 메서드이다.
  • private 메서드는 인터페이스의 static 키워드가 없는 메서드에서만,
    private static 메서드는 인터페이스의 모든 메서드에서 사용가능하다.

예제 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface MyInterface {
void method01();
default void method02(){
System.out.println("method 02 : default method");
method04();
method05();
}
static void method03(){
System.out.println("method 03 : static method");
// method04(); compile error
method05();
}
private void method04(){
System.out.println("method 03 : private method");
}
private static void method05(){
System.out.println("method 04 : private static method");
}
}
1
2
3
4
5
6
class MyInterfaceImpl {
@Override
public void method01(){
System.out.println("Live Study 8주차 - 인터페이스 학습중");
}
}
1
2
3
4
5
6
7
8
9
class DevAndy {
public static void main(String[] args){
MyInterface myInterface = new MyInterfaceImpl();

myInterface.method01();
myInterface.method02();
MyInterface.method03();
}
}
1
2
3
4
5
6
Live Study 8주차 - 인터페이스 학습중
method 02 : default method
method 03 : private method
method 04 : private static method
method 03 : static method
method 04 : private static method

인터페이스에서 private 메서드를 사용하면 외부(구현체)에 공개하지 않는 메서드를 가질 수 있게된다.


인터페이스와 추상클래스의 차이

Java 8 이전까지의 인터페이스와 추상클래스의 차이는 아래와 같았다.

인터페이스 추상클래스
구현 객체의 같은 동작을 보장하기 위함 추상 클래스를 상속받아서 기능을 이용, 확장시키기 위함
다중 상속이 가능하다 다중 상속이 불가능하다
추상 메서드만 사용 가능하다 일반 메서드와 일반 변수 모두 사용가능하다
상수+추상메서드 일반 변수 + 일반 메서드 + 추상 메서드
생성자와 일반 변수를 가질 수 없다. 생성자와 일반 변수를 모두 가질 수 있다
implements 사용 extends 사용

그러나 Java 8에 추가된 디폴트 메서드, static 메서드 그리고 Java 9에서 추가된 private 메서드까지를 보니 이제 인터페이스와 추상클래스의 차이가 모호해진것 같다. Java 9까지의 인터페이스의 변경사항이 반영된 인터페이스와 추상클래스의 차이를 정리해보니 아래처럼 정리가 되는 것 같다.

인터페이스 추상클래스
구현 객체의 같은 동작을 보장하기 위함 추상 클래스를 상속받아서 기능을 이용, 확장시키기 위함
다중 상속이 가능하다 다중 상속이 불가능하다
implements 사용 extends 사용

여기까지 정리해보았을때엔 인터페이스가 생성자만 가지지 못할뿐 인터페이스간의 상속이 가능하고, 다중상속도 가능하기 때문에 모든 부분에서 인터페이스가 추상클래스보다 부족하지 않아보인다.