순간을 성실히, 화려함보단 꾸준함을

8주차 과제: 인터페이스 본문

백기선님과 함께 하는 자바 스터디

8주차 과제: 인터페이스

폭발토끼 2021. 6. 22. 00:24

목표

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

학습할 것 (필수)

  1. 인터페이스 정의하는 방법
  2. 인터페이스 구현하는 방법
  3. 인터페이스 레퍼런스를 통해 구현체를 사용하는 방법
  4. 인터페이스 상속
  5. 인터페이스의 기본 메소드 (Default Method), 자바 8
  6. 인터페이스의 static 메소드, 자바 8
  7. 인터페이스의 private 메소드, 자바 9

인터페이스란?
- 클래스들이 동작해야 될 지정하는데 사용되는 추상 자료형이다. 다수의 개발자들이 하나의 프로젝트를 개발한다고 하면 서로 구현해야 될 각 기능들을 인터페이스를 정의하게 되면 내부적으로 어떤 로직을 가지고 있던간에 상관없이 기능을 사용하는데 문제가 발생하지 않게된다.

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

1. 인터페이스 정의하는 방법

interface 키워드를 메소드명 앞에 붙여 사용한다.

public interface Jumpable {
    public void jump();
}

인터페이스를 정의할때 조건

  • 인터페이스는 public(default) 접근제한자만 붙일 수 있다.
  • 모든 필드값은 자동으로 public static final로 취급된다.
  • 모든 메소드는 자동으로 public으로 취급된다.
  • 인터페이스는 추상메소드보다 추상화가 더 높으므로 abstract 키워드를 사용해줄 필요성이 없다.

2. 인터페이스 구현하는 방법

인터페이스는 자신 스스로 인스턴스가 될 수가 없다.
따라서 class에서 implements라는 키워드를 사용하여 반드시 선언된 메소드를 구현해야 한다.
만약 하나라도 메소드를 구현하지 않게 된다면 추상클래스가 되버린다.
또한, 하나의 클래스는 다수의 인터페이스를 implements 할 수 있다.

interface 인터페이스이름{
    //메소드 선언
}

public class 클래스이름 implements 인터페이스이름{
    //메소드 구현
}

만약 인터페이스에 선언된 메소드 중 하나라도 구현하지 않는다면 추상메소드가 되어버림 따라서 abstract 키워드를 붙여야 됨

interface Act{
    void sound();
       void jump();
}

abstract public class Dog implements Act{
    public void sound(){
        System.out.println("멍멍");
    }

    public static void main(String[] args) {
        Dog dog = new Dog();                //Compile Error 발생
        dog.sound();
        dog.jump();
    }
}

그러나 추상메소드는 인스턴스를 생성하여 만들 수가 없다 따라서 에러가 발생하는 것. 이 부분을 주의해야 한다.

3. 레퍼런스를 통해 구현체를 사용하는 방법

우린 전에 다형성이라는 개념을 배우면서 인스턴스를 부모타입의 참조변수로 참조할 수 있다는 것을 배웠다.

인터페이스 또한 인터페이스를 참조변수로 참조할 수 있으며, 인터페이스로 타입을 변환할 수 있다.

interface Act{
    void sound();
    void jump();
}

public class Dog implements Act{
    public void sound(){
        System.out.println("멍멍");
    }
    public void jump(){
        System.out.println("껑충껑충");
    }
    public void walk(){
        System.out.println("성큼성큼");
    }
    public static void main(String[] args) {
        Act dog = new Dog();
        dog.sound();
        dog.jump();

        //dog.walk();       Compile Error 발생
    }
}

보다 시피 부모타입의 참조변수로 선언하고 자식타입의 인스턴스를 가리키도록 해주었다.
그러나 dog.walk() 에서 컴파일 에러가 발생하는데 이는 지난 포스팅에서도 언급했듯이 스택메모리에는 Act 인터페이스에 관한 메소드만 올라오고 컴파일러는 실행할때 walk()라는 메소드를 스택메모리에 올라와 있지 않는 걸 확인하기 때문에 컴파일에러가 발생해버리는 것이다.

4. 인터페이스 상속

  • extends 키워드를 사용해서 인터페이스를 상속할 수 있다.
  • 인터페이스는 클래스 상속과 달리 다중상속이 가능하다.
  • 필드는 디폴트로 static 이다.
interface Act{
    void sound();
}
interface Talk extends Act{
    int count = 10;
}
public class Dog implements Talk{
    public void sound(){
        System.out.println("멍멍");
    }
    public static void main(String[] args) {
        Act dog = new Dog();
        dog.sound();
        //System.out.println(dog.count);            Compile Error
        Talk another_dog = new Dog();
        another_dog.sound();
        System.out.println(another_dog.count);

        //another_dog.count=32;                        Compile Error
    }
}

Talk 인터페이스는 Act 인터페이스를 상속했고 각각 인터페이스에 대해 참조변수를 선언하였다.
눈여겨 볼 것은 count 변수는 static이라고 선언하지 않았음에도 자동으로 static 변수가 된다.
따라서 값을 변경하려고 하면 컴파일 에러가 발생한다

