[OOP] SOLID 원칙

2022. 12. 5. 17:47Dev/Documents

반응형
  1. S, SRP
  2. O, OCP
  3. L, LSP
  4. I, ISP
  5. D, DIP

1. SRP

Single Responsibility Principle

단일 책임 원칙

하나의 객체가 하나의 책임만 갖는다
(= 클래스를 단 하나의 목표만 가지고 작성한다.)

높은 유지보수성
모듈 전반의 가시성을 제공한다

1.1. SRP 적용 예 1

특정 자원을 저장하는 기능과 조회하는 기능은 책임이 다릅니다.
저장에 관한 비즈니스 로직과 조회에 관한 비즈니스 로직을 서로 다른 클래스로 분리하여 하나의 클래스가 하나의 목표만을 가지도록 만듭니다.

저장에 관한 기능이 변경되었을 때에, 조회에 관한 서비스는 변경되지 않는다.


1.2. SRP 적용 예 2

계좌에 관한 도메인이 있을 때,
계좌에 관한 비즈니스 로직은 계좌 도메인 안에 작성하고,
계좌 도메인을 다른 DTO 클래스로 변환하는 매퍼 함수들은 매퍼 클래스 안에 작성합니다.

계좌 도메인의 변경은 계좌 도메인의 비즈니스 로직이 변경되었음을 의미하며, 단순 매퍼 함수 변경은 도메인 코드에 영향을 주지 않습니다.


2. OCP

Open Closed Principle

개방 폐쇄 원칙

소프트웨어 컴포넌트는 확장에 관하여 열려있으나, 수정에 관해서는 닫혀있어야 한다는 원칙

클래스별로 내부 동작은 다르더라도 공통적으로 원하는 기능이 있다면, 그것은 interface나 abstract class에 선언해두고,
그것을 확장하는 상속 클래스에서 상세 동작에 차이점을 둡니다.

2.1. OCP를 만족하지 않는 코드

Shape 구현체의 area를 계산하는 기능을 만들려 할때,
아래와 같이 작성할 수도 있습니다.

하지만 이럴 경우에는 Shape를 상속받은 새로운 클래스인 Square가 추가될 경우 ShapeCalculator를 수정해야하는 일이 발생됩니다.
Shape 를 상속하는 클래스가 추가될 때 마다 Shape를 이용하는 아래 클래스도 매번 수정해줘야하는 불편함이 있습니다.

interface Shape

data class Triangle(val width: Int, val height: Int):Shape {
}
data class Circle(val radius: Int):Shape {
}
data class ShapeCalculator(val shapes: List<Shape>) {
    fun sum(): Double {
        return shapes.stream()
            .mapToDouble {
                when (it) {
                    is Triangle -> it.width * it.height / 2.0
                    is Circle -> Math.PI * it.radius.toDouble().pow(2)
                    else -> 0.0
                }
            }
            .sum()
    }
}

2.2. OCP를 만족하는 코드

OCP를 만족하도록 코드를 작성할 경우
추후 다른 클래스가 추가되는(확장) 일이 있더라도 그 동작을 활용하는 코드는 수정되지 않습니다.

아래 클래스에서 Shape를 상속하는 Square 클래스가 추가되더라도 Shape의 getArea() 함수를 이용하는 ShapeCalculator 클래스는 수정할 부분이 없습니다.

import kotlin.math.pow

interface Shape {
    fun getArea(): Double
}

data class Triangle(val width: Int, val height: Int):Shape {
    override fun getArea(): Double {
        return width * height / 2.0
    }
}
data class Circle(val radius: Int):Shape {
    override fun getArea(): Double {
        return Math.PI * radius.toDouble().pow(2)
    }
}
data class ShapeCalculator(val shapes: List<Shape>) {
    fun sum(): Double {
        return shapes.stream()
            .mapToDouble(Shape::getArea)
            .sum()
    }
}
fun main() {
    val shapes = listOf(Triangle(3, 5), Triangle(6, 6), Circle(5))
    println(ShapeCalculator(shapes).sum())
}

3. LSP

Liskov Substitution Principle

리스코프 치환 원칙

서브 클래스 객체는 반드시 슈퍼 클래스 객체를 완벽하게 대체할 수 있어야 합니다.

서브 클래스의 객체는 슈퍼 클래스의 객체와 반드시 같은 방식으로 동작해야 합니다.


3.1. LSP를 만족하지 못한 코드

프리미엄 회원은 영상보기, 음악보기가 가능하지만
공짜 회원은 영상보기만 되고 음악보기가 안된다면

아래와 같은 구조로 작성할 경우 LSP를 만족하지 못한 코드입니다.

코드상으로는 문제가 없어보이나, FreeMemeber는 실질적으로 listenMusic 기능을 수행하지 않아야 하기 때문에 기능적 오류라고 볼 수 있습니다.

