반응형

Swift의 데이터 타입과 대수학(Algebra)의 관계에 대해서 알아보도록 하겠습니다.

데이터 타입과 대수학이 관계가 깊다는 얘기를 우연히 접한적이 있습니다. 데이터 타입은 제가 String, Int, Bool, Long, Short, unsigned Int 등 문자열, 실수 정수 등 여러 종류의 데이터를 나타내기 위한 것이다라는 생각을 바로 할 수 있었는데요

 

대수학은 뭘까요??…. 나무위키에 따르면 초중등교육에서는 미지수에 변수를 대입하는 기술이라고 하는데.... 그 이상의 개념은 저는 잘 모르겠어서 암튼 여기까지 하고 일단 넘어가 보겠습니다.

 

 

구조체와 대수학

 

먼저 아래와 같은 구조체를 정의해보겠습니다.

struct Some<T, U> {
	let first: T
	let second: U
}

위 구조체는 제네릭 타입으로 T, U를 받고 first와 second라는 2개의 필드에 각각 T, U의 타입을 가지고 있습니다.

 

만약 T와 U가 Boolean값이라면 Some이라는 구조체가 가질 수 있는 경우의 수는 총 4가지 입니다.

(true, true), (true, false), (false, true), (false, false)

 

만약 Boolean을 T에 3가지의 경우를 가질 수 있는 열거형을 U타입에 넘기면 어떻게 될까요?

enum SomeEnum {
	case a
	case b
	case c
}

Some<Bool, SomeEnum>(first: true, second: .a)
Some<Bool, SomeEnum>(first: true, second: .b)
Some<Bool, SomeEnum>(first: true, second: .c)
Some<Bool, SomeEnum>(first: false, second: .a)
Some<Bool, SomeEnum>(first: false, second: .b)
Some<Bool, SomeEnum>(first: false, second: .c)

이렇게 총 6가지의 경우가 나옵니다. 2*3이죠

 

만약 U의 위치에 Void를 넣으면 어떻게 될까요?? Void는 경우의 수가 () 한 개뿐이므로

2*1의 2가지 경우가 나오죠.

 

Void는 수학적으로 1을 의미한다고 해석하면 될 것 같기도 합니다?!

 

그렇다면 0은 어떻게 표현할까요??

 

간단합니다. Never라는 키워드를 Result타입의 Failure타입에서 보신적이 있으실 거에요.

Failure타입에 있을 경우 발생하지 않는다는 뜻을 나타내게 되는데요. Never의 실제 구현은 아래와 같습니다.

@frozen enum Never { }

케이스가 없는 열거형이네요.

실제로 아래와 같이 U에 Never를 넘길 경우 아예 구조체 인스턴스를 만들 수 조차 없습니다.

Some<Bool, Never>(first: true, second: ...???)

자 잠깐 정리하면

Bool은 2, Void는 1, Never는 0 이네요.

 

그리고 구조체에서는 T와 U의 경우의 수의 곱만큼 실제 구조체가 가질 수 있는 경우의 수가 존재하게 됩니다.

<Bool, Bool> = 2*2

<Bool, Void> = 2*1

<Void, Void> = 1*1

<Bool, Never> = 2*0

 

 

열거형과 대수학

열거형에서는 어떻게 될까요

enum ExampleEnum<T, U> {
	case a(T)
	case b(U)
}

위와 같은 열거형이 있다고 해봅시다. 그리고 구조체에서와 같이 아래의 총 4개의 열거형을 정의해보겠습니다.

ExampleEnum<Bool, Bool>
ExampleEnum<Bool, Void>
ExampleEnum<Void, Void>
ExampleEnum<Bool, Never>

그러면 열거형이 가질 수 있는 경우의 수는 아래와 같습니다

ExampleEnum<Bool, Bool>.a(true)
ExampleEnum<Bool, Bool>.a(false)
ExampleEnum<Bool, Bool>.b(true)
ExampleEnum<Bool, Bool>.b(false)
// 2+2

ExampleEnum<Bool, Void>.a(true)
ExampleEnum<Bool, Void>.a(false)
ExampleEnum<Bool, Void>.b(())
// 2+1

ExampleEnum<Void, Void>.a(())
ExampleEnum<Void, Void>.b(())
// 1+1

ExampleEnum<Bool, Never>.a(true)
ExampleEnum<Bool, Never>.a(false)
// 2+0