다중상속

인터페이스는 클래스와 달리 다중 상속이 가능하다. 단, 상위 인터페이스에서 메소드명과 파라미터 형식이 같지만 리턴타입이 다른 메소드를 가지고 있는 인터페이스가 존재하면 이는 다중상속이 불가능하다.

어떤 인터페이스를 상속 받느냐에 따라 규칙이 달라지기 때문에 이를 정의할 수가 없다.

interface Act{
    void sound();
}
interface Talk{
    int sound();
}
interface Walk extends Act,Talk{}    //Compile Error
'sound()' in 'com.example.Talk' clashes with 'sound()' in 'com.example.Act'; methods have unrelated return types

에러가 발생하는 것을 볼 수 있다.

두 인터페이스를 상속 받고 있을때 메소드 구현

중복이 되는 메소드를 재정의하여 사용할 수 있다.

interface Act{
    default void sound(){
        System.out.println("Here Act");
    }
}
interface Talk{
    default void sound(){
        System.out.println("Here Talk");
    }
}
public class Dog implements Talk,Act{
    public void sound(){
        Act.super.sound();
        Talk.super.sound();
    }
    public static void main(String[] args) {
        Act dog = new Dog();
        dog.sound();
    }
}
//출력
Here Act
Here Talk

super 키워들르 사용하여 각 인터페이스에 선언된 메소드에 접근할 수 있다.

5. 인터페이스의 기본 메소드 (Default Method)

Default Method는 자바8에서 추가된 기능입니다. 인터페이스에 선언한 메소드를 구현할 수 있게끔 해주는 키워드 이다.
근데 인터페이스는 에초에 추상메소드의 일종으로 메소드의 선언만 가능한게 정의인데 왜 대체 구현할 수 있는 기능을 추가한 것 일까?? 이런 키워드를 사용함으로써 기능들을 추가할거면 인터페이스의 정의와 모순이 생기는데 말이다.

정답은 바로 자바의 탄생과 변화 때문이다. 자바라는 언어는 오래전 탄생했고 지금까지 수많은 변화를 거쳐오며 계속적으로 업데이트 되어 왔다. 자연스럽게 기존 가지고 있던 철학들도 점차 변화를 가져오게 되는 것이다.

이미 많은 프로그램들이 자바 언어로 구현되어 있고 인터페이스에 새로운 메소드를 추가하려고하면 이 메소드를 구현하고 있는 모든 클래스에 메소드를 추가해야 하는 굉장히 효율적이지 못한 작업을 해야했었다.

이를 좀 더 유연하게 해결하기 위해 Default 키워드가 탄생하게 되었던 것.

interface Talk{
    default void sound(){
        System.out.println("Here Talk");
    }
}
public class Dog implements Talk{
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.sound();
    }
}
//출력
Here Talk

사용 방법은 메소드에 default키워드만 추가한 후 참조변수.메소드명 으로 접근해서 사용하면 된다.
defult 메소드는 @Override 가 가능하다.

interface Talk{
    default void sound(){
        System.out.println("Here Talk");
    }
}
public class Dog implements Talk,Act{
    @Override
    public void sound(){
        System.out.println("Here Dog");
    }
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.sound();
    }
}
//출력
Here Dog

6. 인터페이스의 static 메소드

static 메소드 또한 자바8에서 추가된 기능이다. static 또한 선언한 메소드를 구현할 수 있다.
그러나 @Override가 불가능 하다.

interface Act{
    static void sound(){
        System.out.println("Here Act");
    }
}
public class Dog implements Act{
    @Override
    public void sound(){                //Compile Error 
        System.out.println("Here Dog");
    }
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.sound();
    }
}
Method does not override method from its superclass

오버라이딩 하려고 했으나 컴파일에러가 발생하는 것을 확인 할 수 있다. static 메소드는 인스턴스를 생성하여 접근 할 수 없다.

7. 인터페이스의 private 메소드

private 키워드는 자바 9에서 추가된 기능이다.

  • 메소드 body 가 존재하고 abstract 가 아니다
  • static 이거나 non-static 일 수 있다.
  • 오직 해당 인터페이스에서만 사용가능하며 상속을 받거나 서브 인터페이스에서는 사용할 수 없다. 이 때문에 private 키워드와 abstract 키워드를 동시에 사용하면 컴파일 에러가 발생한다.

왜 private 키워드를 사용하는 것일까?

- 해당 인터페이스에서만 사용되는 기능들을 굳이 외부로 노출 시킬 필요성은 없다. 이런 이유로 과거에는 굉장히 복잡하고 긴 메소드명을 사용했지만 너무나도 불편하고 효율적이지 못한 단점을 가지고 있었다. 이 때문에 private키워드로 간단하게 캡슐화를 씌울 수 있게 한 것이다.

interface Act{
    public abstract void sound1();

    public default void sound2(){
        System.out.println("Sound2()");
        sound4();
        sound5();
    }

    public static void sound3(){
        System.out.println("Sound3()");
    }

