문서의 각 단락이 어느 리비전에서 마지막으로 수정되었는지 확인할 수 있습니다. 왼쪽의 정보 칩을 통해 작성자와 수정 시점을 파악하세요.

객체 지향 설계 | |
이름 | |
영문명 | Object-Oriented Design |
분류 | |
핵심 개념 | |
주요 목표 | 유지보수성, 재사용성, 확장성, 모듈화 |
관련 패러다임 | |
대표적 원칙 | |
상세 정보 | |
설계 단계 | 요구사항 분석, 개념적 설계, 상세 설계 |
주요 활동 | 객체 식별, 관계 정의, 클래스 설계, 인터페이스 정의 |
설계 모델 | UML (클래스 다이어그램, 시퀀스 다이어그램 등) |
설계 패턴 | GoF 디자인 패턴 (생성, 구조, 행동 패턴) |
품질 속성 | 응집도, 결합도, 복잡도 관리 |
주요 이점 | 실세계 모델링 용이, 코드 재사용성 향상, 시스템 유연성 증가 |
도전 과제 | 과도한 설계, 객체 간 의존성 관리, 성능 고려 |
관련 도구 | |
응용 분야 | 대규모 엔터프라이즈 시스템, 웹 애플리케이션, 게임 개발 |
역사적 배경 | 1960년대 Simula 언어에서 시작, 1990년대 Gang of Four의 디자인 패턴으로 정립 |

객체 지향 설계는 소프트웨어 공학에서 객체 지향 프로그래밍 패러다임을 바탕으로 시스템을 구성하는 방법론이다. 이 설계 방식은 현실 세계의 사물이나 개념을 객체라는 독립적인 단위로 모델링하고, 객체들 간의 상호작용을 통해 전체 시스템의 기능을 구현하는 데 중점을 둔다. 설계의 궁극적 목표는 변경에 유연하고, 이해하기 쉬우며, 재사용 가능한 소프트웨어 구조를 만드는 것이다.
이 설계의 토대는 클래스와 객체라는 개념 위에 세워진다. 클래스는 공통된 속성과 행동을 정의한 청사진 역할을 하며, 객체는 이 클래스를 기반으로 생성된 구체적인 인스턴스이다. 설계 과정에서는 시스템의 요구사항을 분석하여 핵심이 되는 객체들을 식별하고, 각 객체가 담당할 책임을 부여하며, 객체들 사이에 메시지를 주고받는 관계를 정의한다.
객체 지향 설계는 단순히 코드를 작성하는 기술을 넘어, 소프트웨어의 복잡성을 관리하고 품질을 높이기 위한 체계적인 접근법을 제공한다. 이를 통해 개발자는 보다 모듈화되고 유지보수가 용이한 시스템을 구축할 수 있다. 이 설계 방식은 현대의 대규모 소프트웨어 개발에서 사실상의 표준으로 자리 잡았다.

