Justin의 개발 로그
article thumbnail

인터페이스 분리 원칙은 클라이언트가 자신이 이용하지 않는 메서드에는 의존하지(depend on) 않아야 한다는 원칙이다. 인터페이스를 구현(implement)하거나, 클래스를 상속(extends)했는데, 사용하지 않거나 구현할 필요가 없는 메소드가 있다면 분리를 해줘야 한다는 원칙이다. 

인터페이스 분리 원칙은 큰 덩어리의 인터페이스들을 구체적이고 작은 단위들로 분리시킴으로써 클라이언트들이 꼭 필요한 메서드들만 이용(구현)할 수 있게 한다. 이와 같은 작은 단위들을 역할 인터페이스라고도 부른다. 인터페이스 분리 원칙을 통해 시스템의 내부 의존성을 약화시켜 리팩토링, 수정, 재배포를 쉽게 할 수 있다.

 

 

 

인터페이스 (Interface)

  • 클래스 사용의 권고사항을 제시하는 역할을 하는 특수형태의 클래스
  • 아무런 구현이 되어 있지 않으며, 모든 메소드가 추상 메소드이다. (Java 1.8에서 변경됨)
  • be able to ~ , ~할수있는에 해당하는 기능을 부여하며 ~able로 네이밍하는경우가 많다.

인터페이스의 특징

  • class 가 아닌 interface 키워드를 사용한다.
    • public과 default 접근제어자 사용가능하다.

  • 멤버 변수는 항상 "public static final"이다.
    • 인터페이에는 클래스 멤버 변수(static) 밖에 생성하지 못한다.
    • 인터페이스는 객체를 생성하지 않기 때문에 객체 멤버 변수가 없다.
      • 객체를 생성할 수 없기 때문에 생성자도 없다.
    • "public static final" 키워드는 생략 가능 하다.
  • 멤버 메소드도 항상 "public abstract"이다.  (Java 1.8에서 default 메소드 허용됨)
    • 정의상으로 모든 메서드가 추상메서드인 클래스이기 때문에 어찌며보면 당연한 이야기
    • 오버라이딩을 위한 메서드이기 때문에 private 메서드는 의미가 없다
    • "public abstract"는 생략 가능하다
  • 정적 메소드도 선언이 가능하다.
    • 정적 메소드는 추상메서드가 될 수 없다. (static abstract X)
      • 정적 메소드는 구현부가 있어야 한다.
    • 정적 메소드는 오버라이딩 할 수 없다.
public interface Idoable {
  public static void staticDo() {
    // do something;
  }
  
  void do();  // public abstract 생략 가능
}

public class DoSomething implements IDoable {

  @override 
  public void do() {
    //do something2;
  }
}

public class Test {
  public static void main(String[] args) {
    Idoable.staticDo();	//인터페이스의 static 메소드 호출 방법
    
    Idoable iDo = new DoSomething();
    iDo.do();
  }
}
  • 인터페이스의 상속은 implements 키워드 사용한다.
  • 클래스는 하나만 상속할 수 있으나, 인터페이스는 여러 개를 구현 가능하다.
    • extends 에는 하나의 클래스만 적을 수 있지만, 인터페이스는 implements 하고 여러 개의 인터페이스를 적을 수 있음
      • 이때 구상클래스(구현 클래스)에서는 인터페이스에 정의된 메소드를 모두 구현하여야 한다.
    • 인터페이스를 이용하면 다중 상속을 한 것 처럼 사용할 수 있다.
  • 인터페이스명을 자료형으로 하여 구현체의 객체를 참조할 수 있다.
    • 상속관계의 경우와는 다르게 인터페이스의 객체는 생성되지 않는다.    new Interface(); // 불가
    • 그러나 메서드는 오버라이딩 하기 때문에 가상 메서드 호출은 일어난다.
    • 이를 통하여 다형성을 구현할 수 있다.
      • 오버라이딩 overriding : 하위 클래스에서 상위 클래스의 메소드를 재정의 하는 것
      • 오버로딩 overloading : 이름은 동일하지만 매개변수가 다른 경우
  • 추상메서드가 없다면 구현체에서 메서드 구현을 안해도 된다.
    • 구현해야 할 메서드가 하나도 정의되지 않은 경우
    • default 메소드만 존재하는 경우
  • 인터페이스는 객체를 생성할 수 없다.
interface IFoo{
    public static final int MEMBER_VAR = 10; // (상수는 대문자와 '_'조합)
                                            // public static final 생략 가능

    public abstract void methodA(int param); // public abstract 생략가능
    void methodB(int param); // public abstract
}

class Foo implements IFoo{

    @Override
    public void methodA(int param) {
        System.out.println(param);
    }

