개발, 공부, 일상 블로그

[회고] 성능 측정의 필요성 - 성능 테스트 자동화 플랫폼 개발기 (1)

|

성능 측정의 필요성

weredoomed

2021년 8월 9일 이파피루스에 입사 후 벌써 다음달이면 입사 1년차를 바라보고있습니다.

그동안 제가 진행했던 가장 큰 프로젝트인 PdfGateway 차세대 버전 출시가 얼추 마무리되고 제품을 검증하기 위한 테스트가 내부적으로 진행되었습니다.

10만건, 100만건의 변환 작업을 투입하고 오류가 발생하지는 않는지, 병목이 발생하지는 않는지 검증했고 안정적으로 부하를 견딘다고 판단했고 제품을 출시하게 되었습니다.

그래서, 뭐가 더 좋아진건데요?

내부적으로 구현이 깔끔해졌다. 복잡한 데이터베이스 의존성을 단순하게 변경했다. 락에 의한 병목을 최소화 했다. 등등…

더 나아졌을거라는 느낌은 있었지만 이를 확실하게 보여줄만한 자료는 없었습니다.

그 때 일부 고객들이 제품의 성능 측정 자료를 요구했고, 그런 준비가 되어있지 않았던 우리는 부랴부랴 지표를 만들어내기 시작했습니다.

무엇을 성능의 지표로 삼을것인가?

일단 만들긴 해야겠는데, PdfGateway의 성능이란 무엇이고 어떻게 측정할 수 있을까에 대한 고민을 하게되었습니다.

PdfGateway의 가장 주요한 기능은 문서변환!

따라서 PdfGateway는 동일한 파일에 대한 변환 과정에서 변환 시작 부터 변환 완료 까지의 시간이 성능의 지표가 될 것이라고 생각했습니다.

고객의 관심사

고객은 우리의 제품이 어떻게 구현되었고, 어디에서 병목이 발생하는지에 대해서는 크게 관심을 두지 않습니다.

그저 문서 변환에 얼마나 걸리는지가 주된 관심사일 것이라고 생각했습니다.

우리의 관심사

개발자인 우리는 우리의 제품이 어디에서 병목이 발생하는지, 기능 추가 이후 성능 저하는 없는지에 대한 꾸준한 검증이 필요했습니다.

일단 고객이 먼저다

기업을 상대로 제품을 판매하는 B2B 회사인 만큼, 우리 회사의 타겟은 고객이 최우선이였습니다.

따라서 일단 고객이 원하는 지표를 생성하는 프로토타입을 만들어보자가 첫번째 과제였습니다.

프로토타입 개발

첫번째 프로토타입은 PgPerformanceTester라는 프로젝트였습니다.

파일 서버에 변환 테스트 대상 파일들을 확장자, 크기별로 준비해놓고

아래와 같은 지표를 설정했습니다.

  • 해당 파일들에 대해 1, 2, 4, 8, 16회 동시변환 시 시간이 얼마나 걸리는지
  • 이 속도라면 1시간동안 얼마나 변환될지
  • 하루에 몇개나 변환할 수 있을지

그리고 코드를 작성했습니다…

데이터 생성되는걸 보는것만이 유일한 목표였기 때문에…
근본 언어인 C언어 스타일로 개발했습니다.

.
.
.

메인_함수님만_믿습니다.jpg

trash-code

PdfGateway는 배치프로그램이기 때문에, 변환 결과를 Http 응답으로 주지 않습니다.

그 대신 요청시 callbackUri를 body로 보내고, 변환 완료 시 해당 callbackUri로 변환 결과를 POST 요청하는 비동기 방식입니다.

따라서 callback 요청을 처리할 서버가 필요했습니다…

알고 계셨나요? 자바에는 내장 HttpServer가 있답니다~

com.sun.net.httpserver.HttpServer