구조체와 다르게 곱연산이 아니라 합연산만큼 경우의 수가 있는 것을 볼 수 있습니다.

 

Optional을 예로 들어보겠습니다.

enum Optional<A> {
	case none
	case some<A>
}

여기서 none은 none(Void)로 볼 수 있을 것 같습니다. Optional.none(())와 Optional.none은 언제나 한가지의 경우만 존재하니까요.

 

그래서 Optional<A>의 경우의 수는 1 + A의 경우의 수만큼 존재할 수 있음을 알 수 있고 이를

Optional<A> = A? = 1 + A = Void + A

이렇게 볼 수 있겠네요

 

이걸 아는게 왜 중요할까요???

결론부터 얘기하면 실제 서비스를 만들기 위해 데이터를 모델링할 때 존재하지 않을 케이스를 만들지 않아서 불필요한 작업을 사전에 방지하는데 도움이 되기 때문입니다.

 

유저 정보를 API호출로 받아오는데 거기에는 이름과 나이 전화번호를 준다고 해봅시다.

또 전화번호는 국가코드와 국가코드 번호 그리고 숫자 번호가 있다고 해봅시다. 그리고 전화번호는 유저가 회원가입 시 입력할 수도 있고 안 할수도 있습니다.

이를 모델링 해보겠습니다.

 

첫 번째 방법

struct UserInfo {
	let name: String
	let age: Int
	let phoneNationalCode: String?
	let phoneNationalCodeNumber: String?
	let phoneNumber: String?
}

두 번째 방법

struct UserInfo {
	let name: String
	let age: Int
	var phoneNumber: PhoneNumber?
}

struct PhoneNumber {
	let nationalCode: String
	let nationalCodeNumber: String
	let number: String
}

제 생각에는 2 번째 방법이 보다 잘 도메인을 잘 표현했다고 생각이 듭니다. 단순히 optional unwrapping의 횟수를 3번에서 1번으로 줄여서가 아니라 개념적으로 동시에 존재해야만 존재할 수 있는 3가지 개념들을 제대로 표현해낸 방법이 2번째이기 때문입니다.

 

 

다른 예를 또 들어보겠습니다.

 

대기열 기능을 만드려고 합니다

대기열에는 다음 3가지 상태가 있습니다.

  • 들어가기 전 ready상태
  • 대기열에 들어가서 기다리는 waiting 상태로 이 때는 내 앞에 남은 사람의 숫자 n을 같이 보내줍니다
  • 들어갈 수 있는 상태 finish

서버측에서 API Response 형상 예시를 보내왔습니다. 아래처럼 말이죠

{
	"status": "ready"
}

{
	"status": "waiting",
	"remaining_count": 6,
}

{
	"status": "finish"
}

어떻게 모델링하면 좋을까요??

 

 

첫 번째 방법

enum WaitingStatus {
	case ready
	case waiting
	case finish
}

struct Wating {
	case status: WaitingStatus
	case remainingCount: Int?
}

두 번째 방법

enum WaitingStatus {
	case ready
	case waiting(Int)
	case finish
}

당연히 2번째 방법이 좋습니다. 첫 번째 방법의 경우는 ready인데 9명이 기다리고 있다라는 상태를 가질 수 있는 가능성을 모델링에서 막아주고 있지 않기 때문입니다.

 

 

오랜만에 하는 포스팅이기 때문에 가벼운 글부터 시작해 보았는데요. 도움이 되셨을 지 모르겠네요ㅎㅎ

 

참고

https://namu.wiki/w/대수학

https://www.pointfree.co/episodes/ep4-algebraic-data-types

반응형
반응형

테스트 더블(Test Double)이란 저자인 xUnit Test Patterns의 제라드 메스자로스(Gerard Meszaros)가 만든 용어로 테스트를 진행하기 어려울 때 대신 테스트를 진행할 수 있도록 도와주는 객체를 말합니다.

테스트 객체는 크게 5개의 종류로 나뉩니다요.

 

Dummy

Martin Fowler에 의하면 Dummy는 전달되지만 실제로는 쓰지 않을 객체를 말합니다. 그저 init할 때 parameter를 채우기 위한 용도로만 쓰이구요. 내부 구현은 하지 않습니다.

protocol SomeProtocol {
	func doSomething()
	func logSomething() -> String
}

class DummySomeProtocol: SomeProtocol {
	func doSomething() {}
	func logSomething() -> String {
		return ""
	}
}

Stub

