[TIL] 03.16

rbw·2023년 3월 16일
0

TIL

목록 보기
75/97

스위프트와 GRASP 원칙

참조

https://codesquad-yoda.medium.com/%EC%8A%A4%EC%9C%84%ED%94%84%ED%8A%B8%EC%99%80-grasp-%ED%8C%A8%ED%84%B4-d5e37a1bb5dc

JK님의 글을 보고 적으면서 공부한 글 내용은 거의 동일함니다.


GRASP란, 9가지 General Responsibility Assignment Software Patterns 집합이다.

객체 책임을 할당하는 것은 OOD(객체 지향 설계)핵심 설계 방법 중에 하나이다.

정보 담당자 (Information Expert)

  • 문제 정의: 객체에 책임을 할당하는 기본적인 원칙은 무엇인가 ?
  • 해결 방안: 해당 객체에 필요한 정보를 채워넣는 것을 우선적으로 책임으로 할당한다.

코드를 보면 Customer 클래스는 모든 Orders를 참조하고, 주문에 대한 총합을 계산하기 위한 책임을 가짐

class Entity {
    var name : String = ""
    init(name : String) {
        self.name = name
    }
}

class Order {
    private (set) var id = UUID()
    private (set) var date = Date()
    private (set) var price = 0
    func isToday() -> Bool {
        let calendar = Calendar(identifier: .gregorian)
        return calendar.isDateInToday(date)
    }
}

protocol AggregateProtocol {
    var id : UUID { get }
}

protocol CustomerUniqueCheckerDelegate {
    func isUnique(_ object: AggregateProtocol) -> Bool
}

enum ValidationError : Error {
    case outOfOrders
    case duplicatedId
}

class Customer : Entity, AggregateProtocol
{
    private (set) var id : UUID
    private (set) var email : String
    private var orders : Array<Order>
    
    init?(email : String, name : String, uniqueChecker : CustomerUniqueCheckerDelegate) {
        id = UUID.init()
        self.email = email
        self.orders = Array<Order>()
        super.init(name: name)
        guard uniqueChecker.isUnique(self) else { return nil }
    }
    
    func add(newOrder: Order) throws {
        guard orders.filter({ $0.isToday() }).count < 2 else { throw ValidationError.outOfOrders }
        orders.append(newOrder)
    }
    
    func totalPriceOrders() -> Int {
        return self.orders.reduce(0) { (value, order) -> Int in
            return value + order.price
        }
    }
}

이런 값과 관련된 역할이 가장 기본적인 규칙이다. 반대로 꼭 필요하지 않은 데이터라면 책임을 할당하지 않는게 좋다.

소유권한(Creator)

  • 문제정의: 누가 객체 A를 생성하고 소유하는가 ?
  • 해결방안: 다음사항을 고려해서 적어도 하나 이상이라면 A객체를 생성하는 책임을 객체 B에 할당함
  1. B가 A를 포함하거나 협력을 위해 조합하는 경우
  2. B가 A를 기록하는 경우
  3. B가 A를 긴밀하게(복잡하게) 사용하는 경우
  4. B가 A의 초기값을 갖고 있는 경우
class Customer :
            Entity,
            AggregateProtocol
{
    private (set) var id : UUID
    private (set) var email : String
    private var orders : Array<Order>
    
    ...
    
    func add(orderProducts: Product) throws {
        let newOrder = Order(orderProducts) //Creator
        guard orders.filter({ $0.isToday() }).count < 2 else { throw ValidationError.outOfOrders }
        orders.append(newOrder)
    }
    
    ...
}

Customer 클래스가 orders 속성을 협력을 위해 사용하고, order를 기록하고, 초기값을 넘겨주고 있다. 따라서 Order를 생성하고 소유하기에 적합한 후보이다.

컨트롤러(Controller)

문제정의 : UI 계층 뒤에서 시스템 제어를 받아주고 관장하는 객체는 무엇인가?

해결방안 : 다음사항을 고려해서 적어도 하나 이상이라면 컨트롤러 역할을 할당한다.

  • 시스템 전체, 최상위 객체, 디바이스에서 동작하는 소프트웨어나 가장 중심의 서브시스템을 표현하는 경우
  • 시스템 동작을 구성하는 사용자 시나리오(use case)를 표현하는 경우

이 구현 규칙은 시스템에 대한 상위 수준 설계에 따라 달라진다. 늘 비즈니스 로직 처리를 위한 전반적인 조정을 담당하는 객체를 정의해야 한다. 다음 예제 코드에서는 전형적인 컨트롤러 역할을 담당하고 있다.

다른 객체에서 명령을 전달 받아 내부에 있는 manager객체에 전달하는 역할을 책임진다.

class Product {
    private var id = UUID()
    
}

class CustomOrderRequest {
    private (set) var products = [Product]()
}

protocol OrderCommand {
    //
}

protocol OrderManagerProtocol {
    func send(command : OrderCommand)
}

