인터페이스 vs 구현

dependency

  • 소프트웨어 모듈 A가 제대로 작동하려면 다른 모듈 B가 필요한 경우
  • 여기서 모듈은 클래스라 봐도 무방
  • 클래스 A가 클래스 B에 의존한다는 의미

종종 듣는 이상한 이야기

클래스간에 의존성이 있으면 잘못된 oo설계의 냄새가 풍긴다.(code smell)

의존성이 있어야 좋은 설계

  • 각 클래스의 목적이 뚜렷하다는 의미
  • 캡슐화가 잘 되어있다는 의미
  • 클래스를 재사용할 수 있다는 의미
    • 함수 재사용성과 마찬가지
    • 의존성을 오나전히 없애려면 프로그램 전체를 함수 하나에 작성하면 됨.

의존성이 BAD라고 생각하는 이유

  1. 결합도란 용어와 혼용해서 사용해서
  2. 그나마 그 용어도 너무 생략해서 잘못 사용

결합도 coupling

  • 종종 커플링이라고 그대로 음차해서 사용
  • 원래 의미는 두 소프트웨어 모듈간에 상호 의존성 정도
    • 클래스 a가 클래스 b에 의존
    • 클래스 b도 클래스 a에 의존
    • a와 b중 하나도 독자 생존이 불가능
  • 여러가지 종류의 결합도가 존재

oo에서 흔히 논하는 결합도

A,B가 의존하는 상황에서 B를 변경할 때 프로그램이 잘 작동하는가?

  1. A의 내부르 변경 안해도 제대로 동작
    1. 의존하나 그 정도가 높지 않음 A depends lightly on B
    2. 결합도가 낮음 loose coupling
  2. A 내부를 변경해야만 제대로 동작
    1. A가 B에 의존하는 정도가 높음 (A depends heavily on B)
    2. 결합도 높음 tight coupling

한 줄 정리 : B 코드 변경 시, A 코드도 변경해야 하면 결합도가 높은것

높은 결합도를 의미하는 몇 가지 표현

잘못됬지만 사용하는 표현

  • A and B are coupled
  • A depends on B
  • There is a dependency between A and B

올바른 표현

  • A depends heavily on B
  • A and B are tightly coupled

  • 높은 겷합도는 나쁘다 : 동의 가능
  • 의존성이 있어서 나쁘다 : X

낮은 결합도를 의미하는 몇가지 표현

잘못됐지만 사용하는 표현

  • A and B are decoupled

  • A and B are loosely coupled
  • A depends lightly on B

결합도를 줄이는 것을 의미하는 몇 가지 표현

엄밀히 말하면 틀린 표현

  • Decouple A and B
  • Break dependencies between A and B

올바른 표현

  • Reduce coupling between A and B

결합도 줄이는 법.,

미리 Head 개체를 만들어서 전달

public final class Robot{
  private int hp;
  private Head head;
  ...
  public Robot(int initinalHp, Head head){
    this.hp = initialHp;
    this.head = head;
  }
  ...
}

의존성 주입 dependency injection DI

  • 생성자를 통해서 주입 생성자 주입
  • setter 주입 이라는 방식도있음

DI 가 다른 것은 의미하기도 함.

  • 의존성 주입 컨테이너 DI container
  • 의존성 역전 dependency inversion

DI를 통해 얻은 것과 잃은 것

얻은 것

  • 결합도를 낮춤
  • Head의 생성자가 바뀌어도 Robot을 바꿀 필요가 없음
  • Head가 바뀌면 이 클래스만 따로 컴파일해서 배포가능

잃은것

  • 편의성
  • 프로그래머의 원래 의도를 잘 보여주는 클래스
    • 분리/합체 로봇이 아님

상속관계에서

  • 컴포넌트로 부모클래스를 가진다면 의존성이 낮아짐
  • 인터페이스도 마찬가지

디커플링이 적합한 곳

단순한 구조에서는 실익이 크지 않음

  • 방금 본 예는 간단한 예
  • 뭔가 바뀌더라도 한두 군데 고치면 끝
  • 컴파일러가 일찍 문제를 잡아주니 큰 문제없음
  • 변경이 불가능한 상황이 아니라면 굳이 커플링을 줄일 실익이 미미함

복잡한 시스템에서 문제

  • SimpleHead를 5000개 클래스에서 상속해서 사용
  • 만약 전부 다 SmartHead를 상속하게 바꾸려면?
  • 다시 원상복구 해야한다면?

함수포인터도 디커플링의 좋은 예