Martin Fowler에 의하면 Stub은 미리 정한 값(예를 들면 더미값)을 테스트 시에 그대로 반환하는 역할을 합니다.

// 예제
class RecentKeywordsStub: RecentKeywordsRepository {
	let keywords = ["a", "b", "c"]
	func getRecentKeywords() -> [String] {
		return keywords
  }
}
// 테스트
func test_getRecentKeywords() {
   XCTAssertEqual(sut.getRecentKeywords(), sut.keywords)
}

Spy

스텁과 같은 역할을 하며 더불어 메소드 호출 여부와 호출 횟수를 기록하는 역할을 합니다. 솔직히 스파이는 써보진 않았습니다. 이 행위를 검증하는 역할을 Mock을 만들어서 검증을 많이 하였습니다. 그래도 일단 아래와 같이 사용할 수 있다고는 하네요. 테스트 마지막 부분을 보면 호출이 정확히 되었는지를 확인하고 있습니다

class QuotesServiceSpy: QuotesService {
    private (set) var quoteCalls: [[String]] = []
    private let stocks: [Stock]
    private let error: Error?
    
    init(stocks: [Stock] = [], error: Error? = nil) {
        self.stocks = stocks
        self.error = error
    }
    
    func getQuotes(symbols: [String], 
                   completion: @escaping (Result<[Stock], Error>) -> Void) {
        quoteCalls.append(symbols)
        if let error = error {
            completion(.failure(error))
        } else {
            completion(.success(stocks))
        }
    }
}

class WatchlistViewModelTests: XCTestCase {    
    func test_getWatchlistWithSymbols_callGetQuotes() {
        let stockSymbols = ["AAPL", "MSFT", "GOOG"]
        let watchlistStore = WatchlistStoreStub(stockSymbols: stockSymbols)
        let quotesService = QuotesServiceSpy()

        let sut = WatchlistViewModel(store: watchlistStore, 
                                     service: quotesService)
        sut.getWatchlist()

				// 여기서 정확히 호출이 되었나를 확인해 테스트 통과/실패
        XCTAssertEqual(quotesService.quoteCalls[0], stockSymbols)
    }
}

Fake

Martin Fowler에 의하면 Fake 객체는 동작하는 구현을 실제로 가지고 있지만 실제 상품으로 내기보다는 테스트를 위해 간편하게 만든 구현을 가지고 있습니다

아래 Fake만보기의 start함수의 구현을 예로 들면 실제 걸음수를 가지고 와서 업데이트 하는 것이 아니라 1초마다 걸음 수를 올리는 것처럼 말이죠. 이렇게 하면 실제 디바이스가 아닌 시뮬레이터에서 테스트를 하더라도 걸음수가 올라가는지 테스트를 할 수 있겠죠?!

import Foundation

class SimulatorPedometer: Pedometer {
  var timer: Timer?
  var distance = 0.0

  func start()
    timer = Timer(timeInterval: 1, repeats: true) { _ in
      self.distance += 1
    }
    RunLoop.main.add(timer!, forMode: RunLoop.Mode.default)
  }
}

Mock

Martin Fowler에 의하면 Mock은 함수들이 원하는데로 호출되었는지를 검증할 때 쓴다고 합니다. 으음.. 저는 여기까지만 읽었을 때에는 Spy와 너무 비슷한 느낌이 드는데요..? 뭐가 다른건지 참.

다음에 네트워크를 Mocking하는 것을 한 번 정리해보려고합니다. 맨 밑에 이미지에 각 테스트 더블들의 범위(?)를 나타낸 사진이 있는데 솔직히 저는 Mock은 행위 검증이든 상태검증이든 다 할 수 있는 것 같은 느낌이 들더라구요.

아무튼 테스트 더블의 종류들의 경계가 살짝 모호한 것 같고 칼로 딱 나눠자를 수 없다는 생각이 들었는데 저는 아래 그림처럼 이해를 해보니까 조금 더 도움이 되더랍니다.ㅎㅎㅎㅎ.. 누군가에게 도움이 되었기를

*https://docs.microsoft.com/en-us/archive/msdn-magazine/2007/september/unit-testing-exploring-the-continuum-of-test-doubles*

Although these types seem distinct in theory, the differences become more blurred in practice. For that reason, I think it makes sense to think of test doubles as inhabiting a continuum, as illustrated in Figure 2. - MSDN Magazine -

 

참고 및 출처