class AddCustomerOrderCommand : OrderCommand {
    private var id : UUID
    private var products = [Product]()
    
    init(id: UUID, products: [Product]) {
        self.id = id
        self.products.append(contentsOf: products)
    }
}

class CustomerOrderController {
    private (set) var orderManager : OrderManagerProtocol
    
    init(manager: OrderManagerProtocol) {
        self.orderManager = manager
    }
    
    func addCustomerOrderCommand(customerId : UUID, request: CustomOrderRequest) {
        orderManager.send(command: AddCustomerOrderCommand(id: customerId, products: request.products))
    }
}

느슨한 연결(Low Coupling)

문제의식 : 어떻게 변화에 대한 충격을 줄일 수 있나? 어떻게 의존성을 줄이고 재사용성을 높일 수 있나 ?

해결방안 : (불필요한) 연결을 줄이도록 책임을 할당한다.

연결은 하나의 요소가 다른 요소와 얼마나 관련이 있는지를 나타내는 지표다.

느슨한 연결은 객체들끼리 서로 독립적이면서 분리되어 있는 것을 의미한다. 분리되어 있는 객체들은 다른 객체가 변하더라도 걱정할 필요가 없고, 무언가 깨지지 않는 것을 의미한다.

SOLID에서 강조하는 추상화한 인터페이스로 역할을 분리해서 느슨하게 연결한 예제 코드

protocol CustomerRepositoryProtocol { }
protocol ProductRepositoryProtocol { }

struct ConversionRate {
    var fromCurrency : String
    var toCurrency : String
    var ratio = 0.0
}

protocol ForienExchangeProtocol {
    func conversionRates() -> [ConversionRate]
}

protocol RequestHandlerProtocol {
    associatedtype command
    func handle(request : command, canCancel : Bool)
}

class AddCustomerOrderCommandHandler : RequestHandlerProtocol {
    typealias command = AddCustomerOrderCommand
    
    private (set) var customerRepository : CustomerRepositoryProtocol
    private (set) var productRepositoty : ProductRepositoryProtocol
    private (set) var foreignExchange : ForienExchangeProtocol
    
    init(customerRepository : CustomerRepositoryProtocol,
         productRepositoty : ProductRepositoryProtocol,
         foreignExchange : ForienExchangeProtocol) {
        self.customerRepository = customerRepository
        self.productRepositoty = productRepositoty
        self.foreignExchange = foreignExchange
    }
    
    func handle(request: AddCustomerOrderCommand, canCancel: Bool) {
        //
    }
}

높은 응집도(High cohesion)

문제정의 : 객체 자체에 집중해서 객체를 이해하기 쉽고, 관리하기 편하고, 느슨한 연결을 추구하려면 어떻게 해야할까 ?

해결방안 : 응집도가 높아지도록 책임을 할당하자.

응집도는 객체가 모든 책임이 얼마나 관련이 높은지 나타내는 지표다. 다시 말해 내부 요소에 있는 부분들이 서로 얼마나 관련이 높은지를 의미한다.

낮은 응집도를 가진 클래스는 관련이 없는 데이터나 관령이 없는 행동을 포함하게 된다. 예를 들어 Customer클래스는 Orders를 관리한다는 한가지에 집중하기 때문에 응집도가 높다. 만약 여기에 가격을 관리하는 책임을 추가한다면 직접 관련이 없는 가격에 대한 것 때문에 응집도는 떨어진다.

간접참조(Indirection)

문제정의 : 두 객체 이상에서 직접적으로 연결을 피하도록 어디에 책임을 할당해야 할까 ?

해결방안 : 두 개의 서비스나 컴포넌트를 직접 연결하지 말고, 중간 매개체에게 책임을 할당해라

아래 코드의 ControllerService가 직접 참조가 일어난다.

class CustomerOrderController {
    private (set) var orderService : OrderService
    
    init(service: OrderService) {
        self.orderService = service
    }
}

중재자 역할을 하는 Manager를 활용하면 간접참조를 만들 수 있다.

위의 컨트롤러 부분에 코드를 참고하면 됨니다~

다형성(Polymorphism)

문제정의 : 타입을 (다른 타입으로) 대체할 방법은 어떻게 가능한가 ?

해결방안 : 타입(또는 클래스)에 따라 다른 동작이나 대체 수단은, 각 타입에서 해당 동작이 다형성을 갖도록 책임을 할당하라

이는 객체지향 설계에 기본 원칙이고, 전략 패턴(Strategy Pattern)과 매우 연관이 깊다.

위에 살펴본 Customer 클래스는 초기화할 때 CustomerUniquenessCheckerDelegate 프로토콜을 전달 받는다.

class Customer :
            Entity,
            AggregateProtocol
{
    private (set) var id : UUID
    private (set) var email : String
    private var orders : Array<Order>
    
    init?(email : String, name : String, uniqueChecker : CustomerUniqueCheckerDelegate) {
        id = UUID.init()
        self.email = email
        self.orders = Array<Order>()
        super.init(name: name)
        guard uniqueChecker.isUnique(self) else { return nil }
    }
    
	 //...중간생략
}