    @Override
    public void methodB(int param) {
        System.out.println(param);
    }
}
  • 인터페이스의 이름의 예시
    • (1)interface Ifoo < class Foo
      • 상속하는 자식에서 부모쪽으로 화살표 한다
    • (2)interface Printable <- class Bar
    • (1)과 같이 인터페이스명에 대문자 I를 붙여서 구분한다.
      • 혹은 (2)와 같이 형용사로 표현하기 좋은 이름(~able)은 I를 생략한다.

인터페이스 간의 상속

  • 인터페이스로 인터페이스를 상속할 경우, extends를 사용한다.
    • 인터페이스를 구현 할때만 implements를 사용한다.
    • 클래스 <- 클래스 상속과 달리 다중 상속 가능하다.
  • 인터페이스의 상속으로 다형성을 구현할 수 있다.
  • 인터페이스를 상속한 인터페이스를 구현할 때는 상속받은 모든 부모 인터페이스의 메서드를 구현해야한다.
  
interface Walkable{
    void walk();
}

interface Runnable{
    void run();
}

interface Jumpable extends Walkable, Runnable{
    void jump();// walk(), run()도 포함되어 있을 것
}

class Jumper implements Jumpable{

    @Override
    public void walk() {
        System.out.println("walk");
    }

    @Override
    public void run() {
        System.out.println("run");
    }

    @Override
    public void jump() {
        System.out.println("jump");
    }
}
  • 이기능으로 인터페이스의 다형성 구현할 수 있을 것 같다.
    • 만약 구현체에서 3개의 인터페이스를 따로 implements했다면 각각의 인터페이스로 자녀객체 생성해봤자 각자의 영역만 사용가능하다
    • 하지만 이렇게 하나의 인터페이스로 모두 상속받아 대표 인터페이스를 구현한다면 대표 인터페이스로 다형성을 구현할 수 있겠다!!유레카!

 

 

JDK 1.8 이후의 인터페이스

  • JDK 1.8 이후 인터페이스에서는 default 메소드를 구현할 수 있다.
  • default 메서드는 모든 하위 클래스에 필수이며, 중복되는 메서드로 중복 구현을 방지하는 역할을 한다.
    • 인터페이스의 철학(정의)과는 맞지 않으나, 모든 자식 메서드가 동일하게 구현되어야 하는 메소드가 생긴 경우 쉽게 기능을 추가하기위해 만들어졌다.
  • default 메서드 특징
    • default 메소드는 항상 public이다.
      • default는 접근제어자의 default가 아니고 인터페이스의 default 메서드를 나타낸다.
      • 인터페이스를 구현하는 모든 클래스에서 사용 될 메서드이기 때문에 private은 의미가 없다.
    • default 메소드는 구현체에서는 반드시 구현할 필요는 없다.
      • abstract 메서드의 경우 구현체에서 반드시 구현하여야 하지만 default 메서드의 경우 구현하지 않아도 된다.
      • 자녀 클래스에서 구현하지 않은 default 메서드에 접근시 부모클래스의 default 메서드에 접근된다.
interface IFooTwo {
    void abstractMethod();
    default void defaultMethod(){ // 디폴트 메소드, 구현 클래스에서 오버라이드 가능
        System.out.println("Default method"); // 구현을 해주어야함
    }
}


class FooTwo implements IFooTwo{

    @Override
    public void abstractMethod() {

    }
    @Override // Overriding not necessary
    public void defaultMethod(){
        System.out.println("Overriden default method");
    }
}

class SuperFoo{
   public void defaultMethod(){
        System.out.println("Default method in Super class");
    }
}

class FooThree extends SuperFoo implements IFooTwo{

}

// 인터페이스의 static 메소드
interface IBar {
    static void staticMethod(){
        System.out.println("static method"); //바로 구현 가능
    }
}

 

  • 상속받은 클래스와 구현한 인터페이스의 모두 default method가 있다면 상속받은 부모클래스의 default method가 실행된다.
    • 이때 인터페이스의 default method는 실행되지 않는다.
    • 부모와 인터페이스에 모두 동일한 메서드가 있는 경우 부모클래스에 있는 메소드를 실행한다.
    • 그렇다면 두 개 이상의 인터페이스에 동일한 default method가 있다면 구현 클래스에서 default method는 뭐가 호출될까???
  • 인터페이스의 정적 메서드가 있는경우
    • 인터페이스 명으로 호출 가능 ex ) IBar.staticMethod();
    • 구현한 클래스명으로 호출 불가능 ex) // Bar.staticMethod();
      • 상속에서는 가능하지만 인터페이스에서는 불가능 하다.