객체 지향 설계의 핵심 원칙은 캡슐화, 상속, 다형성, 추상화 네 가지로 구성된다. 이 원칙들은 소프트웨어를 유연하고 재사용 가능하며 유지보수하기 쉬운 모듈식 구조로 구성하는 데 기반을 제공한다.
캡슐화는 데이터와 해당 데이터를 처리하는 메서드를 하나의 단위, 즉 객체로 묶는 것을 의미한다. 객체의 내부 상태(데이터)는 외부에서 직접 접근할 수 없도록 숨기고, 공개된 메서드를 통해서만 상태를 변경하거나 조회할 수 있게 한다. 이는 데이터의 무결성을 보호하고, 객체 간의 결합도를 낮추며, 구현 세부 사항을 변경해도 외부에 영향을 미치지 않게 한다. 상속은 기존 클래스의 속성과 메서드를 새로운 클래스가 물려받아 재사용하거나 확장할 수 있게 하는 메커니즘이다. 이는 코드의 중복을 줄이고 계층적인 분류 체계를 구축하여 코드 재사용성을 높인다.
다형성은 하나의 인터페이스나 메서드 호출이 서로 다른 클래스의 객체에 대해 각기 다른 방식으로 실행될 수 있는 능력을 말한다. 이는 주로 상속과 메서드 오버라이딩을 통해 구현된다. 다형성을 활용하면 조건문을 줄이고, 새로운 객체 유형을 추가하더라도 기존 코드를 크게 수정하지 않아도 되어 시스템의 확장성을 크게 향상시킨다. 추상화는 복잡한 현실 세계의 개념을 핵심적인 속성과 동작만을 가진 모델로 단순화하는 과정이다. 구체적인 구현 세부 사항은 숨기고 필수적인 인터페이스만을 노출시켜 사용자가 복잡성을 관리할 수 있게 돕는다. 추상 클래스와 인터페이스는 추상화를 구현하는 주요 수단이다.
이 네 가지 원칙은 서로 긴밀하게 연결되어 작동한다. 예를 들어, 캡슐화는 객체의 경계를 정의하고, 상속과 다형성은 객체들 간의 관계와 협력 방식을 규정하며, 추상화는 전체 시스템의 복잡성을 관리하는 틀을 제공한다. 이 원칙들을 효과적으로 적용하면 변경에 유연하고 이해하기 쉬운 소프트웨어 구조를 설계할 수 있다.
캡슐화는 객체의 상태(데이터)와 그 상태를 조작하는 행위(메서드)를 하나의 단위, 즉 클래스로 묶는 것을 의미한다. 더 나아가, 객체의 내부 데이터에 대한 직접적인 접근을 제한하고, 공개된 메서드를 통해서만 상호작용하도록 하는 정보 은닉의 개념을 포함한다. 이는 객체의 세부 구현 내용을 외부로부터 숨겨, 객체 간의 결합도를 낮추고 모듈성을 높이는 데 목적이 있다.
캡슐화를 구현하는 주요 수단은 접근 제어자이다. 클래스의 멤버 변수(필드)는 주로 private으로 선언하여 외부에서 직접 접근할 수 없게 한다. 대신, 해당 데이터를 읽거나 수정해야 할 필요가 있을 때는 public으로 선언된 게터와 세터 메서드를 제공한다. 예를 들어, balance라는 계좌 잔액 필드에 직접 접근하는 대신 getBalance()와 deposit(int amount) 같은 메서드를 통해 안전하게 조작한다. 이를 통해 데이터의 유효성을 검사하거나, 내부 표현 방식을 변경하더라도 외부 코드에 영향을 주지 않을 수 있다[1].
캡슐화의 이점은 크게 세 가지로 요약된다. 첫째, 데이터의 무결성을 보장할 수 있다. 세터 메서드 내부에 검증 로직을 추가함으로써 객체가 항상 유효한 상태를 유지하도록 강제한다. 둘째, 코드의 유지보수성이 향상된다. 클래스의 내부 구현이 변경되더라도 공개된 인터페이스(메서드 시그니처)가 동일하게 유지된다면, 해당 클래스를 사용하는 다른 코드를 수정할 필요가 없다. 셋째, 보안성을 높일 수 있다. 중요한 데이터를 외부에 노출시키지 않음으로써 의도하지 않은 변경이나 오용을 방지한다.
상속은 기존에 정의된 클래스의 속성과 메서드를 새로운 클래스가 물려받는 메커니즘이다. 이때 속성과 메서드를 물려주는 클래스를 부모 클래스 또는 슈퍼클래스라고 하며, 물려받는 클래스를 자식 클래스 또는 서브클래스라고 한다. 상속을 통해 코드의 재사용성을 높이고, 계층적인 클래스 계층 구조를 형성하여 시스템의 복잡성을 관리한다.
상속의 주요 형태는 단일 상속과 다중 상속으로 구분된다. 단일 상속은 하나의 자식 클래스가 오직 하나의 부모 클래스만을 가질 수 있는 구조이다. 자바와 C# 같은 언어가 이 방식을 채택한다. 다중 상속은 하나의 자식 클래스가 두 개 이상의 부모 클래스로부터 상속받을 수 있는 구조이다. C++가 대표적인 예시이며, 이 경우 다이아몬드 문제[2]와 같은 복잡성이 발생할 수 있다.
상속은 is-a 관계를 모델링하는 데 적합하다. 예를 들어, '자동차'라는 부모 클래스가 있고 '트럭'이라는 자식 클래스가 있다면, '트럭은 자동차이다'라는 관계가 성립한다. 이 관계를 통해 트럭 클래스는 자동차 클래스의 기본적인 특성(예: 바퀴 수, 엔진)과 동작(예: 주행, 정지)을 재정의하거나 확장 없이 그대로 사용할 수 있다. 또한 자식 클래스는 부모 클래스의 메서드를 자신의 필요에 맞게 재정의하는 메서드 오버라이딩을 수행할 수 있다.
그러나 상속의 과도한 사용은 깊은 상속 계층과 취약한 기반 클래스 문제[3]를 초래할 수 있다. 이러한 문제를 완화하기 위해 컴포지션(합성)을 우선적으로 고려하는 '상속보다는 컴포지션을'이라는 원칙이 널리 제안된다. 컴포지션은 다른 클래스의 인스턴스를 자신의 필드로 포함하는 방식으로, 상속에 비해 더 유연하고 느슨한 결합을 가능하게 한다.
다형성은 하나의 인터페이스나 부모 클래스의 참조를 통해 여러 자식 클래스의 객체를 동일하게 다룰 수 있게 하는 특성이다. 이는 "여러 형태를 가진다"는 의미로, 동일한 메시지를 서로 다른 객체에 보냈을 때 각 객체의 타입에 따라 다르게 반응하는 방식으로 구현된다. 다형성을 통해 코드는 구체적인 클래스 타입에 의존하지 않고, 추상화된 상위 계층에만 의존하게 되어 유연성과 확장성이 크게 향상된다.
다형성의 주요 구현 방법은 메서드 오버라이딩과 메서드 오버로딩이다. 메서드 오버라이딩은 상속 관계에서 부모 클래스에 정의된 메서드를 자식 클래스에서 재정의하는 것을 말한다. 반면, 메서드 오버로딩은 같은 클래스 내에서 메서드 이름은 같지만 매개변수의 타입이나 개수가 다른 여러 메서드를 정의하는 기법이다. 일반적으로 다형성의 핵심은 오버라이딩을 통한 런타임 시의 동적 바인딩에 있다.
다형성의 이점은 다음과 같이 정리할 수 있다.
이점 | 설명 |
|---|---|
코드 재사용성 증가 | 공통 인터페이스를 사용함으로써 유사한 동작을 하는 코드를 중복 작성하지 않아도 된다. |
유지보수성 향상 | 새로운 클래스를 추가하더라도 기존 코드를 수정할 필요가 적다. |
결합도 감소 | 클라이언트 코드가 구체적인 구현 클래스가 아닌 추상적인 인터페이스에 의존하게 된다. |
예를 들어, Shape라는 부모 클래스에 draw() 메서드가 있고, Circle과 Rectangle 클래스가 이를 상속받아 각자의 방식으로 draw() 메서드를 구현했다면, 클라이언트 코드는 Shape 타입의 참조 변수로 두 객체를 모두 가리키며 동일한 draw() 메시지를 보낼 수 있다. 실제 실행되는 메서드는 런타임에 객체의 실제 타입(Circle 또는 Rectangle)에 따라 결정된다. 이로 인해 새로운 도형 클래스를 추가하더라도 기존 코드를 변경하지 않고도 시스템을 확장할 수 있다.
추상화는 복잡한 현실 세계의 개념을 핵심적인 속성과 동작만을 추려내어 단순화된 모델로 표현하는 과정이다. 객체 지향 설계에서 추상화는 구체적인 구현 세부 사항을 숨기고, 사용자에게 필수적인 인터페이스만을 노출시키는 기법을 의미한다. 이는 시스템의 복잡성을 관리하고, 이해하기 쉬운 모델을 구축하는 데 핵심적인 역할을 한다.
구현의 세부 사항을 감추고 인터페이스를 통해 상호작용하도록 하는 것을 정보 은닉이라고 하며, 이는 추상화를 실현하는 주요 수단이다. 예를 들어, 자동차의 운전자는 엔진의 내부 작동 원리를 알지 못해도 핸들, 액셀, 브레이크라는 추상화된 인터페이스를 통해 자동차를 조작할 수 있다. 소프트웨어에서도 마찬가지로, 클래스는 내부 데이터와 메서드의 구체적인 구현을 숨기고, 공개된 메서드(인터페이스)를 통해 외부와 소통한다.
추상화는 클래스와 인터페이스를 통해 구현된다. 추상 클래스나 인터페이스는 메서드의 시그니처(이름, 매개변수, 반환 타입)만을 정의하고, 구체적인 구현은 이를 상속하거나 구현하는 하위 클래스에 위임한다. 이를 통해 다양한 구체적인 객체들을 하나의 추상적인 타입으로 다룰 수 있게 되어, 코드의 유연성과 확장성이 크게 향상된다.
추상화 수준 | 예시 | 설명 |
|---|---|---|
고수준 추상화 |
|
|
중간 수준 추상화 |
| 신용카드 결제의 일반적인 로직을 포함할 수 있다. |
저수준 추상화 |
| 비자 카드사 특정 API를 호출하는 구체적인 구현을 담는다. |
이러한 계층적 추상화는 설계를 모듈화하고, 고수준의 정책이 저수준의 세부 사항에 의존하지 않도록 하여 시스템을 변경에 유연하게 만든다. 결국 추상화는 복잡성을 통제하고, 핵심적인 개념에 집중할 수 있도록 도와주는 객체 지향 설계의 근본적인 도구이다.