이런식으로 프로토콜로 요구사항을 추상화해서 다른 구현체를 전략적으로 선택할 수 있다. 이런 구조를 가진다면 동일한 입력과 출력에도 전혀 다른 알고리즘으로 동작하도록 만들 때 유용하다.

순수 조립(Pure Fabrication)

문제정의 : 높은 응집도와 낮은 연결을 깨고 싶지 않은데, 다른 원칙을 적용하기 애매한 상황에는 어떤 객체가 책임을 가져야 할까 ?

해결방안 : 문제에 대한 도메인을 표현하지 않는 편의상 또는 인공적인 객체가 높은 응집도를 갖도록 책임을 할당하라.

가끔은 어디에 어떤 책임을 할당해야만 할지 알아내기 어려운 경우도 있다. 도메인-주도 설계에 도메인 서비스 개념이 있는 이유이다. 도메인 서비스는 엔티티와 관련이 없는 로직을 포함한다.

예로, 이커머스 시스템에서는 환율에 따라서 외환을 환전하는 기능이 필요하다. 이런 구현을 위해 어디서 새 클래스나 프로토콜을 만들어야 하는지 혼란스러울 때가 있다.

아래 예제코드는 Low Coupling 코드에 다음 부분입니다.

struct ConversionRatesCache {
    static let Key = "ConversionRatesCache.Key"
    var rates = [ConversionRate]()
    var timestamp : Date
    
    init(rates : [ConversionRate]) {
        self.rates = rates
        self.timestamp = Date()
    }
}

protocol CacheStoreDelegate {
    func value(for key: String) -> ConversionRatesCache?
    func add(value: ConversionRatesCache, for key: String)
}

class ForeignExchangeMock : ForeignExchangeProtocol {
    private (set) var cacheStore : CacheStoreDelegate
    
    init(cacheStore : CacheStoreDelegate) {
        self.cacheStore = cacheStore
    }
    
    func conversionRates() -> [ConversionRate] {
        if let ratesCache = cacheStore.value(for: ConversionRatesCache.Key) {
           return ratesCache.rates
        }
        let rates = conversionRatesFromExternal()
        cacheStore.add(value: ConversionRatesCache(rates: rates), for: ConversionRatesCache.Key)
        return rates
    }
    
    private func conversionRatesFromExternal() -> [ConversionRate] {
        var conversionRates = [ConversionRate]()
        conversionRates.append(ConversionRate(fromCurrency: "USD", toCurrency: "EUR", ratio: 0.88))
        conversionRates.append(ConversionRate(fromCurrency: "EUR", toCurrency: "USD", ratio: 1.14))
        return conversionRates
    }
}

이렇게 구현하면 환전 처리를 위한 높은 응집도와 ForiegnExchangeProtocol을 채택하도록 하여 느슨한 연결도 유지가 가능하다. 이런 클래스는 유지보수도 편리하고, 재사용성도 높고, 테스트하기에도 좋은 구조를 가진다.

변화에 대한 보호(Protected Variations)

문제의식 : 변화에 따라서 다른 요소에 영향을 덜 주도록, 객체나 시스템을 설계하는 방법은 무엇인가 ?

해결방안 : 불안정적인 요소나 변화할 요소를 예측하고 분류해서, 안정적인 인터페이스를 갖도록 책임을 할당하라.

소프트웨어에서 가장 중요한 점은 쉽게 변화를 주는것이다. 요구사항이 바뀔 것에 대해 대비가 되어 있어야 한다. 수많은 디자인 가이드라인, 원칙, 패턴과 변화를 위해 다양한 수준의 추상화를 연습해야 한다. 아래 소개하는 내용들 ~

  • SOLID 원칙 (그 중에서도 Open-Close 원칙)
  • GoF (Gang of Four) 디자인 패턴
  • 캡슐화 Encapsulation
  • 디미터 법칙 Law of Demeter
  • 서비스 디스커버리 Service Discovery
  • 가상화와 컨테이너화 Virtualization and Containerization
  • 비동기 메시징 asynchronous messaging, 이벤트-주도 아키텍처 Event-driven architectures
  • 오케스트레이션 Orchestration, 코레오그래피 Choreography

마무리하며, 재사용과 변화에 대해 유연함이 소프트웨어에서는 매우매우 중요한 가치임을 또 한 번 깨달았다. 알지만 지키기 어렵긴한데ㅔㅔ

글을 읽으면서 느낀건 일단 많이 쪼개고, 책임을 다 분리하여 연결을 하면서 짠다면 좀 더 좋은 프로그램이 나오지 않을까 싶다. 매우 좋은 내용인듯하여 더 공부해야하지 싶다...

profile
hi there 👋

0개의 댓글