private void startHttpServer() {
  new Thread(() -> {
    try {
      this.server = HttpServer.create(new InetSocketAddress(8081), 0);
      this.server.createContext("/callback", exchange -> {
        PgResult pgResult =
          gson.fromJson(new InputStreamReader(exchange.getRequestBody()), PgResult.class);
        Optional.ofNullable(unitSent.get(pgResult.getId()))
                .ifPresent(testUnit -> testUnit.getResult().complete(pgResult));
        String res = "ok";
        exchange.sendResponseHeaders(200, res.length());
        OutputStream os = exchange.getResponseBody();
        os.write(res.getBytes());
        os.close();
      });
    } catch (IOException e) { throw new RuntimeException(e); }
    this.server.start();
  }).start();
}

그냥 스프링을 쓰는게 더 좋았을텐데…

처음 써본 HttpServer로 아주 간단한 callback 서버를 만들어서 테스트를 진행했습니다..

그 결과는…

매번 다른 변환시간

아까는 1초 걸렸는데 이번엔 3초 걸리네

테스트 대상 서버 환경에 따라 변환에 걸리는 시간 차이가 많이 벌어졌습니다.

어차피 한번 쓰고 버릴 프로그램이라 다시 만들기는 귀찮고, 해야할 다른 일은 많고…




그냥 여러번 돌려서 통계를 내자!

어쨌든 목표 달성 😄

우여곡절 끝에 부랴부랴 만들어진 산출물…

만나서 반가웠고 다신 보지말자 :)

report

매번 성능 측정이 필요할때마다 이걸 계속 수동으로 반복해야한다니…

처음 만든 사람이 앞으로도 계속 문서작업을 해야하지 않을까? 하는 생각이 들었습니다.

동시에 목표 또한 생겼습니다.

성능 측정 자동화가 필요하겠다.

팀장님과 몇번의 대화 그리고 회의 끝에 성능 테스트 자동화 플랫폼 개발이라는 과제가 저에게 주어졌습니다.

어떻게 만들것인가?

그건 다음에 알아보죠~ 😊

[Java] 1. 자바의 입출력과 변수

|

자바의 입출력과 변수

입출력 (I/O)

표준 스트림

  • input (stdin) - 0
  • output (stdout) - 1
  • error (stderr) - 2

입력 (Input)

  • System.in 스트림 사용
  • Scanner를 사용해서 스트림으로 입력된 값을 받음
    Scanner scanner = new Scanner(System.in);
    String oneWord = scanner.next();
    String oneLine = scanner.nextLine();
    int oneInteger = scanner.nextInt();
    

출력 (Output)

  • System.out 스트림 사용 (에러 출력을 위해서는 System.err)
    System.out.print("Hello "); // 출력 후 개행하지않음
    System.out.println("World"); // 출력 후 개행
    System.out.printf("%d %s\n", 1, "안녕"); // 문자열 포맷팅 (String formatting)
    

변수 (Variable)

원시 타입 (Primitive Type) - 8개

실제 데이터 값을 저장
소문자로 시작 (int, char, short …)

  • 문자형 (문자 하나 표현 a, b, c, d, …)

    한글을 표현할 수 있음
    C의 경우 char는 1바이트의 크기를 가지지만
    Java는 Unicode를 사용하여 2바이트의 크기를 가짐

    • char : 2 Bytes -> ASCII
  • 정수형 (-붙은 자연수, 0, 자연수)
    • byte: 1 Bytes (8bits)
    • short : 2 Bytes
    • int : 4 Bytes
    • long : 8 Bytes
  • 실수형 (소수점이 포함된 수, -붙은, 0)
    • float : 4 Bytes
    • double : 8 Bytes
  • 논리형 - true, false 값을 가짐
    • boolean : 1 Bytes

레퍼런스 타입 (Reference Type)

객체의 주소를 참조