Member 슈퍼 클래스를 상속받는 클래스는 반드시 거기서 제공하는 기능들을 온전하게 수행해야하는 것이 LSP를 만족하는 코드입니다.

abstract class Member(open val name: String) {
    abstract fun watchYoutube()
    abstract fun listenMusic()
}

class PremiumMember(name: String) : Member(name) {
    override fun watchYoutube() {
        println("광고없이 유튜브 시청하기")
    }

    override fun listenMusic() {
        println("음악 듣기")
    }
}
class FreeMember(name: String) : Member(name) {
    override fun watchYoutube() {
        println("광고 포함 유튜브 시청하기")
    }

    override fun listenMusic() {
        // FreeMember 는 음악 못듣습니다.
    }
}

3.2. LSP를 만족하는 코드

Member에 들어있는 기능은 PremiumMember, FreeMember에서 모두 정상적으로 동작되고
PremiumOption에 들어있는 기능은 그것을 상속하고 있는 PremiumMember에서 정상적으로 동작합니다.

abstract class Member(open val name: String) {
    abstract fun watchYoutube()
}

interface PremiumOption {
    fun listenMusic()
}

class PremiumMember(name: String) : Member(name), PremiumOption {
    override fun watchYoutube() {
        println("광고없이 유튜브 시청하기")
    }

    override fun listenMusic() {
        println("음악 듣기")
    }
}
class FreeMember(name: String) : Member(name) {
    override fun watchYoutube() {
        println("광고 포함 유튜브 시청하기")
    }
}

4. ISP

Interface Segregation Principle

인터페이스 분리 원칙

특정 인터페이스를 상속받은 클래스에서는 사용하지 않을 메서드를 강제로 구현하는 일을 없도록 만듭니다.

하나의 인터페이스 내에 주제가 다른 메서드 선언문이 많을 경우, 그 인터페이스를 상속받을 경우 불필요하게 메서드를 override를 해야만 하는 상황이 발생될 수 있습니다.

인터페이스는 기능에 맞춰서 최대한 쪼개는 것을 권장합니다.


4.1. ISP를 만족하지 않는 코드

Connection 인터페이스에 성질이 다른 메서드를 여러개 함께 가지고 있을 경우
그 인터페이스를 상속한 클래스에서 사용하지 않을 메서드를 불필요하게 override해야하는 경우가 발생됩니다.

interface Connection {
    fun connect();
    fun http();
    fun socket();
}
class HttpConnection: Connection {
    override fun connect() {
        println("HttpConnection 연결")
    }

    override fun http() {
        println("HTTP 연결")
    }

    // HttpConnection 에서는 사용하지 않는 메서드
    override fun socket() { }
}

class SocketConnection: Connection {
    override fun connect() {
        println("SocketConnection 연결")
    }

    // SocketConnection 에서는 사용하지 않는 메서드
    override fun http() { }

    override fun socket() {
        println("소켓 연결")
    }
}

4.2. ISP를 만족하는 코드

인터페이스나 추상화클래스는 최대한 쪼개서 아래와 같이 불필요한 메서드를 override하는 것을 없애야 합니다.

interface Connection {
    fun connect();
}
class HttpConnection: Connection {
    override fun connect() {
        println("HttpConnection 연결")
    }

    fun http() {
        println("HTTP 연결")
    }
}

class SocketConnection: Connection {
    override fun connect() {
        println("SocketConnection 연결")
    }
    fun socket() {
        println("소켓 연결")
    }
}

5. DIP

Dependency Inversion Principle

의존관계 역전 원칙

구체화 된 클래스가 아닌 추상화에 의존하도록 만드는 것
추상화를 이용하여 코드를 작성할 경우, 코드의 확장성이 커집니다.

5.1. DIP 예제

추상화 클래스인 Datasource와
그 클래스를 구체화한 MariadbDatasource, H2Datasource가 있다고 할 때

abstract class Datasource(val dbName: String) {
    abstract fun getJdbcUrl(): String
}

class MariadbDatasource(dbName: String): Datasource(dbName) {
    override fun getJdbcUrl(): String {
        return "jdbc:mariadb://localhost:3306/${dbName}"
    }
}
class H2Datasource(dbName: String): Datasource(dbName) {
    override fun getJdbcUrl(): String {
        return "jdbc:h2:mem:${dbName}"
    }
}

데이터베이스 연결을 구체화된 특정 클래스(MariadbDatasource, H2Datasource)가 아닌 그 클래스의 추상화클래스인 Datasource를 활용할 경우,

ConnectDatabase 클래스의 connect메서드에서는 Datasource를 상속한 모든 클래스에서 기능을 수행할 수 있게 됩니다.

class ConnectDatabase {
    fun connect(datasource: Datasource) {
        println("database 연결 ... ${datasource.getJdbcUrl()}")
    }
}

728x90
반응형

'Dev > Documents' 카테고리의 다른 글

Programming naming cases  (0) 2021.11.24