SOLID는 객체 지향 설계의 다섯 가지 기본 원칙을 나타내는 약어이다. 이 원칙들은 로버트 C. 마틴이 제안했으며, 유연하고 유지보수하기 쉬운 소프트웨어를 만들기 위한 지침을 제공한다. SOLID 원칙을 따르면 시스템의 결합도는 낮아지고 응집도는 높아져, 변경에 강하고 재사용이 용이한 설계를 달성할 수 있다.
원칙 | 약어 | 핵심 개념 |
|---|---|---|
단일 책임 원칙 | SRP | 하나의 클래스는 하나의 책임만 가져야 한다. |
개방-폐쇄 원칙 | OCP | 소프트웨어 요소는 확장에는 열려 있고, 변경에는 닫혀 있어야 한다. |
리스코프 치환 원칙 | LSP | 하위 타입 객체는 상위 타입 객체를 대체할 수 있어야 한다. |
인터페이스 분리 원칙 | ISP | 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않아야 한다. |
의존관계 역전 원칙 | DIP | 고수준 모듈은 저수준 모듈에 의존하지 않아야 하며, 둘 다 추상화에 의존해야 한다. |
첫 번째 원칙인 단일 책임 원칙은 하나의 클래스가 변경되어야 하는 이유는 단 하나뿐이어야 함을 의미한다. 이는 클래스의 기능을 명확히 분리함으로써 복잡성을 관리하고 수정의 영향을 최소화한다. 두 번째 원칙인 개방-폐쇄 원칙은 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있도록 설계해야 함을 강조한다. 주로 추상화와 다형성을 활용하여 구현된다.
세 번째 원칙인 리스코프 치환 원칙은 상속 관계에서 하위 클래스는 상위 클래스의 행동 규약을 반드시 지켜야 함을 규정한다. 이를 위반하면 예상치 못한 오동작을 초래할 수 있다. 네 번째 원칙인 인터페이스 분리 원칙은 범용적인 하나의 큰 인터페이스보다는, 특정 클라이언트를 위한 작고 구체적인 인터페이스 여러 개를 만드는 것이 더 낫다고 설명한다. 이는 불필요한 의존성을 제거한다. 마지막 원칙인 의존관계 역전 원칙은 구체적인 구현 클래스가 아닌, 추상화된 인터페이스나 추상 클래스에 의존하도록 함으로써 모듈 간의 결합을 느슨하게 만든다.
단일 책임 원칙은 객체 지향 설계의 핵심 원칙 중 하나로, SOLID 원칙의 첫 글자에 해당한다. 이 원칙은 "하나의 클래스는 하나의 책임만 가져야 한다"는 의미를 담고 있다. 여기서 '책임'이란 변경의 이유를 의미하며, 클래스가 변경되어야 하는 이유는 오직 하나뿐이어야 한다는 설계 지침을 제공한다.
이 원칙을 따르면 각 클래스는 명확하고 제한된 기능을 수행하게 된다. 예를 들어, 사용자 정보를 관리하는 클래스가 데이터베이스 연결 로직까지 포함한다면, 이 클래스는 사용자 정보 변경과 데이터베이스 기술 변경이라는 두 가지 이유로 수정될 수 있다. 단일 책임 원칙은 이러한 두 관심사를 분리하여, 사용자 관리 클래스와 데이터베이스 연결 클래스로 나눌 것을 권장한다.
적용 이점은 다음과 같다.
이점 | 설명 |
|---|---|
유지보수성 향상 | 변경이 필요한 부분이 명확해지고, 수정 시 다른 기능에 미치는 영향이 최소화된다. |
가독성 향상 | 클래스의 목적이 단순해져 코드를 이해하기 쉬워진다. |
재사용성 향상 | 특정 책임만 가진 잘 정의된 클래스는 다른 컨텍스트에서 재사용하기 더 용이하다. |
테스트 용이성 | 단일 기능을 테스트하는 것이 복합 기능을 테스트하는 것보다 간단해진다. |
이 원칙은 클래스뿐만 아니라 메서드나 모듈 수준에서도 적용될 수 있다. 핵심은 변경의 축을 식별하고, 각 축을 따라 책임을 분리하는 데 있다. 이를 통해 시스템은 더욱 견고하고 유연한 구조를 갖게 된다.
개방-폐쇄 원칙은 객체 지향 설계의 핵심 원칙 중 하나로, 소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다는 원칙이다. 이 원칙은 1988년 버트런드 메이어가 처음 명확히 제시하였다[4].
이 원칙의 목적은 기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있도록 설계하는 것이다. 이를 통해 시스템의 안정성을 유지하면서 요구사항의 변화에 유연하게 대응할 수 있다. 핵심 메커니즘은 추상화와 다형성을 활용하는 것이다. 구체적인 구현보다는 안정적인 인터페이스나 추상 클래스에 의존하도록 설계하면, 새로운 기능은 해당 인터페이스를 구현하는 새로운 클래스를 추가하여 확장할 수 있다. 기존 모듈은 수정할 필요가 없게 된다.
예를 들어, 다양한 형태의 도형 넓이를 계산하는 시스템을 생각해 볼 수 있다. 다음과 같은 추상 클래스나 인터페이스를 정의하는 것이 일반적인 접근법이다.
구성 요소 | 역할 |
|---|---|
|
|
|
|
|
|
새로운 도형(예: 삼각형)이 추가되어야 할 때, 기존 Rectangle이나 Circle 클래스는 전혀 수정하지 않고, Triangle이라는 새로운 클래스를 만들어 Shape 인터페이스를 구현하기만 하면 된다. 이렇게 하면 시스템은 새로운 기능(확장)에 열려 있으면서, 기존 코드(수정)에는 닫혀 있는 상태를 유지한다. 이 원칙을 준수하면 코드의 재사용성이 높아지고, 변경으로 인한 사이드 이펙트의 위험을 줄일 수 있다.
리스코프 치환 원칙은 바버라 리스코프가 1987년에 제안한 원칙으로, 객체 지향 프로그래밍의 핵심 설계 원칙 중 하나이다. 이 원칙은 상속 관계에 있는 객체들 사이의 대체 가능성을 정의한다. 간단히 말해, "S가 T의 하위 타입이라면, 프로그램에서 T 타입의 객체를 S 타입의 객체로 치환해도 프로그램의 정확성에 영향을 미치지 않아야 한다"는 것이다[5].
이 원칙은 상속을 올바르게 사용하기 위한 지침을 제공한다. 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 대체했을 때, 프로그램이 여전히 예상대로 동작해야 함을 의미한다. 이를 위반하는 전형적인 예는 자식 클래스가 부모 클래스의 메서드를 재정의(오버라이드)할 때, 부모 클래스의 사전 조건을 강화하거나 사후 조건을 약화시키는 경우이다. 예를 들어, 부모 클래스의 메서드가 정수를 입력받아 처리한다고 할 때, 자식 클래스에서 양의 정수만 받도록 제한하는 것은 리스코프 치환 원칙을 위반한다.
원칙 준수 예시 | 원칙 위반 예시 |
|---|---|
|
|
|
|
이 원칙을 준수하면 다형성을 안전하게 활용할 수 있고, 코드의 신뢰성과 확장성을 높일 수 있다. 또한, 이 원칙은 개방-폐쇄 원칙을 지키는 데 중요한 기반이 된다. 잘 설계된 상속 계층 구조는 클라이언트 코드가 구체적인 하위 타입을 알 필요 없이 상위 타입을 통해 객체를 조작할 수 있게 한다.
인터페이스 분리 원칙은 객체 지향 설계의 다섯 가지 핵심 SOLID 원칙 중 네 번째 원칙이다. 이 원칙은 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 함을 강조한다. 다시 말해, 하나의 범용적인 인터페이스보다는 클라이언트의 특정 요구에 맞춘 여러 개의 세분화된 인터페이스를 설계하는 것이 바람직하다.
이 원칙을 위반하는 전형적인 예는 '비대한 인터페이스' 또는 '팽창된 인터페이스'이다. 예를 들어, 프린터, 스캐너, 팩스 기능을 모두 포함하는 MultifunctionDevice 인터페이스를 설계한다고 가정해 보자. 이 인터페이스를 구현하는 실제 프린터 객체는 스캔이나 팩스 기능을 전혀 사용하지 않음에도 불구하고 해당 메서드들을 구현해야 하는 부담을 지게 된다. 이는 인터페이스 분리 원칙에 맞지 않는다. 대신, Printer, Scanner, FaxMachine과 같이 기능별로 인터페이스를 분리하면, 클라이언트는 정확히 필요한 기능에만 의존할 수 있다.
이 원칙을 준수하면 시스템의 유지보수성과 유연성이 향상된다. 인터페이스가 분리되면, 한 인터페이스의 변경이 이를 사용하지 않는 다른 클라이언트들에게 영향을 미치지 않는다. 또한, 새로운 기능을 추가하거나 기존 기능을 수정할 때 관련된 인터페이스만 수정하면 되므로 변경 범위가 최소화된다. 결과적으로 결합도는 낮아지고, 코드의 재사용성과 이해하기 쉬운 구조를 얻을 수 있다.
의존관계 역전 원칙은 객체 지향 설계의 다섯 가지 핵심 원칙(SOLID) 중 마지막 원칙을 가리킨다. 이 원칙은 고수준 모듈이 저수준 모듈에 의존해서는 안 되며, 둘 모두 추상화에 의존해야 한다고 명시한다. 또한, 추상화는 세부 사항에 의존해서는 안 되며, 세부 사항이 추상화에 의존해야 한다. 이는 전통적인 계층형 아키텍처에서의 의존성 방향을 뒤집는 개념이다.
일반적으로 상위 수준의 정책이나 비즈니스 로직을 담당하는 모듈은 하위 수준의 구체적인 구현(예: 데이터베이스 접근, 파일 입출력)에 의존하게 된다. 의존관계 역전 원칙은 이러한 의존 관계를 끊고, 양쪽 모두 추상적인 인터페이스나 추상 클래스에 의존하도록 설계할 것을 요구한다. 예를 들어, 결제 처리 모듈이 특정 신용카드 회사의 구체적인 결제 API에 직접 의존하는 대신, '결제 처리기'라는 추상 인터페이스에 의존하도록 만든다. 그러면 실제 신용카드 결제 구현체는 이 추상 인터페이스를 구체화하는 방식으로 작성된다.
이 원칙을 적용하면 시스템의 유연성과 재사용성이 크게 향상된다. 고수준 모듈은 저수준 모듈의 구현 변경으로부터 보호받으며, 저수준 모듈을 다른 구현으로 쉽게 교체할 수 있다. 이는 의존성 주입 패턴의 이론적 기반이 되며, 단위 테스트를 용이하게 만드는 핵심 요소이다. 테스트 시 실제 데이터베이스나 외부 서비스 대신 가짜 객체(모의 객체)를 쉽게 주입할 수 있기 때문이다.
전통적 의존 관계 | 역전된 의존 관계 |
|---|---|
고수준 모듈 → 저수준 모듈 | 고수준 모듈 → 추상화 ← 저수준 모듈 |
변경이 어렵고 테스트하기 복잡함 | 모듈 간 결합도가 낮아 유지보수와 테스트가 용이함 |
구체적인 구현에 직접 의존 | 추상적인 계약에 의존 |
결국, 의존관계 역전 원칙은 소프트웨어 구성 요소 간의 강한 결합을 약화시키고, 변화에 유연하게 대응할 수 있는 설계의 토대를 마련한다. 이 원칙은 개방-폐쇄 원칙을 가능하게 하는 메커니즘으로도 작동한다.

