2023. 1. 7. 18:51ㆍ기본에 충실하자
객체 지향 5대 원칙
1. 단일 책임 원칙 ( Single Responsibility Principle(SRP) )
2. 개방 폐쇄 원칙 ( Open-Closed Principle(OCP) )
3. 리스코프 치환 원칙 ( Liskov Substitution Principle(LSP) )
4. 인터페이스 분리 원칙 ( Interface Segregation Principle(ISP) )
5. 의존성 역전 원칙(dependency inversion principle)
1. 단일 책임 원칙 ( Single Responsibility Principle )
클래스는 오직 하나에 대해서만 책임져야 한다.
클래스는 객체를 구체화하여 만든 작업 지시서 같은 것이다.
만약 티비라는 클래스를 만들면 티비는 티비에 대한 것만 책임지고 다루게 된다.
또 노트북이라는 클래스는 노트북에 관한 것들만 다루면 된다.
객체를 만들 때 어떻게 만드는지 생각해 보면 답은 아주 간단하다.
객체 지향의 특징 첫 번째, 추상화는 핵심적인 개념 또는 기능, 속성을 추출하여 하나의 클래스로 만든다.
그럼 추출하고 남은 속성들은 이제 그 속성을 가지는 별도의 클래스로 분리가 가능하다.
이렇게 추상화를 통해 클래스를 분리하고
하나의 클래스는 하나의 객체에 대해 다루게 하면 단일 책임의 원칙을 자연스럽게 따르게 된다.
아래의 코드를 보자.
open class Product(var name: String, var price: Int) {
fun turnOn(){
}
fun turnOff(){
}
fun getKORPrice() : String {
return "${price}만원"
}
}
class TV(name: String, price: Int, var inch: Int) : Product(name, price) {
fun channelUp(){
print("채널을 올립니다.")
}
fun channelDown(){
print("채널을 내립니다.")
}
}
class Refrigerator(name: String, price: Int, var width: Int, var height: Int) : Product(name, price) {
fun upTemp(){
}
fun downTemp(){
}
}
제품에는 이름과 가격이 있고
티비와 냉장고는 제품을 상속받는다
티비와 냉장고는 둘 다 전원을 켜고 끌 수 있다.
공통의 행위를 제품으로 올려 제품을 켜다. 끄다고 제품 클래스에 만들어준다.
티비는 채널을 올리고 내릴 수 있고, 냉장고는 온도를 내리고 올릴 수 있다.
만약 티비와 냉장고가 하나의 클래스에 있게 된다면
그 클래스는 티비 냉장고 두 개에 대한 책임을 지게 된다.
그러면 단일 책임 원칙을 위배하는 것이다.
오직 하나의 클래스에는 하나의 책임을 져야 한다.
2. 개방 폐쇄 원칙 ( Open-Closed Principle )
개방 폐쇄 원칙은 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다는 원칙이다.
티비에 새로운 3D 화면 기능이 추가된다고 가정해 보자.
3d 기능의 추가로 기존의 티비 객체들의 변경이 생긴다면
객체들의 엄청난 수정이 발생하게 될 것이다.
이건 말도 안 된다. 있을 수 없는 일이다.
만약 기능을 하나 추가, 변경할 일이 생길 때마다 일정 객체 혹은 전부 수정해야 하는 일이 발생한다면
정말 엄청난 낭비가 발생할 것이다.
추상화를 잘했다면 기능 하나 추가는 어렵지 않다.
open class Product(var name: String, var price: Int) {
open fun turnOn() {
}
open fun turnOff() {
}
open fun getKORPrice(): String {
return "${price}만원"
}
}
class TV(name: String, price: Int, var inch: Int) : Product(name, price) {
fun channelUp(){
print("채널을 올립니다.")
}
fun channelDown(){
print("채널을 내립니다.")
}
fun turnOn3D(){
}
}
만약 3D 티비의 객체를 따로 만들어야 한다면??
우리에겐 다형성과 상속성이라는 무기가 있다.
fun main(args: Array<String>) {
val tv3 : TV_3D = TV_3D(
name = "3d티비",
price = 700,
inch = 48,
display3D = "어쩌구 디스플레이"
)
}
class TV3D(
name: String,
price: Int,
inch: Int,
var display3D : String
) : TV(name, price, inch) {
override fun getKORPrice(): String {
return super.getKORPrice()
}
override fun turnOn3D() {
super.turnOn3D()
}
}
확장에 아주 잘 열려있다. 3d 티비 하나 만든다고 코드를 어디서부터 손봐야 하는지 손톱을 뜯으며 고민할 필요가 전혀 없다. TV 클래스를 상속받아서 overriding method 하면 내가 원하는 기능으로 만들 수 있다. Product 기능을 손댈 필요도, TV 기능을 손댈 필요도 전혀 없다.
인터페이스를 사용한다면 더욱 깔끔하게 만들 수 있을 것이다.
3D 인터페이스 만들고 구현해 주면 된다.
3. 리스코프 치환 원칙 ( Liskov Substitution Principle ) - 상속성 확장 권고 원칙
리스코프 치환 원칙이란 상위 타입의 객체를 하위 타입의 객체로 치환해도 코드가 문제없이 동작해야 한다는 원칙.
보통 여러 블로그들의 글을 찾아보면 정사각형 직사각형 예를 많이 들어 설명을 한다.
수학적으로 직사각형 안에 정사각형이 포함되지만 이를 컴퓨터로 구현하여 Rectangle의 부모타입으로 Square를 만들면 가로와 새로의 초기화의 문제로 Area 설정 메서드에도 문제가 발생한다고 한다. 상위 타입인 직사각형의 타입으로 하위 타입 정사각형 객체로 선언해도 문제가 없어야 리스코프 치환 원칙에 위배되지 않는다. 하지만 결과는 그렇지 않다.
리스코프 치환 원칙의 포인트는 부모 클래스 타입인 A를 사용한 코드가 자식 클래스 B를 사용하여 실행할 때도 문제가 없어야 한다 또한 부모 클래스의 조건을 자식도 따라야 한다는 것이다.
조건은
- 하위클래스에서 메서드 파라미터의 반공변성
- 하위클래스에서 반환형의 공변성
- 하위클래스에서 메서드는 상위클래스 메서드에서 던져진 예외사항을 제외하고 새로운 예외사항을 던지면 안 된다.
- 하위클래스에서 선행조건은 강화될 수 없음
- 하위클래스에서 후행 조건은 약화될 수 없음
- 하위형에서 상위형의 불변조건은 반드시 유지되어야 한다.
6개가 있다.
공변성 :
S가 T의 하위형이라면, Class <S> 타입은 Class <T>로 사용함에 문제가 없다.
<? extends T> 자바, out 코틀린
자기 자신과 자식 객체를 허용
반공변성 :
S가 T의 하위형이라면, Class <T> 타입은 Class <S>로 사용함에 문제가 없다.
<? super T> 자바, in 코틀린
자신과 부모 객체만 허용
리스코프 치환 법칙이 맞게 적용되려면 위의 6가지가 지켜져야 한다.
결국 공변성과 반공변성이 잘 지켜지고 기능의 명세가 정확하게 이루어졌을 때 알맞게 만들었다고 볼 수 있다.
4. 인터페이스 분리 원칙 ( Interface Segregation Principle )
인터페이스 분리 원칙(ISP)은 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙.
인터페이스는 지나치게 광범위하거나 많은 기능을 구현해서는 안되고, 사용하는 객체를 기준으로 잘게 분리되어야 한다는 의미이다.
반드시 객체가 자신에게 필요한 기능만을 가지도록 제한하고 불필요한 상속과 구현을 최대한 방지함으로써 객체의 불필요한 책임을 제거해야 한다.
인터페이스를 상속할 때 사용하지 않는
가령 fly 추상메서드를 가지고 있는 인터페이스를 사람이 implementation 하여 구현할 필요가 없는 경우
필요하지 않은 메서드는 분리하여야 합니다.
어떻게 하면 이런 '필요하지 않은 메서드를 분리' 할 수 있을까??
간단하게 인터페이스를 쪼개 메서드를 선언하면 됩니다.
Fly 메서드가 필요한 객체는 Flyable interface를 따로 만들어 그 안에 fly 메서드를 선언해 주고
객체에 implement 해주면 구현부를 객체에서 정의하여 사용하면 됩니다.
interface Animal {
fun eat()
fun sleep()
}
interface Cryable {
fun cry()
}
interface Flyable {
fun fly()
}
interface Swimmable {
fun swim()
}
class Bird : Animal, Cryable, Swimmable, Flyable {
override fun cry() {
TODO("Not yet implemented")
}
override fun fly() {
TODO("Not yet implemented")
}
override fun eat() {
TODO("Not yet implemented")
}
override fun sleep() {
TODO("Not yet implemented")
}
override fun swim() {
TODO("Not yet implemented")
}
}
class Fish() : Animal, Swimmable {
override fun eat() {
TODO("Not yet implemented")
}
override fun sleep() {
TODO("Not yet implemented")
}
override fun swim() {
TODO("Not yet implemented")
}
}
5. 의존성 역전 원칙
의존성 역전 원칙이란 고수준 모듈이 저수준 모듈에 의존하지 말아야 한다.
추상화를 통해 의존해야 한다.
인터페이스를 통해 의존해야 한다로 바꿔 말합니다.
아래의 예를 보자.
class GasolineEngine(val name: String) {
fun start(){
println(name+"스타트")
}
}
여기 가솔린엔진이 있다. 엔진의 이름을 받고 start 함수를 통해 엔진의 시동을 건다.
이 엔진을 자동차 클래스에 주입해서 사용할 것이다.
class Car(val engine: GasolineEngine){
fun startCar(){
engine.start()
}
}
자동차는 가솔린 엔진을 생성자로 받습니다. 그리고 자동차를 스타트하면 엔진의 시동도 켜지게 됩니다.
현재 자동차는 엔진에 의존하고 있습니다.
이렇게 하게 되면 엔진을 변경할 때 자동차 클래스를 변경해야 하므로 개방 폐쇄와 의존성 역전 원칙에 위배됩니다.
따라서
interface Engine {
fun start()
}
인터페이스로 엔진을 정의해 주고
class GasolineEngine(val name: String): Engine {
override fun start(){
println(name+"스타트")
}
}
가솔린엔진에서 Engine 인터페이스를 구현해 주면
class Car(val engine: Engine){
fun startCar(){
engine.start()
}
}
가솔린 엔진을 직접 주입하는 것이 아닌 인터페이스의 고수준 모듈로 사용하여
의존성을 역전하게 됩니다.
그로 인해 자동차 클래스에서 만약 전기엔진 자동차를 만들게 되면
가솔린 엔진을 주입한 경우, 엔진을 변경하거나 새로운 전기자동차 클래스를 만들어야 했지만
이제는 그럴 필요 없이 아래처럼 전기 엔진 클래스를 만들고 메인에서 사용하면 됩니다.
class Car(val engine: Engine){
fun startCar(){
engine.start()
}
}
interface Engine {
fun start()
}
class GasolineEngine(val name: String): Engine {
override fun start(){
println(name+"스타트")
}
}
class ElectricEngine(val name: String): Engine {
override fun start() {
TODO("Not yet implemented")
}
}
'기본에 충실하자' 카테고리의 다른 글
한 번의 글로 이해하는 소프트웨어 아키텍처 패턴 ( MVC, MVP, MVVM ) (4) | 2023.04.18 |
---|---|
의존성 주입 Dependency Injection (0) | 2022.08.20 |