본문 바로가기
Swift

[Swift] 인터페이스 설계: 클래스 기반과 컴포지션 패턴의 비교

by Jimmy_iOS 2024. 8. 13.

소프트웨어 설계에서 인터페이스와 클래스 구조를 어떻게 구성하느냐에 따라

코드의 유지보수성, 확장성, 재사용성이 크게 달라집니다.

이번 포스팅에서는 자동차 인터페이스를 설계하는 두 가지 접근 방식

단일 클래스 기반 설계컴포지션 패턴을 비교해보고,

이를 높은 응집력(High Cohesion)단일 책임 원칙(SRP) 관점에서 해석해 보겠습니다.

1️⃣ 첫 번째 예제: 단일 클래스 기반 설계

첫 번째 방법은 자동차의 각 기능을 구체적인 클래스에서 직접 구현하는 방식입니다.

예를 들어 현대소나타, 현대아이오닉, 현대코나하이브리드와 같은 클래스는 각각 달리기, 멈추기

그리고 각 자동차 유형에 맞는 경고 기능을 구현합니다.

💡 인터페이스 정의

protocol 달리기능력 {
	func 달리기()
}

protocol 멈춤능력 {
	func 멈추기()
}

protocol 엔진경고능력 {
	func 엔진오일경고등표시()
}

protocol 배터리경고능력 {
	func 베터리과열표시()
}

protocol 자동차: 달리기능력, 멈춤능력 {
	var isRunning: Bool
}

protocol 엔진자동차: 자동차, 엔진경고능력 {
	var isEngineOilWarningOn: Bool
}

protocol 전기자동차: 자동차, 배터리경고능력 {
	var isBatteryOverheatWarningOn: Bool
}

protocol 하이브리드자동차: 자동차, 엔진경고능력, 배터리경고능력 {
	var isEngineOilWarningOn: Bool
	var isBatteryOverheatWarningOn: Bool
}

💡 클래스 구현

각 자동차 클래스는 자신의 기능을 구현하며, 상태를 관리하기 위해

isRunning, isEngineOilWarningOn, isBatteryOverheatWarningOn 변수를 사용합니다.

class 현대소나타: 엔진자동차 {
	var isRunning: Bool = false
	var isEngineOilWarningOn: Bool = false

	func 달리기() {
		isRunning = true
		print("현대소나타가 달립니다.")
	}

	func 멈추기() {
		isRunning = false
		print("현대소나타가 멈춥니다.")
	}

	func 엔진오일경고등표시() {
		isEngineOilWarningOn = true
		print("현대소나타의 엔진오일 경고등이 켜집니다.")
	}
}

class 현대아이오닉: 전기자동차 {
	var isRunning: Bool = false
	var isBatteryOverheatWarningOn: Bool = false

	func 달리기() {
		isRunning = true
		print("현대아이오닉이 달립니다.")
	}

	func 멈추기() {
		isRunning = false
		print("현대아이오닉이 멈춥니다.")
	}

	func 베터리과열표시() {
		isBatteryOverheatWarningOn = true
		print("현대아이오닉의 배터리 과열 경고등이 켜집니다.")
	}
}

class 현대코나하이브리드: 하이브리드자동차 {
	var isRunning: Bool = false
	var isEngineOilWarningOn: Bool = false
	var isBatteryOverheatWarningOn: Bool = false

	func 달리기() {
		isRunning = true
		print("현대코나하이브리드가 달립니다.")
	}

	func 멈추기() {
		isRunning = false
		print("현대코나하이브리드가 멈춥니다.")
	}

	func 엔진오일경고등표시() {
		isEngineOilWarningOn = true
		print("현대코나하이브리드의 엔진오일 경고등이 켜집니다.")
	}

	func 베터리과열표시() {
		isBatteryOverheatWarningOn = true
		print("현대코나하이브리드의 배터리 과열 경고등이 켜집니다.")
	}
}

💡 단일 클래스 기반 유닛 테스트

