결합도와 응집도 이야기
OOP를 다루는 개발자라면 혹은 컴퓨터 공학을 전공하는 사람이라면 누구나 이런 얘기를 들어본 적이 있을 것이다.
"결합도는 낮추고, 응집도는 높여야 유지보수하기 쉬운 좋은 프로그램이 된다"
개발자로 산지 거의 5년이라는 시간이 지났지만, 저 말을 제대로 이해하고 있는지도 의문이었다.
그래도 최근에 '클린코드' 라는 책을 정독하면서 나름대로 잘 이해하게 된 것 같아, 내용을 좀 정리해보려고한다.
결합도 (Coupling)
결합도는 다른 모듈간의 의존도를 의미한다.
아래그림을 보자.
파란색 사각형이 프로그램의 모듈이라고 생각해보자. 보통 프로그램은 하나의 모듈이 아닌 여러개의 모듈로 이루어져있다. 결합도는 각각의 모듈간의 영향을 주는 즉 의존도를 의미한다.
응집도 (Cohesion)
응집도는 하나의 모듈 내부의 기능 집중도를 의미한다.
하나의 모듈에는 그 모듈이 정상적으로 수행되기 위해 여러 기능들이 있는데, 그 기능들이 모듈의 목적을 달성하도록 연관있게 연결되어 있어야 한다.
결합도가 높은 클래스?
그렇다면 만약 결합도가 높은 클래스가 존재하면 어떻게 될까?
연관된 클래스가 변경이 되어버리면 해당 클래스는 수정이 필요해지게 된다.
프로그램이 작을대는 문제가 되진 않겠지만 만약 큰 프로그램이라면 이는 굉장히 복잡하고 번거로운 작업이 될 것이다. 수정을 위해서는 연관된 클래스를 모두 이해하고 있어야하는데, 프로그램이 커지면 커질 수록 이는 어려운 일이 된다.
응집도가 낮은 클래스?
만약 응집도가 낮은 클래스가 존재 하면 어떨까?
응집도가 낮다는 말은 한 클래스에 여러기능이 있다는 말인데, 이는 코드를 읽는 사람이 해당 클래스를 쉽게 이해하지 못한다는 말이 된다. 그리고 이러한 클래스는 보통 재사용하기가 어려운 코드일 가능성이 매우 높다.
다시 아래 문구를 보자.
"결합도는 낮추고, 응집도는 높여야 유지보수하기 쉬운 좋은 프로그램이 된다"
위와 같이 결합도는 낮고, 응집도는 높은 프로그램을 만들려면 어떻게 만들어야할까?
낮은 결합도 예시
아래 코드는 클린코드라는 책에서 가져온 예시이다.
public interface StockExchange {
Money currentPrice(String symbol);
}
public class Portfolio {
private StockExchange exchange;
public Portfolio(StockExchange exchange) {
this.exchange = exchange
}
// ...
}
위의 코드를 보자.
Portfolio 클래스는 한국(KoreaStockExchange), 미국(JpanStockExchange), 일본(JapanStockExchange) 등 외부 API를 활용하여 포트폴리오 값을 계산해야하지만 위의 예제에서는 StockExchange라는 인터페이스를 이용하여 어떤 외부 API든 다 구현이 가능하도록 설계를 해뒀다.
간단하게 코드를 설명해 보면 위의 코드는 SOLID 원칙 중 DIP원칙을 지켜 추상화를 이용하여 설계를 한 사례이다. 이전에 SOLID에 대해 설명을 했었는데 DIP는 클래스가 상세한 구현이 아니라 추상화에 의존하도록 구현하는 원칙이다. 혹시 SOLID가 궁금하시다면 아래 링크를 참고하여 공부하자.
https://devkingdom.tistory.com/296
이러한 코드 설계는 Portfolio와 StockExchange간의 결합도를 낮춘 사례로 훌륭한 설계라 할 수 있다.
위와 같은 코드는 아래처럼 테스트 코드를 짤때도 큰 효율을 발휘한다.
public class PortfolioTest {
private FixedStockExchangeStub exchange;
private Portfolio portfolio;
@Before
protected void setUp() throws Exception {
exchange = new FixedStockExchangeStub exchange;
exchange.fix("MSFT", 100);
}
@Test
public void GivenFiveMSFTTotalShouldBe500() throws Exception {
portfolio.add(5, "MSFT");
Assert.assertEquals(500, portfolio.value());
}
}
테스트 코드를 보면 FixedStockExchangeStub 클래스를 통해 마이크로소프트 주가로 언제나 100불을 반환하도록 만들어져 있다. 객체를 Mocking 하여 변경되는 클래스도 테스트가 가능해진 것이다.
이게 가능한 이유는 바로 시스템의 결합도를 낮췄기 때문에 가능한 것이다.
시스템 결합도를 낮추면 유연성과 재사용성이 높아진다.
그렇다고 무조건 머리아프게 추상화를 하여 모듈을 설계하라는 말은 아니다. 확장될 가능성이 낮은 경우에는 일단 결합하고 추후에 추상화해도 된다.
높은 응집도 예시
높은 응집도를 가진 예제코드도 작성해 보도록하겠다.
public class Stack {
private int topOfStack = 0;
private List<Integer> elements = new LinkedList<Integer>();
public int size() {
return topOfStack;
}
public void push(int element) {
topOfStack++;
elements.add(element);
}
public int pop() throws PoppedWhenEmpty {
if (topOfStack == 0) {
throw new PoppedWhenEmpty();
}
int element = elements.get(--topOfStack);
elements.remove(topOfStack);
return element;
}
}
위의 코드는 Stack을 구현한 코드이다. 보통 메서드가 인스턴스 변수를 많이 사용하면 할 수록 메서드와 클래스는 응집도가 높아지는데 위의 코드는 각 메서드 마다 인스턴스변수를 사용해줌으로써 응집도를 높여줬다. 보통 클래스의 메서드는 인스턴스변수를 하나 이상 사용해 줘야한다.
응집도가 높다는 것은 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 것이다.
논리적인 단위로 묶인다는 것은 서로 관계있는 놈들만 모아준다는 것이다.
그런데 여러분들이 설계를 하다보면 인스턴스 변수가 많아질 때가 있을 것이다.
만약에 몇몇 메서드가 몇몇의 인스턴스 변수만 사용한다면? 이런 경우는 클래스가 응집력을 잃어가고 있다는 뜻이다.
응집력을 잃는다면 클래스를 쪼개줘야한다.
큰 메서드를 작은 메서드 여러개로 쪼개다 보면 종종 작은 클래스 여러개로 쪼갤 수 있게 되는데, 이렇게 해주면 프로그램은 점점 체계가 잡히고 구조가 투명해진다.
오늘 깔끔하게 결합도 응집도에 대해 정리를 해보았다.
이 얘기는 OOP 에서 굉장히 중요한 요소임으로 프로그래밍할때 잘 생각해서 적용해보기를 권한다.
끝.
Ref.
로버트 C.마틴, 『클린코드』, 도서출판인사이트(2021), p172-p191