디자인패턴

SOLID - SRP

blogger903 2024. 9. 19. 21:13
728x90

이번 포스팅에서는 SOLID 원칙중 SRP의 개념을 이해하고, kotlin 코드로 개념을 익혀보려고 합니다.

SRP의 개념은 다음과 같습니다.

 

SRP, 단일 책임 원칙은 어떤 클래스를 변경의 이유가 하나여야 한다

 

조금 더 쉽게 풀어보면

**단일 책임 원칙(SRP)**은 하나의 클래스가 오직 하나의 책임만 가져야 한다는 원칙입니다. 즉, 클래스가 변경되는 이유는 하나뿐이어야 하며, 여러 가지 책임을 가진다면 이를 적절하게 분리해야 합니다. 각 객체가 특정 역할만 담당하게 되면, 각 책임에 대해 독립적으로 변경 및 유지보수가 가능해집니다.

 

SRP내용을 이해하기 위해서 가상의 개발자와 코드리뷰를 하는 형식으로 내용을 작성했습니다.

 

OOP를 모르는 논리적인 개발자가 개발한 코드를 SRP을 적용하여 질답을 하는 구조로 SRP에 대해서 정리하겠습니다.

 

우선 SOLID건 SRP건 정리하기 전에 설명을 돕기 위해 가정을 추가했습니다.

  • 프로젝트를 혼자 개발하거나 유지보수하지 않는다.
  • 코드를 작성하는 사람은 유지보수는 내가 아닌 다른 사람이 할 수 있다라는 가정하에 내가 작성한 코드를 수정할 내가 아닌 미래의 another 개발자를 위한 혹은 프로의식을 갖고 개발을 오랫동안 하고싶은 사람 ( 유지보수 못하는 코드가 git에 남는게 싫은 사람) 이어야 합니다.

SOLID는 OOP를 모르고 논리적인 코드만 작성해온 1인 개발자에는 해당하지 않는 원칙입니다. 논리적인 코드를 잘 작성하는 개발자에게는 오히려 이해가 안되고 거슬리는 원칙일 뿐입니다.

 

자 이제 우리는 모두 여러명 (5명 이상의 개발자)가 프로젝트를 유지보수하며 다양한 이해관계로 인해 복잡도가 높은 프로젝트를 개발하고 유지보수하고 있습니다. 기능을 추가하거나 수정할때 100라인 500라인씩 새로 만드는 일이 없어야 합니다. 우리는 코드의 수정이 적어야 유지보수하기 편하며, 코드리뷰도 가능!합니다

 

그러면 SRP 위반사례를 보겠습니다.

SRP 위반 사례

// SRP를 위반하는 설계

data class Product(val id: String, val name: String, var stockQuantity: Int)

class Order(val id: String, val items: List<OrderItem>) {
    fun process() {
        // 주문 처리 로직
        println("주문 ${id} 처리 중...")

        // 재고 확인 로직 (새로 추가됨)
        for (item in items) {
            val product = getProductFromDatabase(item.productId)
            if (product.stockQuantity < item.quantity) {
                throw IllegalStateException("상품 ${product.name}의 재고가 부족합니다.")
            }
            product.stockQuantity -= item.quantity
            updateProductInDatabase(product)
        }

        println("주문 ${id} 처리 완료")
    }

    private fun getProductFromDatabase(productId: String): Product {
        // 데이터베이스에서 상품 정보 조회
        return Product(productId, "상품 $productId", 100) // 예시 데이터
    }

    private fun updateProductInDatabase(product: Product) {
        // 데이터베이스의 상품 정보 업데이트
        println("상품 ${product.id}의 재고를 ${product.stockQuantity}로 업데이트")
    }
}

data class OrderItem(val productId: String, val quantity: Int)

// 사용 예시
fun main() {
    val order = Order("ORD-001", listOf(OrderItem("PROD-001", 2), OrderItem("PROD-002", 1)))
    order.process()
}
주문 ORD-001 처리 중...
상품 PROD-001의 재고를 98로 업데이트
상품 PROD-002의 재고를 99로 업데이트
주문 ORD-001 처리 완료

Process finished with exit code 0

위 코드는 주문 처리로직에 재고 확인 요구사항이 추가해야하는 상황을 가정하겠습니다.
논리적인 개발자는 뿌듯해하며 빠르게 개발을 끝내고 PR을 합니다.

같은 팀에서 유지보수하는 개발자는 당황한 나머지 리뷰를 달지만 설계적인 내용은 사실상 코드리뷰에서 할수 있는 내용은 아닙니다.
이런건 코드 리뷰단에서 논의해야할 것이 아니고, 기능을 개발할때 설계디자인 문서를 작성할때 논의되야합니다.

