DailyCode
[SOLID] 인터페이스 분리 법칙(ISP) 본문
인터페이스 분리 법칙(Interface Segregation Principle, ISP)은 객체 지향 프로그래밍에서 SOLID 원칙 중 하나로, 인터페이스의 설계와 관련된 원칙입니다.
이 법칙은 큰 범용 인터페이스를 작은, 특정 목적에 맞는 여러 인터페이스로 분리하는 것을 권장하는 법칙입니다. 이를 통해 코드의 결합도를 낮추고, 유지 보수와 확장성을 향상시킬 수 있습니다.
정의
• 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
- 이 원칙은 각각의 클라이언트에게 필요한 기능만을 제공하는 인터페이스를 여러 개 작성하는 것이, 모든 기능을 포함하는 하나의 거대한 인터페이스를 작성하는 것보다 훨씬 효과적이라는 것을 강조합니다.
• 기능 구현 및 상속 시 불필요한 메서드가 구현될 필요는 없다.
- 클래스가 구현해야 하는 인터페이스는 해당 클래스가 실제로 사용하는 메서드 들로만 구성되어야 합니다. 이를 통해 불필요한 메서드를 구현하는 것을 방지하고, 클래스의 책임과 결합도를 최소화할 수 있습니다.
• 클라이언트가 사용하지 않는 메서드에 의존하는 경우 추상화가 잘못되었음을 의미한다.
- 인터페이스 분리 원칙을 따르지 않으면, 클라이언트는 필요하지 않은 메서드 에도 의존하게 됩니다. 이는 클라이언트와 인터페이스 간의 불필요한 결합을 만들어 유지 보수와 확장성에 문제가 발생할 수 있으며, 이러한 문제는 추상화가 잘못되었음을 나타냅니다.
장점
- 낮은 결합도 (Low Coupling): ISP를 따르면, 인터페이스를 작고 목적에 맞게 분리함으로써 클래스와 인터페이스 간의 결합도를 낮출 수 있습니다. 이로 인해 클래스가 서로 덜 의존하게 되며, 변경에 더 유연하게 대응할 수 있습니다.
- 높은 응집도 (High Cohesion): 작고 목적에 맞게 분리된 인터페이스는 책임이 잘 정리되어 있어 응집도가 높습니다. 이를 통해 코드의 구조가 명확해지고, 관련된 기능이 한 곳에 모여 있게 됩니다.
- 높은 재사용성 (High Reusability): 작은 인터페이스를 사용하면, 필요한 기능만을 제공하는 인터페이스를 각각의 클라이언트에 적용할 수 있습니다. 이렇게 되면 인터페이스 및 구현 클래스의 재사용성이 증가하게 됩니다.
- 쉬운 유지 보수 (Easier Maintenance): ISP를 따르면, 인터페이스가 작고 명확해지기 때문에 변경 사항이 발생했을 때 영향을 받는 부분이 최소화됩니다. 따라서 유지 보수가 더욱 쉬워집니다.
- 높은 확장성 (High Extensibility): ISP를 적용하면 각 인터페이스가 작고 독립적이기 때문에, 새로운 기능을 추가하거나 기존 기능을 변경하는 경우에도 기존 시스템에 영향을 주지 않고 확장할 수 있습니다.
- 쉬운 테스트 (Easier Testing): 작은 인터페이스를 사용하면, 테스트가 더 쉬워집니다. 각 인터페이스는 특정 책임을 가지고 있으므로, 테스트할 범위가 명확하고 간단해집니다.
예제
import Foundation
//Bad Case: 불필요한 프로토콜 상속으로 메서드 구현
protocol Country {
func kingName() -> String?
func setOndolTemperature() -> CGFloat?
}
class Korea: Country {
func kingName() -> String? {
return nil
}
func setOndolTemperature() {
return 32.5
}
}
class Japan: Country {
func kingName() -> String? {
return “나루히토“
}
func setOndolTemperature() {
return nil
}
}
let korea = Korea()
let data = korea.kingName()
data! << nil 참조로 오류 발생
이 나쁜 예제에서 Country 프로토콜은 kingName() 메서드와 setOndolTemperature() 메서드를 모두 포함하고 있습니다. 하지만 실제로는 한국에는 온돌이 있지만 일본에는 없으며, 일본에는 왕이 있지만 한국에는 왕이 없습니다. 따라서 각 클래스에서는 한 가지 메서드만 구현해야 하지만, 이 예제에서는 두 메서드 모두 구현되어 있습니다.
이렇게 되면 다음과 같은 문제가 발생합니다.
- 오버헤드 발생: 각 클래스에서 불필요한 메서드를 구현하게 되면, 메모리와 실행 시간에 추가적인 오버헤드가 발생합니다. 이는 프로그램의 성능을 저하시킬 수 있습니다.
- 예기치 않은 오류: 불필요한 메서드에 접근할 경우 예기치 않은 오류가 발생할 수 있습니다. 예를 들어, Korea 클래스에서 kingName() 메서드를 호출하면 nil이 반환되지만, 이 값을 참조할 때 nil 접근 이슈가 발생할 수 있습니다. 또한, 별도의 데이터 참조로 인한 문제도 발생할 수 있습니다.
이러한 문제를 해결하기 위해 인터페이스 분리 원칙(ISP)을 적용해야 합니다. ISP를 따르면, 인터페이스를 작고 목적에 맞게 분리하여 각 클래스가 필요한 메서드만 구현하도록 할 수 있습니다. 이를 통해 결합도를 낮추고 응집도를 높이며, 예기치 않은 오류를 방지할 수 있습니다
Refactoring
// Refactoring
protocol onDol {
func setOndolTemperature() -> CGFloat?
}
protocol King {
func kingName() -> String?
}
class Korea: onDol {
func setOndolTemperature() {
return 32.5
}
}
class Japan: King {
func kingName() -> String? {
return “나루히토“
}
}
이 코드는 인터페이스 분리 원칙(ISP)에 따라 리팩토링된 예제입니다. 이전에는 불필요한 메서드를 각 클래스에서 구현해야 했지만, 이 예제에서는 각 클래스가 필요한 메서드만 구현하도록 프로토콜을 분리했습니다.
1. 프로토콜 세분화: 이전의 Country 프로토콜은 onDol 프로토콜과 King 프로토콜로 분할되었습니다.
- onDol 프로토콜은 setOndolTemperature() 메서드를 포함하고 있습니다.
- King 프로토콜은 kingName() 메서드를 포함하고 있습니다.
2. 클래스별 구현:
- Korea 클래스는 onDol 프로토콜을 구현하며, setOndolTemperature() 메서드만 구현합니다. 이 메서드는 온돌 온도를 설정하고 반환합니다.
- Japan 클래스는 King 프로토콜을 구현하며, kingName() 메서드만 구현합니다. 이 메서드는 일본 왕의 이름을 반환합니다.
목표
이 리팩토링의 목표는 각 클래스가 필요한 작업 집합만 실행하도록 일련의 작업을 더 작은 집합으로 분할하는 것입니다.
이를 통해 각 클래스가 필요한 메서드만 구현하게 되어 코드의 결합도를 낮추고 응집도를 높일 수 있습니다. 이러한 구조는 유지 보수와 확장성이 향상되며, 테스트가 더 쉬워집니다.
'디자인 패턴' 카테고리의 다른 글
[SOLID] 의존관계 역전 법칙(DIP) (0) | 2023.04.05 |
---|---|
[SOLID] 리스코프 치환 법칙(LSP) (0) | 2023.03.25 |
[SOLID] 개방 폐쇄 원칙(OCP) (0) | 2023.03.24 |
[SOLID] 단일 책임 원칙(SRP) (0) | 2023.03.16 |
템플릿 메서드 패턴(TemplateMethod Pattern) (0) | 2023.03.09 |