설계 패턴은 반복적으로 발생하는 설계 문제에 대한 검증된 해결책을 제시하는 재사용 가능한 템플릿이다. 이 패턴들은 특정 프로그래밍 언어나 기술에 종속되지 않는 일반적인 개념으로, 설계의 유연성, 재사용성, 유지보수성을 높이는 데 목적이 있다. 객체 지향 프로그래밍의 맥락에서, 설계 패턴은 객체 간의 관계와 책임을 효과적으로 구성하는 방법을 설명한다. 패턴은 일반적으로 생성, 구조, 행동의 세 가지 주요 범주로 분류된다.
생성 패턴은 객체 생성 메커니즘을 다루며, 객체 생성 과정을 캡슐화하여 시스템이 특정 클래스에 구체적으로 의존하지 않도록 한다. 대표적인 예로는 복잡한 객체를 단계별로 구성하는 빌더 패턴, 객체 생성을 위한 인터페이스를 정의하지만 서브클래스가 인스턴스화할 클래스를 결정하게 하는 팩토리 메서드 패턴, 유일한 인스턴스만을 보장하는 싱글턴 패턴이 있다. 구조 패턴은 클래스나 객체를 더 큰 구조로 조합하는 방법에 중점을 둔다. 이는 상속을 통한 컴포지션을 사용하여 새로운 기능을 구현한다. 주요 패턴으로는 기존 클래스의 인터페이스를 다른 인터페이스로 변환하는 어댑터 패턴, 객체에 동적으로 새로운 책임을 추가하는 데코레이터 패턴, 복잡한 서브시스템에 대한 단순화된 인터페이스를 제공하는 퍼사드 패턴이 포함된다.
행동 패턴은 객체 간의 책임 할당과 알고리즘, 커뮤니케이션 패턴을 중점적으로 다룬다. 객체들이 어떻게 협력하는지와 특정 작업을 수행하는 객체를 어떻게 선택하는지에 대한 솔루션을 제공한다. 널리 사용되는 행동 패턴에는 알고리즘의 구조를 정의하면서 일부 단계를 서브클래스로 연기하는 템플릿 메서드 패턴, 객체 사이의 일대다 의존 관계를 정의하여 한 객체의 상태가 변할 때 모든 의존 객체가 자동으로 통지받도록 하는 옵저버 패턴, 요청을 객체로 캡슐화하여 매개변수화하고 지연 실행, 취소 등을 가능하게 하는 커맨드 패턴이 있다.
이러한 패턴들은 단독으로 사용되기도 하지만, 종종 복합적으로 적용되어 더 복잡한 설계 문제를 해결한다. 설계 패턴을 적용할 때는 문제의 맥락과 패턴의 의도, 결과를 정확히 이해하는 것이 중요하다. 패턴은 만능 해결사가 아니라 특정 상황에서 발생하는 문제에 대한 지침이므로, 맹목적인 적용보다는 상황에 맞는 적절한 선택이 필요하다. 잘 알려진 패턴 카탈로그로는 갱 오브 포의 저서가 있다[6].
생성 패턴은 객체 생성 메커니즘을 다루며, 객체를 생성하는 방법을 추상화하여 시스템이 특정 클래스에 구체적으로 의존하지 않도록 돕는다. 이 패턴들은 객체 생성 과정을 캡슐화함으로써 코드의 유연성과 재사용성을 높인다. 특히 복잡한 객체의 구성이나 생성 조건이 변할 때 유용하게 적용된다.
주요 생성 패턴에는 싱글턴 패턴, 팩토리 메서드 패턴, 추상 팩토리 패턴, 빌더 패턴, 프로토타입 패턴 등이 있다. 각 패턴은 서로 다른 생성 문제를 해결한다. 예를 들어, 싱글턴 패턴은 클래스의 인스턴스를 하나만 생성하도록 보장하고, 팩토리 메서드 패턴은 객체 생성을 서브클래스에 위임한다. 빌더 패턴은 복잡한 객체의 생성 과정을 단계별로 분리하여 표현한다.
아래 표는 주요 생성 패턴과 그 목적을 요약한 것이다.
패턴 이름 | 주요 목적 |
|---|---|
클래스의 인스턴스를 오직 하나만 생성하고 전역 접근점을 제공한다. | |
객체 생성 로직을 서브클래스에 위임하여 클라이언트 코드와 구체 클래스를 분리한다. | |
관련된 객체들의 군을 생성하기 위한 인터페이스를 제공한다. | |
복잡한 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 과정에서 다른 표현을 만들 수 있게 한다. | |
기존 인스턴스를 복제하여 새로운 객체를 생성한다. |
이러한 패턴들을 적절히 활용하면 시스템의 결합도를 낮추고, 새로운 유형의 객체 추가나 생성 방식 변경에 쉽게 대응할 수 있다. 생성 로직이 클라이언트 코드로부터 숨겨지므로, 코드 유지보수성과 확장성이 향상된다.
구조 패턴은 클래스나 객체를 조합하여 더 큰 구조를 형성하는 방법을 정의하는 디자인 패턴의 한 범주이다. 이 패턴들은 시스템의 구조를 유연하고 효율적으로 만들기 위해, 클래스 간의 상속이나 객체 간의 합성(컴포지션) 관계를 어떻게 설정할지에 중점을 둔다. 주로 기존 코드를 재구성하지 않고도 새로운 기능을 추가하거나, 인터페이스를 호환시키는 등의 문제를 해결한다.
대표적인 구조 패턴으로는 어댑터 패턴, 데코레이터 패턴, 퍼사드 패턴, 프록시 패턴, 컴포지트 패턴, 브리지 패턴, 플라이웨이트 패턴 등이 있다. 각 패턴은 특정 구조적 문제를 해결한다. 예를 들어, 어댑터 패턴은 호환되지 않는 인터페이스를 가진 클래스들이 함께 작동하도록 중간에 변환기를 두는 역할을 한다. 데코레이터 패턴은 객체에 동적으로 새로운 책임을 추가할 수 있게 하여, 서브클래싱을 통한 기능 확장보다 유연한 대안을 제공한다.
이 패턴들은 주로 객체 합성을 강조하여 상속의 단점을 보완하고 시스템의 결합도를 낮추는 데 기여한다. 아래 표는 주요 구조 패턴과 그 목적을 정리한 것이다.
패턴 이름 | 주요 목적 |
|---|---|
호환되지 않는 인터페이스 간의 연결 | |
추상화와 구현의 분리로 확장성 향상 | |
객체들을 트리 구조로 구성하여 부분-전체 계층 표현 | |
객체에 동적으로 책임 추가 | |
복잡한 서브시스템에 대한 단순화된 인터페이스 제공 | |
많은 수의 작은 객체들을 효율적으로 공유 | |
다른 객체에 대한 접근을 제어하는 대리자 제공 |
구조 패턴을 적용함으로써 얻는 이점은 시스템의 부분들을 독립적으로 발전시킬 수 있다는 점이다. 이는 특히 대규모 소프트웨어 시스템에서 코드의 재사용성을 높이고 유지보수를 용이하게 만든다. 예를 들어, 퍼사드 패턴은 복잡한 라이브러리나 프레임워크의 내부 구조를 숨기고 사용자에게 깔끔한 API만을 노출시켜 사용 편의성을 극대화한다.
행동 패턴은 객체 간의 책임 할당과 통신 방식을 정의하는 패턴이다. 이 패턴들은 알고리즘이나 책임을 수행하는 객체를 캡슐화하고, 객체들 사이의 상호작용을 유연하게 만드는 데 중점을 둔다. 행동 패턴은 시스템의 동적인 측면을 구조화하여 객체 간의 결합도를 낮추고 코드 재사용성을 높이는 데 기여한다.
주요 행동 패턴으로는 전략 패턴, 옵저버 패턴, 커맨드 패턴, 이터레이터 패턴, 템플릿 메서드 패턴 등이 있다. 각 패턴은 특정한 상호작용 문제를 해결한다. 예를 들어, 전략 패턴은 알고리즘 군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만든다. 옵저버 패턴은 객체 사이의 일대다 의존 관계를 정의하여 한 객체의 상태가 변할 때 그 객체에 의존하는 모든 객체들이 자동으로 통지받고 갱신될 수 있게 한다.
다른 행동 패턴의 예와 그 역할은 다음과 같다.
패턴 이름 | 주요 역할 |
|---|---|
요청을 처리할 수 있는 객체 사슬을 따라 요청을 전달한다. | |
객체 집합이 상호작용하는 방식을 캡슐화하는 객체를 제공한다. | |
객체의 내부 상태를 캡슐화하여 나중에 이 상태로 복구할 수 있게 한다. | |
객체 구조의 요소들에 대해 수행할 연산을 표현한다. 연산을 변경하지 않고도 새로운 연산을 정의할 수 있다. | |
객체의 내부 상태가 바뀜에 따라 객체의 행동을 바꿀 수 있게 한다. |
이러한 패턴들은 복잡한 제어 흐름을 관리하거나, 객체 간의 통신을 효율적으로 조직화할 때 적용된다. 행동 패턴을 적절히 사용하면 시스템의 유연성과 확장성을 크게 향상시킬 수 있으며, 특히 변경이 빈번한 비즈니스 로직을 구현할 때 유용하다.

