의존성과 결합도에 대한 정확한 의미를 이해해본다.
의존성(Dependency)
- A 모듈이 동작하려면 B 모듈이 필요한 경우
- OO에서 모듈 == 클래스
- 클래스 A가 클래스 B에 의존
의존성이 있으면 잘못된 OO 설계다? (code smell)
- 잘못된 말이다.
- 의존성이 있어야 좋은 설계
- 각 클래스의 목적이 뚜렷하다는 말
- 캡슐화가 잘 되어 있다는 말
- 클래스 재사용이 가능하다는 말
- 의존성을 완전히 없애려면 프로그램 전체를 함수하나에 작성하면 됨
왜 의존성이 나쁘다고 생각할까?
- 결합도라는 용어와 혼용해서 사용
- 용어 자체의 의미를 생략해서 잘못 사용
즉, 특정 상황을 설명하는 다른 단어가 있음에도 불구하고 퉁치고 넘어가는 경향이 있다. 제대로된 용어에 대한 정의를 알아보고 제대로 사용해보자.
결합도(Coupling)
- 두 소프트웨어 모듈 간에 상호 의존성 정도
- 상호 참조
- 각각이 독자 생존이 불가
OO에서 논하는 결합도
A가 B를 의존하는 상황에서 B변경시 동작하는가?
- A의 내부를 변경안해도 됨
- A가 B에 의존하나 정도가 높지 않음 (A depends lightly on B)
- 결합도가 낮음 (loose coupling)
- A의 내부를 변경해야 함
- 의존도가 높음 (A depends heavily on B)
- 결합도가 높음 (tight coupling)
정리: B 코드 변경시, A 코드도 변경해야 하면 결합도가 높다.
높은 결합도를 의미하는 표현들
- A와 B가 결합되어 있다. (A and B are coupled.)
- “결합” 자체는 나쁜의미를 지니고 있지 않음
- “결합 == 의존”의 의미를 필연적으로 내포하기 있기 때문
- 의존 자체도 나쁜 것이 아님
- 높은 결합도는 나쁜 것.
- 하지만, 결합되어 있다는 말로 사용하는 것이 보통 높은 결합도를 생각하고 말하는 경우가 많음
- A가 B에 의존한다. (A depends on B.)
- A와 B사이에 의존성이 있다. (There is a dependency between A and B.)
이렇게 변경되어야 한다.
- A가 B에 심하게 의존한다. (A depends heavily on B.)
- A와 B의 결합도가 높다. (A and B are tightly coupled.)
낮은 결합도를 의미하는 표현들
- A와 B가 결합되어 있지 않다. (A and B are decoupled.)
이렇게 바꿔야 한다.
- A와 B의 결합도가 낮다. (A and B are loosely coupled.)
- A가 B에 미미하게 의존한다. (A depends lightly on B.)
결합도를 줄이는 것을 의미하는 표현들
- A와 B의 결합관계를 제거한다. (Decouple A and B.)
- A와 B의 의존성을 제거한다. (Break dependencies between A and B.)
이렇게 바꿔야 한다.
- A와 B의 결합도를 줄인다. (Reduce coupling between A and B.)
의존성 주입 (Dependency Injection)
- 클래스 간에 의존성은 있는 것이 좋다.
- 그런데, 이 의존성이 강하게 엮여 있는 경우 좋지 않다.
- A가 B에 의존할 때, B코드를 변경할 경우 A도 변경해야 한다면 좋지 않다.
- 보통 이런 경우 A가 B에 의존할 때, B자체를 생성해서 사용하는 경우 발생한다.
- 즉, 의존성을 자체적으로 해결하고 있는 것.
- 이런 경우 이 의존성 자체를 바깥에서 받아올 수는 없을까?
- 그것이 의존성 주입이다.
DI 용어 주의
다음과 같은 것들을 의미하기 위해 사용할 수도 있다.
- 의존성 주입 (Dependency Injection)
- 의존성 주입 컨테이너 (DI Container)
- 의존성 역전 (Dependency Inversion)
의존성 주입 종류
- 생성자 주입
- setter 주입
- setter 함수하나 만들고 그 함수를 통해 넣어주는 방식
setter 주입의 문제
- 생성자 주입을 없앨 수는 있으나..
- 개체의 유효한 상태에 해가 될 수 있다.
- 개체는 생성시 부터 유효한 상태를 가져야 한다.
- setter가 마구 추가된다면? 캡슐화가 깨질 수도..
의존성 주입의 불편함
- 로봇을 만드는데, 머리를 전달해줘야만 만들어지는 것이 이상하다.
- 즉, 하나의 개체로 생각하고 로봇() 이렇게 쓰고 싶은데,
- 머리는 뭐고, 다리는 뭐고 이런식으로 전달하는 것이 불편하다.
- 하나의 완제품으로 생성해서 쓰고 싶다!
DI를 통해 얻은 것과 잃은 것
- 얻은 것
- 결합도를 낮춤
- Head의 생성자가 바뀌어도 Robot을 바꿀 필요가 없음
- Head가 바뀌면, 이 클래스만 따로 컴파일하여 배포 가능
- 잃은 것
- 편의성 - 로봇을 생성하려면 머리도 알아야 함
- 프로그래머의 원래 의도를 잘 보여주는 클래스
- 머리 넣을 필요없이 완제품이야~ 라는 의도를 보여줄 수 없음
장단점이 존재한다. 무조건적으로 결합도를 낮춰야 한다는 주장은 있을 수 없다. 한 방식이 무조건 옳다고 할 수 없다.
복잡한 시스템에서 문제인 커플링
- 간단한 구조에서는 DI의 실익이 크지 않다.
- 하지만 복잡한 시스템에서는 다르다.
디커플링은 유연성/재사용성을 높임
- 추상화는 유연성/재사용성을 높임
- 디커플링도 마찬가지
- 미래의 변화에 대비되어 있다는 의미
- 하지만 그로 인한 단점도 있다.
단점
- 직관적이지 못하다.
- 내부를 알아야 좋은 경우도 있다.
import java.util.Collection;
public final class DataSource {
public void mergeTo(Collection<Data> dataset) {
// source로 부터 모든 데이터를 가져와 "중복 없이" dataset에 반영
}
}- 위와 같은 상황이라면, 이
dataset이 어떤 클래스냐에 따라 차이가 생긴다.- Set: 중복 체크 필요없음, add만 호출
- ArrayList: contains로 중복검사, 아닌 경우만 add
- 정렬된 ArrayList: 이진 탐색으로 중복 검사, 아닌 경우만 add
- 이런 경우들을 모두 아우르기 위해서는 가장 느리고 일반적인 방법으로 처리해야 한다.
- contains로 중복검사, 아닌 경우만 add
- 즉, 성능이 중요한 경우에는 일반화/추상화가 비효율적일 수 있다.
- 구체 타입이 들어오면 최적화가 가능
- 구현을 알아야 문제해결이 가능
- 될 것이라 생각하고 작성했는데, 알고보니 내부 구현사항에 문제가 있는 경우