2024.12.19 - [분류 전체보기] - [Computer Science] OOP 에서 아주 중요한 SOLID 원칙에 대해 알아보자 (1/2) 에서 이어집니다.
3. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
하위 (자식) 클래스가 부모 (슈퍼) 클래스의 기능을 대체할 수 있어야 한다는 원칙으로, 부모 클래스에 의존하는 코드는 하위 클래스에서도 문제없이 작동해야 한다.
// 부모 클래스
open class Bird {
open fun fly() {
println("이 새는 날고있는 중입니다.")
}
}
// 자식 클래스 1 참새
class Sparrow : Bird() {
override fun fly() {
println("참새는 날고있는 중입니다.")
}
}
// 자식 클래스 2 펭귄
class Penguin : Bird() {
override fun fly() {
throw Exception("펭귄은 날 수 없습니다.")
}
}
// 함수: 모든 새가 날 수 있다고 가정
fun letBirdFly(bird: Bird) {
bird.fly()
}
fun main() {
val sparrow = Sparrow()
letBirdFly(sparrow) // Sparrow is flying
val penguin = Penguin()
letBirdFly(penguin) // 펭귄은 날 수 없으므로 Exception 발생
}
위와 같은 상황에서 Bird 는 모두 날 수 있다고 가정하고 있지만, 실제로는 이를 상속한 Penguin 클래스에서는 날 수 없으므로 에러를 내뱉고 있다.
이렇게 상위 클래스를 상속한 하위 클래스가 그 기능을 대체하지 못한다면 이는 리스코프 치환 원칙을 위배하는 것이다.
4. Interface Segregation Principle (인터페이스 분리 원칙)
인터페이스를 구현할 때, 특정 클래스가 사용하지 않는 메소드가 포함된 인터페이스를 구현하지 않도록 설계해야한다는 원칙.
위에서 설명한 Bird 클래스를 예시로 들어보자.
interface Bird {
fun fly()
fun eat()
}
class Sparrow : Bird {
override fun fly() {
println("참새는 날고있다.")
}
override fun eat() {
println("참새는 식사를 하고있다.")
}
}
class Penguin : Bird {
override fun fly() {
throw UnsupportedOperationException("펭귄은 날 수 없다.")
}
override fun eat() {
println("펭귄은 식사를 하고있다.")
}
}
Penguin class 는 Bird 의 fly 메소드를 사용하지 못하므로 이것은 ISP 위반이다.
ISP 위반에서 벗어나기 위해서는 날지 못하는 새 Interface 를 따로 구현해줘야한다.
// 공통 인터페이스: 모든 새가 구현해야 할 동작
interface Bird {
fun eat() // 모든 새는 먹는 행동을 할 수 있음
}
// 날 수 있는 새 인터페이스
interface FlyableBird : Bird {
fun fly() // 날 수 있는 새는 날기 기능을 추가로 가짐
}
class Sparrow : FlyableBird {
override fun fly() {
println("참새는 날고있다.")
}
override fun eat() {
println("참새는 식사를 하고있다.")
}
}
class Penguin : Bird {
override fun eat() {
println("펭귄은 식사를 하고있다.")
}
}
이처럼 구현을 해주면 ISP 위반에서 벗어나게 된다 !
5. 의존 역전 원칙 (Dependency Inversion Principle, DIP)
상위 모듈이 하위 모듈에 의존해서는 안되고, 모두 추상화에 의존해야한다는 원칙으로, 구체적인 클래스가 아닌 추상화에 의존해야한다는 원칙이다.
class Keyboard {
fun type() {
println("키보드 타이핑")
}
}
class Computer {
private val keyboard = Keyboard() // 하위 모듈(구현 클래스)에 직접 의존
fun useKeyboard() {
keyboard.type()
}
}
위처럼 Keyboard 라는 구체적인 클래스에 의존하는 Computer 클래스 가 존재할 때, 만약 Computer 클래스에 A 타입의 키보드가 추가된다고 하면 컴퓨터 내부 코드를 고쳐줘야 Computer 클래스에서 A 키보드를 사용할 수 있다.
DIP 는 이러한 상황을 예방하기 위한 원칙으로 아래처럼 추상화를 통한 구현 방식을 이용하면 이를 예방할 수 있다.
interface Keyboard {
fun type()
}
class KeyboardImpl: Keyboard {
override fun type() {
println("타이핑중")
}
}
class GyuCustomKeyboardImpl: Keyboard {
override fun type() {
println("커스텀 키보드 타이핑중")
}
}
// 생성자로 Keyboard 인터페이스 받음
class Computer(private val keyboard: Keyboard) {
fun useKeyboard() {
keyboard.type()
}
}
fun main(){
val keyboard = Computer(KeyboardImpl())
keyboard.useKeyboard()
val customKeyboard = Computer(GyuCustomKeyboardImpl())
customKeyboard.useKeyboard()
}
위와 같이 추상화를 이용한다면 Computer 내부를 수정해줄 필요 없이 매개변수로 서로 다른 키보드 객체를 넘겨주기만 하면 된다.
지금까지 OOP 의 5대 원칙 SOLID 원칙에 대해서 알아보았다.
객체 지향 프로그래밍을 하면서 이에 대해 잘 몰랐었는데 간단히라도 알아보는 시간을 가지며, 앞으로 이 원칙들을 유의하며 코드를 짜야겠다는 생각이 들었다.
'프로그래밍 > CS' 카테고리의 다른 글
[Computer Science] 프로그램의 메모리 구조에 대해 알아보자 (Code, Data, Stack, Heap) (0) | 2025.02.20 |
---|---|
[Computer Science] 프로그래밍 패러다임에 대해 알아보자. (1) | 2025.02.19 |
[Computer Science] OOP 에서 아주 중요한 SOLID 원칙에 대해 알아보자 (1/2) (0) | 2024.12.20 |
[Computer Science] 객체 지향 프로그래밍 (OOP) 란 무엇일까? (1) | 2024.12.19 |
[Computer Science] 프로세스 (Process) 와 스레드 (Thread) 란 무엇인가? (1) | 2024.12.17 |