객체 지향 설계를 실천하는 구체적인 접근법을 제시하는 여러 설계 방법론이 존재한다. 이 방법론들은 소프트웨어의 복잡성을 관리하고, 유지보수성을 높이며, 비즈니스 요구사항을 효과적으로 반영하는 데 초점을 맞춘다.
도메인 주도 설계(Domain-Driven Design, DDD)는 복잡한 비즈니스 도메인을 중심으로 소프트웨어를 설계하는 접근법이다. 이 방법론의 핵심은 개발자와 도메인 전문가가 공통 언어(유비쿼터스 언어)를 구축하여 소통하고, 이를 바탕으로 도메인의 핵심 개념과 규칙을 도메인 모델로 표현하는 데 있다. DDD는 엔티티, 값 객체, 애그리게이트, 리포지토리와 같은 패턴을 활용하여 도메인의 복잡성을 코드 구조에 직접 반영한다. 이를 통해 소프트웨어가 비즈니스의 변화에 유연하게 대응할 수 있도록 한다.
테스트 주도 개발(Test-Driven Development, TDD)은 설계와 구현 과정에 테스트를 선도적으로 활용하는 방법론이다. TDD의 기본 주기는 '실패하는 테스트 작성' -> '최소한의 코드로 테스트 통과' -> '코드 리팩토링'의 세 단계로 이루어진다[7]. 이 접근법은 구현 전에 사용자 관점에서의 인터페이스와 동작을 명확히 정의하도록 강제함으로써, 깔끔한 인터페이스와 낮은 결합도를 가진 설계를 유도한다. 또한, 자동화된 테스트 스위트를 구축함으로써 코드 변경에 대한 신뢰도를 높이고 리팩토링을 안전하게 수행할 수 있는 기반을 마련한다.
이 두 방법론은 상호 보완적으로 적용될 수 있다. 예를 들어, DDD로 도메인 모델의 경계와 관계를 설계한 후, TDD를 통해 각 애그리게이트나 도메인 서비스의 구체적인 행동을 구현하는 방식이다. 이러한 설계 방법론들은 단순히 코드를 작성하는 기술이 아니라, 지속적으로 발전하는 소프트웨어의 품질과 구조를 보장하기 위한 체계적인 사고와 실천의 틀을 제공한다.
도메인 주도 설계(Domain-Driven Design, DDD)는 복잡한 소프트웨어의 핵심인 도메인 모델을 설계와 구현의 중심에 두는 접근 방식이다. 에릭 에반스가 2003년 저서 《Domain-Driven Design: Tackling Complexity in the Heart of Software》에서 정립한 개념이다. 이 방법론은 개발자와 도메인 전문가가 공통 언어(유비쿼터스 언어)를 구축하고, 이를 바탕으로 소프트웨어의 구조와 코드 자체를 반영하도록 유도한다.
도메인 주도 설계의 핵심 구성 요소는 도메인 모델, 엔티티, 값 객체, 애그리거트, 리포지토리, 도메인 서비스 등이다. 특히 애그리거트는 연관된 객체들을 하나의 군집으로 묶어 일관된 경계를 제공하는 중요한 패턴이다. 설계 과정에서는 바운디드 컨텍스트를 식별하는 것이 핵심 단계이다. 바운디드 컨텍스트는 특정 도메인 모델이 정의되고 적용되는 명시적인 경계를 의미하며, 각 컨텍스트는 독립적인 유비쿼터스 언어를 가진다.
도메인 주도 설계는 크게 전략적 설계와 전술적 설계 두 층위로 나뉜다. 전략적 설계는 바운디드 컨텍스트를 식별하고, 컨텍스트 간의 관계(공유 커널, 고객/공급자, 준수자, 공개 호스트 서비스 등)를 매핑하는 높은 수준의 작업에 초점을 맞춘다. 전술적 설계는 개별 바운디드 컨텍스트 내에서 엔티티, 값 객체, 애그리거트와 같은 구체적인 패턴을 적용하여 풍부한 도메인 모델을 구현하는 것을 다룬다.
이 방법론은 특히 비즈니스 규칙이 복잡하고 변화가 빈번한 엔터프라이즈 애플리케이션 개발에 적합하다. 도메인 로직이 도메인 모델에 명확하게 응집되어 유지보수성과 확장성을 높이는 반면, 초기 설계 비용이 크고 학습 곡선이 가파르다는 단점도 존재한다.
테스트 주도 개발(Test-Driven Development, TDD)은 소프트웨어 공학에서 코드를 작성하기 전에 먼저 테스트 케이스를 작성하는 개발 방법론이다. 이 방법론은 애자일 개발 프로세스와 밀접하게 연관되어 있으며, 설계와 테스트를 동시에 진행하는 특징을 가진다. TDD의 핵심 사이클은 '실패하는 테스트 작성(Red) -> 최소한의 코드 작성으로 테스트 통과(Green) -> 코드 리팩토링(Refactor)'의 세 단계로 구성된다. 이 반복적인 과정을 통해 개발자는 명확한 요구사항을 정의하고, 깔끔한 인터페이스를 설계하며, 불필요한 기능을 구현하지 않도록 유도받는다.
TDD의 주요 실천법은 다음과 같다. 먼저, 구현해야 할 기능 하나에 대해 실패하는 단위 테스트를 먼저 작성한다. 이 테스트는 아직 존재하지 않는 기능을 검증하므로 당연히 실패한다. 그 다음, 이 테스트를 통과시키기 위한 최소한의 코드만을 작성한다. 이 단계에서는 설계의 우아함이나 코드의 중복을 고려하지 않고 단순히 테스트를 성공시키는 데 집중한다. 마지막으로, 테스트가 통과된 상태에서 코드를 리팩토링하여 중복을 제거하고 구조를 개선한다. 이 세 단계는 매우 짧은 주기(보통 몇 분)로 반복된다.
TDD를 적용하면 여러 가지 설계상의 이점이 발생한다. 가장 큰 장점은 테스트 가능한 설계를 자연스럽게 유도한다는 점이다. 테스트를 먼저 작성하려면 모듈 간의 결합도가 낮고 각 모듈의 응집도가 높아야 하므로, 관심사의 분리 원칙을 따르는 구조가 만들어진다. 또한, 테스트 스위트가 자동화된 회귀 테스트 역할을 하여 리팩토링의 안전성을 보장한다. 이는 설계를 지속적으로 개선할 수 있는 자신감을 개발자에게 제공한다.
그러나 TDD는 학습 곡선이 존재하고, 초기 개발 속도가 느려질 수 있다는 비판도 있다. 특히 데이터베이스, 사용자 인터페이스, 외부 시스템과의 연동과 같이 테스트하기 어려운 영역을 다룰 때는 추가적인 기술(예: 목 객체(Mock Object) 사용)이 필요하다. 효과적인 TDD 실천을 위해서는 격리된 단위 테스트를 작성하는 능력과 리팩토링 기술이 필수적이다.