디커플링은 유연성/재사용성을 높임

  • 추상화는 유연성 재사용성을 높인다고 했음
  • 디커플링도 유연성 재사용성을 높임
  • 그로인해 단점도 있음

단점 1. 직관적이지 못하다.

  • Robot에서 Head를 받는데 얘가 SmartHead가 들어오는지 잘모르겠음
  • Robot.java에서 알 수 없음
  • 즉 필요한 정보를 한 번에 못 찾음

시도 1 호출자를 찾는다.

  • Robot 생성자에 SimpleHead를 전달
  • Robot 생성자에 SmartHead를 전달.
  • 라이브러리를 사용하는 모든 프로그램의 코드가 들어있는 경우
    • 프로그램 A는 SimpleHead만 사용
    • 프로그램 B는 SmartHead만 사용

엄밀하게 다형성을 잘못사용하고 있는 예 실행파일 하나에 실제 사용하고있는 구현체 하나 -> 다형성으로 구현하면 안됨 실행파일 하나에 그안에 여러개의 개체가있을때 다형성을 사용해야함. 실행파일마다 달라지는 경우 라이브러리를 공유하려는 경우 컴파일러 플래그를 바꾸는,컴파일 스위치를 바꿔서 해당 구체 클래스만 쓰도록 해야함.

시도 2 한 프로그램에서만 검색한다.

  • 소스 코드에 new SimpleHead()나 new SmartHead()가 아예 없음.
  • DI 컨테이너 이용해서 실제 개체 생성에 사용하는 클래스는 코드에 없고 텍스트 파일로 관리하는 경우

시도3 실행 중에 확인

  • 로봇 생성하는 단계까지 가려면 프로그램은 2시간 실행하는 경우

단점 2 내부를 알아야 좋은 경우도 잇다.

import java.util.Collection;

public final class DataSource{
  public void MergeTo(Collection<Data> dataset){
    //소스로부터 모든 데이터를 얻어와 중복 없이 dataset에 넣는다.
  }
}
  • dataset의 실제 클래스에 따라 상당한 차이
    • Set : add 메서드만 호출하면 끝
    • ArrayList : contains로 중복 검사 중복이 아닌경우 add
    • 정렬된 ArrayList : 이진탐색으로 빠른 중복검사, 아닌경우 add
  • 모든 경우에 다 작동하게 만들려면 가장 느리고 일반적인 방법을 선택
  • contains로 중복 검사, 아닌경우에 add
  • 구체적인 컬렉션 클래스를 사용하면 최적화 가능
  • 성능이 중요한 경우에 일반화/추상화가 비효율적일 수 있음

모든건 인터페이스여야 한다는 주장

  • 다른 클래스에서 다른클래스에 속한 개체를 직접 호출 하면 안된다.
  • 그대신 모든걸 인터페이스로 만들어라.
  • oo에서 디커플링이 언제나 제일 중요하기 때문
public final class Head implements IHead
{
    ...
  public IRobot pickEnemy(){
    IRobot robot;
    ...
    return robot;
  }
  ...
}
public final class Robot implements IRobot{
    ...
    private IHead head;
    ...
    public Robot(int initialHp, IHead head){
        ...
    }
}

인터페이스가 생기나 다형적인 인터페이스는 아님.

  • 혹시라도 다른구현이 생길수도있으니~
  • 대부분의 경우에 미래에 대한 대비가 잘 안되어있는 경우임, 소프트웨어에 모든 경우를 대비하려하는건 사람이 생각하는 생각과 많이 떨어져있음
  • 인터페이스 : 구현 = 1 : 1은 이상한 일
  • 다형성이 필요 없는데도 클래스마다 인터페이스를 만드는 꼴

OO에서 인터페이스: 우리의 정의

  • 주류 언어의 문법을 따름
  • 상태도 메서드 구현도 없는 순수 추상 클래스
  • public 메서드 시그내처만 모아 놓은것
  • C 언어의 헤더 파일 같은 존재

OO에서 인터페이스: 주용도에 따른 이해

  • 함수포인터처럼 사용할 수 있는 것
  • 다중 상속을 흉내 낼 수 있는 방법
  • 변화에 대비해 결합도를 낮추는 것(다형성이 필요할 때)
  • 다형성 없는 인터페이스는 없다라는 주장이 OO에서 일반적

OO에서 조차 다양한 인터페이스의 정의

  • 프로그래밍 언어마다 인터페이스라는 용어 및 키워드를 다르게 사용
  • 개체가 이해하는 명령을 나열한 것이라는 개념적 정의도 있음
  • 그 자체로는 우리의 정의와 크게 다르지 않은 괜찮은 정의
  • 다형성을 뺀 채 이 개념만 받아들이면 좀 이상해지기도 함
  • 개체에 내릴 수 있는 명령만 중시하는 진영 중 특히 극단적인 의견
    • 인터페이스는 명령을 나열한 것이므로 oo의 기본
    • OO의 본질은 사람처럼 이해하는 게 아니라 커플링을 줄이는 것
    • 모든 것에 인터페이스를 사용할 것.

