패스트캠퍼스 노수진님의 슈퍼앱 운영을 위한 확장성 높은 앱 아키텍처 구축 강의를 듣고 정리한 내용입니다.
Composition Pattern
유래
Massive View Controller (3000~4000), Massive Ribs
a. MVVM, MVP, VIP, Ribs 등 여러 패턴 도입
b. 여러 패턴을 적용 하였지만, Massive View Model, Massive Interactors 등의 문제가 발생
c. 결국, 아키텍처만의 문제는 아니었음.
d. Composition을 연습하고 활용하지 않으면, 어떤 아키텍처를 선택하든 지, Massive 시리즈에서 벗어날 수 없다.
정의
객체를 작게 쪼개고 로직을 분산시킨 다음, 이를 합쳐서 원하는 기능을 만들어내야 한다.
쪼갤수록 단일 책임 원칙, Don't repeat yourself Dry 규칙 준수 가능
작은 객체의 장점
a. 재사용성
b. 유지보수성
c. 테스트 용이 (public API 적고, 파라미터도 적다.)
상속보다 컴포지셔널을 더 활용하자
상속대신 객체 합성을 더 활용해라. (by Gang of Four, Design Patterns)
""상속을 제일 잘 활용한 것은 상속을 쓰지 않는 것이다."라는 말이 있음
상속의 단점
상속은 코드의 강한 결합으로 인해 유연성이 떨어진다.
부모의 행동을 거부하는 경우가 있다. (리스코프 원칙을 위배하는 코딩 ㅠㅠ)
예를 들어, UIView의 frame 프로퍼티는 위치와 사이즈를 바꿀 수 있고, UIView의 상속체들도 똑같이 적용이 되지만, UISwitch는 사이즈를 바꾸어도 사이즈의 값이 제대로 적용되지 않는다.
그래서, 코드를 재사용해야 할 때, 바로 상속부터 시작하게 된다면, 요구사항이 변할 때 민첩하게 변화하기 힘든 코드가 된다.
Swift에는 이러한 컴포지션이 잘 녹아져 있다.
예로, SwiftUI의 뷰들은 value타입으로 기능을 확장하기 위해서는 뷰 모디파이어를 활용해서 상속없이 기능을 확장할 수 있다.
타입, 인터페이스, 모듈, 함수까지도 조합할 수 있다.
iOS, Swift에서 컴포지션이 활용된 예
iOS 프레임워크에서도 컴포지션이 다양하게 활용된다.
1. UIViewController
✨ 상속은 화이트박스, 컴포지셔널은 블랙박스 ✨
B가 A를 상속받게 되면, A의 모든 것을 알게 된다. 프로그래밍을 할 때 하나의 객체가 너무 많은 걸 알고 있고, 많은 로직과 데이터를 가지고 있는 경우가 좋을 때는 거의 없다. 더 작고 더 적은 정보를 아는 객체를 만드는게 유지보수를 할 때 더 좋기 때문이다. 그래서 C를 이용하여 조합하게 되면 A와 B는 서로 모르게 된다.
class A: UIViewController { }
class B: UIViewController { }
class C: UIViewController {
private let a: UIViewController
private let b: UIViewController
}
2. UIView
UIView는 여러개의 서브뷰를 가질 수 있다. 여러개의 서브뷰를 가진 UIView도 하나의 뷰처럼 위치와 속성을 같이 적용받음으로, 객체들을 구별 없이 다루게 해준다.
컴포지트 패턴 (컴포지셔널에서 파생된 패턴)
3. UINavigationController, UITabBarController
ViewController들을 화면에 배치시키고 서로 이동시키는 역할을 한다.
함수: map, filter, reduce
함수형 프로그래밍을 위한 고차함수들을 컴포지셔널하여 더 복잡한 작업을 파이프라인처럼 구성할 수 있다.
Map을 예시로 보자
Sequence, Optional, Result, Publisher 타입에 map이 선언되어 있다.
map 같은 경우 A -> B로 변환하는 함수가 있다면, F -(map)-> F와 같이 동작한다. F는 4가지 타입에 공통으로 활용될 수 있다. 그렇다면, flatmap은 무엇일까? A -> B로 타입 변환할 때, Failurable한 메소드에 의해서 Optional이 반환이 되는 경우에 사용된다. 즉 A -> Optiona(B)와 같이 반환이 되는 것이다. 아래의 예시를 보자.
let ageString: String? = "10"
let result = ageString.map { Int($0) }
// Optional<A> -(map)-> Optional<Optional<B>>
중첩된 옵셔널을 풀어줄 수는 있지만, 알아보기 어려운 코드를 작성하게 된다. 이 때 flatmap은 중첩된 옵셔널을 제거한다. transform 함수의 optional를 반환하더라도 최종결과물은 optional라는 것을 알 수 있다.
let result = ageString.flatmap { Int($0) }
// Optional<A> -(flatmap)-> Optional<B>
앱에서 일어나는 작업들은 타입의 변환이 일어나는 작업이라고 생각하면 타입의 변환은 아래와 같다.
tableView의 아이템 하나를 선택해서 그에 해당하는 데이터를 서버에서 가져와서 보여주는 작업
UIEvent -> IndexPath -> Model -> URL -> Data -> Model -> ViewModel -> View
그렇다면 위의 작은 고차함수들을 활용하여 더 복잡한 작업들을 하나의 흐름으로 조합해볼 수 있을 것이다. map과 flatmap을 활용한 간단한 조립은 아래와 같다.
// 이전
if let data = UserDefaults.standard.data(forKey: "my_data_key") {
if let model = try? JSONDecoder().decode(MyModel.self, from: data) {
let welcomeMessage = "Hello \(model.name)"
myLabel.text = welcomeMessage
}
}
// 이후 ✅
UserDefaults.standard.data(forKey: "my_data_key")
.flatMap { try? JSONDecoder().decode(MyModel.self, from: $0) }
.map(\.name)
.map { "Hello \($0)" }
myLabel.text = welcomeMessage
모듈에서의 컴포지션
복잡한 기능들이 단 하나의 모듈 안에 있게 된다면, 내부 객체들끼리 참조가 자유롭기 때문에 혼잡한 참조 관계에 매우 취약하게 된다.
예시로, 택시 호출 모듈의 경우 결제, 위치, 쿠폰, 택시호출, UI 등 여러가지 기능들이 존재하고 이들이 하나의 모듈 안에 있게 된다면 복잡한 참조관계가 이루어진다.
이를 독립적인 모듈들로 분리하게 된다면 복잡한 참조관계가 많이 해소된다.
장점
public 으로 공개하지 않은 부분은 다른 모듈에서 접근이 불가능하므로 사이드이펙트도 덜 일어난다.
모듈의 public 인터페이스만 보아도 모듈의 내부를 들여다보지않아도 무슨 역할을 하는 지 쉽게 파악된다.
UI도 자신이 맡은 역할의 모듈로 분리하여 그 역할을 담당하게 바꾼다면, 택시 호출 모듈에서도 각 UI들의 위치만 잡아주면 된다.
아키텍처 패턴에서의 컴포지션
The Composable Architecture, ReSwift에서도 앱이 점점 커질수록 로직이 많아지면서, 상태와 상태를 업데이트하는 Reducer도 점점 커질 수 밖에 없다. 이 때, 각 역할의 State와 Reducer로 쪼개서 관리할 수 있다. 컴포지션을 염두에 두고 만든 아키텍처이므로 sub reducer, sub state로 쪼개서 조합시킬 수 있는 구조가 가능하다.
그래서 트리 구조로 시각화할 수가 있다. 화면이 아무리 복잡해져도 그리고 앱이 아무리 복잡해져도, 쪼개서 조합할 수 있기 때문에 3000줄의 코드를 300줄로 줄여 코드를 나눠 작성할 수 있다. 이는 개발자가 이해하기 쉬워지며 유지보수에도 용이해지게 된다.
패스트캠퍼스 노수진님의 슈퍼앱 운영을 위한 확장성 높은 앱 아키텍처 구축 강의를 듣고 정리한 내용입니다.
Composition Pattern
유래
정의
상속보다 컴포지셔널을 더 활용하자
상속대신 객체 합성을 더 활용해라. (by Gang of Four, Design Patterns)
상속의 단점
Swift에는 이러한 컴포지션이 잘 녹아져 있다. 예로, SwiftUI의 뷰들은 value타입으로 기능을 확장하기 위해서는 뷰 모디파이어를 활용해서 상속없이 기능을 확장할 수 있다.
타입, 인터페이스, 모듈, 함수까지도 조합할 수 있다.
iOS, Swift에서 컴포지션이 활용된 예
iOS 프레임워크에서도 컴포지션이 다양하게 활용된다.
1. UIViewController
2. UIView
3. UINavigationController, UITabBarController
함수: map, filter, reduce
Sequence, Optional, Result, Publisher 타입에 map이 선언되어 있다.
4가지 경우의 공통점은 아래와 같다.
map 같은 경우 A -> B로 변환하는 함수가 있다면, F -(map)-> F와 같이 동작한다. F는 4가지 타입에 공통으로 활용될 수 있다. 그렇다면, flatmap은 무엇일까? A -> B로 타입 변환할 때, Failurable한 메소드에 의해서 Optional이 반환이 되는 경우에 사용된다. 즉 A -> Optiona(B)와 같이 반환이 되는 것이다. 아래의 예시를 보자.
중첩된 옵셔널을 풀어줄 수는 있지만, 알아보기 어려운 코드를 작성하게 된다. 이 때 flatmap은 중첩된 옵셔널을 제거한다. transform 함수의 optional를 반환하더라도 최종결과물은 optional라는 것을 알 수 있다.
앱에서 일어나는 작업들은 타입의 변환이 일어나는 작업이라고 생각하면 타입의 변환은 아래와 같다.
그렇다면 위의 작은 고차함수들을 활용하여 더 복잡한 작업들을 하나의 흐름으로 조합해볼 수 있을 것이다.
map
과flatmap
을 활용한 간단한 조립은 아래와 같다.모듈에서의 컴포지션
장점
아키텍처 패턴에서의 컴포지션
The Composable Architecture, ReSwift에서도 앱이 점점 커질수록 로직이 많아지면서, 상태와 상태를 업데이트하는 Reducer도 점점 커질 수 밖에 없다. 이 때, 각 역할의 State와 Reducer로 쪼개서 관리할 수 있다. 컴포지션을 염두에 두고 만든 아키텍처이므로 sub reducer, sub state로 쪼개서 조합시킬 수 있는 구조가 가능하다.
그래서 트리 구조로 시각화할 수가 있다. 화면이 아무리 복잡해져도 그리고 앱이 아무리 복잡해져도, 쪼개서 조합할 수 있기 때문에 3000줄의 코드를 300줄로 줄여 코드를 나눠 작성할 수 있다. 이는 개발자가 이해하기 쉬워지며 유지보수에도 용이해지게 된다.
Ref