원시타입을 제외한 모든 타입
대문자로 시작 (String, Object, Integer …)
모두 Object를 상속받음, null 값을 가질 수 있음

  • 문자열 : String
  • 원시타입의 Wrapper 클래스들
    • char : Character (16 Bytes)
    • byte : Byte (16 Bytes)
    • short : Short (16 Bytes)
    • int : Integer (16 Bytes)
    • long : Long (24.5 Bytes)
    • float : Float (16 Bytes)
    • double : Double (24.5 Bytes)
    • boolean : Boolean (16 Bytes)

원시타입 VS 레퍼런스 타입

  • 원시타입이 레퍼런스보다 속도가 빠름
  • 원시타입이 레퍼런스보다 메모리를 적게먹음
  • 레퍼런스는 다양한 원시 타입들로 구성됨

예제

정수 두개를 입력받아 더하여 출력하는 프로그램

Scanner scanner = new Scanner(System.in);
int a, b;
a = scanner.nextInt();
b = scanner.nextInt();
System.out.printf("%d + %d = %d\n", a, b, a + b);

참고

[Spring] 스프링의 시작, 옳게된 객체지향이란 무엇인가에 대해 알아보자 (SOLID)

|

스프링, 너 누구야!

spring

스프링의 시작, 옳게된 객체지향이란 무엇인가에 대해 알아보자

객체 지향적(Object Oriented)인 것

객체 지향적인 것은 무엇일까요?

객체 지향 프로그래밍은 프로그램을 명령어의 목록으로 보는 시각에서 벗어나, 프로그램은 여러 개의 독립적인 객체들의 모임이며, 이들간의 협력상호작용을 통해 메시지를 주고받고, 데이터를 처리하는 것으로 보는 관점입니다.

객체 지향에는 크게 4가지의 특징이 있습니다.

  1. 추상화 - 불필요한 부분은 생략하고 객체의 속성 중 가장 중요한 것에만 중점을 두어 간소화하는 것 -> 객체의 공통적인 속성과 기능을 중심으로 추상화

    ex) 송인걸, 유도곤, 김현수는 공통적으로 이름, 나이, 성별이라는 속성을 가지며, 걷기, 먹기, 일하기 등의 행위(기능)을 가진 사람으로 추상화 가능

  2. 캡슐화 - 객체의 공통적인 속성(필드)과 행위(메소드)를 가지는 하나의 클래스로 묶는 것

    ex) 송인걸, 유도곤, 김현수의 이름, 나이, 성별 속성과 걷기, 먹기, 일하기 기능을 하나의 사람 이라는 클래스에서 동작하도록 캡슐화

  3. 상속 - 이미 정의된 상위 클래스(부모 클래스)의 모든 속성과 연산을 하위 클래스가 물려받는 것

    ex) 사람동물의 나이, 성별 속성과 걷기, 먹기 행위를 물려받을 수 있음, 만약 고양이라는 새로운 클래스를 생성한다면, 고양이 역시 동물의 속성과 행위를 물려받을 수 있음

  4. 다형성 - 서로 다른 클래스의 객체가 같은 메시지를 받았을 때 각자의 방식으로 동작하는 능력

    ex) 사람고양이인사 라는 행위는 각각 “안녕하세요”, 또는 “야옹”과 같이 다르게 동작

이러한 객체 지향적인 프로그램은 유연하고, 변경하기 쉬워 대규모 소프트웨어 개발에 사용하기 좋습니다.

객체지향을 실생활에 비유해보자

notebook

배우역할에 대해서 생각해봅시다.

예를 들어, 영화 노트북의 주인공 노아 캘훈, 앨리슨 해밀튼역할입니다.
노아 캘훈라이언 고슬링이, 앨리슨 해밀튼레이첼 맥아담스가 배역을 맡았습니다.

만약 라이언 고슬링이 개인적인 사정으로 영화 촬영을 할 수 없게 된다면 노아 캘훈이라는 역할을 수정해야 할까요?

아니죠, 역할은 그대로, 배우만 교체하면 됩니다.

이 때 역할인터페이스, 배우를 그 인터페이스를 구현하는 클래스라고 생각해보십시오.

왜 객체지향이 유연하고 변경하기 쉬운지 이해하실 수 있을 것입니다.

