0. 디자인 패턴이란? (Design Pattern)
프로그램을 설계할 때 발생한 문제점들을 객체 간의 상호 관계 등을 이용하여 해결할 수 있도록 하나의 '규약' 형태로 만들어 놓은 것. 즉, 쉽게 말하면 개발하면서 발생한 문제를 어떻게 해결할 것인지에 대한 해결 방안이다.
1. 싱글톤 패턴 (Singleton Pattern)
하나의 클래스에 하나의 인스턴스만 가지는 것으로, 해당 인스턴스를 다른 모듈이 공유하며 사용한다. 안드로이드 개발에서는 애플리케이션 전반에 걸쳐 상태를 관리하거나 리소스를 효율적으로 관리할 때 주로 사용된다. 예를 들어, SharedPreferences를 관리하는 클래스나 네트워크 통신을 위한 API 클라이언트 객체가 있다.
1) 안드로이드 개발에서의 싱글톤 패턴
코틀린에서는 Object를 사용하여 싱글톤 패턴을 구현할 수 있다. Object는 클래스를 정의하는 동시에 인스턴스를 생성하기 때문에 별도의 생성자 없이 바로 함수명/변수명으로 직접 호출할 수 있다.
object NetworkManager {
fun fetchData() {
// 네트워크 데이터 가져오는 로직
}
fun postData() {
// 네트워크 데이터 전송 로직
}
}
fun main() {
NetworkManager.fetchData()
NetworkManager.postData()
}
2) 싱글톤 패턴의 장점
- 사용하기 쉽다.
- 실용적이다.
- 인스턴스 생성 비용을 줄일 수 있다.
3) 싱글톤 패턴의 단점
- TDD(Test Driven Development)를 할 때 진행하는 각 단위 테스트마다 독립적인 인스턴스를 만들기 어렵다.
- 모듈 간의 결합을 강하게 만들 수 있다.
4) 의존성 주입 (DI, Dependency Injection)
싱글톤 패턴 사용 시, 모듈 간의 결합이 강해지는 것을 해결하기 위해 사용하는 방식이다. 메인 모듈(상위 모듈)이 직접 다른 하위 모듈에 의존성을 주는 것이 아닌, 의존성 주입자가 이 부분을 가로채 메인 모듈이 간접적으로 의존성을 주입하는 방식이다.(=디커플링)
* 의존성이란? A가 B에 의존성이 있다는 것은 B의 변경 사항에 대해 A도 변해야 된다는 것을 의미.
- 장점 : 애플리케이션 의존성 방향이 일관되고, 애플리케이션을 쉽게 추론할 수 있기 때문에 모듈 간의 관계들이 명확해진다.
- 단점 : 모듈의 분리가 심해지므로 클래스 수가 늘어나 복잡성이 증가된다.
- 원칙 : 상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 하며, 둘 다 세부 사항에 의존하지 않는 추상화에 의존해야 한다.
* 아래 코드는 글쓴이가 졸업 프로젝트 당시 사용한 코드의 일부로, 날 것의 코드이니 싱글톤 패턴 개념 참고만 부탁드립니다.
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
@Named("baseOkHttpClient")
fun provideOkHttpClient(
@Named("authInterceptor") authInterceptor: Interceptor,
loggingInterceptor: HttpLoggingInterceptor
): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.addInterceptor(loggingInterceptor)
.build()
@Provides
@Singleton
@Named("baseRetrofit")
fun provideRetrofit(
@Named("baseOkHttpClient") okHttpClient: OkHttpClient
): Retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
@Provides
@Singleton
fun provideRetrofitService(
@Named("baseRetrofit") retrofit: Retrofit
): RetrofitService = retrofit.create(RetrofitService::class.java)
}
안드로이드 개발에서는 수동적으로 싱글톤 패턴을 구현하는 방법과 Hilt 라이브러리를 사용하여 대신 객체의 생성과 관리를 담당해주는 방법이 있다. 이 게시글에서는 Dagger Hilt를 활용한 싱글톤 구현에 대해 설명하고자 한다.
- @Module : 클래스가 Hilt의 모듈임을 나타낸다.
- @InstallIn(SingletonComponent::class) : 모듈의 의존성들이 애플리케이션 생명 주기와 동일한 SingletonComponent에 설치되도록 지시한다. 즉, 이 모듈에서 제공하는 모든 객체는 애플리케이션이 실행되는 동안 단 하나의 인스턴스만 유지된다.
- @Provides : 함수가 어떤 유형의 인스턴스를 제공하는지 Hilt에 알려준다.
- @Singleton : @Provides 함수 위에 @Singleton을 붙이면, Hilt가 해당 함수가 반환하는 객체(Interceptor, OkHttpClient, Retrofit 등)를 애플리케이션 내에서 단 하나의 인스턴스만 생성하여 관리하도록 지시한다.
2. 팩토리 패턴 (Factory Pattern)
팩토리 패턴은 이름 그대로 '객체를 생성하는 공장' 역할을 하는 디자인 패턴이다. 우리가 공장에서 제품을 찍어내듯, 팩토리 패턴은 복잡한 객체 생성 과정을 추상화하여 대신 객체를 만들어준다. 상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고 하위 클래스가 객체 생성에 관한 구체적인 내용을 결정하는 패턴이다.
1) 안드로이드 개발에서의 팩토리 패턴
안드로이드에서는 주로 뷰모델(ViewModel)을 생성할 때 팩토리 패턴을 사용한다. 팩토리 패턴은 뷰모델에 복잡한 의존성(Repository, Service 등)을 주입할 때 ViewModelProvider.Factory가 의존성을 대신 주입받아 뷰모델을 생성해준다.
* 아래 코드는 글쓴이가 동아리 프로젝트 당시 사용한 코드의 일부로, 날 것의 코드이니 팩토리 패턴 개념 참고만 부탁드립니다.
class HomeViewModelFactory(
private val repository: HomeRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(HomeViewModel::class.java)) {
return HomeViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
이 코드는 뷰모델을 팩토리 패턴을 통해 직접 생성하는 역할을 하기 때문에, 뷰(Activity, Fragment)에서는 뷰모델을 간접적으로 생성할 수 있다. 즉, 뷰와 뷰모델의 기능을 분리하여 결합도를 낮추는데 용이하다. 하지만 위에서 설명했던 Hilt 라이브러리를 사용하면 직접 팩토리 코드를 작성하지 않아도 내부적으로 팩토리 패턴의 역할을 대신 수행해주기 때문에 코드가 복잡해지는 번거로움을 제거할 수 있다.
2) 팩토리 패턴의 장점
- 상위 클래스와 하위 클래스가 분리되어 느슨한 결합을 가진다.
- 상위 클래스에서 인스턴스 생성 방식을 알 필요가 없기 때문에 유연성이 증가한다.
- 객체 생성 로직이 따로 떼어져 있기 때문에 리팩토링 시 한 곳만 고치면 되므로 유지 보수성이 증가된다.
3. 전략 패턴 (Strategy Pattern)
객체의 행위를 바꾸고 싶은 경우 직접 수정하지 않고 전략이라고 부르는 '캡슐화한 알고리즘'을 컨텍스트 안에서 바꾸어 상호 교체가 가능하게 만드는 패턴이다. 즉, 어떠한 행위를 할 때 이렇게도 할 수 있고 저렇게도 할 수 있으니 각각 별도의 클래스로 만들어 필요할 때 바꿔 쓰자는 개념이다.
1) 안드로이드 개발에서의 전략 패턴
주로 뷰모델(데이터 로드, 정렬 등의 작업)의 비즈니스 로직 분리와 상태 관리, 데이터 유효성 검사에 사용된다.
- 전략 인터페이스 (Strategy) : 모든 전략 클래스가 공통으로 구현해야하는 인터페이스
interface SortStrategy {
fun sort(list: List<Article>): List<Article>
}
- 구체적인 전략 (Concrete Strategy) : 각 전략 클래스에 대한 구체적인 로직을 구현하는 클래스
class LatestSortStrategy : SortStrategy {
override fun sort(list: List<Article>): List<Article> = list.sortedByDescending { it.date }
}
class PopularSortStrategy : SortStrategy {
override fun sort(list: List<Article>): List<Article> = list.sortedByDescending { it.likes }
}
- 컨텍스트 (Context) : 전략 인터페이스 객체를 받아서 실제로 사용하는 주체
class ArticleViewModel(private val repository: ArticleRepository) : ViewModel() {
private var sortStrategy: SortStrategy = LatestSortStrategy()
fun setSortStrategy(strategy: SortStrategy) {
this.sortStrategy = strategy
}
fun getSortedArticles(): List<Article> {
val allArticles = repository.getAllArticles()
return sortStrategy.sort(allArticles)
}
}
2) 전략 패턴의 장점
- 새로운 행위가 추가되어도 기존 코드를 수정할 필요 없이 새로운 전략만 추가하면 된다.
- 각 전략 클래스는 독립적이므로 재사용이 가능하다.
- Context는 구체적인 전략에 대해 알 필요 없으므로 결합도가 감소한다.
4. 옵저버 패턴 (observer pattern)
주체가 객체의 상태 변화를 관찰하는 디자인 패턴이다. 한 객체의 상태가 변하면 옵저버 목록에 있는 옵저버들에게 변화를 알려준다. 주체(subject)란 객체의 상태 변화를 보고 있는 관찰자이며 상태 변화를 알릴 객체이다. 옵저버(observer)란 주체의 상태 변화를 받는 객체이다.
1) 안드로이드 개발에서의 옵저버 패턴
주로 버튼의 클릭리스너, Jetpack 라이브러리의 LiveData와 코루틴의 StateFlow 등에서 사용한다. 뷰모델의 데이터가 변경되면, observe() 메서드로 관찰하고 있던 UI를 자동으로 업데이트 한다.
* 아래 코드는 글쓴이가 동아리 프로젝트 당시 사용한 코드의 일부로, 날 것의 코드이니 옵저버 패턴 개념 참고만 부탁드립니다.
class HomeViewModel(private val repository: HomeRepository): ViewModel() {
private val _totalReviews = MutableStateFlow<List<TotalReviewResponse>>(emptyList())
val totalReviews= _totalReviews.asStateFlow()
fun loadTotalReviews(accessToken: String) {
viewModelScope.launch {
_totalReviews.value = repository.getTotalReviews(accessToken)
}
}
}
이 코드는 코루틴 내에서 사용되는 StateFlow를 통해 옵저버 패턴의 원리를 적용하고 있다.
- 주체 (Subject) : MutableStateFlow 인스턴스가 옵저버의 주체 역할을 한다. 데이터의 값이 변경될 때마다 등록된 모든 관찰자에게 알림을 보낸다.
- 옵저버 (Observer) : collect(), collectAsState()와 같은 메서드를 사용하여 구독합니다. 데이터의 값이 변경되면, 구독하고 있는 UI 컴포넌트(Activity, Fragment)가 자동으로 업데이트 된다.
2) 옵저버 패턴의 장점
- 느슨한 결합이다.
- 주체는 옵저버의 구체적인 구현을 몰라도 되고, 옵저버는 주체의 상태 변화만 알면 되므로 서로 의존성이 낮아진다.
'CS 스터디' 카테고리의 다른 글
| 2-1. 네트워크의 기초 [02]- 네트워크 분류, 네트워크 성능 분석 명령어, 네트워크 프로토콜 표준화 (0) | 2025.09.26 |
|---|---|
| 2-1. 네트워크의 기초 [01]- 처리량과 지연 시간, 네트워크 토폴로지와 병목 현상 (0) | 2025.09.26 |
| 1-2. 프로그래밍 패러다임 - 선언형과 함수형 프로그래밍, 객체지향 프로그래밍, 절차형 프로그래밍 (0) | 2025.09.19 |
| 1-1. 디자인 패턴 [03] - MVC 패턴, MVP 패턴, MVVM 패턴 (1) | 2025.09.15 |
| 1-1. 디자인 패턴 [02] - 프록시 패턴과 프록시 서버, 이터레이터 패턴, 노출모듈 패턴 (0) | 2025.09.15 |