https://medium.com/@pena.fernan/test-doubles-by-example-in-swift-558e9f47de52

https://hudi.blog/test-double/

https://mokacoding.com/blog/swift-test-doubles/

https://www.martinfowler.com/bliki/TestDouble.html

https://codinghack.tistory.com/92

https://sujinnaljin.medium.com/swift-mock-을-이용한-network-unit-test-하기-a69570defb41

https://www.swiftbysundell.com/articles/mocking-in-swift/

반응형
반응형

 

iOS 앱을 만들 때 모든 것을 다 Swift로 짜면 좋으련만 프로젝트를 진행하다보면 그렇지 못할 경우가 간혹 생깁니다.

Web RTC를 쓴다던지 게임 엔진을 같이 사용해야 한다던지 등등..

 

회사에서 cocos2dx 게임엔진이 메탈을 지원하기 때문에 cocos2dx를 swift와 같이 사용할 일이 생겼습니다. 

이 때 처음으로 C++ 소스를 Swift와 같이 사용하게 되며 공부를 하였는데 어떻게 사용할 수 있는지 그 방법을 다시 정리해보았습니다.

 

결론을 말씀드리면 C++는 Objective C++로 Wrapping해서 Swift에서도 쓸 수 있습니다.

 

Photo credit: Cecilia Humlelu

Swift는 Objective C(++)과 C는 직접 상호동작이 가능하지만 C++은 그렇지 못한 걸 알 수 있습니다.

 

 

아래 순서대로 진행할 생각입니다.

1. Xcode 프로젝트 Swift로 생성

2. C++ 파일 작성

3. Objective C++ Wrapper만들기

4. 브릿지 헤더에 만든 Objective C++의 헤더 추가하기

5. Swift에서 호출하기

 

1. Xcode 프로젝트 Swift로 생성

편한 이름으로 언어는 Swift를 선택하시고 iOS 앱 프로젝트를 생성해주세요. 저는 CppTest로 하였습니다.

 

 

2. C++ 파일 작성

[File] -> [New] -> [File]을 눌러(혹은 ⌘N) 새로운 C++ source file을 만들어주세요. 

생성 시 Also create a header file을 체크하고 만들어주세요. 저는 MyCpp로 하였습니다.

그러면 Bridge header를 만들 것인지 묻습니다. 만들어주세요

브릿지 헤더를 만듭니다

다 만들고 나면 아래와 같이 파일들이 생성될 것입니다.

C++ 파일 생성

 

*.hpp 파일을 열고 아래와 같이 입력해주세요.

// MyCpp.hpp

#include <string>
#include <iostream>

class MyCpp {
public:
    MyCpp();							// 생성자
    MyCpp(const std::string &text);				// 변수를 입력 받는 생성자
    ~MyCpp();							// 소멸자(Destructor)

public:
    void sayHello();						// Hello world를 출력하는 함수

    void setText(const std::string &text);			// m_text의 getter & setter
    const std::string &getText();
    
    void setNumber(const int number);				// m_number의 getter & setter
    int getNumber();

private:
    std::string m_text;						// 문자열 변수
    int m_number;						// 정수값 변수
};

 

 

 

*.cpp 파일을 열고 아래와 같이 입력해주세요.

//  MyCpp.cpp


#include "MyCpp.hpp"

MyCpp::MyCpp():m_text() {}
MyCpp::MyCpp(const std::string &text): m_text(text) {}
MyCpp::~MyCpp() {}

void MyCpp::sayHello()
{
    std::cout << "Hello world!" << std::endl;
    
}

void MyCpp::setText(const std::string &text)
{
    m_text = text;
}
const std::string &MyCpp::getText()
{
    return m_text;
}

void MyCpp::setNumber(const int number)
{
    m_number = number;
}

int MyCpp::getNumber()
{
    return m_number;
}

 

설명이 필요없는 아주 기본적인 각각 문자열과 정수값을 설정하고 반환하는 함수들입니다.

 

3. Objective C++ Wrapper만들기

새로운 objective-c 파일을 만들어주세요. 그리고 확장자를 .mm으로 바꿉니다. 저는 이름을 CWrapper.mm로 하였습니다.

방금 만든 파일의 헤더(header)도 만들어주세요. 제 파일은 CWrapper.h입니다.

Objective-C++을 생성하고 나서의 모습입니다. 아래쪽에 *.mm과 *.h가 하나씩 추가되었습니다.

 