func UnitTest {
    // 현대소나타의 달리기 기능 테스트
    desc("현대소나타가 잘 달리는지 테스트") {
        let 소나타 = 현대소나타()
        소나타.달리기()
        assert(소나타.isRunning == true, "소나타는 달리고 있어야 합니다.")
    }
    
    // 현대아이오닉의 멈추기 기능 테스트
    desc("현대아이오닉이 잘 멈추는지 테스트") {
        let 아이오닉 = 현대아이오닉()
        아이오닉.달리기()
        아이오닉.멈추기()
        assert(아이오닉.isRunning == false, "아이오닉은 멈춰 있어야 합니다.")
    }
    
    // 현대코나하이브리드의 엔진오일 경고등 기능 테스트
    desc("현대코나하이브리드의 엔진오일 경고등이 잘 작동하는지 테스트") {
        let 코나하이브리드 = 현대코나하이브리드()
        코나하이브리드.엔진오일경고등표시()
        assert(코나하이브리드.isEngineOilWarningOn == true, "엔진오일 경고등이 켜져 있어야 합니다.")
    }
    
    // 현대코나하이브리드의 배터리 과열 경고등 기능 테스트
    desc("현대코나하이브리드의 배터리 과열 경고등이 잘 작동하는지 테스트") {
        let 코나하이브리드 = 현대코나하이브리드()
        코나하이브리드.베터리과열표시()
        assert(코나하이브리드.isBatteryOverheatWarningOn == true, "배터리 과열 경고등이 켜져 있어야 합니다.")
    }
}

장점:

  1. 단순성: 각 클래스가 고유의 기능을 직접 구현하고 있으므로, 테스트도 각 클래스의 메서드만 호출해서 검증하면 됩니다. 코드가 직관적이어서 이해하기 쉽습니다.
  2. 테스트 범위 명확성: 클래스 내의 각 기능을 독립적으로 테스트할 수 있습니다. 테스트할 메서드가 클래스 내에 명확히 정의되어 있으므로, 테스트할 범위가 명확합니다.
  3. 적은 복잡성: 각 클래스는 특정 기능을 포함하고 있으며, 테스트 시 다른 클래스에 대한 의존성이 적습니다.

단점:

  1. 중복 코드: 여러 클래스에서 동일한 기능을 구현하기 때문에, 테스트 코드에도 중복이 발생할 수 있습니다. 예를 들어, 달리기() 메서드를 여러 클래스에서 동일하게 테스트해야 합니다.
  2. 확장성의 한계: 새로운 기능이나 자동차 종류를 추가할 때마다 해당 클래스와 유닛 테스트를 모두 추가해야 합니다. 이는 코드 유지보수와 테스트의 복잡성을 증가시킬 수 있습니다.
  3. 테스트 재사용성 부족: 동일한 기능이 다른 클래스에서 반복 구현되기 때문에, 테스트 코드도 중복 작성되어야 하며, 이는 테스트 코드의 재사용성을 저하합니다.

2️⃣ 두 번째 예제: 컴포지션 패턴 사용

두 번째 방법은 컴포지션 패턴을 사용하여 자동차 기능을 개별적으로 구현하고,

이를 조합하여 자동차 클래스를 구성하는 방식입니다.

이 방법은 각 기능을 독립된 인터페이스와 클래스에 분리하여 높은 유연성과 재사용성을 제공합니다.

💡 인터페이스 정의

각 기능을 담당하는 인터페이스를 정의합니다.

protocol 달리기능력 {
	func 달리기() -> Bool
}

protocol 멈춤능력 {
	func 멈추기() -> Bool
}

protocol 엔진경고능력 {
	func 엔진오일경고등표시() -> Bool
}

protocol 배터리경고능력 {
	func 베터리과열표시() -> Bool
}

💡 컴포지션 패턴을 활용한 클래스 구현

컴포지션 패턴을 사용하여 자동차 구현체 클래스를 작성하고, 상태 변수를 추가하여 관리합니다.

