Language/Java

[자바 스터디 8주차] 인터페이스

소프 2021. 1. 9.

github.com/whiteship/live-study/issues/8

 

8주자 과제: 인터페이스 · Issue #8 · whiteship/live-study

목표 자바의 인터페이스에 대해 학습하세요. 학습할 것 (필수) 인터페이스 정의하는 방법 인터페이스 구현하는 방법 인터페이스 레퍼런스를 통해 구현체를 사용하는 방법 인터페이스 상속 인터

github.com

목표

자바의 인터페이스에 대해 학습하세요.

학습할것

  - 인터페이스란(자체 추가)

  - 인터페이스 정의하는 방법

  - 인터페이스 구현하는 방법

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

  - 인터페이스 상속

  - 인터페이스의 기본 메소드 (Default Method), 자바 8

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

  - 함수형 인터페이스(자체 추가)

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

  - Constant Interface(자체 추가)

  - 추상 클래스가 필요할까요?(자체 추가)

 

인터페이스란

인터페이스는 일종의 추상클래스이다. 인터페이스를 추상클래스처럼 추상메소드를 가지만 추상클래스보다 추상화 정도가 높아서 추상클래스와 달리 일반 메소드 또는 멤버변수를 구성원으로 가질 수 없다.

오직 추상메소드와 상수만을 멤버로 가질 수 있으며, 그 외의 다른 어떠한 요소도 허용하지 않는다.

 

즉, 추상클래스보다 더욱 강력한 추상화를 제공하는 도구이고, 그를 통해 다형성을 더욱 강력하게 해주는 도구라 볼 수 있다.

 

※ 추상 클래스 : 추상 메서드를 포함하는 일반 클래스

  - 생성자, 인스턴스 변수 등을 멤버로 가질 수 있다.

abstract class Player{
	boolean pause;	// 인스턴스 변수
	
	Player(){}		// 생성자

	abstract void stop(); // 추상메서드

	void play(){} 		  // 인스턴스 메서드
}

 

인터페이스의 역할 : 개발 코드와 객체가 서로 통신하는 접점

 

https://github.com/yeGenieee/java-live-study/blob/main/%5B8%5DJava%20Live%20Study.md

위 그림처럼, 개발 코드가 인터페이스의 메소드를 호출하면 -> 인터페이스는 객체의 메소드를 호출한다.

  - 이렇게 되면, 개발 코드는 객체의 내부 구조를 알 필요가 없고, 인터페이스의 메소드만 알면 된다는 장점을 가진다.


그럼 개발 코드가 직접 객체 메소드를 호출하면 되는거 아닌가? 굳이 왜 인터페이스를 두는 걸까?

  - 개발 코드를 수정하지 않고, 사용하는 객체를 변경할 수 있도록 하기 위해서 인터페이스를 쓴다.

  - 인터페이스는 하나의 객체가 아니라, 여러 객체들과 사용이 가능하다.

    - 그래서, 어떤 객체를 사용하느냐에 따라서 실행내용과 리턴값이 다를 수 있다.

    - 개발코드 측면에서는 코드 변경 없이 실행 내용과 리턴값을 다양화 할 수 있다는 장점을 가진다.

=

인터페이스의 장점

   표준화 가능

    - 프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음, 개발자들에게 인터페이스를 구현하여 프로그램을 작성하도록 하면 보다 일관되고 정형화된 프로그램의 개발이 가능하다.

    - 대표적인 예로 JDBC(Java Database Connectivity), API(Application Programming Interface)가 있다.

https://ahnyezi.github.io/java/javastudy-8-interface/#5-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EC%9D%98-%EC%97%AD%ED%95%A0%EC%9D%98%EB%AF%B8

과거에 DB를 사용해서 자바 어플리케이션을 개발하면, 사용하는 DB에 따라서 코드가 달라졌다. 오라클 DB를 사용한다면 오라클에 맞는 자바코드를 짜야했고, 다른 DB로 바꿀 경우 코드를 다 변경해야 하는 문제가 있었다. 즉, A가 B에 의존적일 때 B에서 C로 바뀌면 A도 많이 바꿨어야 하는 형태였다.

 

이러한 문제를 해결하기 위해 중간에 JDBC라는 인터페이스 집합(껍데기)을 두기로 했다. JDBC 인터페이스를 각 DB 회사들에게 제공하고, 회사들은 해당 인터페이스에 맞춰서 자사의 서비스를 개발하였다. 이렇게 된다면 어플리케이션을 개발하는 회사는 해당 인터페이스에 맞게 개발하면되기 때문에, JDBC 인터페이스 자체가 변경되지 않는 이상 다양한 종류의 DB를 코드 수정없이 사용할 수 있게 되었다.

 

  ● 서로 관계없는 클래스들 간의 관계를 맺어준다.

    - 하나의 인터페이스를 공통적으로 구현하도록 하여 관계를 맺어 줄 수 있다.

    - 다음과 같은 상속관계를 가지는 클래스들이 있다.

