모듈은 하나의 코드 묶음.
하나의 프레임워크, 하나의 라이브러리, 하나의 애플리케이션이 모듈의 단위입니다.
더 간단하게 말하면, Swift의 import키워드를 사용하여 다른 모듈에서 가져올 수 있는 framework 또는 응용 프로그램.
우리가 import해서 사용하는 라이브러리 = 모듈
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 등 로직)
View
Presenter
위와 같이 되어있고
아래와 같이 두개의 View가 있습니다.
StartView | MainView |
---|---|
![]() | ![]() |
Xcode -> File -> New -> Project -> Framework 생성
![]() | ![]() |
---|
Swift 파일 생성 또는 만들어둔 domain이 있으면 프레임워크 파일 안에 넣기
초기 파일
저는 원래 프로젝트의 domain폴더가 있어서 옮겨줬습니다.
만든 파일들을 옮겨 넣은 후 아래와 같이 생겼습니다.
domain을 옮겨서 domain이 없는 프로젝트 현재 파일
프로젝트 우클릭 -> Add Files -> domain(Framework) 프로젝트 넣기
![]() | ![]() |
---|
Project -> TARGETS -> General -> Frameworks, Lib~~ -> + 버튼
에서 domain.framework 추가해주기
![]() | ![]() |
---|
domain 프로젝트가 잘들고 와졌는지 테스트 해보기
domain 테스트 실행 부분
import domain
을 하면 에러가 안나는지 테스트
사유는 Swift에서는 접근 제어자가 internal
가 기본 접근 제어자라 가져오지 못함
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
디자인패턴부터 모듈화, 테스트까지 엄청 알찬 글 잘 읽었습니다~
다음에 모듈화랑 테스트도 공부해봐야겠네요..
혹시 domain은 모델과 관련된 코드들을 지칭하는 디자인패턴 관련 용어인가요?