class 엔진자동차구현체 {
    private let 달리기능력변수: 달리기능력
    private let 멈추기능력변수: 멈추기능력
    private let 엔진경고능력변수: 엔진경고능력
    
    var isRunning: Bool = false
    var isEngineOilWarningOn: Bool = false
    
    init(
        달리기능력변수: 달리기능력,
        멈추기능력변수: 멈추기능력,
        엔진경고능력변수: 엔진경고능력
    ) {
        self.달리기능력변수 = 달리기능력변수
        self.멈추기능력변수 = 멈추기능력변수
        self.엔진경고능력변수 = 엔진경고능력변수
    }
    
    func 달리기() {
        isRunning = 달리기능력변수.달리기()
    }
    
    func 멈추기() {
        isRunning = 멈추기능력변수.멈추기()
    }
    
    func 엔진오일경고등표시() {
        isEngineOilWarningOn = 엔진경고능력변수.엔진오일경고등표시()
    }
}
class 전기자동차구현체 {
    private let 달리기능력변수: 달리기능력
    private let 멈추기능력변수: 멈추기능력
    private let 베터리경고능력변수: 배터리경고능력
    
    var isRunning: Bool = false
    var isBatteryOverheatWarningOn: Bool = false
    
    init(
        달리기능력변수: 달리기능력,
        멈추기능력변수: 멈추기능력,
        베터리경고능력변수: 배터리경고능력
    ) {
        self.달리기능력변수 = 달리기능력변수
        self.멈추기능력변수 = 멈추기능력변수
        self.베터리경고능력변수 = 베터리경고능력변수
    }
    
    func 달리기() {
        isRunning = 달리기능력변수.달리기()
    }
    
    func 멈추기() {
        isRunning = 멈추기능력변수.멈추기()
    }
    
    func 베터리과열표시() {
        isBatteryOverheatWarningOn = 베터리경고능력변수.베터리과열표시()
    }
}
class 하이브리드자동차구현체 {
    private let 달리기능력변수: 달리기능력
    private let 멈추기능력변수: 멈추기능력
    private let 엔진경고능력변수: 엔진경고능력
    private let 베터리경고능력변수: 배터리경고능력
    
    var isRunning: Bool = false
    var isEngineOilWarningOn: Bool = false
    var isBatteryOverheatWarningOn: Bool = false
    
    init(
        달리기능력변수: 달리기능력,
        멈추기능력변수: 멈추기능력,
        엔진경고능력변수: 엔진경고능력,
        베터리경고능력변수: 배터리경고능력
    ) {
        self.달리기능력변수 = 달리기능력변수
        self.멈추기능력변수 = 멈추기능력변수
        self.엔진경고능력변수 = 엔진경고능력변수
        self.베터리경고능력변수 = 베터리경고능력변수
    }
    
    func 달리기() {
        isRunning = 달리기능력변수.달리기()
    }
    
    func 멈추기() {
        isRunning = 멈추기능력변수.멈추기()
    }
    
    func 엔진오일경고등표시() {
        isEngineOilWarningOn = 엔진경고능력변수.엔진오일경고등표시()
    }
    
    func 베터리과열표시() {
        isBatteryOverheatWarningOn = 베터리경고능력변수.베터리과열표시()
    }
}

💡 각 컴포지션에 대한 유닛 테스트 코드