SCV, Tank, Dropshop 클래스에 수리를 위한 메서드를 추가하려 한다.

 

1.첫번째 방법 : 메서드 오버로딩(수리가 필요한 클래스를 매개변수로 설정)

void repair(SCV s){}
void repair(Tank t){}
void repair(Dropship d){}

비효율적이고 반복되는 코드가 여럿 생긴다.

 

2.두번째 방법 : (다형성 이용)SCV,Tank의 부모인 GroundUnit을 repair의 매개변수 타입으로 설정

void repair(GroundUnit gu){}

Marine 클래스는 불필요한 repair 메서드를 가지게 된다.

 

3. 세번째 방법 : (인터페이스 이용) 빈 인터페이스를 작성하여, SCV, Tank, Dropshop가 해당 인터페이스를 implements하게 한다.

interface Repairable{}

class SCV extends GroundUnit implements Repairable{}
class Tank extends GroundUnit implements Repairable{}
class Dropship extends AirUnit implements Repairable{}

...
void repair(Repairable r){} // 인터페이스를 매개변수로 하여, Repairable 구현클래스만 올 수 있게함

repairable 인터페이스는 아무런 내용도 없지만 이를 구현한 클래스들에 공통점이 생김

  - 즉, 서로 관계 없는 클래스들의 관계를 맺어줌

인터페이스를 repair 메서드의 매개변수로 설정

  - 해당 인터페이스 구현 클래스만 repair 메서드를 사용할 수 있게함.

 

  ● 독립적인 프로그래밍이 가능하다.

    - 인터페이스를 이용하여 클래스의 선언과 구현을 분리시킬 수 있기 때문에 실제 구현에 독립적인 프로그램을 작성하는 것이 가능하다.

 

선언과 구현 동시에

class B {
    public void method() {
        System.out.println("methodInB");
    }
}

선언과 구현 분리

// 선언(설계)
interface I {
    public void method();
}

// 구현
class B implements I {
    public void method() {
        System.out.println("methodInB");
    }
}

강한 결합과 느슨한 결합(★)

 

인터페이스를 쓰는 주된 이유중 하나

 

왼쪽의 그림(강한 결합)

  - A는 B에 의존하고 있다.(A가 B를 사용)

  - 이 때, A가 C를 사용하게 하려면?

  - A는 B를 의존하고 있는 코드를 C를 의존하게끔 변경해야 한다.

 

오른쪽 그림(느슨한 결합)

  - A는 I 인터페이스에 의존하고 있고, I 인터페이스를 구현한 B를 사용한다.

  - 이 때, A가 C를 사용하게 하려면?

  - A는 I에 의존하고 있기 때문에, I 인터페이스를 구현한 C를 사용한다면 따로 코드를 변경하지 않아도 된다.

 

👉 [강한결합] 직접적인 관계의 두 클래스 (A -> B)

class A {
    public void methodA(B b) { // B를 사용!!(따라서 B와 관계 있음)
        b.methodB();
    }
}

class B {
    public void methodB() {
        System.out.println("methodB()");
    }
}

class InterfaceTest {
    public static void main(String args[]) {
        A a = new A();
        a.methodA(new B());
    }
}

👉 [느슨한결합] 간접적인 관계의 두 클래스 (A -> I -> B)

  - methodB()를 추상 메서드로 갖는 인터페이스 작성

  - 해당 인터페이스를 구현한 클래스 생성

  - 인터페이스 타입을 매개변수를 사용해서 다형성을 구현

class A {
    public void methodA(I i) {// I를 사용! (따라서 A는 B클래스와 관계 없음.I 인터페이스랑만 관계 있음)
        i.methodB();
    }
}

// 껍데기
interface I {
    public abstract void methodB();
}

// 알맹이
class B implements I {
    public void methodB() {
        System.out.println("methodB()");
    }
}

// 나중에 B를 C로 변경하여도 C만 변경하면 됨. methodB를 호출하는 A를 변경할 필요 없음
class c implements I {
    public void methodB() {
        System.out.println("methodB() in C");
    }
}

즉, A가 B의 메서드를 호출하는 형태였다가 C의 메서드를 호출하게 바뀐다면, 강한결합 형태는 A가 B를 직접 의존하기 때문에, A의 내부를 변경해줘야 한다. 하지만 인터페이스를 사용한 느슨한 결합은 A가 I를 거쳐 B를 의존하기 때문에 A 내부를 변경해주지 않아도 된다.

 

  ● 개발 시간 단축

    - 인터페이스가 작성되면 이를 사용해서 프로그램을 작성하는 것이 가능하다.

    - 메서드를 호출하는 쪽에서는 선언부만 알면 된다.

    - 강한결합 형태는 A가 B를 직접 의존하기 때문에, B가 완성된 후에 A를 개발할 수 있다. 

    - 하지만, 느슨한결합 형태는 B가 완성되지 않아도 껍데기인 I를 이용해서 A를 개발할 수 있다. A에서 I의 추상메서드를 호출할 수 있기 때문에, 메서드가 완성되었다고 가정하고 개발하는 것이다.

 

