[iOS] 모듈 분리하기(framework로 만들기)

김민석·2024년 5월 24일
0

iOS

목록 보기
6/12

Module?

모듈은 하나의 코드 묶음.
하나의 프레임워크, 하나의 라이브러리, 하나의 애플리케이션이 모듈의 단위입니다.

더 간단하게 말하면, Swift의 import키워드를 사용하여 다른 모듈에서 가져올 수 있는 framework 또는 응용 프로그램.

우리가 import해서 사용하는 라이브러리 = 모듈

Why?

App이 커질 수 록 프로젝트를 작은 단위의 모듈로 분리하는 것이 유지보수 측면에서 유리할 수 있습니다.
모듈화를 하기 위해서 Tuist(다음에 해볼 예정) 같은 프레임 워크를 사용해도 좋지만, 이번엔 Xcode에서 재공하는 Custom Framework를 만들어 모듈화를 해보겠습니다.

모듈화의 장점

빌드는 기본적으로 코드가 수정된 모듈만 빌드되기 때문에 모듈화가 어느정도 빌드속도를 줄여주는 장점이 있습니다.
모듈간 결합도를 낮추고, 모듈은 다른 프로젝트에서도 재활용하여 사용하기 용이합니다.













모듈화전 파일

위와 같이 폴더 구조가 있었고 좀더 설명을 위해 자세히 폴더를 보면

├── Main
│   ├── MainContract
│   ├── MainPresenter
│   └── MainViewController
├── Start
│   ├── StartContract
│   ├── StartPresenter
│   └── StartViewController
├── View
│   ├── StartView.storyboard
│   └── MainView.storyboard
└── Domain
    ├── Ball
    │   ├── Ball.swift
    │   ├── OpponentBall.swift
    │   └── UserBall.swift
    ├── NumberGenerator
    │   ├── NumberGenrator.swift
    │   └── RandomNumberGenrator.swift
    ├── LifeCount.swift
    ├── Referee.swift
    └── Score.swift

MVP 디자인패턴으로 만들어두고 domain을 모듈을 빼주는 작업을 했습니다.





파일 설명

Model(비즈니스 로직 Domain, Data access 등 로직)

  • Ball : 상대방과 유저의 볼 데이터
  • NumberGenrator : 숫자 생성기(상대방 숫자 생성기)
  • LifeCount : 사용자의 생명 데이터
  • Referee : 게임 결과를 판단하기 위한 데이터
  • Score : 게임 결과를 출력하기 위한 데이터

View

  • storyboard(UiKit에 storyboard 형식)
  • viewController(storyboard에서 못 만든 뷰 생성)

Presenter

  • Contract (Protocol(View, Presenter))
  • ViewController (View, View 동작 등)
  • Presenter (Domain과 상호작용)

위와 같이 되어있고
아래와 같이 두개의 View가 있습니다.

StartViewMainView













모듈화 전체적인 방법

1.모듈 만들기

Xcode -> File -> New -> Project -> Framework 생성






2.domain 모듈 파일 만들기

Swift 파일 생성 또는 만들어둔 domain이 있으면 프레임워크 파일 안에 넣기

초기 파일

저는 원래 프로젝트의 domain폴더가 있어서 옮겨줬습니다.

만든 파일들을 옮겨 넣은 후 아래와 같이 생겼습니다.






3.프로젝트 파일 안에 모듈(Framework)파일 넣기

1. 현재 프로젝트 파일 확인

domain을 옮겨서 domain이 없는 프로젝트 현재 파일


2. domain(framework) 폴더 추가하기

프로젝트 우클릭 -> Add Files -> domain(Framework) 프로젝트 넣기


최종 결과






3. Project 설정해주기

Project -> TARGETS -> General -> Frameworks, Lib~~ -> + 버튼
에서 domain.framework 추가해주기






4. Test 해보기

domain 프로젝트가 잘들고 와졌는지 테스트 해보기

domain 테스트 실행 부분

import domain을 하면 에러가 안나는지 테스트

🚨 하지만 domain에 있는 LifeCount Class를 찾지 못함!!

사유는 Swift에서는 접근 제어자가 internal가 기본 접근 제어자라 가져오지 못함






5. 접근 제어자 바꿔주기

4번에서 에러가 계속 나서 확인을 해보니까 위에서도 언급 했듯이
Swift에서는 internal가 기본 접근 제어자라 에러가 발생!!
그래서 public을 전부 추가해줬습니다.

추가 전 코드
enum LifeCountError: Error {
    case IllegalArgumentException
}

import Foundation

class LifeCount {
    private(set) var lifes: Int
    init(lifes: Int = 1) throws {
        self.lifes = lifes
        
        guard (MIN_LIFE_COUNT...MAX_LIFE_COUNT).contains(self.lifes) else {
             throw LifeCountError.IllegalArgumentException
        }
    }
    
    func increase() -> Bool {
        return lifes < MAX_LIFE_COUNT ? { lifes += 1; return true }() : false
    }
    
    func decrease() -> Bool {
        return lifes > MIN_LIFE_COUNT ? { lifes -= 1; return true }() : false
    }
    
    private let MIN_LIFE_COUNT = 1
    private let MAX_LIFE_COUNT = 20
}
추가 후 코드
enum LifeCountError: Error {
    case IllegalArgumentException
}

import Foundation

