|

객체지향 설계에서 결합도가 중요한 이유

객체지향 설계에서 결합도와 변경 전파를 설명하는 대표 이미지
변경이 여러 곳으로 번지는 구조와 역할 분리 구조를 비교해 결합도의 의미를 설명한다

객체지향 설계에서 결합도가 중요한 이유는 생각보다 단순합니다. 결제 수단 하나를 추가했을 뿐인데 주문 로직, 예외 처리, 테스트까지 함께 수정해야 한다면 그 시스템은 이미 변화에 민감하게 얽혀 있는 상태입니다. 이 글에서는 잘못 설계했을 때 왜 수정 범위가 커지는지, 그리고 객체지향 설계에서 그 문제를 어떻게 줄일 수 있는지를 쉬운 예시로 설명하겠습니다.


객체지향 설계에서 결합도를 왜 보게 될까

개발에서 진짜 비용이 커지는 순간은 코드가 조금 길어졌을 때가 아닙니다. 더 큰 비용은 작은 변경이 여러 곳으로 퍼질 때 생깁니다. 예를 들어 결제 정책 하나가 바뀌었는데 주문 서비스도 수정해야 하고, 에러 처리도 다시 확인해야 하고, 기존 테스트까지 줄줄이 손봐야 한다면 작업은 금방 무거워집니다.

그래서 객체지향 설계에서 결합도를 본다는 것은 단순히 연결이 많다, 적다를 따지는 일이 아닙니다. 더 중요한 것은 변경이 어디까지 퍼지는가입니다. 결합도는 여러 방식으로 설명할 수 있지만, 실무에서는 한 부분의 변경이 다른 부분으로 얼마나 쉽게 퍼지는지로 이해하면 가장 도움이 됩니다.


나쁜 예시: 결제 방식이 늘어날수록 주문 코드가 커지는 구조

온라인 주문 시스템을 하나 떠올려보겠습니다. 처음에는 카드 결제만 지원합니다. 이 단계에서는 아래처럼 단순하게 구현하고 싶어집니다.

class OrderService {
    fun placeOrder(order: Order, paymentType: String) {
        if (paymentType == "CARD") {
            CardPaymentGateway().approve(order.totalPrice)
        }

        // 주문 저장
        // 알림 발송
        // 로그 기록
    }
}

처음만 보면 큰 문제가 없어 보입니다. 하지만 계좌이체, 간편결제, 해외 결제 수단, 결제 실패 처리 같은 요구가 붙기 시작하면 `OrderService` 안에 조건문과 세부 구현 지식이 계속 쌓입니다.

class OrderService {
    fun placeOrder(order: Order, paymentType: String) {
        if (paymentType == "CARD") {
            CardPaymentGateway().approve(order.totalPrice)
        } else if (paymentType == "BANK_TRANSFER") {
            BankTransferService().transfer(order.totalPrice)
        } else if (paymentType == "EASY_PAY") {
            EasyPayClient().requestPayment(order.totalPrice)
        }

        // 결제 방식에 따라 다른 예외 처리
        // 결제 방식에 따라 다른 로그 처리
        // 주문 저장
        // 알림 발송
    }
}

문제는 if 문이 늘어난다는 사실 자체가 아닙니다. 더 본질적인 문제는 `OrderService`가 결제 방식의 세부 구현을 너무 많이 알고 있다는 점입니다. 원래 변화가 생기는 곳은 결제 방식인데, 지금 구조에서는 결제 변화가 주문 흐름 전체로 전파됩니다.


잘못 설계하면 왜 수정 범위가 커질까

이제 간편결제 추가라는 요구사항이 들어왔다고 해보겠습니다. 겉으로 보기에는 새 결제 수단 하나만 더 넣으면 끝날 것처럼 보이지만, 실제로는 수정 포인트가 빠르게 늘어납니다.

  • `OrderService`의 분기문 수정
  • 간편결제 API 호출 코드 추가
  • 결제 실패 처리 분기 추가
  • 로그 처리 수정
  • 테스트 케이스 수정
  • 기존 카드/계좌이체 흐름에 영향 없는지 재확인
  • 문자열 비교나 조건 분기 누락 여부 점검

나는 결제 수단 하나를 추가하고 싶은데 실제로는 주문 서비스 전체를 다시 읽고 있는 셈입니다. 높은 결합도가 불편한 이유도 바로 여기에 있습니다. 문제가 되는 것은 의존성이 있다는 사실 자체가 아니라, 한쪽 변화가 다른 쪽 수정을 너무 쉽게 강제하는 상태입니다.


의존성과 결합도는 왜 다르게 봐야 할까

주문은 결제를 사용해야 합니다. 그렇다면 주문이 결제에 어느 정도 의존하는 것은 자연스럽습니다. 객체지향은 원래 객체들이 혼자 일하는 구조가 아니라 서로 협력하는 구조이기 때문입니다.

문제는 어디까지 알고 있느냐입니다. 주문 서비스가 결제가 필요하다는 사실만 아는 것과, 카드 결제는 이렇게 호출하고 계좌이체는 저렇게 처리하고 간편결제는 또 다르게 예외를 다뤄야 한다는 세부 구현까지 다 아는 것은 전혀 다릅니다. 결합도가 높다는 말은 단순히 연결이 많다는 뜻보다 변경에 대한 민감도가 높다는 뜻으로 이해하는 편이 실무에서 훨씬 유용합니다.


개선 예시: 주문은 결제의 역할만 알고 구현은 분리하는 구조

이제 같은 상황을 조금 다르게 설계해보겠습니다. 핵심은 주문이 결제의 구현 방식까지 알지 않게 만드는 것입니다.