좋은 객체지향 설계의 5가지 원칙 (SOLID)

그럼 객체지향이라고 다 좋은 객체지향이냐, 그건 또 아닙니다.

“좋은” 객체지향에 대해 클린코드의 저자 로버트 마틴이 정리한 원칙이 있습니다.

  • SRP (Single Responsibility Principle): 단일 책임 원칙
    • 하나의 클래스는 하나의 책임만 가져야 한다.
  • OCP (Open/Closed Principle): 개방-폐쇄 원칙
    • 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀 있어야 한다.
      • 소스코드를 변경하지 않고, 구현 객체를 변경할 수 있어야한다.
      • 이를 위해 객체를 생성하고, 연관관계를 맺어주는 별도의 무언가가 필요하다..
  • LSP (Liskov Substitution Principle): 리스코프 치환 원칙
    • 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다. (하위 타입의 클래스는 인터페이스의 규약을 모두 지켜야한다.)
  • ISP (Interface Segregation Principle): 인터페이스 분리 원칙
    • 범용 인터페이스 하나보다 특화된 여러개의 인터페이스로 분리하는 것이 낫다.

      ex) 자동차 인터페이스 -> 액셀 인터페이스, 핸들 인터페이스, 브레이크 인터페이스, 변속 인터페이스…

      • 이렇게 분리하면 변속 기어를 수동, 자동으로 변경해도 액셀과 핸들, 브레이크에는 영향을 주지 않을 수 있음
  • DIP (Dependency Inversion Principle): 의존관계 역전 원칙
    • 구현체가 아닌 추상화에 의존해야 한다.
    • 구현 클래스에 의존하지 말고 인터페이스에 의존하라는 말이다.
    • 그러나 어쨌든 우린 구현 클래스를 통해 인스턴스를 생성해야한다…
      어떻게 해야 구현 클래스가 아닌 인터페이스에 의존할 수 있을까? 별도의 무언가가 필요하다…

일반적으로 객체지향의 다형성만으로는 OCP, DIP를 지킬 수 없습니다. 별도의 무언가의 도움이 필요합니다.

빈(Bean)과 컨테이너(Container)

bean

일반적으로, 우리는 클래스의 인스턴스가 필요할 때 생성자를 호출하여 인스턴스를 생성합니다. 더 나아가, 오로지 하나의 인스턴스만 존재하여 재사용할 수 있도록 싱글톤패턴으로 구현하기도 합니다.

그러나 만약에 우리가 직접 인스턴스를 생성하지 않고, 해당 인스턴스를 생성하기 위해 필요한 모든 의존성들을 자동으로 생성해주고, 라이프사이클까지 관리해주는 무언가가 있다면 얼마나 좋을까요?

이렇게 하면 그저 인터페이스를 통해 필요한 객체를 접근해서 쓸 수 있고 OCP, DIP 원칙을 지킬 수 있을텐데요…

이 때 우리가 필요한 객체를 생성하고 관리해주는 것의 개념을 컨테이너(Container), 컨테이너에 의해 관리되는 객체를 빈(Bean) 이라고 합니다.

이러한 개념이 구현되어 아주 오랫동안 엔터프라이즈급 웹애플리케이션 시장을 점유했던 EJB (Enterprise Java Beans) 라는 프레임워크가 있습니다.

EJB는 EJB 컨테이너라는 것으로 빈을 관리했는데, 이를 사용하기 위해 필요한 상속 및 구현이 많아 비즈니스 로직에 집중하기 어려웠다고 합니다.

또한 객체지향적이지 않고, 특정 환경이나 기술에 종속적이며, 객체는 컨테이너 안에서만 동작 가능하여 테스트하기 어려운 (불가능에 가까운) 등 매우 생산성이 떨어지는 단점이 있었습니다.

스프링

추운 겨울이 지나고 봄이 찾아왔읍니다.