*.h를 작성해 주세요

//  CWrapper.h

#import <Foundation/Foundation.h>

@interface CWrapper : NSObject
- (instancetype)initWithText:(NSString*)text;
- (void)helloWorld;

- (void)setText:(NSString*)text;
- (NSString*)getText;

- (void)setNumber:(int)number;
- (int)getNumber;
@end

 

C++에 있던 것과 마찬가지로 문자열을 입력받는 상속자와 문자열, 정수값을 설정하고 반환하는 함수들을 선언하였습니다.

 

이제 *.mm을 작성해 주세요.

만약 복붙이 아니라 직접 입력을 하신다면 헤더 파일을 만들어서 자동완성에 함수가 뜨는 것을 볼 수 있습니다.

#import "CWrapper.h"
#include "MyCpp.hpp"

@interface CWrapper()
@property MyCpp *cppItem;
@end
@implementation CWrapper
- (instancetype)init
{
    self = [super init];
    self.cppItem = new MyCpp();
    return self;
}
- (instancetype)initWithText:(NSString*)text
{
    self = [super init];
    self.cppItem = new MyCpp(std::string([text cStringUsingEncoding:NSUTF8StringEncoding]));
    
    return self;
}

- (void)helloWorld
{
    printf("Hello world");
}

- (void)setText:(NSString *)text
{
    self.cppItem->setText(std::string([text cStringUsingEncoding:NSUTF8StringEncoding]));
}

- (NSString *)getText
{
    return [NSString stringWithUTF8String:self.cppItem->getText().c_str()];
}

- (void)setNumber:(int)number
{
    self.cppItem->setNumber(number);
}

- (int)getNumber
{
    return self.cppItem->getNumber();
}
@end

 

 

4. 브릿지 헤더에 만든 Objective C++의 헤더 추가하기

아래와 같이 방금 만든 Objective C++의 헤더를 추가해주세요. 이제 준비는 끝났습니다.

[project name]-Bridging-Header.h

// CppTest-Bridging-Header.h

#import "CWrapper.h"

 

5. Swift에서 호출하기

 

ViewController.swift

//  ViewController.swift

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let cppItem = CWrapper();

        cppItem.helloWorld()
        print(cppItem.getText())
        cppItem.setText("This is test string")
        print(cppItem.getText())

        
        let cppItem2 = CWrapper(text: "Hi my name is cpp");
        print(cppItem2?.getText())
        
        cppItem2?.setNumber(33)
        print(cppItem2?.getNumber())
    }
}

 

실행하면 콘솔에 다음과 같은 로그를 볼 수 있습니다.

(std::endl은 콘솔에서는 동작하지 않네요ㅠ ㅎㅎ)

 

[참조]

(첫 번째 사진)

https://medium.com/@cecilia.humlelu/using-c-c-and-objective-c-frameworks-in-swift-apps-6a60e5f71c36

 

Using C, C++ and Objective-C frameworks in Swift apps

I did a talk in try!Swift Tokyo this year about using C, C++ and Objective-C frameworks in Swift apps. The presentation was quite concise…

medium.com

https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/importing_swift_into_objective-c

 

Importing Swift into Objective-C | Apple Developer Documentation

Article Importing Swift into Objective-C Access Swift types and declarations from within your Objective-C codebase. OverviewYou can work with types declared in Swift from within the Objective-C code in your project by importing an Xcode-generated header fi

developer.apple.com

https://www.ekreative.com/blog/using-c-code-and-libraries-in-applications-written-in-swift/

 

Using C ++ code and libraries in applications written in Swift

Swift is a cool high-level programming language, but it’s still quite young and doesn’t have as many libraries and components as Objective-C.

www.ekreative.com

 

반응형
반응형

터미널을 열고 프로젝트가 있는 디렉토리에서 아래 명령어를 입력해주면 됩니다.

 

xcodebuild -project {project name}.xcodeproj -target iosSample -showBuildSettings | grep {variable name}

xcodebuild -project test.xcodeproj -target iosSample -showBuildSettings | grep PROJECT_ROOT

 

반응형

'swift' 카테고리의 다른 글

Data Types and Algebra  (0) 2024.01.21
테스트 더블이란  (2) 2023.04.13
Swift, Objective-C, C++ 같이 사용하기  (0) 2020.04.25
[Swift] 원하는 앱의 document 폴더 확인하기  (0) 2020.04.06

+ Recent posts