인터페이스 정의하는 방법

인터페이스를 작성하는 것은 클래스를 작성하는 것과 같다. 다만 키워드로 class 대신 interface를 사용한다.

interface에도 class와 같이 접근제어자로 public 또는 default를 사용할 수 있다.

interface [인터페이스 이름] extends [부모 인터페이스명 ...] {

		// 상수 (static final)
		public static final String example = "myExample"; 
		// public static final은 생략될 수 있습니다만, 여전히 그 의미는 유지됩니다.

		// 추상 메소드 (public abstract)
		public abstract void printExample();
		// public abstract 는 생략될 수 있습니다. 하지만 그 의미는 유지됩니다.
}

  - 인터페이스는 다중 상속이 가능하다.(extends 이용)

  - 인터페이스는 static final 필드만 가질 수 있다. 필드를 선언할 때 public static final이 생략되어 있다.

  - 인터페이스에 적는 모든 메소드들은 추상 메소드로 간주된다.

  - 인터페이스 내에 존재하는 메소드는 무조건 "public abstract"로 선언되며, 이를 생략할 수 있다.

  - 인터페이스 내에 존재하는 변수 무조건 "public static final"로 선언되며, 이를 생략할 수 있다.

 

※추가 지식

추상 클래스와 인터페이스의 공통점과 차이점을 알아보자.

 

공통점

  - 자기 자신을 객체화할 수 없으며 다른 객체가 상속, 구현을 하여 객체를 생성할 수 있다.

  - 상속, 구현을 한 하위 클래스에서는 상위에서 정의한 추상 메서드를 반드시 구현하여야 한다.

 

https://k3068.tistory.com/34

그럼 추상클래스만 쓰지 왜 인터페이스가 존재할까?

 

추상 클래스는 IS - A "~는 ~이다"의 개념이다. -> 소프는 사람이다.

인터페이스는 HAS - A "~는 ~를 할 수 있다"의 개념이다. -> 소프는 축구를 할 수 있다.

 

Ex) Soap는 사람(Person)이면서 축구(Soccer)를 할 수 있다.

class SSON extends Person implements Developable

 

※추가 지식2

자바는 클래스의 다중상속이 금지되어 있다. 부모들이 메서드가 자손에서 충돌나는 문제를 방지하기 위해서이다. 하지만 인터페이스 존재하는 추상메서드는 선언부(head)만 존재하기 때문에 충돌날 가능성이 없다. 따라서 다중상속이 가능하다.

  - 인터페이스의 조상은 인터페이스만 가능(Object가 최고 조상이 아니다)

 

추상메서드를 1개씩 가지고 있는 인터페이스 Movable과 Attackable

interface Movable {
	// 지정된 위치(x,y)로 이동하는 메서드
	void move(int x, int y);
}

interface Attackable{
	// 지정된 대상(u)를 공격하는 메서드
	void attack(Unit u);
}

 

 

Movable과 Attackable 인터페이스를 다중 상속 받은 Fightable 인터페이스

interface Fightable extends Movable, Attackable{ }
// Fightable 인터페이스는 자동으로 멤버를 2개 가지게 됨 (move, attack)

인터페이스 구현하는 방법

1) implements 키워드 사용

 

  - 인터페이스를 구현하기 위해서는, 정의된 모든 추상메서드를 구현해야 한다.

  - 일부 추상메서드만 구현할 경우 해당 클래스는 abstract class가 된다.

class 클래스이름 implements 인터페이스이름{
	// 인터페이스에 정의된 추상메서드를 모두 구현해야 한다.
}

Fightable 인터페이스 구현

interface Fightable{
	// public abstract 생략된 형태
	void move(int x, int y);
	void attack(Unit u);
}

// 모두 구현
class Fighter implements Fightable{
	public void move(int x,int y){}
	public void attack(Unit u){}
}

// 일부만 구현(추상클래스)
abstract class Fighter implements Fightable{
	public void move(int x, int y){ /* 구현 내용*/}
	// public abstract void attack(Unit u); 가 생략된 형태
}

 

주의할 점

  - 인터페이스의 모든 메서드는 public 접근제어를 가진다.

  - 구현 클래스에서 메서드를 오버라이딩 할 때, 오버라이딩 메서드 접근 제어 범위가 조상보다 좁아서는 안된다.

interface Fightable { // 인터페이스의 모든 메서드는 public abstract이다.
	void move(int x, int y); // public abstract가 생략됨
}

class Fighter implements Fightable{
	// 오버라이딩 규칙: 조상(public)보다 접근제어자 범위가 좁으면 안된다.

	public void move(int x, int y){   // public 안쓰면 default가 되므로 컴파일 에러 발생
		System.out.println("이동");
	}
}

 

2) 익명 구현 객체 사용

인터페이스는 원칙적으로는 객체화 될 순 없지만, 익명 객체를 통해서 인터페이스를 구현하는 일회용 객체를 만들 수 있다.