왜 인터페이스만 중요시 하는가?

  • 이 진영에서 성서처럼 받드는 책
  • GoF의 디자인 패턴
  • 책 내용 자체는 괜찮음
  • 자꾸 곡해하는 사람이 문제
  • Programming to an Interrface, not an Implementation
  • 이렇게 잘못 생각함 : 무조건 자바의 interface를 사용하라는 이야기구나
  • 다형성이 없으니 말도 안되는 이야기

증거 

  1. GoF의 디자인 패턴 책에서 사용한 언어는 C++
    1. C++은 interface를 지원하지않음.  
  2. 다형성이 인터페이스에 의존한다고 명시
    1. 상속에서 동작의 재사용성만 논하는 건 반쪽짜리 이야기
    2. 상속을 통해 자식이 따라야 하는 공통 인터페이스를 정의하는 것도 중요
    3. 그 이유는 다형성이 공통 인터페이스에 의존하기 때문
    4. 자식 클래스가 상속을 제대로 한 경우는 다음의 일만 했을 때임
    5. 추상 클래스의 연산을 오버라이딩함
    6. 추상 클래스에 없는 새로운 연산만 추가함    5. 즉 저자는 인터페이스의 존재 의의가 다형성이라 명시
  3. 부모의 다형적 메서드의 시그내처란 의미
    1. 추상 클래스에 있는 인터페이스만 사용하면 이런 장점이 있음
      1. 클라이언트는 자기가 사용하는 개체의 형이 정확히 뭔지 몰라도 됨
      2. 클라이언트는 이 개체들을 구현하는 클래스에 대해서 몰라도 됨
      3. 그 대신 추상 클래스가 강제하는 인터페이스만 따르면 됨

인터페이스에 대해 프로그래밍하라는 의미

  • 위에 있는 클래스를 사용할수록 결합도가 줄어든다는 의미

함수 == 블랙박스 라는말에다형성을 추가

  • 함수레벨 
    • 함수속에서 이상한 짓하는 코드에 의존해서 호출자 코드를 작성하지 말 것
    • 함수의 입력과 출력은 함수 시그내처와 반환형으로 정의
  • 클래스레벨
    • 특정 자식 클래스만의 메서드에 의존해서 호출자 코드를 작성하는 것을 피할 것
    • 부모에서 정의한 다형적 메서드에 의존하면 어떤 자식 개체도 제대로 동작

Programming to interface

  • 다형성을 가진 일반적 메서드 시그내처를 정의하는게 핵심
  • java의 interface 키워드가 아님

디커플링이 언제나 제일 중요하다는 주장

  • 다소 의심스러운 행적을 보여온 분의 오래된 주장
  • OO의 본질은 사람처럼 생각하는 방법이 아니라고 함
    • OO 초창기에 홍보를 위해 사용한 마케팅 문구고 거짓이라 함
    • OO의 가장 중요한 본질은 디커플링을 통한 의존성 감소라고 함
    • 무조건 인터페이스를 사용하라 함
  • 이미 오랜 시간이 흘러 대충 틀린 말이라는 결론이 난 사항

협업 시 가장 중요한 목표는 실수 예방

  • 소프트웨어 개발은 협업 환경
  • 모두가 직관적으로 이해할 수 있는 방법은 실수를 줄임
    • 주관성이 그나마 적음
    • 많은 사람들이 직관적으로 이해할 수 있는것이 OO의 캡슐화
  • 다형성,포인터, 재귀함수 등이 이보다 이해하기 어려운 이유
    • 한단계 건너뛰어서(indirection) 생각해야하기 때문
    • 직관성이 줄어듦
    • 추상적
  • 기본적으로 추상화를 안 하는게 실수가 적다.

일단 주관성이 적은 방향으로 이해하자

  • 그냥 주류 언어와 주 용도에 따라 interface를 이해할 것.
    • 상태도 구현도 없이 메서드 시그내처만 있는 추상 클래스
    • 함수포인터처럼, 다중 상속 대신 사용 가능
  • 이게 가장 기본이고 그나마 객관성이 높은 정의

