객체지향 5원칙(SOLID)이란?

쓰리원·2022년 5월 28일
0

java 개념

목록 보기
9/10
post-thumbnail

1. SOLID란?

1. 단일 책임 원칙(Single responsibility principle) : SRP
2. 개방 폐쇄 원칙(Open/closed principle) : OCP
3. 리스코프 치환 원칙(Liskov substitution principle) : LSP
4. 인터페이스 분리 원칙(Interface segregation principle) : ISP
5. 의존관계 역전 원칙(Dependency inversion principle) : DIP

위와 같은 5개의 원칙의 앞글자를 따서 만든 원칙이 SOLID 원칙 입니다. 그러면 차근히 순서대로 살펴보겠습니다.

2. 단일 책임 원칙(Single responsibility principle) : SRP

작성된 클래스는 하나의 기능만을 가지며 클래스가 제공하는 서비스는 그 하나의 기능을 수행하는데 집중되어야 한다는 원칙입니다. SRP를 적용하면 책임 영역이 모듈화가 잘 이루어져서 변경사항에 적용이 수월하여 유지보수성에 도움이 됩니다. 그리고 역할에 따라 코드가 나뉘어서 기능별 가독성 이 향상되는 효과까지 있습니다.

  • 코틀린 예제 설명
<Wrong example>

data class User(
    var id: Long,
    var name: String,
    var password: String
) {
    fun signIn() {
        // Authetication 을 통한 로그인...
    }

    fun signOut() {
        // Authetication 을 통한 로그아웃...
    }
}

위의 예제에서 Authetication Process(인증 프로세스)에 구현을 하고 있습니다. Authetication Process(인증 프로세스)에 변경이 필요하다고 가정을 하면 Authetication을 User에서 다루고 있어서 User 클래스에도 영향이 갈 수도 있습니다. Authetication을 User 클래스 내부에서 SignIn, SignOut을 구현해서 다른 곳에서 기능을 가져와서 사용하기 어렵게 됩니다. 위 경우는 후자의 가능성이 훨씬 높아서 문제가 생길 것으로 보이긴 합니다.


<Right example>

data class User(
    var id: Long,
    var name: String,
    var password: String
)

class AuthenticationService() {
    fun signIn() {
        // 로그인 관련 구현...
    }

    fun signOut() {
        // 로그아웃 관련 구현...
    }
}

즉, 유저 클래스는 유저에 대한 정보만 가지고 있는 것으로 구현되어야하고, 만약 로그인/로그아웃 같은 유저 인증 관련 프로세스를 다루고 싶다면 인증 관련 프로세스를 다루는 새로운 클래스는 추가를 해야 SRP를 달성할 수 있게 됩니다.

3. 개방 폐쇄 원칙(Open/closed principle) : OCP

클래스는 확장에는 열려있고 변경에는 닫혀있어야 한다는 원칙입니다. 기존의 코드를 변경하지 않으면서(Closed), 기능을 추가할 수 있도록(Open) 설계가 되어야 한다는 원칙을 말합니다.

  • 코틀린 예제 설명

Open : 클래스에 새로운 기능을 추가 혹은 확장 할 수 있습니다. 사용하고 있는 코드에 몇가지 의존성 변경이 있을 때, 클래스는 쉽게 추가되고 변경되어야 합니다. 가장 좋은것은 의존성이 거의 없어서 코드의 변경으로 다른 코드의 수정이 안일어나게 하는 것 입니다.

Closed : 클래스의 기본 기능들은 변경이 되어서는 안된다는 의미입니다. 스마트폰 그리고 스마트폰 서비스 정보를 담고 있는 MobilePhoneUser 클래스가 있다고 가정하겠습니다. 이 클래스는 스마트폰 서비스 정보를 사용하여 스마트폰을 동작하게 구현되어 있습니다. 그리고 스마트폰 서비스에는 SMS, GMS 라고 불리는 2개의 다른 서비스가 있다고 하겠습니다.