객체지향적이고, 특정 환경이나 기술에 종속적이지 않으며, 객체가 컨테이너 밖에서도 동작 가능하고, 테스트 하기 쉬운 프레임워크가 있다면 얼마나 좋을까요…

스프링 : thatsme

스프링은 의존성 주입: DI (Dependency Injection)라는 기술로 OCP, DIP를 가능하게 했습니다.

또한 순수 자바 객체: POJO (Plain Old Java Object)를 빈으로 관리하는 DI 컨테이너와 함께 봄이 찾아왔읍니다…

다음 이 시간에는 스프링의 DI 개념, 컨테이너, 빈을 알아보며 벚꽃놀이를 즐기도록 하겠습니다.

그럼 이만,,,

sakura

[Swift] 지네릭 (Generics)

|

지네릭

지네릭은 더 유연하고 재사용 가능한 함수와 타입의 코드를 작성하는 것을 가능하게 해준다.

지네릭 함수

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let tmp = a
    a = b
    b = tmp
}

var someIntA = 1
var someIntB = 2
swapToValues(&someIntA, &someIntB) // someIntA = 2, someIntB = 1

var someStringA = "Hello"
var someStringB = "World"
swapToValues(&someStringA, &someStringB) // someStringA = World, someStringB = Hello

지네릭 타입

지네릭을 이용하여 스택 자료구조를 구현하면 다음과 같다.

struct Stack<Element> {
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

var stackOfStrings = Stack<String>()
stackOfStrings.push("a")
stackOfStrings.push("b")
stackOfStrings.push("c")
stackOfStrings.push("d")

지네릭 타입의 확장

익스텐션을 이용해 지네릭 타입을 확장할 수 있다.

extension Stack {
    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
}

if let topItem = stackOfStrings.topItem {
    print("스택의 최상단 아이템: \(topItem).")
}

타입 제한

지네릭 타입이 특정 타입을 따르도록 제한할 수 있다.

아래 예제에서는, T가 Equatable 프로토콜을 따르는 경우에만 사용할 수 있다.

Equatable 프로토콜:
== 연산자를 정의하는 프로토콜

func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25]) // 옵셔널, nil
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"]) // 옵셔널, 2

연관 타입

특정 타입을 동적으로 지정해 사용할 수 있다.
associatedtype 키워드를 사용한다.

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
    var items = [Int]()
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
    // typealias를 사용해 Item의 별칭을 지정한다.
    typealias Item = Int
    mutating func append(_ item: Int) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Int {
        return items[i]
    }
}
struct Stack<Element>: Container {
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }

    mutating func append(_ item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

연관 타입에 제약 조건 추가

protocol Container {
    associatedtype Item: Equatable // 제약 조건
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

익스텐션을 사용하여 제약조건을 추가할 수 있다.
아래의 예제는 Suffix가 SuffixableContainer프로토콜을 따르고 Item타입이 반드시 Container의 Item타입이어야 한다는 제약조건이다.

protocol SuffixableContainer: Container {
    associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
    func suffix(_ size: Int) -> Suffix
}
extension Stack: SuffixableContainer {
    func suffix(_ size: Int) -> Stack { 
        var result = Stack()
        for index in (count-size)..<count {
            result.append(self[index])
        }
        return result
    }
    // Inferred that Suffix is Stack.
}
var stackOfInts = Stack<Int>() 
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2) // 20, 30

또는 다음과 같이 사용할 수 있다.

extension IntStack: SuffixableContainer {
    func suffix(_ size: Int) -> Stack<Int> { 
        var result = Stack<Int>()
        for index in (count-size)..<count {
            result.append(self[index])
        }
        return result
    }
}

지네릭의 Where 절

where 절을 사용하여 지네릭의 제약조건을 자세하게 설정할 수 있다.

func allItemsMatch<C1: Container, C2: Container>
    (_ someContainer: C1, _ anotherContainer: C2) -> Bool 
    where C1.Item == C2.Item, C1.Item: Equatable { // C1의 Item과 C2의 Item 타입은 같아야 하고, C1의 Item은 Equatable 프로토콜을 따라야한다.
        if someContainer.count != anotherContainer.count {
            return false
        }

        for i in 0..<someContainer.count {
            if someContainer[i] != anotherContainer[i] {
                return false
            }
        }
        
        return true
}

Where 절을 포함하는 지네릭의 익스텐션

extension Stack where Element: Equatable { // Element가 Equatable을 따르는지
    func isTop(_ item: Element) -> Bool {
        guard let topItem = items.last else {
            return false
        }
        return topItem == item
    }
}

struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue)  // Error
extension Container where Item == Double { // Item이 Double인지
    func average() -> Double {
        var sum = 0.0
        for index in 0..<count {
            sum += self[index]
        }
        return sum / Double(count)
    }
}
print([1260.0, 1200.0, 98.6, 37.0].average())
// Prints "648.9"