func UnitTest {
    // 달리기능력 테스트
    desc("현대엔진의 달리기 기능이 잘 동작하는지 테스트") {
        let 엔진 = 현대엔진()
        엔진.달리기()
        assert(엔진.isRunning == true, "현대엔진이 잘 달려야 합니다.")
    }
    
    // 멈춤능력 테스트
    desc("현대브레이크의 멈추기 기능이 잘 동작하는지 테스트") {
        let 브레이크 = 현대브레이크()
        브레이크.멈추기()
        assert(브레이크.isStopped == true, "현대브레이크가 잘 멈춰야 합니다.")
    }
    
    // 엔진경고능력 테스트
    desc("현대엔진경고등의 엔진오일경고등표시 기능이 잘 동작하는지 테스트") {
        let 엔진경고등 = 현대엔진경고등()
        엔진경고등.엔진오일경고등표시()
        assert(엔진경고등.isEngineOilWarningOn == true, "현대엔진경고등이 엔진오일 경고를 잘 표시해야 합니다.")
    }
    
    // 배터리경고능력 테스트
    desc("테슬라배터리경고등의 배터리과열표시 기능이 잘 동작하는지 테스트") {
        let 배터리경고등 = 테슬라베터리경고등()
        배터리경고등.베터리과열표시()
        assert(배터리경고등.isBatteryOverheatWarningOn == true, "테슬라배터리경고등이 배터리 과열 경고를 잘 표시해야 합니다.")
    }

    // 현대엔진과 기아브레이크 조합 테스트
    desc("현대엔진과 기아브레이크의 조합이 잘 동작하는지 테스트") {
        let 엔진 = 현대엔진()
        let 브레이크 = 기아브레이크()
        
        엔진.달리기()
        assert(엔진.isRunning == true, "현대엔진이 잘 달려야 합니다.")
        
        브레이크.멈추기()
        assert(브레이크.isStopped == true, "기아브레이크가 잘 멈춰야 합니다.")
    }
}

class 현대엔진: 달리기능력 {
	func 달리기() {
		ㄴㄴㅁㅁㅇ
	}
}
func UnitTest {
    // 엔진자동차구현체의 달리기 기능 테스트
    desc("엔진자동차구현체가 잘 달리는지 테스트") {
        let 현대소나타 = 엔진자동차구현체(
            달리기능력변수: 현대엔진(),
            멈추기능력변수: 현대브레이크(),
            엔진경고능력변수: 현대엔진경고등()
        )
        엔진자동차.달리기()
        assert(엔진자동차.isRunning == true, "엔진자동차는 달리고 있어야 합니다.")
    }
    
    // 전기자동차구현체의 배터리 과열 경고등 기능 테스트
    desc("전기자동차구현체의 배터리 과열 경고등이 잘 작동하는지 테스트") {
        let 현대아이오닉 = 전기자동차구현체(
            달리기능력변수: 현대모터(),
            멈추기능력변수: 기아브레이크(),
            베터리경고능력변수: 테슬라베터리경고등()
        )
        전기자동차.베터리과열표시()
        assert(전기자동차.isBatteryOverheatWarningOn == true, "배터리 과열 경고등이 켜져 있어야 합니다.")
    }
    
    // 하이브리드자동차구현체의 모든 기능 테스트
    desc("하이브리드자동차구현체의 모든 기능이 잘 작동하는지 테스트") {
        let 하이브리드자동차 = 하이브리드자동차구현체(
            달리기능력변수: 현대모터(),
            멈추기능력변수: 기아브레이크(),
            엔진경고능력변수: 기아엔진경고등(),
            베터리경고능력변수: 테슬라베터리경고등()
        )
        하이브리드자동차.달리기()
        assert(하이브리드자동차.isRunning == true, "하이브리드자동차는 달리고 있어야 합니다.")
        
        하이브리드자동차.멈추기()
        assert(하이브리드자동차.isRunning == false, "하이브리드자동차는 멈춰 있어야 합니다.")
        
        하이브리드자동차.엔진오일경고등표시()
        assert(하이브리드자동차.isEngineOilWarningOn == true, "엔진오일 경고등이 켜져 있어야 합니다.")
        
        하이브리드자동차.베터리과열표시()
        assert(하이브리드자동차.isBatteryOverheatWarningOn == true, "배터리 과열 경고등이 켜져 있어야 합니다.")
    }
}