Fightable fight = new Fightable() {
      @Override
      public void void() {
          System.out.println("moving");
      }
  };

익명 객체를 활용하여 인터페이스 자체만으로 구현할 수 있다.

이렇게 쓰는 경우 익명 객체를 만들어 변수 fight에 주입시켜 준다.

  - java의 인터페이스에는 생성자가 없으므로, 항상 Fightable()의 괄호는 빈 상태

  - 단순히 인터페이스의 구현역할을 하는 익명객체일뿐, 상속이나 또다른 인터페이스를 구현하는 것은 불가능하다.

  - 또한, 이것은 expresison의 일종이므로 뒤에 세미콜론을 붙여야 한다.

 

응용버전

List<Runnable> actions = new ArrayList<Runnable>();
actions.add(new Runnable() {
    @Override
    public void run() {
        ...
    }
});

 

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

다형성을 공부하면 자손클래스의 인스턴스를 부모타입의 참조변수로 참조하는 것이 가능하다는 것을 알 수 있다.

인터페이스도 이를 구현한 클래스의 부모라 할 수 있으므로 해당 인터페이스 타입의 참조변수클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로 형변환도 가능하다.

 

인터페이스로 구현 객체 사용 : 인터페이스 변수를 선언하고 구현 객체를 대입

인터페이스 변수 = 구현 객체;

  - 인터페이스로 구현 객체를 사용하려면 -> 인터페이스 변수를 선언하고 구현 객체를 대입해야 한다.

  - 인터페이스 변수는 참조타입이므로, 구현 객체가 대입될 경우, 구현 객체의 번지를 저장한다.

interface Flyable{
    void fly();
}

interface Jumpable {
	 void jump();
}

class Bird implements Flyable, Jumpable {
    @Override
    public void fly() {
        System.out.println("Bird's Flying");
    }
		@Override
		public void jump() {
        System.out.println("Bird's Jumping");
		}
}
Flyable bird = new Bird();
bird.fly();
bird.jump(); // Cannot resolve method 'jump' in 'Flyable'

flayable을 통해서 bird를 선언하게 되면, flayable가 가질 수 있는 메소드만 사용할 수 있다.

 

이는 자바의 다형성의 대표적인 예시로 활용된다.

List<Flyable> list = ArrayList<Flyable>();
list.add(new Bird());
list.add(new Airplane());
list.add(new SuperMan());

for (Flyable element : list) {
	element.fly(); // fly 메소드로 통일하여 호출 할 수 있습니다.
}

인터페이스 상속

interface [인터페이스 이름] extends [부모 인터페이스명 ...] {

}

인터페이스는 extends키워드를 통해 상속이 가능하다.

 

인터페이스 자식은 상속된 상위 인터페이스가 가지고 있는 메서드까지 모두 구현해야 한다.

interface Drawble {
	void draw();
}
interface Printable extends Drawble {
	void print();
}

class Circle implements Printable {
	@Override
	public void draw() {
	}
	@Override
	public void print() {
	}
}

다중상속 또한 가능하다.

/** 인터페이스의 다중 상속 예제 */
interface Dancable {
	void perform();
}
interface Flyable {
	void perform();
}

interface Perfomable extends Dancable, Flyable {
}

class Superman implements Perfomable {
	@Override
	public void perform() {
	}
}

 

인터페이스의 기본 메소드 (Default Method), 자바 8

자바8 이전까지의 인터페이스는 기능에 대한 선언만 가능하기 때문에, 실제 코드를 구현한 로직은 포함될 수 없다.

하지만 자바8 이후부터나온 디폴트 메서드는 인터페이스 내부에 존재할 수 있는 구현 메서드이다. 인터페이스를 implements 하면 메소드 구현없이 바로 사용할 수 있다.

 

등장 배경

 

사실 인터페이스는 기능에 대한 구현보다는, 기능에 대한 '선언'에 초점을 맞춰서 사용하는데, 디폴트 메소드는 왜 등장했을까?

...(중략)... 바로 "하위 호환성"때문이다. 예를 들어 설명하자면, 여러분들이 만약 오픈 소스코드를 만들었다고 가정하자. 그 오픈소스가 엄청 유명해져서 전 세계 사람들이 다 사용하고 있는데, 인터페이스에 새로운 메소드를 만들어야 하는 상황이 발생했다. 자칫 잘못하면 내가 만든 오픈소스를 사용한 사람들은 전부 오류가 발생하고 수정을 해야 하는 일이 발생할 수도 있다. 이럴 때 사용하는 것이 바로 default 메소드다. (자바의 신 2권)

기존에 존재하던 인터페이스를 이용하여서 구현된 클래스를 만들고 사용하고 있는데,

인터페이스를 보완하는 과정에서 추가적으로 구현해야 할, 혹은 필수적으로 존재해야 할 메소드가 있다면,

이미 이 인터페이스를 구현한 클래스와의 호환성이 떨어지게 된다. 이러한 경우 default메소드를 추가하게 된다면