지네릭의 연관 타입에 where절 적용

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }

    associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
    func makeIterator() -> Iterator
}

protocol ComparableContainer: Container where Item: Comparable { }

지네릭 서브스크립트

지네릭 서브스크립트에도 조건을 걸 수 있다.

extension Container {
    subscript<Indices: Sequence>(indices: Indices) -> [Item]
        where Indices.Iterator.Element == Int { // Indices가 Sequence 타입을 따라야 하며, Iterator의 Element가 Int여야함
            var result = [Item]()
            for index in indices {
                result.append(self[index])
            }
            return result
    }
}

[Swift] 프로토콜 (Protocols)

|

프로토콜

특정 기능 수행에 필수적인 요소를 정의한 청사진.
프로토콜을 만족시키는 타입을 프로토콜을 따른다(conform)고 말한다.
프로토콜에 필수 구현을 추가하거나 추가적인 기능을 더하기 위해 프로토콜을 확장(extend)할 수 있다.

자바의 interface와 유사하다.

프로토콜 문법

protocol 키워드를 사용하여 정의

protocol SomeProtocol {
    ...
}

프로토콜을 따르는 타입을 정의하기 위해서 상속처럼 표현한다.

class SomeClass: SomeProtocol, AnotherProtocol {
    ...
}

서브클래싱인 경우 슈퍼클래스를 프로토콜 앞에 적어준다.

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class definition goes here
}

프로퍼티 요구사항

프로토콜에서는 프로퍼티가 저장된 프로퍼티인지 계산된 프로퍼티인지 명시하지 않는다.
하지만 프로퍼티의 이름타입 그리고 gettable, settable 한지는 명시한다.
필수 프로퍼티는 항상 var로 선언해야한다.

protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

타입 프로퍼티는 static 키워드를 적어 선언한다.

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
}

메소드 요구사항

타입 메소드

protocol SomeProtocol {
    static func someTypeMethod()
}

인스턴스 메소드

protocol RandomNumberGenerator {
    func random() -> Double
}

변경 가능한 메소드 요구사항

protocol Togglable {
    mutating func toggle()
}
enum ToggleSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}
var lightSwitch = ToggleSwitch.off
lightSwitch.toggle()

초기자 요구사항

protocol SomeProtocol {
    init(someParameter: Int)
}

프로토콜에서 특정 이니셜라이저가 필요했다고 명시했기 때문에 구현에서 required 키워드를 붙여줘야 한다.

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        ...
    }
}

타입으로써의 프로토콜

타입 사용이 허용되는 모든 곳에 프로토콜을 사용할 수 있다. (자바의 interface 처럼)

  • 함수, 메소드, 이니셜라이저의 파라미터 타입 혹은 리턴 타입
  • 상수, 변수, 프로퍼티의 타입
  • 컨테이너인 배열, 사전 등의 아이템 타입
protocol RandomNumberGenerator {
    func random() -> Double
}

class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0
    func random() -> Double {
        lastRandom = ((lastRandom a + c).truncatingRemainder(dividingBy:m))
        return lastRandom / m
    }
}