<Wrong example>

class MobilePhone {
    lateinit var brandName: String
}

class MobilePhoneUser {
    fun runMobileDevice(mobileServices: Any, mobilePhone: MobilePhone) {
        if (mobileServices is SMServices) {
            println("This device is running with SMServices")
        }
    }
}

class SMServices {
    fun addMobileServiceToPhone(mobilePhone: MobilePhone) { 
    	println("SMServices") 
    }
}

위의 코드를 보면 스마트폰 서비스 타입을 if-else 문으로 체크하고 있습니다.

이 경우는 좋지 못한 사례입니다. 왜냐하면 새로운 스마트폰 서비스 타입이 나오면, 그 때마다 if-else 문을 통해 추가해야하기 때문입니다. Open-closed 원칙을 토대로, 우리는 모든 스마트폰 서비스를 위한 하나의 인터페이스를 가지는 것을 생각해볼 수 있습니다.

그리고 각각의 스마트폰 서비스 타입은 인터페이스를 구현하고, 고유한 특성을 가지고 있게 만들 수 있습니다. 그러면 if-else 문을 통해 스마트폰 서비스 타입을 체크할 필요가 없습니다.


<Right example>

class MobilePhone {
    lateinit var brandName: String
}

class MobilePhoneUser {
    fun runMobileDevice(mobileServices: IMobileServices, mobilePhone: MobilePhone) {
        mobileServices.addMobileServiceToPhone(mobilePhone)
    }
}

interface IMobileServices {
    fun addMobileServiceToPhone(mobilePhone: MobilePhone)
}

class SmServices: IMobileServices {
    override fun addMobileServiceToPhone(mobilePhone: MobilePhone) { 
    	mobilePhone.brandName = "Samsung"
    	println("SMServices")
    }
}

class GmServices: IMobileServices {
    override fun addMobileServiceToPhone(mobilePhone: MobilePhone) { 
    	mobilePhone.brandName = "Google"
    	println("GMServices") 
    }
}

4. 리스코프 치환 원칙(Liskov substitution principle) : LSP

확장에는 열려있고 수정에는 닫혀있는. 기존의 코드를 변경하지 않으면서(Closed), 기능을 추가할 수 있도록(Open) 설계가 되어야 한다는 원칙을 말합니다. 이는 부모클래스를 자식클래스로 교체하여도 정상적으로 작동해야 됨을 뜻하는데 이는 상속이 다형성을 통한 확장성 획득을 목표로하기 때문입니다.

  • 코틀린 예제 설명

<Wrong example>

abstract class Vehicle {
    protected var isEngineWorking = false
    abstract fun startEngine()
    abstract fun stopEngine()
    abstract fun moveForward()
    abstract fun moveBack()
}

class Car: Vehicle() {
    override fun startEngine() {
        println("Engine started")
        isEngineWorking = true
    }

    override fun stopEngine() {
        println("Engine stopped")
        isEngineWorking = false
    }

    override fun moveForward() {
        println("Moving forward")
    }

    override fun moveBack() {
        println("Moving back")
    }
}

class Bicycle: Vehicle() {
    override fun startEngine() {
        // 필요없는 메서드
    }

    override fun stopEngine() {
        // 필요없는 메서드
    }

    override fun moveForward() {
        println("Moving forward")
    }

    override fun moveBack() {
        println("Moving back")
    }
}

그러나 위의 코드는 보다시피 만약 우리가 Bicycle 클래스를 자식 클래스로 만들 때, Bicycle 은 엔진이 없기 때문에, startEngine, stopEngine 메서드는 필요 없어집니다.

위 상황의 고치기 위해서는 Vehicle을 상속받는 새로운 자식 클래스를 만들어 해결할 수 있습니다. 그리고 새롭게 추가하는 클래스는 엔진을 가지고 있는 Vehicle 들에 대한 클래스로 정의 될 것 입니다.


<Right example>

interface Vehicle {
    fun moveForward()
    fun moveBack()
}