이클립스 API

  • java IDE
  • 이클립스 API를 이용한 수 많은 도구들이 존재
  • API 버전이 바뀔 때마다 메서드 시그내처가 바뀐다면?
  • 수많은 도구들이 고장남
  • 변화를 최대한 줄이는 쪽으로 해야함
  • 설꼐에 충분한 시간을 투자
    • 나중에 바뀌는 일을 방지하기 위해 미리 꼼곰히 설계함
    • 그러나 전혀 바뀌지 않는 API는 발전도 없음
    • 다음 두 java 키워드에 의미를 부여하고 약속
      • interface : 절대로 안바뀌는것
      • class 언제든 바뀔 수 있음

그럼에도 interface도 발전시켜야했음

  • 아무리 설계를 미리 잘해놔도 모든 미래를 예측할 수는 없었음
  • 기존 인터페이스에 없는 동작을 추가할 일이 생김
  • 이때 기존의 인터페이스에 메서드 시그내처를 추가하면
  • 기존의 모든것을 뽀개게됨
  • 따라서 그전 버전과 호환되는 새로운 인터페이스를 만듦 
    • IWorkbenchPrat
    • IWorkbenchPrat2
  • 나중에 IWorkbenchPrat2기능이 필요할시 기존 인터페이스를 바꾸고 요한기능만 추가해서 구현
  • 새로운 사용자에게는 깔끔하지않고 나쁜 변수명이 단점

이클립스 API의 원칙

장점

  • interface와 class의 의미를 확실히 분리
  • 기존코드를 망가뜨리지않음

단점

  • 인터페이스가 깔끔하게 유지 안됨
  • 변화가 느림

이런 프로젝트는 흔하지 않음

  • 비용의 문제
  • 이 정도로 범용적인 라이브러리도 많지 않음

현재에도 의미는 있음

  • 많은 사람들이 의존하는 라이브러리 제작시 충분히 고려해볼 만한 부분
  • 특히 클라이언트가 회사 내부 팀이 아닐때
  • 허나 요즘은 발전 속도를 더 중시하는 경향이 있음   

    실무에서 조금 더 흔한 예

  • 회사 자체 개발팀 소유
  • 프로그래머 수만 100+ 명
  • 10개의 제품팀
  • 다른 팀 하나는 코어 팀
    • 회사내의 핵심 라이브러리 만듦
    • 회사내부에서만 사용

예2

  • 라이브러리를 고칠 때마다 클래스 혹은 인터페이스의 public 메서드 시그내처가 바뀜

이때 인터페이스와 커플링 하면 조금 더나음

  • 구체 클래스하고의 커플링을 제거하면 새로운 커플링이 생김
  •   추상 클래스 또는 인터페이스와의 커플링
  •   추상 클래스, 인터페이스의 시그내처가 바뀌면 똑같은 문제
  •   추상적인 클래스일수록 안 바뀔 가능성이 많음
    •   구체클래스를 사용할 때는 그 속에 들어있는 데이터에 의존하는 경우가 많음
    • 추상적인 클래스를 사용할수록 안 그럴 가능성이 많음
    • 단 많은 구체 클래스로부터 추상화 한 인터페이스일수록 잘 안 변함

핵심은 클라이언트와의 약속

  • 무엇을 변경하고 무엇을 안 변경할지 약속
  • 약속을 깨야 할 일이 생기면 다른 팀과 일정 조율
  • interface에 의미를 부여하는 것도 약속 중 하나

요즘 사람들은 breaking 업데이트에 익숙

  • 요즘은 버전 업 속도가 빨라짐
  • 업데이트 후 오작동을 고치는 건 클라이언트 몫
  • 고통을 줄이기 위한 방법들
    • 라이브러리 제작자는 마이그레이션(이주) 가이드를 제공
    • 지원기간이 긴 LTS(long term support) 버전을 가끔 제공
  • 추가적인 실용적인 인터페이스 사용법

  • 기본적으로 클래스를 사용
  • 기존 다음과 같은 경우에만 인터페이스를 사용
    • 함수 포인터
    • 다형성 있는 다중 상속이 필요한 경우
  • 추가 : 변화에 대비할 필요가 있다면 커플링을 줄이려 사용할 것.
    • 내 클래스에 의존하는 코드들을 쉽게 바꿀 수 없는 경우

변화에 대한 대비 == 쉽게 바꿀 수 없는 경우에 대한 대비

  • 쉽게 바꿀수있으면 인터페이스 사용 실익이 줄어듦
    • 시스템이 크지않음
    • 내 라이브러리를 사용하는 외부 클라이언트가 많지않음
    • 여러 버전을 특정 기간 동안 지원할 수 있음
  • 쉽게 바꿀 수 없으면 인터페이스 사용 실익이 커짐