class Dice {
    let sides: Int
    let generator: RandomNumberGenerator
    init(sides: Int, generator: RandomNumberGenerator) {
        self.sides = sides
        self.generator = generator
    }
    func roll() -> Int {
        return Int(generator.random() Double(sides)) + 1
    }
}

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
    print("주사위의 눈: \(d6.roll())")
}

위임

클래스 혹은 구조체 인스턴스에 특정 행위에 대한 책임을 넘길 수 있게 해주는 디자인 패턴

protocol DiceGame {
    var dice: Dice { get }
    func play()
}
protocol DiceGameDelegate: AnyObject {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}
class OddEven: DiceGame {
    let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
    weak var delegate: DiceGameDelegate?
    func play() {
        delegate?.gameDidStart(self)
        delegate?.game(self, diceRoll: dice.roll())
        delegate?.gameDidEnd(self)
    }
}

익스텐션을 이용해 프로토콜 따르게 하기

이미 존재하는 타입에 새 프로토콜을 따르게 하기 위해 익스텐션을 사용할 수 있다.

protocol TextRepresentable {
    var textualDescription: String { get }
}

extension Dice: TextRepresentable {
    var textualDescription: String {
        return "\(sides)면체 주사위"
    }
}

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription) // 12면체 주사위

조건적으로 프로토콜 따르기

where 절을 사용하여 Array의 원소들이 특정 프로토콜을 따르는 경우에만 프로토콜을 따르도록 할 수 있다.

extension Array: TextRepresentable where Element: TextRepresentable {
    var textualDescription: String {
        let itemsAsText = self.map { $0.textualDescription }
        return "[" + itemsAsText.joined(separator: ", ") + "]"
    }
}

let d6 = Dice(sides: 12, generator: LinearCongruentialGenerator())
let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())

let dices = [d6, d12]
print(dices.textualDescription) // [6면체 주사위, 12면체 주사위]

익스텐션을 이용해 프로토콜을 따른다고 선언하기

이미 프로토콜의 모든 조건을 만족하지만 프로토콜을 따른다는 선언을 하지 않은 경우
빈 익스텐션으로 선언할 수 있다.

struct Hamster {
    var name: String
    var textualDescription: String {
        return "A hamster named \(name)"
    }
}
extension Hamster: TextRepresentable {}
// textualDescription이 구현되어 있으므로 TextRepresentable 프로토콜을 따를 수 있다.

프로토콜 상속

프로토콜도 프로토콜을 상속할 수 있다.

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    ...
}

클래스 전용 프로토콜

클래스 전용 프로토콜인 경우 프로토콜에 AnyObject를 추가한다.

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    ...
}

선택적 프로토콜 요구조건

@objc 키워드를 사용하여 필수 구현이 아닌 선택적 구현 조건을 정의할 수 있다.
프로토콜 앞에 @objc 키워드를 붙이고, 함수나 프로퍼티에 @objcoptional 키워드를 붙인다.
@objc 프로토콜은 클래스 타입에서만 사용할 수 있다.

@objc protocol CounterDataSource {
    @objc optional func increment(forCount count: Int) -> Int
    @objc optional var fixedIncrement: Int { get }
}

class Counter {
    var count = 0
    var dataSource: CounterDataSource?
    func increment() {
        // dataSource의 increment 메소드가 구현되어있지 않을 수 있기 때문에 옵셔널 체이닝을 이용한다.
        if let amount = dataSource?.increment?(forCount: count) {
            count += amount
        // dataSource의 fixedIncrement 프로퍼티가 구현되어있지 않을 수 있기 때문에 옵셔널 체이닝을 이용한다.
        } else if let amount = dataSource?.fixedIncrement {
            count += amount
        }
    }
}

프로토콜 익스텐션

익스텐션을 이용해 프로토콜을 확장할 수 있다.

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        // 이미 정의된 random 메소드를 사용하여 randomBool() 메소드를 추가
        return random() > 0.5
    }
}