public class LifeCount {
    public private(set) var lifes: Int
    public init(lifes: Int = 1) throws {
        self.lifes = lifes
        
        guard (MIN_LIFE_COUNT...MAX_LIFE_COUNT).contains(self.lifes) else {
             throw LifeCountError.IllegalArgumentException
        }
    }
    
    public func increase() -> Bool {
        return lifes < MAX_LIFE_COUNT ? { lifes += 1; return true }() : false
    }
    
    public func decrease() -> Bool {
        return lifes > MIN_LIFE_COUNT ? { lifes -= 1; return true }() : false
    }
    
    private let MIN_LIFE_COUNT = 1
    private let MAX_LIFE_COUNT = 20
}

public을 추가하니까 아래와 같이 에러가 안발생했습니다.
접근이 허락되어서 life를 들고오게 됨.












마지막 접근 제어자 에러

internal가 기본 접근제어자라 public으로 전부 바꿔줘도
몇개의 에러가 계속나서 확인해보니

import Foundation

public class Referee{
    
    public func getGameScore(baseNumbers: [Int], targetNumbers: [Int]) -> Score{
        return Score(ball: getBallCount(baseNumbers: baseNumbers, targetNumbers: targetNumbers), strike: getStrikeCount(baseNumbers: baseNumbers, targetNumbers: targetNumbers))
    }
    
    public func getBallCount(baseNumbers: [Int], targetNumbers: [Int]) -> Int {
        return baseNumbers.filter { num in
            targetNumbers.contains(num) && num != targetNumbers[baseNumbers.firstIndex(of: num)!]
        }.count
    }
    
    public func getStrikeCount(baseNumbers: [Int], targetNumbers: [Int]) -> Int {
        return baseNumbers.enumerated().filter { $0.element == targetNumbers[$0.offset] }.count
    }
}



위 코드는 따로 init이 없지만

let referee = Referee()

을 domain과 연결하는 Presenter에서 생성을 해서 사용합니다.
하지만 위와 같이 생성 시 internal으로 자동으로 init이 되어서 에러가 발생했습니다.


그래서 public init() { }을 추가해줬습니다.



최종 코드

import Foundation

public class Referee{
    
    public init() { }
    
    public func getGameScore(baseNumbers: [Int], targetNumbers: [Int]) -> Score{
        return Score(ball: getBallCount(baseNumbers: baseNumbers, targetNumbers: targetNumbers), strike: getStrikeCount(baseNumbers: baseNumbers, targetNumbers: targetNumbers))
    }
    
    public func getBallCount(baseNumbers: [Int], targetNumbers: [Int]) -> Int {
        return baseNumbers.filter { num in
            targetNumbers.contains(num) && num != targetNumbers[baseNumbers.firstIndex(of: num)!]
        }.count
    }
    
    public func getStrikeCount(baseNumbers: [Int], targetNumbers: [Int]) -> Int {
        return baseNumbers.enumerated().filter { $0.element == targetNumbers[$0.offset] }.count
    }
}











최종 프로젝트 구조

최종적으론 domain과 프로젝트 파일이 분리되서 아래와 같이 됐습니다!!

따로 기능도 다 잘 작동이 되고 서로 분리하여 테스트가 가능하며
import에러 발생 시 VC 등에서는 에러가 안나고 presenter에서만 에러가 발생하는걸 확인하니 따로 View에서는 참조를 안하는걸 확인할 수 있었습니다.

.
├── domain
│   ├── domain
│   │   ├── Ball
│   │   │   ├── Ball.swift
│   │   │   ├── OpponentBall.swift
│   │   │   └── UserBall.swift
│   │   ├── LifeCount.swift
│   │   ├── NumberGenerator
│   │   │   ├── NumberGenerator.swift
│   │   │   └── RandomNumberGenerator.swift
│   │   ├── Referee.swift
│   │   └── Score.swift
│   └── domainTests
│       ├── Ball
│       │   ├── OpponentBallTest.swift
│       │   └── UserBallTest.swift
│       ├── LifeCountTests.swift
│       ├── RefereeTest.swift
│       ├── ScoreTest.swift
│       └── domainTests-Bridging-Header.h
└── iOSbaseball
    ├── Main
    │   ├── MainContract.swift
    │   ├── MainPresenter.swift
    │   └── MainViewController.swift
    ├── Start
    │   ├── StartContract.swift
    │   ├── StartPresenter.swift
    │   └── StartViewController.swift
    └── View
        ├── Base.lproj
        │   └── StartView.storyboard
        └── MainView.storyboard










참고 자료

전체 프로젝트 코드

접근 제어자 관련 : https://velog.io/@yoonah-dev/Swift-Basic-%EC%A0%91%EA%B7%BC-%EC%A0%9C%ED%95%9C%EC%9E%90#-internal

모듈 만들기 참고 사이트 : https://siwon-code.tistory.com/15

profile
개발을 배우는 대학생입니다!

3개의 댓글

comment-user-thumbnail
2024년 6월 6일

디자인패턴부터 모듈화, 테스트까지 엄청 알찬 글 잘 읽었습니다~
다음에 모듈화랑 테스트도 공부해봐야겠네요..

혹시 domain은 모델과 관련된 코드들을 지칭하는 디자인패턴 관련 용어인가요?

2개의 답글

관련 채용 정보