객체 지향 분석은 소프트웨어 요구사항을 이해하고, 이를 객체 지향 시스템의 설계로 전환하기 위한 초기 단계의 활동이다. 이 과정은 사용자 요구를 식별하고, 핵심 개념과 그들 간의 관계를 도출하며, 시스템의 정적 구조를 모델링하는 데 중점을 둔다. 분석의 결과물은 이후의 객체 지향 설계 단계에서 구체적인 클래스와 상호작용으로 정제되는 기초를 제공한다.
주요 분석 기법으로는 유스케이스 다이어그램과 클래스 다이어그램이 널리 사용된다. 유스케이스 다이어그램은 시스템 외부의 액터와 시스템이 제공해야 하는 기능(유스케이스) 간의 상호작용을 시각적으로 표현한다. 이는 사용자 관점에서 시스템의 행위적 요구사항을 파악하는 데 유용하다. 반면, 클래스 다이어그램은 시스템을 구성할 객체들의 유형(클래스), 그 속성, 연산, 그리고 클래스 간의 관계(연관, 집합, 일반화 등)를 보여주어 시스템의 정적 구조를 정의한다.
효과적인 객체 지향 분석을 위해 다음과 같은 활동이 수행된다.
활동 | 설명 |
|---|---|
도메인 개념 식별 | 문제 영역(도메인)에서 핵심이 되는 명사와 개념을 찾아 후보 클래스로 도출한다. |
책임 할당 | 식별된 각 클래스가 수행해야 할 일(책임)과 가지고 있어야 할 정보(속성)를 결정한다. |
관계 정의 | 클래스 간의 연관, 상속, 의존 등의 관계를 규명하여 전체 구조를 만든다. |
동적 행위 분석 | 객체 간의 메시지 교환 순서를 나타내는 시퀀스 다이어그램 등을 활용해 동적인 상호작용을 분석한다. |
이러한 분석 작업은 사용자, 도메인 전문가, 개발자 간의 소통을 촉진하고, 요구사항의 오해와 누락을 줄이는 데 목적이 있다. 잘 수행된 분석은 변경에 유연하고 이해하기 쉬운 객체 지향 설계의 토대가 된다.
유스케이스 다이어그램은 객체 지향 분석 과정에서 시스템의 기능적 요구사항을 시각적으로 표현하는 도구이다. 이 다이어그램은 시스템이 외부 액터들에게 제공해야 하는 기능, 즉 유스케이스를 중심으로 시스템의 경계와 외부와의 상호작용을 보여준다.
주요 구성 요소는 시스템, 액터, 유스케이스, 관계로 이루어진다. 시스템은 분석 대상의 경계를 사각형으로 표시한다. 액터는 시스템과 상호작용하는 외부 사용자나 다른 시스템을 사람 모양 아이콘으로 나타낸다. 유스케이스는 시스템이 수행하는 개별 기능을 타원형으로 그린다. 관계는 액터와 유스케이스, 또는 유스케이스 간의 연결을 선으로 표현하며, 포함(include), 확장(extend), 일반화(generalization) 관계를 사용한다.
유스케이스 다이어그램은 개발 초기 단계에서 이해관계자와 의사소통하는 데 유용하다. 복잡한 시스템을 사용자의 관점에서 단순화하여 보여주므로, 요구사항을 명확히 하고 시스템의 범위를 정의하는 데 도움을 준다. 또한, 각 유스케이스는 이후에 상세한 시나리오(정상 흐름, 대안 흐름)로 구체화되어 클래스 다이어그램이나 시퀀스 다이어그램과 같은 다른 UML 다이어그램을 만드는 기초가 된다.
클래스 다이어그램은 객체 지향 설계에서 시스템의 정적인 구조를 시각적으로 표현하는 UML 다이어그램이다. 시스템을 구성하는 클래스, 그 속성과 메서드, 그리고 클래스들 사이의 관계를 보여준다. 이 다이어그램은 소프트웨어의 청사진 역할을 하며, 개발자와 설계자 간의 의사소통과 시스템 이해를 돕는다.
클래스 다이어그램은 크게 세 부분으로 구성된 상자를 사용하여 각 클래스를 표현한다. 상단에는 클래스 이름, 중간에는 속성 또는 필드, 하단에는 메서드 또는 연산이 위치한다. 클래스 간의 관계는 다양한 선과 화살표로 표시된다. 주요 관계에는 연관 관계, 일반화 관계(상속), 집합 관계, 합성 관계, 의존 관계 등이 있다.
관계 유형 | 표기법 | 설명 |
|---|---|---|
연관 | 실선 | 클래스들이 서로 알고 있음을 나타내는 일반적인 연결이다. |
일반화 | 실선 + 속이 빈 화살표 | 한 클래스(자식)가 다른 클래스(부모)로부터 상속받는 관계다. |
집합 | 실선 + 속이 빈 마름모 | '전체-부분' 관계로, 부분이 전체와 독립적으로 존재할 수 있다. |
합성 | 실선 + 속이 찬 마름모 | '전체-부분' 관계로, 부분이 전체와 생명주기를 함께한다. |
의존 | 점선 + 열린 화살표 | 한 클래스의 변화가 다른 클래스에 영향을 줄 수 있는 관계다. |
이 다이어그램은 분석 단계에서 도메인 모델을 만들거나, 설계 단계에서 상세한 구현 구조를 정의할 때 활용된다. 또한 코드 생성의 기초가 되거나, 기존 코드로부터 리버스 엔지니어링을 통해 생성될 수도 있다. 올바르게 작성된 클래스 다이어그램은 시스템의 복잡성을 관리하고, 결합도를 낮추며, 응집도를 높이는 설계를 이끌어내는 데 기여한다.

