[개체지향] 인터페이스 vs 구현
인터페이스 vs 구현
dependency
- 소프트웨어 모듈 A가 제대로 작동하려면 다른 모듈 B가 필요한 경우
- 여기서 모듈은 클래스라 봐도 무방
- 클래스 A가 클래스 B에 의존한다는 의미
종종 듣는 이상한 이야기
클래스간에 의존성이 있으면 잘못된 oo설계의 냄새가 풍긴다.(code smell)
의존성이 있어야 좋은 설계
- 각 클래스의 목적이 뚜렷하다는 의미
- 캡슐화가 잘 되어있다는 의미
- 클래스를 재사용할 수 있다는 의미
- 함수 재사용성과 마찬가지
- 의존성을 오나전히 없애려면 프로그램 전체를 함수 하나에 작성하면 됨.
의존성이 BAD라고 생각하는 이유
- 결합도란 용어와 혼용해서 사용해서
- 그나마 그 용어도 너무 생략해서 잘못 사용
결합도 coupling
- 종종 커플링이라고 그대로 음차해서 사용
- 원래 의미는 두 소프트웨어 모듈간에 상호 의존성 정도
- 클래스 a가 클래스 b에 의존
- 클래스 b도 클래스 a에 의존
- a와 b중 하나도 독자 생존이 불가능
- 여러가지 종류의 결합도가 존재
oo에서 흔히 논하는 결합도
A,B가 의존하는 상황에서 B를 변경할 때 프로그램이 잘 작동하는가?
- A의 내부르 변경 안해도 제대로 동작
- 의존하나 그 정도가 높지 않음 A depends lightly on B
- 결합도가 낮음 loose coupling
- A 내부를 변경해야만 제대로 동작
- A가 B에 의존하는 정도가 높음 (A depends heavily on B)
- 결합도 높음 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를 사용하라는 이야기구나
- 다형성이 없으니 말도 안되는 이야기
증거
- GoF의 디자인 패턴 책에서 사용한 언어는 C++
- C++은 interface를 지원하지않음.
- 다형성이 인터페이스에 의존한다고 명시
- 상속에서 동작의 재사용성만 논하는 건 반쪽짜리 이야기
- 상속을 통해 자식이 따라야 하는 공통 인터페이스를 정의하는 것도 중요
- 그 이유는 다형성이 공통 인터페이스에 의존하기 때문
- 자식 클래스가 상속을 제대로 한 경우는 다음의 일만 했을 때임
- 추상 클래스의 연산을 오버라이딩함
- 추상 클래스에 없는 새로운 연산만 추가함 5. 즉 저자는 인터페이스의 존재 의의가 다형성이라 명시
- 부모의 다형적 메서드의 시그내처란 의미
- 추상 클래스에 있는 인터페이스만 사용하면 이런 장점이 있음
- 클라이언트는 자기가 사용하는 개체의 형이 정확히 뭔지 몰라도 됨
- 클라이언트는 이 개체들을 구현하는 클래스에 대해서 몰라도 됨
- 그 대신 추상 클래스가 강제하는 인터페이스만 따르면 됨
- 추상 클래스에 있는 인터페이스만 사용하면 이런 장점이 있음
인터페이스에 대해 프로그래밍하라는 의미
- 위에 있는 클래스를 사용할수록 결합도가 줄어든다는 의미
함수 == 블랙박스 라는말에다형성을 추가
- 함수레벨
- 함수속에서 이상한 짓하는 코드에 의존해서 호출자 코드를 작성하지 말 것
- 함수의 입력과 출력은 함수 시그내처와 반환형으로 정의
- 클래스레벨
- 특정 자식 클래스만의 메서드에 의존해서 호출자 코드를 작성하는 것을 피할 것
- 부모에서 정의한 다형적 메서드에 의존하면 어떤 자식 개체도 제대로 동작
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) 버전을 가끔 제공
-
추가적인 실용적인 인터페이스 사용법
- 기본적으로 클래스를 사용
- 기존 다음과 같은 경우에만 인터페이스를 사용
- 함수 포인터
- 다형성 있는 다중 상속이 필요한 경우
- 추가 : 변화에 대비할 필요가 있다면 커플링을 줄이려 사용할 것.
- 내 클래스에 의존하는 코드들을 쉽게 바꿀 수 없는 경우
변화에 대한 대비 == 쉽게 바꿀 수 없는 경우에 대한 대비
- 쉽게 바꿀수있으면 인터페이스 사용 실익이 줄어듦
- 시스템이 크지않음
- 내 라이브러리를 사용하는 외부 클라이언트가 많지않음
- 여러 버전을 특정 기간 동안 지원할 수 있음
- 쉽게 바꿀 수 없으면 인터페이스 사용 실익이 커짐