인터페이스와 추상메서드의 차이점

  • 추상메서드는 멤버 변수가 있기 때문에 자녀 클래스들에 공통 속성을 설정할 수 있다. 또한 일반메서드와 추상메서드로 공통의 기능도 구현할 수 있다. 그래서 추상메서드는 어떤 개념에 상위 개념으로 세분화 되는 하위 클래스들을 대표하는 클래스로 사용할 수 있다.
    • 대표적인 예로 Animal 이란 큰 대분류안에 많은 종류의 Animal 을 상속하는 동물클래스들을 작성할 수 있으며 그들의 공통 속성인 name, lifespan, color 것을 공통적으로 상속해 줄 수 있고 또한 공통 기능인 walk(),eat()등의 기능들도 상속해 줄 수 있다.
    • 추상메서드의 역할을 is kind of(어떤 종류의) 라고 하나보다..
  • 반면에 인터페이스는 추상메서드 밖에 없지만 다중 상속이 가능하기 때문에 세분화된 기능을 나타내는데 활용될 수 있다.
    • 예를 들면 Animal의 하위클래스들 중에 세분화 기능이라고 볼 수 있는 flyable, ridable 등을 표시해줄 수 있으며 동물별로 여러개의 세분화 된 기능들을 추가하는데 사용할 수 있다
    • 그래서 인터페이스의 역할을 able to(~를 할 수 있는)라고 하나보다..
  • 부모 속성(또는 공통)의 멤버 변수가 반드시 필요한 경우라면 추상 클래스를 이용하고, 처리할 기능(able to)을 하나의 그룹으로 모아야 하는 경우에는 interface로 선언한다.
  • Iterator 인터페이스 실제 예
public interface Iterator<E> {
    boolean hasNext();

    E next();

    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
}
// 직접 구현하는 방법
import java.util.Iterator;
public class PancakeHouseMenu implements Iterator<MenuItem> {
  MenuItem[] items;
  int position = 0;
  
  public PancakeHouseMenu() {
    items = new MenuItem[3];
    
    MenuItem bread = new MenuItem("Bread");
    MenuItem soup = new MenuItem("soup");
    MenuItem juice = new MenuItem("juice");
    
    items[0] = bread;
    items[1] = soup;
    items[2] = juice;
  }

  public boolean hasNext() {
    if(position >= menuItems.length || items[position] == null)
      return false;
    else
      return true;
  }
  
  public MenuItem next() {
    MenuItem item = items[position];
    position++;
    return menuItem;
  }
  
  //not necessary
  //public void remove() {}
}


// 간단한 방법
public class PancakeHouseMenu {

  List<MenuItem> menuItems;
  
  public PancakeHouseMenu() {
    menuItems = new ArrayList<MenuItem>();
    
    MenuItem bread = new MenuItem("Bread");
    MenuItem soup = new MenuItem("soup");
    MenuItem juice = new MenuItem("juice");
    
    menuItems.add(bread);
    menuItems.add(soup);
    menuItems.add(juice);
  }

  public Iterator<MenuItem> createIterator() {
    return menuItems.iterator();
  }
}
//java.util.ArrayList 발췌

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
... 생략 ...

    transient Object[] elementData;  //transient : serialize 제외할 멤버변수 선언

    public Iterator<E> iterator() {
        return new Itr();
    }

    /**
     * An optimized version of AbstractList.Itr
     */
    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        // prevent creating a synthetic constructor
        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
...
}

 

 

인터페이스 분리 원칙에 위배되는 케이스

interface Machine {
  int showCurrentTime();
  int playMP3(string path);
  int calcucateAdd(int a, int b);
}

class MP3Player implements Machine {
  MP3Player() {}

  @override
  int showCurrentTime() {
    System.out.print(currentTime());
  }
  
  @override
  int playMP3(string path) {
    System.out.print("Play : " + path);
  }
  
  @override
  int calculateAdd(int a, int b) {
    throw new Exception("Not Support - I'm not a calculator");
  }
}

class Calculator implements Machine {
... 생략 ...

  @override
  int playMP3(string path) {
    throw new Exception("Not Support - I'm not a MP3Player");
  }
}
  • 각 구현 머신(MP3Player, 계산기 등)에 불필요한 메소드까지 구현해야 함
  • Interface에 메소드가 추가되면 연관없는 구현 클래스까지 수정해야 함
  • 인터페이스로 객체를 생성했을 때 메소드가 반드시 동작한다는 보장을 못함

 

ISP(Interface Segregation Principle) 준수하도록 개선

interface Playable {
  void play(string path);
}

interface IMachineTime {
  int showCurrentTime();
}

interface calculable {
  int calcualteAdd(int a, int b);
}

class Machine implements IMachineTime {
  string name;
  
  Machine(String name) {
    this.name = name;
  }
  
  @override 
  public int showCurrentTime() {
    return System.currentTimeMillis();
  }
}
  
class MP3Player extends Machine implements Playable {
  MP3Player(String name) {
    super(name);
  }
  
  @override
  public void play(string path) {
    System.out.println("Play : " + path);
  }
}
  • 불필요한 메서드 구현이 필요없음
  • 소리를 내는 기계(MP3Player, Piano, Radio)는 playerable 을 구현하면 되고, 계산기는 Calculable 을 구현하면 됨

 

profile

Justin의 개발 로그

@라이프노트

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!