하위 호환성은 유지되고 인터페이스의 보완을 진행할 수 있다.

 

 

※추가 지식

List -> Collection -> Iteratble 구조로 상속을 받고 있다.

Iterable의 forEach는 default메소드로 되어있다.

 

자바9부터 List.of를 이용해 add()를 쓰지않고 간편하게 디폴트 데이터 생성 가능

 

 

default method 선언 방식

[public] default 리턴타입 메소드명(매개변수, ...) { ... }

  - 클래스의 인스턴스 메소드와 형태는 동일, default 키워드가 리턴 타입 앞에 붙음

  - public의 특성을 가짐 (public 키워드를 생략해도 컴파일 과정에서 자동으로 붙음)

 

default method 선언 예제

/* 디폴트 메소드 선언 */
/* RemoteControl 인터페이스에서 무음 처리 기능을 제공하는 setMute() 디폴트 메소드를 선언 */
public interface RemoteControl {
    ...

    // 디폴트 메소드
    // 무음 처리 기능을 제공하는 setMute() 메소드 (실행 내용까지 작성)
    default void setMute(boolean mute) {
        if (mute) {
            System.out.println("무음 처리합니다.");
        } else {
            System.out.println("무음 해제합니다.");
        }
    } 
}

 

default method 사용

  - 디폴트 메소드는 인터페이스에 선언되지만, 인터페이스에서 바로 사용할 수 없다.

  - 디폴트 메서드는 추상 메소드가 아닌 인스턴스 메소드이다.

  - 따라서, 구현 객체가 있어야 사용 가능하다.

 

default method 예제

  - RemoteControl 인터페이스는 setMute()라는 디폴트 메소드를 가지고 있지만, 아직 RemoteControl의 구현객체가 없어서 호출할 수 없다.

RemoteControl.setMute(true); // 호출 불가 

  - 호출 가능하게 하려면, RemoteControl의 구현 객체가 필요하다.

RemoteControl rc = new Television(); // Television 객체를 인터페이스 변수에 대입하고 나서, setMute()를 호출할 수 있음
rc.setMute(true);

  - 비록, setMute()가 Television 클래스에 선언되지는 않았지만, Television 객체가 없으면 setMute()도 호출할 수 없다.

  - 어떤 구현 객체는 디폴트 메소드의 내용이 맞지 않아, 수정이 필요할 수도 있다.

  - 구현 클래스를 작성할 때 디폴트 메소드를 재정의(오버라이딩)해서 자신에게 맞게 수정하면 디폴트 메소드가 호출될 때 자신을 재정의한 메소드가 호출된다.

 

default method 오버라이딩 부터 사용 예제

Audio 클래스에서 default Method setMute() 오버라이딩

/* 디폴트 메소드를 사용한 구현 클래스 */
public class Audio implements RemoteControl {
    // 필드
    private int volume;
    private boolean mute;

 		...

    // 디폴트 메소드 재정의
    @Override
    public void setMute(boolean mute) {
        this.mute = mute;

        if (mute) {
            System.out.println("Audio 무음 처리합니다");
        } else {
            System.out.println("Audio 무음 해제합니다");
        }
    }
}

default method 사용 예시 - RemoteConrolExample 클래스

/* 디폴트 메소드 사용 예제 */
public class RemoteControlExample {
    public static void main(String[] args) {
        RemoteControl rc = null;

        rc = new Television();
        rc.turnOn();
        rc.setMute(true);

        rc = new Audio();
        rc.turnOn();
        rc.setMute(true);
    }
}
/* Result 
TV를 켭니다.
무음 처리합니다.
Audio를 켭니다.
Audio 무음 처리합니다.
*/

 

default method 필요성

  - default method는 인터페이스에 선언된 인스턴스 메소드이기 때문에 구현 객체가 있어야 사용할 수 있다. 선언은 인터페이스에서 하고, 사용은 구현 객체를 통해 한다.

 

결론은 기존 인터페이스를 확장해서 새로운 기능을 추가하기 위해서 필요하다.

 

  - 기존 인터페이스의 이름과 추상 메소드의 변경없이 디폴트 메소드만 추가할 수 있기 때문에 이전에 개발한 구현 클래스를 그대로 사용할 수 있으면서 새롭게 개발하는 클래스는 디폴트 메소드를 활용할 수 있다.

 

default method가 있는 interface 상속

 

부모 인터페이스에 default method가 정의되어 있는 경우, 자식 인터페이스에서 디폴트 메소드를 활용하는 방법은 아래와 같다.

1. 디폴트 메서드를 단순히 상속만 받음

2. 디폴트 메서드를 재정의해서 실행 내용을 변경

3. 디폴트 메소드를 추상 메소드로 재선언

public interface ParentInterface {
		public void method1();
		public default void method2() { // 실행문 }
}

//...

public interface ChildInterface3 extends ParentInterface {
		@Override
		public void method2(); // 추상 메소드로 재선언
		public void method3();
}

 

주의사항

만약 아래와 같이 Snake 클래스에서 두 인터페이스를 상속하는데 각 인터페이스에 print() 기본 메소드가 정의되어 있다면?

// 인터페이스
public interface PrintableAnimal {