코드 품질을 측정하고 개선하기 위한 핵심 지표로 결합도와 응집도가 있다. 결합도는 모듈 간의 상호 의존 정도를 나타내며, 응집도는 하나의 모듈 내부 구성 요소들이 서로 관련된 정도를 나타낸다. 좋은 객체 지향 설계는 낮은 결합도와 높은 응집도를 지향한다. 낮은 결합도는 한 모듈의 변경이 다른 모듈에 미치는 영향을 최소화하여 시스템의 유지보수성과 재사용성을 높인다. 높은 응집도는 모듈이 명확하고 단일한 책임을 가지도록 하여 코드의 가독성과 이해도를 향상시킨다.
지표 | 설명 | 바람직한 상태 |
|---|---|---|
결합도 | 모듈 간의 의존 관계 강도 | 낮음 |
응집도 | 모듈 내부 요소들의 기능적 관련성 | 높음 |
순환 의존성은 두 개 이상의 모듈이 서로를 직접적 또는 간접적으로 참조하는 의존 관계를 형성하는 상황을 말한다. 이는 시스템을 변경하기 어렵게 만들고, 컴파일 시간을 증가시키며, 모듈의 독립적인 재사용을 방해한다. 순환 의존성을 해결하는 일반적인 방법으로는 의존관계 역전 원칙을 적용하거나, 중간 매개체(예: 인터페이스, 추상 클래스)를 도입하여 의존 방향을 단방향으로 재구성하는 것이 있다.
이 외에도 코드 품질을 평가하는 지표로는 복잡도(순환 복잡도 등), 유지보수성 지수, 코드 중복률, 테스트 커버리지 등이 있다. 이러한 지표들은 정적 분석 도구를 통해 측정되며, 지속적인 통합 과정에서 모니터링되어 설계 결함을 조기에 발견하고 개선하는 데 활용된다.
결합도는 모듈 또는 클래스 간에 서로 의존하는 정도를 나타내는 지표이다. 낮은 결합도는 한 모듈의 변경이 다른 모듈에 미치는 영향을 최소화하여 시스템의 유지보수성과 재사용성을 높인다. 결합도는 내용 결합도, 공통 결합도, 외부 결합도, 제어 결합도, 스탬프 결합도, 자료 결합도 등으로 세분화하여 평가할 수 있다[8]. 객체 지향 설계에서는 인터페이스를 통한 상호작용, 의존관계 주입 활용, 직접적인 전역 변수 사용 피하기 등의 방법으로 결합도를 낮추려고 노력한다.
응집도는 하나의 모듈 또는 클래스 내부의 요소들이 단일한 목적이나 책임을 위해 얼마나 밀접하게 연관되어 있는지를 측정한다. 높은 응집도는 모듈이 명확하고 집중된 기능을 수행함을 의미하며, 이는 코드의 가독성, 이해도, 그리고 다시 사용 가능성을 향상시킨다. 응집도의 유형에는 기능적 응집도, 순차적 응집도, 교환적 응집도, 절차적 응집도, 시간적 응집도, 논리적 응집도, 우연적 응집도 등이 있다.
좋은 객체 지향 설계는 일반적으로 "낮은 결합도와 높은 응집도"를 지향한다. 이 두 원칙은 서로 상호보완적 관계에 있다. 높은 응집도를 가진 모듈은 명확한 책임을 가지므로, 다른 모듈과 불필요하게 얽히지 않아 자연스럽게 결합도가 낮아지는 경향이 있다. 반대로 결합도가 지나치게 높으면 모듈의 경계가 모호해져 응집도가 떨어지기 쉽다.
품질 지표 | 설명 | 바람직한 상태 | 나쁜 상태의 예 |
|---|---|---|---|
결합도 | 모듈 간 상호 의존성 | 낮음 | 클래스 A가 클래스 B의 내부 필드를 직접 변경함 |
응집도 | 모듈 내부 요소의 관련성 | 높음 |
|
이러한 지표는 단일 책임 원칙과 깊은 연관이 있다. 하나의 클래스가 하나의 명확한 책임만 가질 때 응집도는 높아지고, 다른 클래스와는 필요한 최소한의 인터페이스로만 소통할 때 결합도는 낮아진다.
순환 의존성은 두 개 이상의 소프트웨어 모듈이 서로를 직접적이거나 간접적으로 참조하는 상태를 말한다. 예를 들어, 클래스 A가 클래스 B를 사용하고, 클래스 B가 다시 클래스 A를 사용하는 경우가 전형적인 직접 순환 의존성이다. 간접 순환 의존성은 A가 B를, B가 C를, C가 다시 A를 참조하는 식으로 의존 관계가 고리를 이루는 경우에 발생한다. 이는 컴파일 시간을 증가시키고, 모듈의 재사용성을 현저히 떨어뜨린다. 또한 시스템의 한 부분을 변경하려 할 때 예상치 못한 다른 부분에 영향을 미칠 수 있어 유지보수를 어렵게 만든다.
순환 의존성을 해결하는 일반적인 방법은 의존관계 역전 원칙을 적용하거나, 새로운 추상화 계층을 도입하는 것이다. 의존관계 역전 원칙을 통해 두 클래스가 모두 의존하는 인터페이스나 추상 클래스를 생성하면, 구체적인 구현체 간의 직접적인 의존을 끊을 수 있다. 또 다른 방법은 순환 관계에 있는 책임 중 일부를 제3의 클래스로 분리하여 의존 방향을 단방향으로 재조정하는 것이다.
아래 표는 순환 의존성의 유형과 해결 방안의 예시를 보여준다.
유형 | 설명 | 해결 방안 예시 |
|---|---|---|
직접 순환 | 클래스 A ↔ 클래스 B | 의존관계 역전 원칙 적용, 공통 인터페이스 도입 |
간접 순환 | 클래스 A → 클래스 B → 클래스 C → 클래스 A | 중개자 패턴 사용, 책임을 새 클래스로 이동 |
패키지/모듈 수준 순환 | 패키지 P의 모듈이 패키지 Q를 참조하고, Q의 모듈이 다시 P를 참조 | 계층화된 아키텍처 적용, 의존성 방향 재설계 |
이러한 문제를 사전에 탐지하기 위해 정적 분석 도구를 사용하는 것이 일반적이다. 많은 통합 개발 환경과 빌드 도구는 프로젝트 내의 순환 의존성을 경고하거나 빌드를 실패시키는 기능을 제공한다. 순환 의존성을 제거하는 것은 결합도를 낮추고 응집도를 높이는 데 기여하며, 결과적으로 더 유연하고 테스트하기 쉬운 소프트웨어 설계로 이어진다.