    private void sound4(){
        System.out.println("Sound4()");
    }
    private static void sound5(){
        System.out.println("Sound5()");
    }
}
public class Dog implements Act{
    @Override
    public void sound1(){
        System.out.println("Sound1()");
    }
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.sound1();
        dog.sound2();
        Act.sound3();
    }
}

+ 상수들의 집합을 사용할때 인터페이스의 맴버변수로 정의하는 경우가 간혹 있다. 그러나 이는 인터페이스의 존재 목적과 부합하지 않는다. 따라서 상수만을 모아 놓은 클래스를 정의하고 이 클래스 인스턴스를 생성하지 못하도록 private으로 생성자를 선언하도록 하면 훨씬 좋은 방법이 된다.

interface Const{
    int NUM=10;
    String NAME="RABBIT";
}
public class Dog implements Const{
    public static void main(String[] args) {
        System.out.println(Const.NAME);
    }
}

이렇게 상수들을 따로 모아놓은 인터페이스를 정의하여 사용하는 경우가 있다. 그러나 이렇게 사용하게 되면 인터페이스를 사용하는 목적이 아니다. 인터페이스는 규약을 정의하도록 사용하는 기능인데 이는 의미에 부합하지 않게 된다.

interface Const{
    int NUM=10;
    String NAME="RABBIT";
}
public class Dog implements Const{
    private static String NAME = "TUTTLE";
    public static void main(String[] args) {
        System.out.println(NAME);
    }
}
//출력
TUTTLE

이렇듯 상수라고 정의한 인터페이스의 변수들이 덮어져 사용이 될 수 없는 경우가 발생할 수도 있기 때문이다.

상수들을 모아놓은 공간이 필요하다면 private을 사용하도록 하자

class Const{
    public static final int NUM = 10;
    public static final String NAME ="RABBIT";

    private Const(){}
}
public class Dog{
    public static void main(String[] args) {
        Const con = new Const();            //Compile Error 발생
    }
}

이렇듯 private키워드를 사용하여 인스턴스를 생성하지 못하게 막아놓으면 덮어 씌워져 사용될 일도 발생하지 않고 상수들을 안전하게 사용 할 수도 있다.

(번외) 왜 추상클래스를 사용하지 않고 굳이 인터페이스를 사용하는 것인가??둘의 차이점은 큰 의미가 있는 것 인가??

인터페이스의 역할이 가면 갈수록 확장되고 있는 것은 분명한 사실이다. 또한, 이로인해 추상클래스의 존재가치도 하락하고 있는 것도 맞는 말이다. 그러나 가치가 없는 것은 아니라는 것을 말하고 싶다.

public abstract class AbstractJoinMember implements JoinMember{
    private String message = "이런 클래스는 그럼 필요가 없는 것 인가?";

    @Override
    public void preJoin(){
        System.out.println(message);
    }
    public void setMessage(String message){
        this.message=message;
    }
}

간단한 메세지를 출력하는 추상클래스이다. 위의 소스는 간단한 소스지만 만약 이런 비슷한 로직을 수행해야 한다면 이를 인터페이스로도 구현이 가능한 부분인가??

답은 아니다. 인터페이스는 private 맴버변수를 선언할 수 없기 때문이다.

자바8 이하 버전에서는 private 메소드를 사용할 수 없기 때문에 중복된 코드들이 발생하는 경우도 많다. 이를 굳이 default 키워드를 사용하기 보다는 추상클래스를 생성하여 priavte으로 중복코드를 제거하는 것도 한가지 방법이 될 수도 있는 것이다.

결론 : 많이 인터페이스로 가긴 했지만 아직도 추상 클래스의 가치는 존재한다.

(번외) 인터페이스가 너무 많아서 난잡해보여요,,,,

많은 자바코드들을 접하는 사람들이 간간히 느끼는 감정은 너무나도 많은 인터페이스들이 굳이 왜 존재해야 하는지? 또한, 너무나도 코드를 읽기가 힘들다 라는 반응이다.

근본적으로 인터페이스를 왜 우리는 사용하는가?

  1. 객체를 외부 관점에서 단순화 시키는 것
  2. 비슷한 객체를 군집화 시키는 것
  • 단순화란 : 사용자가 객체내부로직이나 맴버변수를 신경 안쓰도록 하는 것
  • 군집화란 : 비슷한 객체의 사용을 동이랗게 취급해 구현노력을 절감 시키는 것

이러한 목적에서 인터페이스 사용을 생각해 본다면

  1. 내가 만든 모듈이 나를 모르는 누군가가 사용하는 것인지?
  2. 현재의 상황을 모델링한 객체가 비슷한 패턴을 보이는지

즉, 외부에 컴포넌트를 제공하지 않거나 반복되는 업무가 아닐 경우는 굳이 인터페이스를 사용할 필요가 없다는 뜻이다.

그럼 왜 이런 쓸데없는 인터페이스들이 넘쳐나는가?를 생각해 보면

  1. 디자인 패턴의 지나친 일반화
  2. 인터페이스가 전혀 필요 없는데 필요할 것이라는 예측
  3. 리팩토링 없는 개발 습관

이런 요인들을 잘 생각해보며 계속 곱씹으면서 개선해 나아가는 자세가 필요하다.