[책] 클린아키텍처: Solid 원칙

문돌이 개발자·2025년 1월 20일

SRP

  • 하나의 모듈은 오직 하나의 액터에 대해서만 책임져야 한다.
  • 모듈은 소스 파일 혹은 함수와 데이터 구조로 구성된 응집된 집합
  • 단일 액터를 책임지는 코드를 함께 묶어주는 힘이 바로 응집성

우발적 중복

SRP를 위반하는 클래스를 살펴보자

class Employee {
    fun calculatePay() {
        regularHours()
    }

    fun reportHours() {
        regularHours()
    }

    fun save() {}
}

fun regularHours() {
//    calculateHours
}

3가지 메서드가 서로 다른 세명의 액터를 책임진다.
calculatePay()를 사용하는 회계팀에서 시간을 계산하는 방법을 바꿀 때 reportHours()를 사용하는 인사팀에 대한 고려를 잊게 되면 의도치 않은 변경이 일어나게 된다.

병합

Employee 테이블 스키마 변경과 reportHours()의 수정이 동시에 일어나게 되면 역시 문제가 발생한다.

해결책

  • 메서드와 데이터 분리하기
class PayCalculator(
    private val data: EmployeeData
) {
    fun calculatePay() {}
}

class HourReporter(
    private val data: EmployeeData

) {
    fun reportHours() {}
}

class EmployeeSaver(
    private val data: EmployeeData
) {
    fun saveEmployee() {}
}

너무 따로 흩어져 있어 개별 클래스를 인스턴스화하고 추적하기가 어렵기 때문에 Facade 패턴을 활용할 수 있다.


class EmployeeFacade(
    private val payCalculator: PayCalculator,
    private val hourReporter: HourReporter,
    private val employeeSaver: EmployeeSaver,
    private val data: EmployeeData
) {
    fun calculatePay() {
        payCalculator.calculatePay()
    }

    fun reportHours() {
        hourReporter.reportHours()
    }

    fun save() {
        employeeSaver.saveEmployee()
    }
}

항상 예시를 드는게 기억에 잘 남는다. 퍼사드 패턴의 경우 사장이 각 담당 직원을 찾아서 일을 시키는 것보다. 중간관리자(퍼사드)에게 일을 시키면 관리자가 해당 직원에게 전달하는 방식으로 이해했다.

OCP

  • 소프트웨어 개체는 확장에는 열려있어야 하고 변경에는 닫혀있어야 한다.
  • 유지보수에 있어 핵심적인 원칙
  • SRP와 DIP를 준수함으로서 달성할 수 있다.
// SRP 위반으로 인해 OCP도 위반
class Human {
    // drive()가 drive 행위 외에 car 생성 책임도 지고 있음
    // complexArgs가 변경되면 Human의 drive()도 변경됨, 변경에 열려있게 되어버림
    fun drive() {
        val args = ""
        val car = Car(args)
        car.drive()
    }
}

class Car(private val complexArgs: String) {
    fun drive() {
    }
}

생성의 책임을 분리하여 외부에서 주입하여 해결할 수 있다.

class Human {
//    이제는 Car의 상세구현이 변경되어도 Human에서 변경할 것이 없다. 변경에 닫힘
//    하지만 확장에는 닫혀있음, Car 대신 다른 탈것을 타고 싶으면 변경이 필요함
    fun drive(car: Car) {
        car.drive()
    }
}

class Car(private val complexArgs: String) {
    fun drive() {
//    drive a car
    }
}

하지만 변경에는 닫혀있지만 확장에 열려있지는 않다. Car 대신 다른 탈 것을 타려고 하면 마찬가지로 Human에 변경이 있어야하기 때문

class Human {
//    이제는 Car의 상세구현이 변경되어도 Human에서 변경할 것이 없다. 변경에 닫힘
//    하지만 확장에는 닫혀있음, Car 대신 다른 탈것을 타고 싶으면 변경이 필요함
    fun drive(vehicle: Vehicle) {
        vehicle.drive()
    }
}

class Car(private val complexArgs: String): Vehicle {
    override fun drive() {
//        drive a car
    }
}

class Truck(private val args: String): Vehicle {
    override fun drive() {
//        drive a truck
    }

}

interface Vehicle {
    fun drive()
}

그래서 더 고수준의 Vehicle 인터페이스를 통해 의존 방향성을 역전시킴
SRP, DIP를 통해 OCP 원칙을 준수하게 되면 저수준에서의 변경은 고수준에 영향을 끼치지 않게 되고 유지보수성이 향상된다.

추이종속성

A -> B -> C 순서로 의존성을 갖게 되면 A -> C 의존성을 갖게되는 걸 의미한다. 소프트웨어의 원칙 중에 '자신이 직접 사용하지 않는 요소에는 절대 의존해선 안된다.'라는 원칙이 있다. 중간에 interface를 통해서 내부를 은닉할 수 있다.

LSP

  • S타입 객체를 다른 T타입으로 모두 치환해도 프로그램 동작에 문제가 없다면 S는 T의 하위타입이다.
package cleanarchitecture

class OPS {
    fun op1() {}
    fun op2() {}
    fun op3() {}
}

interface U1Ops {
    fun op()
}

interface U2Ops {
    fun op()
}

interface U3Ops {
    fun op()
}

class U1OpsImplementation(private val ops: OPS) : U1Ops {
    override fun op() {
        ops.op1()
    }
}

class User1 {
    fun op(u1Ops: U1Ops) {
        u1Ops.op()
    }
}

User1이 OPS에 의존하지 않아 OPS의 op2, op3의 변경으로부터 자유롭다.

ISP와 언어

ISP는 언어 타입에 의존한다고 한다. 동적 타입 언어의 경우 소스코드에 선언문이 존재하지 않아 재컴파일과 재배포가 필요없다.

DIP

  • 소스 코드 의존성이 추상에 의존하며 구현체에는 의존하지 않는 시스템이 '유연성이 극대화된 시스템'이다.
  • java의 String 클래스는 구현체임에도 애써 추상클래스로 만들려고 하지 않는다. 왜냐하면 변경이 거의 일어나지 않으며 엄격하게 통제되기 때문이다.
  • 즉 추상에 의존하려는 이유는 잦은 변경으로부터 자유롭기 위해서다.

안정된 추상화

  • 변동성이 큰 구현체 클래스를 참조하지 마라
    대신 추상 인터페이스를 참조하라. 일반적으로 이 규칙은 객체 생성 방식을 강하게 제약하며, 일반적으로 추상 팩토리를 사용하도록 강제한다.

  • 변동성이 큰 구현체 클래스로부터 파생하지 마라
    '상속을 피하라'

  • 구현체 함수를 오버라이드 하지 마라
    구현체 함수를 재정의하면 소스 코드 의존성을 제거할 수 없고 의존성을 상속하게 된다.

  • 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 마라

팩토리

팩토는 객체지향 언어에서 바람직하지 못한 의존성을 처리할 때 많이 사용되는 패턴

가장 중요한 건 결국 수정하는 비용을 줄이는 것. 본질에만 의존하여 아키텍처답게 뼈대에 집중할 것. 그 외의 구체적인 구현에 의존하게 되면 비용은 커질 수밖에 없다.

profile
까먹고 다시 보려고 남기는 기록

0개의 댓글