활동 내용

  • 모둠 합동 계산기 프로젝트 Class Diagram 작성
  • 모둠원과 Generic Type Stack 구현
  • SOLID 활동 학습
  • 이사하고 짐 정리 계속..

활동 내용 상세

계산기 프로젝트 Class Diagram 작성

타입별 필요할 것으로 예상하는 프로퍼티와 메서드를 작성하였다. 추가 보완할 점은 아래 사항들이 있겠다.

  • 각 타입 간의 관계를 나타내는 UML 화살표를 추가
  • 각 계산기 타입에 공통적인 기능인 덧셈과 뺄셈 기능을 추가해주는PlusAndMinus 프로토콜과 익스텐션 프로퍼티와 메서드 작성
  • 구조체 타입인 Stack<T>list를 변경하거나 변경하는 메서드를 호출하는 메서드에 대해 mutating 키워드를 작성해주는 것

추가로 고민해볼 점은 십진 계산기와 이진 계산기 타입의 경우 애플리케이션에서 여러 인스턴스를 만들 여지가 없으니 내부 요소들을 타입 메서드와 타입 프로퍼티로 선언하는 것이 있겠다.

학습 내용

Generic Type Stack 구현

계산기 프로젝트에서 한 가지의 요구사항으로 스택을 직접 구현하였다. 반드시 이진수 계산기와 십진수 계산기 타입이 있을 것이므로 각 계산기에서 주로 사용하는 타입 (DoubleInt)에 맞게 범용적으로 사용할 수 있는 Stack을 구현하려면 제네릭 타입으로 구현하는 것이 좋다고 판단했다.

struct Stack<T> {
    private var list = [T]()
    
    var isEmpty: Bool {
        return self.list.isEmpty
    }
    
    var top: T? {
        return self.list.last
    }
    
    mutating func push(_ item: T) {
        self.list.append(item)
    }
    
    mutating func pop() -> T? {
        return self.list.popLast()
    }
    
    mutating func reset() {
        self.list.removeAll()
    }
}

실제로 계산을 구현할 때는 top 연산프로퍼티에서 반환되는 값과 새로 사용자가 입력하는 값을 통해 연산하고, push(_:) 메서드를 통해 새로운 계산 결과를 스택에 넣어주는 형식으로 활용할 것이다. pop() 메서드는 활용처를 생각해보지 않았는데 스택의 기본기능이라 구현해보았다. 프로젝트를 진행하면서 활용처를 고민해보아야겠다.

SOLID 원칙

객체지향 프로그래밍 패러다임에 맞는 프로그래밍을 하기 위한 5가지 원칙이다.

들어가기에 앞서

SOLID 원칙은 아래의 가치를 지키기 위한 한 가지 수단이라는 점을 잊지 말아야 한다. 프로그래밍 디자인 패턴과 마찬가지로 더 나은 객체지향 프로그래밍을 하기 위한 수단일 뿐이다. 이들을 따르더라도 더 좋은 결과물을 만들 수 있다는 것을 보장하지 않으며 너무 원칙에 끼워맞추려 할 경우 오히려 아래와 같은 가치를 훼손시키는 본말전도의 사례가 될 수 있다.

SOLID 원칙을 통해 지키고자하는 핵심 가치

가독성과 커뮤니케이션

  • 개발자는 코드를 통해 커뮤니케이션 한다.
  • 이해가 잘되는 코드일수록 좋다. 반면 이해할 수 없는 코드일수록 가치가 낮다.

단순성

  • 코드는 아래와 같은 이유로 인해 단순해야 한다.
    • 커뮤니케이션에 도움이 된다.
    • 버그가 생길 요소가 적어진다.
  • 미래의 확장을 위해 복잡한 패턴은 경계의 대상이 된다.

유연성

  • 유연성을 향상시키면 향후 코드를 수정하는데 소요되는 시간을 절감할 수 있다.
  • 하지만 유연성을 향상시키는 과정에서 코드의 가독성이 떨어질 수 있다.
  • 그러므로 유연성과 단순성은 대부분의 상황에서 trade-off 관계라 할 수 있다.

언제부터 유연성을 고려해야할까?

  • 처음에는 단순하게 짠다.
  • 기획 또는 정책이 변경된다면, 앞으로도 발생할 수 잇는 변경인지를 고려한다.
  • 확장성을 고려할 수 있도록 코드를 재구성한다.

SOLID 원칙