interface PaymentProcessor {
    fun pay(amount: Long)
}

class CardPaymentProcessor : PaymentProcessor {
    override fun pay(amount: Long) {
        // 카드 결제 처리
    }
}

class BankTransferPaymentProcessor : PaymentProcessor {
    override fun pay(amount: Long) {
        // 계좌이체 처리
    }
}

class EasyPayPaymentProcessor : PaymentProcessor {
    override fun pay(amount: Long) {
        // 간편결제 처리
    }
}

class OrderService(
    private val paymentProcessor: PaymentProcessor
) {
    fun placeOrder(order: Order) {
        paymentProcessor.pay(order.totalPrice)

        // 주문 저장
        // 알림 발송
        // 로그 기록
    }
}

이 구조에서 `OrderService`는 더 이상 카드 결제 API가 무엇인지, 계좌이체는 어떤 예외를 던지는지, 간편결제는 어떤 클라이언트를 쓰는지까지 알 필요가 없습니다. 주문에는 결제가 필요하고, 결제는 `pay()`로 요청할 수 있다는 역할만 알면 됩니다.


수정 범위를 비교해보면 결합도의 의미가 더 선명해진다

같은 간편결제 추가 요구사항을 다시 비교해보겠습니다.

  • 잘못 설계했을 때: `OrderService` 수정, 조건문 추가, 결제별 예외 처리 수정, 기존 흐름 영향 확인, 관련 테스트 여러 개 수정
  • 역할을 분리했을 때: `EasyPayPaymentProcessor` 추가, 필요한 연결 설정 추가, 해당 구현 테스트 추가

나쁜 구조에서는 결제 추가가 곧 주문 수정으로 번졌습니다. 반면 역할을 분리한 구조에서는 결제 추가가 비교적 결제 영역 내부 변경에 머뭅니다. 결합도를 낮춘다는 말은 결국 클래스를 보기 좋게 쪼개는 작업이 아니라 변경 범위를 통제하는 작업에 가깝습니다.


보조 예시: 알림 채널이 늘어날 때도 같은 문제가 반복된다

이 문제는 결제에만 있는 것이 아닙니다. 알림 기능에서도 자주 비슷한 일이 벌어집니다. 회원 가입 후 알림을 보내는 기능이 있다고 해보겠습니다. 처음에는 이메일만 보내다가, 나중에는 SMS, 슬랙, 푸시 알림까지 추가될 수 있습니다.

잘못 설계하면 가입 서비스 안에 분기들이 계속 쌓이고, 결국 알림 채널 추가가 곧 가입 서비스 수정이 됩니다. 반대로 알림 발송 역할을 분리해두면 가입 서비스는 알림을 보낸다는 사실만 알고, 이메일인지 SMS인지 슬랙인지는 알림 쪽에서 처리할 수 있습니다.


그렇다고 무조건 분리하면 좋은 것은 아니다

결합도가 문제라고 해서 모든 곳에 인터페이스를 만들고 모든 구현을 미리 분리하는 것이 정답은 아닙니다. 변화 가능성이 거의 없고 구조도 단순한데 너무 일찍 추상화를 도입하면 오히려 코드가 읽기 어려워질 수 있습니다.

중요한 것은 의존성을 없애는 것이 아니라 자주 바뀌는 축을 넓게 퍼뜨리지 않는 것입니다. 즉, 좋은 설계는 무조건 많이 나눈 설계가 아니라 변경 비용이 커질 지점을 잘라내는 설계입니다. 이 부분은 객체지향 설계의 핵심과도 자연스럽게 이어집니다.


실무에서 결합도를 점검할 때 던져볼 질문

  • 기능 하나를 추가할 때 여러 서비스의 조건문을 함께 수정해야 하는가?
  • 구현 방식이 바뀔 때 상위 로직도 자주 흔들리는가?
  • 테스트가 특정 구현 세부사항에 지나치게 묶여 있는가?
  • 한 클래스가 외부 시스템의 세부 동작을 너무 많이 알고 있는가?
  • 변경 이유가 다른 코드들이 한곳에 섞여 있는가?

이 질문을 한 문장으로 줄이면 결국 이겁니다. 원래 바뀌어야 할 것보다 더 많은 곳이 같이 바뀌는가? 만약 그렇다면, 그 구조는 결합도가 높을 가능성이 큽니다. 그리고 여기서 한 걸음 더 나아가 구조 선택 관점까지 보고 싶다면, 다음 글로 상속보다 조합이 낫다는 말은 언제 맞을까를 이어서 읽는 흐름도 좋습니다.


결론: 좋은 설계는 변경 범위를 줄이는 설계다

객체지향 설계에서 결합도가 중요한 이유는 코드를 이론적으로 예쁘게 만들기 위해서가 아닙니다. 핵심은 훨씬 현실적입니다. 수정 한 번이 시스템 전체 수정으로 번지는 상황을 줄이기 위해서입니다.

현실의 소프트웨어는 계속 바뀝니다. 결제 수단도 늘어나고, 알림 채널도 바뀌고, 정책도 수정됩니다. 그래서 좋은 설계는 변화를 없애는 설계가 아니라 변화가 생겼을 때 영향 범위를 작게 유지하는 설계여야 합니다. 결합도를 본다는 것은 결국 이 구조가 변경 비용을 얼마나 키우는지 살펴보는 일입니다.

외부 참고 자료로는 Refactoring.Guru의 관련 설명도 함께 볼 만합니다. 객체가 남의 데이터를 과하게 들여다보는 구조가 왜 유지보수에 불리한지 이해하는 데 도움이 됩니다.

함께보면 좋은 글