SRP를 준수한 예시

// SRP를 준수하는 개선된 코드
class Order(val id: String, val items: List<OrderItem>) {
    fun process(inventoryManager: InventoryManager) {
        println("주문 ${id} 처리 중...")
        inventoryManager.checkAndUpdateStock(items)
        println("주문 ${id} 처리 완료")
    }
}

class InventoryManager {
    fun checkAndUpdateStock(items: List<OrderItem>) {
        for (item in items) {
            val product = getProductFromDatabase(item.productId)
            if (product.stockQuantity < item.quantity) {
                throw IllegalStateException("상품 ${product.name}의 재고가 부족합니다.")
            }
            product.stockQuantity -= item.quantity
            updateProductInDatabase(product)
        }
    }

    private fun getProductFromDatabase(productId: String): Product { ... }
    private fun updateProductInDatabase(product: Product) { ... }
}

// 사용 예시
fun main() {
    val order = Order("ORD-001", listOf(OrderItem("PROD-001", 2), OrderItem("PROD-002", 1)))
    val inventoryManager = InventoryManager()
    order.process(inventoryManager)
}

재고관리하는 객체에게 재고관리에 대한 책임을 위임함으로써 적절한 객체에 적절한 책임을 갖도록 했습니다.
이것은 관심사 분리이기도 합니다. SRP 준수한 예시를 논리적인 개발자에게 설명했을때 논리적인 개발자는 다음과 같은 질문을 할수 있습니다.

Q1. Service line이 긴게 문제면 private 메서드로 빼면 되는거 아닌가요?

메서드를 private으로 분리하는것은 코드의 가독성을 높일 순 있는데, 그건 구현 세부사항을 숨기는 것 뿐입니다.
SRP가 말하는건 책임을 명확히 하고 변경의 영향을 최소화하는것입니다.

Q2. 재고 로직 변경 가능성이 적을것 같고 객체를 분리하는게 파일 개수도 늘리고 오히려 관리가 어려운거 아닌가요?

미래의 변경사항을 예측할수 없지만, 핵심 기능은 자주 변경되거나, 확장되는 경향이 있습니다. 그러면 SRP를 준수하지 않은 코드의 수정이 있을경우에 애초에 절차적으론 논리적인 사고로 개발을 했기 때문에 코드가 논리정연합니다. 하지만 Order부터 라인을 다 파악해야 리뷰가 가능합니다. 그러면 집중이 분산되어 핵심로직을 제대로 리뷰하기도 어렵습니다. InventoryManager는 본인의 책임이 재고관리기 때문에 코드 수정은 checkAndUpdateStock 에서만 발생할 것이고, 코드 추가도 InvertoryManager 내부에서 추가될것 입니다. 코드 리뷰하는 사람 입장에서는 컨텍스트를 굳이 설명하지 않아도 이해할 수 있습니다.

Q3. 클래스를 나누면 오히려 유지보수하는 사람이 알아야하는 로직이 늘어나는것 아닌가요? 한 곳에 모아두는 것이 더 간단하고 명시적으로 보이는데요?

객체지향 패러다임을 이해하지 못한 개발자에게는 이럴 수 있습니다. 다양한 관심사에 대해서 절차적으로 엔트리 포인트에 해당하는 도메인부터 시작해서 관련된 코드를 한곳에 모아 개발한다면 코드를 리뷰하기도 어렵고 코드를 파악해서 기능을 추가하는것도 테스트를 하기에도 어렵습니다. 기본적으로 코드를 작성할때는 그 코드에 대한 테스트를 작성하는것이 필요합니다(권장합니다). 그리고 테스트 코드를 작성하다보면 한곳에 모아서 개발하는 코드를 테스트하기도 어렵고 내용을 파악하기도 어렵다는것을 알게 됩니다. 그리고 효과적인 테스트를 관리하려면 책임을 분리해야 합니다. 적절하게 책임이 분리된 객체는 다양하고 자세한 테스트케이스를 작성하는게 쉽기 때문에 유지보수에도 도움이 됩니다.

Q4. Order클래스가 InventoryManager를 의존하게 되면 이것도 결합인데, 두 클래스의 결합을 추가하면 복잡도만 높아지지 않나요?

OOP에서 잘 구현하는 방법은 느슨한 결합과 높은 응집도입니다. 책임을 분리했기 때문에 오히려 Inventory만 변경하면 되고, Order에 테스트할때에도 Stub이나 Mock으로 효과적으로 Order를 테스트할 수 있습니다.

'디자인패턴' 카테고리의 다른 글

소프트웨어 설계원칙 - 관심사 분리  (0) 2024.08.07
전략패턴  (0) 2024.06.16
템플릿 메서드 패턴  (0) 2024.06.16