일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 뷰 생명주기
- 라이브러리 없이
- 360도 뷰어
- 리액트 네이티브
- Android
- 앱 성능 개선
- 스켈레톤 통합
- 360도 이미지 뷰어
- 네이티브
- 리액트
- 360도 이미지
- data driven construct
- requirenativecomponent
- react-native-fast-image
- React-Native
- panorama view
- 뷰 정체성
- launch screen
- 명시적 정체성
- @sendable
- launchscreen
- 파노라마 뷰
- 구조적 정체성
- native
- SwiftUI
- react
- ios
- completion handler
- React Native
- ssot
- Today
- Total
Neoself의 기술 블로그
BookMatchKit 리펙토링 과정-1/3 (Strategy 패턴, Adapter 패턴, Facade 패턴) 본문
BookMatchKit 리펙토링 과정-1/3 (Strategy 패턴, Adapter 패턴, Facade 패턴)
Neoself 2025. 2. 17. 23:05이번 포스트에서는 도서 추천 및 매칭 시스템인 BookMatchKit의 아키텍처 설계 과정에 대해 살펴보겠습니다.
1. 1차 리펙토링
1차 리펙토링에는 단일 책임원칙 준수를 최우선 순위로 고려해 다음과 같은 주요 모듈들로 구성하게 되었습니다.
BookMatchKit/
├── Sources/
│ ├── BookMatchAPI/ # 외부 API 통신 계층
│ ├── BookMatchCore/ # 핵심 도메인 계층
│ ├── BookMatchKit/ # 비즈니스 로직 계층
│ └── BookMatchStrategy/ # 알고리즘 구현 계층
1.1 BookMatchCore
BookMatchCore는 프로젝트의 핵심 도메인 로직을 담고 있는 모듈로써 설계했습니다.
- BookItem: 도서 정보를 나타내는 모델
- OwnedBook: 사용자 보유 도서 정보 모델
- RawBook: GPT 통신용 기본 도서 정보 모델
- BookMatchError: 도메인 에러 정의
- 기타 프로토콜들: BookMatchable, ImageSimilarityCalculatable 등
이 모듈은 다른 계층의 변경에 영향받지 않는 도메인 및 로직만을 담았습니다.
1.2 BookMatchAPI
그 다음으로는 도서 매칭 및 추천에 핵심적인 외부 API와의 통신만을 담당하는 로직을 담당하는 모듈을 설계했습니다.
- APIConfiguration: API 인증 정보 관리
- DefaultAPIClient: 네이버 책검색 API 및 OpenAI API에 대한 통합 클라이언트
- DTO 구조체들: NaverBookDTO, ChatGPTResponse 등
해당 모듈을 설계할 때 고려하고자 했던 요인은 API 통신 로직의 유연성을 확보하는 것이였습니다. 이를 위해 외부 데이터 구조와 내부 모델인 BookItem을 분리하고, 다른 인터페이스를 가진 두 객체를 호환가능하게 만들어주는 Adapter 패턴을 사용해 향후 API 응답 구조가 변경되어도, Adaper 수정만을 통해 유지보수가 가능토록 하고자 했습니다.
Adapter 패턴:
호환되지 않는 인터페이스를 가진 객체들이 서로 협업할 수 있도록 해주는 구조적 디자인 패턴
- 호환되지 않는 인터페이스를 가진 객체들을 함께 동작하게 만듦
- 기존 코드를 수정하지 않고도 기존 객체를 새로운 인터페이스와 함께 사용 가능
작동 방식
Adapter는 한 객체의 인터페이스를 다른 객체가 이해할 수 있는 형태로 변환 변환 과정의 복잡성을 감추기 위해 객체를 wrapping한 객체의 호출을 다른 객체가 이해할 수 있는 형식으로 변환하여 전달
장점
단일 책임 원칙: 인터페이스 변환 코드를 비즈니스 로직과 분리
개방/폐쇄 원칙: 기존 코드 수정 없이 새로운 타입의 어댑터 도입 가능
사용 시점
기존 클래스의 인터페이스가 코드의 나머지 부분과 호환되지 않을 때 레거시 코드나 서드파티 클래스를 새로운 코드와 통합해야 할 때
struct NaverBookDTO: Codable {
let title: String
...
/// NaverBookDTO 와 BookItem을 호환 가능하게 만들어주는 Adapter
func toBookItem() -> BookItem {
BookItem(
id: isbn,
...
)
}
}
1.3 BookMatchStrategy
이후 텍스트 매칭과 이미지 유사도 계산 알고리즘을 담당하는 모듈을 설계했습니다.
- LevenshteinStrategy: 문자열 유사도 계산
- LevenshteinStrategyWithNoParenthesis: 괄호를 제외한 문자열 유사도 계산
- BookImageSimilarityCalculator: Vision 프레임워크 기반 이미지 유사도 계산
향후 패키지를 유지보수하는 과정에서, 두 객체의 유사도를 연산하는 알고리즘은 언제든지 변경되거나 추가될 수 있다고 판단했으며, 특히 재시도 로직이 수행되는 과정에서 유사도 연산 방식이 변경되는 방향으로 고도화될 수 있다는 점을 의식했습니다.
때문에, 기존 런타임에 다른 전략으로의 교체와 새로운 전략 추가의 용이성을 높이기 위해 Strategy 패턴을 도입하게 되었습니다.
모든 알고리즘은 두 객체 간 유사도를 연산해 , 0에서 1 사이의 Double 값을 반환한다는 공통점을 공유합니다. 이러한 공통점을 프로토콜로 정의한 후, 알고리즘을 사용하는 상위 모듈에서 이 프로토콜에 의존하게 설계하게 되면, 구체적인 알고리즘 구현에 의존하지 않게 됨에 따라 필요에 따라 런타임에 다른 알고리즘 주입이 가능해집니다.
// 변경 전: UIImage 타입 전달 불가능!
public protocol SimilarityCalculatable {
func calculateSimilarity(_ str1: String, _ str2: String) -> Double
}
// 변경 후
public protocol SimilarityCalculatable {
associatedtype T
func calculateSimilarity(_ value1: T, _ value2: T) -> Single<Double>
}
여기서 레벤슈타인 거리 알고리즘은 String 타입 데이터 간 유사도를 연산하지만, 이미지 유사도 계산 구조체의 경우 UIImage 타입 데이터 간 유사도를 연산하는데, 두가지 데이터 타입을 모두 대응할 수 있도록 제너릭 타입을 사용해 프로토콜을 정의했습니다.
Strategy 패턴
알고리즘들의 집합을 정의하고, 각각을 캡슐화하여 교체 가능하게 만드는 행동 디자인 패턴
목적
- 알고리즘군을 정의하고 각각을 별도의 클래스로 캡슐화
- 런타임에 알고리즘을 교체할 수 있게 함
- 알고리즘을 사용하는 클라이언트와 알고리즘을 분리
작동 방식
- Context 클래스가 Strategy 인터페이스를 통해 구체적인 알고리즘과 통신
- Context는 Strategy 객체에게 작업을 위임
- 클라이언트가 Context에 원하는 Strategy를 전달
장점
- 런타임에 알고리즘을 교체할 수 있음
- 알고리즘의 구현 세부사항을 사용 코드로부터 분리
- 개방/폐쇄 원칙을 따름
사용 시점
- 비슷한 알고리즘의 다양한 변형이 필요할 때
- 런타임에 알고리즘을 전환해야 할 때
- 알고리즘의 데이터와 로직을 사용 클래스로부터 분리하고 싶을 때
1.4 BookMatchModule
마지막으로 전체 비즈니스 로직을 조율하는 메인 모듈을 설계했습니다.
- BookMatchModule: 도서 추천 및 매칭 로직 조율
- BookMatchConfiguration: 설정 관리
- BookMatchModuleInput/Output: 입출력 데이터 구조 정의
사용자가 최종적으로 사용하는 기능은 아래와 같습니다
- recommendBooks(): 질문에 대한 적합한 책을 반환
- matchBook(): 촬영된 사진에 대해 적합한 책을 반환.
위 기능들은 네이버 책검색과 OpenAI API 호출을 비롯해, 검색결과와 원본 간 텍스트 유사도 연산을 수행하며, 필요시 Vision 프레임워크를 사용해 이미지 간 유사도 연산도 수행해야 하기 때문에, 하위 모듈들을 모두 알아야합니다.
초기에는 위 메서드를 필요로 하는 패키지 외부(클라이언트) 단에서 하위 모듈을 직접 import 하여 수행하는 방안도 생각해보았습니다만 이는 패키지화의 장점을 상쇄하는 방향이라고 생각했습니다.
패키지화를 할 경우, 코드를 캡슐화해 외부로부터 내부 구현을 숨길 수 있게 됨에 따라, 패키지의 변경이 다른 패키지나 패키지 외부에 미치는 영향을 최소화할 수 있지만, 패키지 외부에서 알게되는 모듈이 많아질수록 패키지 내/외부 간 결합도가 증가하게 되며, 패키지 내부 구현의 변경이 미치는 영향범위가 패키지 외부까지 도달할 가능성이 높아지기 때문입니다.
때문에, 패키지화의 장점을 극대화하기 위해 최바깥단 인터페이스의 간소화를 통해 패키지 외부와의 상호작용을 단순화할 수 있는 Facde 패턴을 도입하게 되었습니다. BookMatchModule이 복잡한 서브 모듈들을 통합하고 단순화된 인터페이스를 제공하는 역할을 수행하게 됨에 따라, 패키지 외부에서 하위 모듈들을 모두 알지 않아도 기능들을 사용할 수 있게 하고자 했습니다.
Facade 패턴
복잡한 하위 시스템에 대해 단순화된 인터페이스를 제공하는 구조적 디자인 패턴
목적
- 라이브러리, 프레임워크 또는 복잡한 클래스 집합에 대한 단순화된 인터페이스 제공
- 복잡한 하위 시스템의 사용을 쉽게 만듦
- 클라이언트와 하위 시스템 간의 결합도 감소
작동 방식
- Facade 클래스가 복잡한 하위 시스템의 기능을 감싸고 단순화된 인터페이스 제공
- 클라이언트는 복잡한 하위 시스템 대신 Facade와만 상호작용
- 하위 시스템의 복잡성을 숨기고 필요한 기능만 노출
장점
- 하위 시스템의 복잡성으로부터 코드 분리 가
- 클라이언트 코드와 하위 시스템 간의 결합도 감소
- 하위 시스템의 변경이 클라이언트에 미치는 영향 최소화
사용 시점
- 복잡한 하위 시스템에 대한 단순하고 직관적인 인터페이스가 필요할 때
- 하위 시스템을 계층으로 구조화하고 싶을 때
- 클라이언트가 하위 시스템의 상세 구현에 직접 접근하는 것을 제한하고 싶을 때
2차 리팩토링 과정은 다음 글에서 이어 작성하도록 하겠습니다.
감사합니다.
'개발지식 정리 > Swift' 카테고리의 다른 글
BookMatchKit 리펙토링 과정-3/3 (Factory Method 패턴) (0) | 2025.02.17 |
---|---|
BookMatchKit 리펙토링 과정-2/3 (Template Method 패턴) (2) | 2025.02.17 |
Vision 프레임워크를 활용한 도서 표지 이미지 매칭 구현하기 (2) | 2025.02.16 |
ChatGPT와 네이버 책검색 api로 도서추천 시스템 구현하기(고도화 과정 3/3) (0) | 2025.01.26 |
ChatGPT와 네이버 책검색 api로 도서추천 시스템 구현하기(구현 과정 2/3) (1) | 2025.01.25 |