다섯 가지 원칙에 대해 별도로 자세히 포스팅한다면 좋을 것 같다. 포스팅하면 링크 예정.

Single-Responsibility Principle (SRP; 단일 책임 원칙)

한 클래스는 한 가지의 책임만을 가져야 한다.

  • 소프트웨어 요소 (클래스, 모듈, 함수 등)는 응집도 있는 하나의 책임을 갖는다.
  • 클래스를 변경해야 하는 이유는 단지 - 응집도여야 한다.

Note: 응집도는 관련성 있는 코드들이 얼마나 잘 묶여있는지를, 결합도는 불필요한 의존성이 있는지를 나타내는 척도이다.

Open-Close Principle (OCP; 개방 폐쇄 원칙)

  • 소프트웨어 요소는 확장가능하도록 열려있고, 변경에는 닫혀 있어야 한다.
  • 새 기능을 추가할 때 변경하지 말고 새 클래스 또는 함수를 만든다.
    • 확장을 할 때는 기존의 코드를 최대한 건드리지 않고 확장한다.
    • 만약 기존의 코드를 수정하게 되면 연쇄적인 수정을 하지 않을 수 있게 하자.
    • 기존 코드의 수정은 버그 발생 가능성이 있고, 이를 테스트 해야한다.

Liskov Substitution Principle (LSP; 리스코프 치환 원칙)

자식 클래스는 부모 클래스로써의 역할을 완벽히 수행할 수 있어야 한다.

  • 상속을 통해 만든 서브 타입은 기본 타입으로 대체 가능해야 한다.
  • 자식 클래스는 부모 클래스 동작(의미)를 바꾸지 않는다.

Dependency-Inversion Principle (DIP; 의존성 역전 원칙)

  • 상위 레벨 모듈은 하위 레벨 모듈에 의존하지 않아야 한다 (둘 다 추상화된 인터페이스에 의존해야 한다. 추상화는 구체화에 의존하면 안 되고, 구체화는 추상화에 의존하면 안 된다.).

Interface-Segregation Principle (ISP; 인터페이스 분리 원칙)

  • 클라이언트 객체는 사용하지 않는 메서드에 의존하지 않아야 한다.
    • 상속 받은 메서드를 퇴화시켜야 하는 경우가 발생할 수 있다.
    • 불필요한 인터페이스에 의존하여 불필요한 빌드가 유발될 수 있다.
  • 큰 인터페이스를 작은 인터페이스로 분리하고, 필요한 부분만 클라이언트가 취사선택하여 사용할 수 있게 하여야 한다.

고민한 점 및 문제점

제네릭 타입을 지원하지 않는 프로토콜의 유연한 타입 적용 방법

struct, class, enum과 같은 타입은 제네릭 타입을 적용할 수 있는 반면, 프로토콜은 그렇지 못하다. 이유를 알고 싶다. 어떻게 찾아보면 될까.. why does protocol not allow generic type in swift 구글링..?
역시 구글링하니 뭔가 나오는군.. Swift Programming Language의 Generic 챕터의 Associated Types 부분을 읽어보면 답이 나올 것 같다..! 현재 예상하는 바로는 associatedtype을 이용하면 한 가지 타입을 선택해야 하는 제네릭과 달리 프로토콜에서 요구하는 프로퍼티나 메서드를 각자 원하는 타입으로 자유롭게 설정이 가능할 것 같다.

해결 방법

  • associatedtype을 이용해서 프로토콜을 준수하기 위한 프로퍼티 또는 메서드를 구현할 때 작성한 타입을 따르게 한다.
protocol UserInputConvertible {
    associatedtype T
    func userInput() -> T 
    // 채택하는 타입에서 T를 자유롭게 설정할 수 있다. 
    // 제네릭 타입처럼 어떠한 타입으로 지정하여도 프로토콜을 준수한다고 판단한다.
}

struct UserInputToString: UserInputConvertible {
    func userInput() -> String? {  // 프로토콜의 T 타입을 String? 타입으로 적용하여도 프로토콜을 준수함.
        return readLine()
    }
}

struct UserInputToInt: UserInputConvertible {
    func userInput() -> Int? { // 프로토콜의 T 타입을 Int? 타입으로 적용하여도 프로토콜을 준수함.
        guard let _userInput = readLine() else { return nil }
        return Int(_userInput)
    }
}
profile
합리적인 해법 찾기를 좋아합니다.

0개의 댓글