    default void print() {
        ...
    }

public interface Printer {

    default void print() {
        ...
    }
}

public class Snake implements PrintableAnimal, Printer {
    // 어떤 print() 메소드를 사용하게 될까...
    //Print inherits unrelated defaults for say() from types Flyable and Printable
}

우선 print()라는 메소드를 정확히 찾을 수 없다고 에러가 나오고 추상메소드처럼 처리한다.

 

public class Snake implements PrintableAnimal, Printer {

    @Override
    public void print() {

    }
}

※ 같은 메서드 시그니처로 추상메소드와 default가 있어도 똑같다.

 

그다음 아래의 3가지 방법이 있다.

 

1. IDE에서 컴파일 에러 발생, 문제를 해결하기 위한 3가지 방법이 있음

 

1-1. 둘다 사용 or 하나만 사용

public class Snake implements PrintableAnimal, Printer {

    // ...

    @Override
    public void print() {
        PrintableAnimal.super.print(); // PrintableAnimal의 print() 메소드 호출
        Printer.super.print(); // Printer의 print() 메소드 호출

        // 혹은 직접 구현...
    }
}

평소 알고 있었던 super의 의미와는 다르게 사용됨.

 

2-1. 혹은 재정의

public class Snake implements PrintableAnimal, Printer {

    // ...

    @Override
    public void print() {
        // 재정의하여 구현...
    }
}

 

만약 디폴트 메소드와 조상 클래스 메소드 간의 충돌이 일어날 경우

조상 클래스의 메소드가 상속되고 디폴트 메소드는 무시된다.

 

 

과거와 현재의 인터페이스

 

과거 인터페이스의 Default 메소드가 제공되지 않은 자바8 이전의 형태

https://www.notion.so/4b0cf3f6ff7549adb2951e27519fc0e6

  - 인터페이스의 여러가지 메소드 중에서 전체가 아닌 일부의 메소드만 구현해야될때 중간에 추상 클래스를 둔다.

  - 추상 클래스안에는 메소드의 껍데기만 존재하고(구현 X) 각각의 하위 클래스에서 상속받아 필요한 것만 구현.

 

※ 주의할점

위와같은 용도로만 사용할려고 추상클래스를 사용하는것이 아님!! 추상클래스의 용도는 더욱 많음

 

https://www.notion.so/4b0cf3f6ff7549adb2951e27519fc0e6

 

대표적인 예제가 스프링의 HandlerInterceptor

  - 자바8로 바뀌면서 디폴트 메서드 사용

 

※ Default Method를 사용하면 개발자는 상속이라는 큰 무기를 가질 수 있다.

 

 

※추가 지식

구석기 시대의 웹MVC 코딩

스프링 등장이전 서블릿으로 웹 MVC 구현시 위와 같이 구현하였음. 상속이라는 큰 무기를 잃어버렸다.😭

이러한 단점을 보완한게 스트러츠

하지만 후에 스프링이 나옴으로써 DI 등 추가적인 기능이 보완됨으로써 스프링이 웹 MVC시장을 휘어잡았다.

 

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

인터페이스의 static 메서드는 인스턴스 생성과 상관없이 인터페이스 타입으로 호출하는 메소드이다.

인터페이스 내에서 이미 body를 구현한 메소드이다(default 메서드와 동일).

하지만 구현 클래스에서 오버라이딩하여 사용할 수 없다.

 

static method 선언 방식

  - 형태는 클래스의 정적 메소드와 동일하다.

  - 정적 메소드는 public 특성을 가짐(public 키워드는 생략해도 컴파일 과정에서 자동으로 붙는다.)

[public] static 리턴타입 메소드명(매개변수, ...) { ... }

 

static method 예제

  - RemoteControl 인터페이스에 changeBattery() static method 선언

/* RemoteControl 인터페이스에서, 배터리 교환하는 기능을 가진 changeBattery() 정적 메소드를 선언 */
public interface RemoteControl {
		...

    // 정적 메소드
    // 배터리를 교환하는 기능을 가진 changeBattery() 정적 메소드
    static void changeBattery() {
        System.out.println("건전지를 교환합니다.");
    }
  
  	...
}

 

static method의 사용

인터페이스의 정적 메소드는 인터페이스로 바로 호출이 가능하다.

RemoteControl의 changeBattery() 정적 메소드를 호출하는 예제

public class RemoteControlExample {
		public static void main(String[] args) {
				RemoteControl.changeBattery();
		}
}

 

※ 주의할 점

인터페이스 안의 static메서드는 오버라이딩이 되지 않는다. 

왜? 접근하는 영역자체가 다르기 때문이다.

인스턴스로 접근하면 static메서드가 호출되지 않는다.

 

default 메소드를 상속으로 덮으려고 하면(static 으로...) 컴파일 에러가 나온다.

interface Dancable {
    void fly();
//    default void fly(){
//        System.out.println("call from default");
//    }
}

interface Flyable extends Dancable {
    static void fly() {
        System.out.println("call from static");
    }
}
// error : Static method 'fly()' in 'Flyable' cannot override instance method 'fly()' in 'Dancable'

class Print implements Flyable {
}

 

default method는 해당 인터페이스를 구현한 구현체가 모르게 추가된 기능임으로 그만큼 리스크가 따른다.