장점:

  1. 높은 재사용성: 각 기능을 별도의 컴포지션(예: 달리기능력, 멈춤능력)으로 분리하여 테스트하므로, 동일한 기능에 대한 테스트를 여러 클래스에서 재사용할 수 있습니다. 이는 코드 중복을 줄이고 유지보수를 쉽게 만듭니다.
  2. 유연성: 새로운 기능을 추가할 때, 기존 테스트 코드의 재사용이 가능하며, 개별 기능의 동작만 확인하면 되므로 새로운 조합에 대해 쉽게 확장할 수 있습니다.
  3. 의존성 주입 테스트: 각 기능이 독립적으로 테스트될 수 있으므로, 기능 간의 의존성을 주입하여 테스트할 수 있습니다. 이는 SOLID 원칙 중 의존성 역전 원칙을 충실히 따르는 방식으로, 테스트의 유연성과 확장성을 높입니다.

단점:

  1. 테스트 복잡성 증가: 컴포지션을 통해 기능을 구성하므로, 테스트 시에도 각 기능 간의 관계를 명확히 이해하고 테스트를 작성해야 합니다. 이는 초반에 설정할 때 복잡성이 증가할 수 있습니다.
  2. 테스트 인프라 필요: 각 컴포지션 간의 의존성을 관리하고 테스트할 수 있는 인프라가 필요합니다. 예를 들어, 다양한 조합에 대해 테스트하기 위해 별도의 모의 객체(Mock Object)나 스텁(Stub)을 설정할 필요가 있을 수 있습니다.
  3. 초기 학습 곡선: 컴포지션 패턴과 의존성 주입에 대한 개념을 잘 이해하고 있어야 효과적으로 테스트를 작성할 수 있습니다. OOP에 익숙하지 않은 개발자에게는 어려울 수 있습니다.

3️⃣ 두 가지 접근 방식의 설계 원칙 분석

높은 응집력(High Cohesion) 관점

  • 첫 번째 예제: 단일 클래스 기반 설계는 각 클래스가 관련된 모든 기능을 직접 구현하기 때문에 높은 응집력을 가집니다. 현대소나타는 엔진자동차로서 필요한 모든 기능을 한 곳에서 관리하며, 이는 클래스의 목적과 역할을 명확히 정의합니다.
  • 두 번째 예제: 컴포지션 패턴은 기능별로 응집력을 높입니다. 각 기능은 독립된 클래스로 구현되어 있어, 달리기, 멈추기, 경고등 표시 등 각 기능이 자신만의 책임을 가지고 있습니다. 이는 기능별로 응집력을 높이고, 클래스가 특정한 하나의 역할에 집중할 수 있도록 합니다.

단일 책임 원칙(SRP) 관점

  • 첫 번째 예제: 단일 클래스 기반 설계는 SRP를 완전히 준수하지 않습니다. 각 클래스는 여러 책임을 가지고 있어, 예를 들어 현대소나타는 달리기와 멈추기 외에도 엔진오일 경고등을 관리하는 책임까지 가지고 있습니다. 이는 코드 중복과 유지보수의 어려움을 초래할 수 있습니다.
  • 두 번째 예제: 컴포지션 패턴은 SRP를 충실히 따릅니다. 각 기능은 독립된 클래스로 분리되어 있으며, 이를 조합함으로써 특정 기능의 책임이 명확해집니다. 예를 들어, 달리기능력, 멈춤능력, 엔진경고능력 등이 각각의 역할에 집중할 수 있습니다.

결론

높은 응집력과 단일 책임 원칙의 관점에서:

  • 첫 번째 예제는 높은 응집력을 가지지만, SRP를 완전히 따르지 않아 유지보수와 확장성에 한계가 있습니다.
  • 두 번째 예제는 높은 응집력과 SRP를 모두 충족하며, 코드의 유연성과 재사용성을 극대화합니다. 그러나 초기 설정 시 복잡성이 다소 증가할 수 있습니다.

결국, 프로젝트의 특성과 팀의 개발 숙련도에 따라 적절한 설계 방식을 선택하는 것이 중요합니다.

각 접근 방식은 상황에 따라 유리할 수 있으므로, 코드의 재사용성, 확장성, 유지보수성을 고려하여 최선의 결정을 하는것이 중요합니다.