abstract class VehicleWithEngine: Vehicle {
    
    private var isEngineWorking = false
    
    open fun startEngine() { 
    	isEngineWorking = true 
    }
    
    open fun stopEngine() {
    	isEngineWorking = false 
    }
}

class Car: VehicleWithEngine() {

    override fun startEngine() {
        super.startEngine()
        println("Engine started")
    }

    override fun stopEngine() {
        super.stopEngine()
        println("Engine stopped")
    }

    override fun moveForward() {
        println("Moving forward")
    }

    override fun moveBack() {
        println("Moving back")
    }
}

class Bicycle: Vehicle {

    override fun moveForward() {
        println("Moving forward")
    }

    override fun moveBack() {
        println("Moving back")
    }
}

5. 인터페이스 분리 원칙(Interface segregation principle) : ISP

클래스는 사용하지않는 인터페이스는 구현하지 말아야 한다는 원칙입니다. 이 원칙에 따르면 하나의 일반적인 인터페이스보다 여러개의 구체적인 인터페이스가 낫습니다. ISP를 통해 자신이 사용하지 않는 기능에 영향 받지 않음으로 변경사항 적용이 수월해지고 SRP와 마찬가지로 각각의 책임이 분리되어 관리의 측면에서 쉽게 됩니다.

안드로이드의 repository의 경우 데이터의 쓰임새 및 형태가 다르기 때문에 각각의 구현하는 메서드도 다른경우가 많습니다. 그러한 원리에 따라 여러개의 구체적인 인터페이스를 만들고 인터페이스에서 클래스에 쓰이지 않는 기능에는 영향을 받지 않게 됩니다.

6. 의존관계 역전 원칙(Dependency inversion principle) : DIP

  • 상위 모듈은 하위 모듈에 의존해서는 안된다
  • 추상화는 세부 사항에 의존해서는 안된다

의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하는 원칙입니다.

즉, 구체적인 클래스보다 인터페이스나 추상 클래스와 관계를 맺으라는 것 입니다. 추상적인 클래스에 의존하면 구체적인 클래스의 수정 상황이 있더라도 영향을 적게 받을 수 있습니다. 그러면 유지보수 및 test에 유리함을 가져 갈 수 있게 됩니다.

    private var modelList: List<Model>,
    private val viewModel: VM,
    private val resourcesProvider: ResourcesProvider,
    private val adapterListener: AdapterListener

어댑터는 지금 위 4개의 파라미터에 대한 생성자 주입을 받는다고 할 수 있습니다. 그렇기 때문에 저 4개에 의존하고 있습니다. 그리고 4개의 파라미터는 각각 인터페이스 및 추상클래스 입니다. 이로인해 DIP를 달성하고 있는 것을 알 수 있습니다. 총 4개의 파라미터가 있지만 전부 알아보기에는 중복된 내용이기 때문에 Model의 경우를 예시로 들어보겠습니다.

구체적인 클래스들은 위의 추상클래스를 상속받게 됩니다. 그래서 구현해야할 data를 주입할 때는 구체적인 클래스로 주입을 해서 다형성을 달성하여 코드를 재활용 할 수 있게 됩니다. 이로써 Adapter코드는 단 1개의 클래스 이지만, data는 수십 수백개로 코드를 재활용 할 수 있게 됩니다.

이렇게 구체클래스를 정해주고, 이 클래스를 실제 구현해야할 어댑터에 주입을 해줍니다.

어댑터를 보면 TownMarketModel로 구체 클래스가 주입된것을 알 수 있습니다.

7. reference

https://ko.wikipedia.org/wiki/SOLID_(%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5_%EC%84%A4%EA%B3%84)
https://ko.wikipedia.org/wiki/%EB%8B%A8%EC%9D%BC_%EC%B1%85%EC%9E%84_%EC%9B%90%EC%B9%99

profile
가장 아름다운 정답은 서로의 협업안에 있다.

0개의 댓글