  - 컴파일 에러는 발생하지 않지만, 특정한 구현체의 로직에 따라 런타임 에러가 발생할 수 있다.

  - 사용하게 된다면, 구현체가 잘못사용하지 않도록 문서화가 필요하다.

 

인터페이스를 상속받은 인터페이스에서 다시 추상 메소드로 변경할 수 있다.

 

함수형 인터페이스

자바 8에서는 함수를 '1급 시민'처럼 다룰 수 있도록, 함수형 인터페이스를 제공

1급 시민이 되면 함수는

  - 변수에 값을 할당할 수 있다.

  - 함수를 파라미터로 넘겨줄 수 있다.,

  - 함수의 반환값이 될 수 있다.

 

함수형 인터페이스는 아래와 같이 사용한다.

@FunctionalInterface
interface Addition {
    int addition(final int num1, final int num2);
}

  - 한개의 추상 메소드만 가져와야 한다.

  - @FunctionalInterface 어노테이션을 붙여야 한다.

이렇게 되면 Addition타입을 가지는 addition이라는 메소드는 이제부터 기존에 썼던 1급 시민처럼 사용할 수 있다.

 

1.변수에 값을 할당

Addition add = (i1, i2) -> i1 + i2; // 함수 형태로 변수에 값을 넣음.
System.out.println(add.addition(1,2)); // 3

2. 함수를 파라미터로 넘겨줌

public static void main(String[] args) {
    test((i1, i2) -> i1 + i2);// 함수가 파라미터로 들어갔습니다.
} 

static void test(Addition addition) {
    System.out.println(addition.addition(1,2));
}

3.함수의 반환값이 될 수 있다.

public static void main(String[] args) {
    Addition add = test();
    System.out.println(add.addition(1,2));
}

static Addition test() {
    return (i1, i2) -> i1 + i2; // 함수가 리턴값에 들어갔습니다.
}

 

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

자바8에서의 default method와 static method는 여전히 불편하다.

왜냐하면 단지 특정 기능을 처리하는 내부 method일뿐인데도, 외부에 공개되는 public method로 만들어야 하기 때문이다.

interface를 구현하는 다른 interface 혹은 class가 해당 method에 액세스 하거나 상속할 수 있는 것을 원하지 않아도, 접근할 수 있는 여지가 있다.

 

java9에서는 위와 같은 사항으로 인해 private methodprivate static method라는 새로운 기능을 제공해준다.

  - 외부에 공개하지 않으면서도 코드의 중복을 피할 수 있다.

    - private 메소드는 오직 해당 인터페이스 내에서만 접근 가능하며, 인터페이스를 상속받은 클래스나 서브 인터페이스에서는 접근할 수 없다.

  - private method를 이용하면 해당 메소드를 인터페이스를 구현하는 클래스에 노출하지 않아도 된다 -> 캡슐화

  

※ 자바9 기준 인터페이스에 사용가능한 멤버

  - 상수

  - abstract 메소드

  - default 메소드

  - static 메소드

  - private 메소드

  - private static 메소드

 

private [static] method 선언 방식

  - private 접근 지시자를 반드시 사용해야 한다.

  - private과 abstract 키워드를 동시에 사용할 수 없다.

    - 동시에 사용시 컴파일 에러

      - private methods

        - private method는 해당 클래스에서만 사용 가능하다. 결국 하위 클래스가 상속할 수 없고, 이 메소드를 재정의할 수 없다는 의미이기 때문에 추상 메소드가 될 수 없다.

        - 즉, 구현이 되어 있어야만 한다.

      - abstract methods

        - 추상 메소드는 구현부가 없는 메소드라는 의미.

        - 즉, 하위 클래스가 상속해서 이 추상 메소드를 구현해야한다.

private [static] 리턴타입 메소드명(매개변수, ...) { ... }

  - private method는 interface 내에서만 사용할 수 있다.

  - private static method는 static & non-static 인터페이스 메소드 안에서 사용할 수 있다.

  - private non-static method는 private static method 내에서 사용할 수 없다.

 

private method 예제

private method를 가지는 CustomInterface 예제

public interface CustomInterface {
  public abstract void method1();
  
  public default void method2() {
    method4(); // default method 내 private method 호출
    method5(); // non-static method 내 private static method 호출
    System.out.println("default method");
  }
  
  public static void method3() {
    method5(); // static method 내 private static method 호출
    System.out.println("static method");
  }
  
  private void method4() {
    System.out.println("private method");
	}
  
  private static void method5() {
    System.out.println("private static method");
	}
}

CustomInterface 를 구현한 CustomClass

public class CustomClass implements CustomInterface {
  @Override
  public void method1() {
    System.out.println("abstract method");
  }
  
  public static void main(String[] args) {
    CustomInterface instance = new CustomClass();
    instance.method1();
    instance.method2();
    CustomInterface.method3();
  }
}

// Output
/*
 * abstract method
 * private method
 * private static method
 * default method
 * private static method
 * static method
 */

private method 실제 예시

두 가지 기능이 있는 계산기 클래스 예제

  1. 입력 받은 숫자들 중 짝수의 합을 구하는 기능 (메소드)

  2. 입력 받은 숫자들 중 홀수의 합을 구하는 기능 (메소드)

 

CustomCalculator 인터페이스

import java.util.function.IntPredicate;
import java.util.stream.IntStream;

public interface CustomCalculator {
  default int addEvenNumbers(int... nums) {
    return add(n -> n % 2 == 0, nums);
	}
  
  default int addOddNumbers(int... nums) {
    return add(n -> n % 2 != 0, nums);
	}
  private int add(IntPredicate predicate, int... nums) {
    return IntStream.of(nums)
		.filters(predicate)
		.sum();
  }
}

CustomCalculator 를 구현한 Main 클래스

public class Main implements CustomCalculator {
  public static void main(String[] args) {
    CustomCalculator demo = new Main();
    
    int sumOfEvens = demo.addEvenNumbers(1,2,3,4,5,6,7,8,9);
    System.out.println(sumOfEvens);
    
    int sumOfOdds = demo.addOddNumbers(1,2,3,4,5,6,7,8,9);
    System.out.println(sumOfOdds);
  }
}

// Output
/*
 * 20
 * 25
 */

 

Constant Interface

Constant Interface는 사용을 추천하지 않는 Anti패턴이다.

Constant Interface는 오직 상수만 정의한 인터페이스이다. 인터페이스에서 변수를 등록할 때 자동으로 public static final이 붙어 상수처럼 어디서든 접근할 수 있다. 또한, 하나의 클래스에 여러 개의 인터페이스를 implement를 할 수 있는데, Constant Interface를 implement 할 경우, 인터페이스의 클래스명을 네임스페이스로 붙이지 않고 바로 사용할 수 있다.

 

사용을 추천하지 않는 이유는 여러가지가 있지만 핵심은 애초에 인터페이스를 정의하는 목적은 상수를 쓰라는 것이 아니라 메서드를 통해 규약을 정의하라고 쓴다.

 

추가안티패턴

상수를 사용하기 위해 인스턴스를 생성하는 것은 좋지 않다.

 

전역적인 상수 사용을 원하면 상수만 모아놓을 클래스를 만드는게 좋다. 또한, private으로 인스턴스 생성을 막는다.

필드의 접근지시자는 유동적으로 정해준다.

 

enum을 써도 괜찮을까요? No!!! 😠

 

public enum Planets {
	EARTH, MARS, JUPITER, SATURN
};

enum은 상수로 쓰는것이 아닌 선택가능한 값들을 고정하기 위해 사용한다.

 

 

위와 같이 사용할 경우 print()메소드의 매개변수인 MyEnum에는 "KEESUN" or "BOOK"만 들어갈 수 있음

 

추상 클래스가 필요할까요?

우선 추상클래스에서 가능한 모든것들을 인터페이스에서 할 수 있을지 생각해봐야함

추상클래스 안에서는 인스턴스 변수 생성 가능, but 인터페이스는 불가능

인터페이스는 private 키워드 사용 불가능, 전부 상수만 가능

 

결론은 많이 인터페이스로 가긴 했지만 아직도 추상클래스의 효용가치가 있다.

상태존재의 여부에 따라 골라서 사용하면 됨

 

※ 추가 지식

인터페이스도 다형성을 구현하는 기술이 사용되었다.

다형성이란?

  - 하나의 타입에 대입되는 객체에 따라서, 실행 결과가 다양한 형태로 나오는 성질

  - 부모 타입에 어떤 자식 객체를 대입하느냐에 따라 실행 결과가 달라지듯이, 인터페이스 타입에 어떤 구현 객체를 대입하느냐에 따라 실행결과가 달라짐

  - 상속과 인터페이스 모두 다형성을 구현하는 기술

  - 상속

    - 같은 종류의 하위 클래스를 만드는 기술

  - 인터페이스

    - 사용 방법이 동일한 클래스를 만드는 기술

 

 

참고

dev-coco.tistory.com/13

https://blog.baesangwoo.dev/posts/java-livestudy-8week/

yadon079.github.io/2021/java%20study%20halle/week-08

www.youtube.com/channel/UCwjaZf1WggZdbczi36bWlBA

github.com/yeGenieee/java-live-study/blob/main/%5B8%5DJava%20Live%20Study.md

ahnyezi.github.io/java/javastudy-8-interface/#3-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EC%9D%98-%EA%B5%AC%ED%98%84

github.com/inhalin/whiteship-live-study/blob/main/week-08.md

 

댓글