본문 바로가기

[Android] Product Flavor를 사용해서 Mock 버전의 Fake 앱 구현하기

@Marchbreeze2025. 5. 26. 23:08

구현 사진

 

하나의 코드베이스에서 서버통신 없이 가짜 데이터를 활용하는 mock 버전과 실제 서비스인 prod 버전을 분리해 관리하는 방법을 구현했습니다.

 

 


1. Product Flavor란

Product Flavor

하나의 Android 프로젝트에서 서로 다른 “버전”의 앱을 만들기 위해 사용되는 기능입니다.

Gradle 빌드 설정만으로, 빌드 타입(debug/release)뿐 아니라 Flavor별로도 리소스·코드·의존성을 분리할 수 있습니다.

다음과 같은 상황에서 사용이 가능합니다.

  • 앱 브랜드가 다를 때 (같은 기능인데 로고, 테마만 다르게)
  • 환경별 분리 (dev, staging, prod)
  • 지역별 버전 (KR, JP, US)
  • 유료 / 무료 앱

 

 


2. Mock / Prod Flavor 설정

(1) 기존 debug / release 분기 처리

기기에 각각의 버전에 해당하는 앱을 설치하기 위해, 기존 debug/release 분기처리에서 id에 suffix를 추가했습니다.

// build.gradle.kts
buildTypes {
    debug {
        manifestPlaceholders["appName"] = "@string/dev_app_name"
        manifestPlaceholders["appIcon"] = "@mipmap/ic_launcher_dev"
	      //...
        applicationIdSuffix = ".debug"
        versionNameSuffix = "-DEBUG"
    }
    release {
        manifestPlaceholders["appName"] = "@string/app_name"
        manifestPlaceholders["appIcon"] = "@mipmap/ic_launcher"
        //...
    }
}

 

(2) flavorDimensions 및 productFlavors 추가

mockprod 두 가지 Flavor를 선언합니다.

// build.gradle.kts
flavorDimensions += "default"
productFlavors {
    create("mock") {
        dimension = "default"
        applicationIdSuffix = ".mock"
        versionNameSuffix = "-MOCK"
    }
    create("prod") {
        dimension = "default"
    }
}
  1. 그룹화를 위해 플레이버 차원(Flavor Dimension) 을 만들고 그 이름을 "default" 로 지정합니다.
  2. 이후 default 차원에 해당하는 두개의 Flavor를 만들고, suffix 작업을 실행합니다. (prod의 경우 기존 패키지명을 사용합니다.)

다음과 같은 suffix 작업 시, 4개의 패키지명이 생성되게 됩니다.

  • prodRelease : org.sopt.santamanitto
  • prodDebug : org.sopt.santamanitto.debug
  • mockRelease : org.sopt.santamanitto.mock (사용 X)
  • mockDebug : org.sopt.santamanitto.mock.debug

 

(3) 직접 패키지 생성

gradle에 flavor를 설정해도 자동으로 해당 flavor에 해당하는 패키지가 생성되지 않기 때문에, 직접 생성해줘야 합니다.

IDE Project 뷰를 Android가 아닌 Project로 보면, 직접 src/mock/, src/prod/를 만들고 그 안에 리소스와 코드를 배치할 수 있습니다.

app/
 └─ src/
    ├─ main/         ← 공통 코드·리소스
    ├─ mock/         ← mockFlavor 전용
    └─ prod/         ← prodFlavor 전용

 

(4) Flavor별 리소스 분기처리

같은 키(appName, appIcon)에 대해서는 buildType 쪽 정의가 flavor 정의를 덮어씌우게 됩니다.

// 잘못된 예시 : 설정해도 적용되지 않음 (debug의 키가 적용됨)
create("mock") {
    manifestPlaceholders["appName"] = "@string/mock_app_name"
    manifestPlaceholders["appIcon"] = "@mipmap/ic_launcher_mock"
    //...

 

이때 동일한 키를 buildTypes 대신 src/<flavor>/res/values/strings.xml에 두면 리소스 오버레이가 가능합니다.

app/
 └── src/
     ├── main/
     │    └── res/values/strings.xml        ← 공통 app_name (release)
     ├── mock/
     │    └── res/values/strings.xml        ← mock 전용 dev_app_name
     └── prod/
          └── res/values/strings.xml        ← prod 전용 dev_app_name
// Prod
<resources>
    <string name="dev_app_name">Rudolf Manitto</string>
</resources>
// Mock
<resources>
    <string name="dev_app_name">Mock Manitto</string>
</resources>
  • 아이콘도 동일하게 mipmap/ 폴더를 src/<flavor>/res/mipmap/에 두어 교체할 수 있습니다.

 

(5) 불필요한 Build Variant 비활성화

기본적으로 buildType 2개와 Flavor Dimension에 해당하는 2개의 flavor가 설정되어, 총 4개의 앱 버전이 생성되게 됩니다.

이때 mockRelease라는 Build Varients는 사용되지 않기 때문에, 비활성화 처리가 필요합니다.

// build.gradle.kts
androidComponents {
    beforeVariants { variantBuilder ->
        if (variantBuilder.buildType == "release" && variantBuilder.flavorName == "mock") {
            variantBuilder.enable = false
        }
    }
}

 

 

이러한 방식으로 flavor를 구축하면, Active Build Variant에서 3가지 버전으로 앱을 실행시킬 수 있음을 확인할 수 있습니다.

 

 


3. Mock / Prod 구현체 분리

각 패키지에 동일한 추상체 연결을 위한 모듈과, 각각에 대응되는 구현체를 설정합니다.

우선 실제 서비스에서 사용되던 구현체를 prod 패키지로 옮겨주고, mock 패키지에서 동일한 위치에 Fake 구현체를 생성합니다.

 

이후 모듈을 통해서 기존 추상체와 각자의 구현체를 연결하는 과정을 진행합니다.

//mock
@InstallIn(SingletonComponent::class)
@Module
class FakeRoomRequestModule {

    @Provides
    fun provideFakeCreateRoomRequest(): RoomRequest = 
		    FakeRoomRequest()
}
  • RoomRequestImpl은 실제 네트워크 roomService를 호출하는 구현체입니다.
//prod
@InstallIn(SingletonComponent::class)
@Module
class RoomRequestModule {

    @Provides
    fun provideCreateRoomRequest(roomService: RoomService): RoomRequest =
        RoomRequestImpl(roomService)
}
  • FakeRoomRequest는 로컬에 정의된 가짜 데이터를 반환하는 구현체입니다.

 

 


4. MockDebug Fake 구현체 구현

(1) 기존 구성 분석

기존 실제 네트워크 roomService를 호출하는 구현체는 다음과 같은 구성을 하고 있습니다.

class RoomRequestImpl(
    private val roomService: RoomService,
) : RoomRequest {

    override suspend fun getRooms(): List<MyManittoModel> {
        val response = roomService.getRooms()

        if (response.statusCode == 200) {
            return response.data
        } else {
            throw Exception("Failed to get rooms: ${response.message}")
        }
    }
    
    //...
}

 

현재 산타마니또 서비스에서는 5가지의 방 종류가 있으며, 각 상태에 해당하는 가짜 아이템이 필요합니다.

 

또한 각 방은 4개의 날짜값에 따라 결정됩니다.

fun MyManittoModel.getRoomState(): RoomState {
    return when {
        // 삭제된 방
        deletedByCreatorDate != null -> RoomState.DELETED
        // 진행중인 방 (매칭됨 && 만료되지 않음)
        matchingDate != null && expirationDate != null &&
                !TimeUtil.isExpired(expirationDate) -> RoomState.IN_PROGRESS
        // 대기중인 방 (매칭 안됨 && 만료되지 않음)
        matchingDate == null && expirationDate != null &&
                !TimeUtil.isExpired(expirationDate) -> RoomState.WAITING
        // 종료된 방 (매칭됨 && 만료됨)
        matchingDate != null && expirationDate != null &&
                TimeUtil.isExpired(expirationDate) -> RoomState.FINISHED
        // 만료된 방 (매칭 안됨 && 만료됨)
        matchingDate == null && expirationDate != null &&
                TimeUtil.isExpired(expirationDate) -> RoomState.EXPIRED

        else -> RoomState.LEFT
    }
}

 

(2) Fake Item 설정

FakeRoomItems Object를 생성하여, 필요한 가짜 데이터들을 모아서 보관하는 방식으로 구현했습니다.

 

1. 가짜 ManittoRoomModel를 생성하는 팩토리 메서드를 구현했습니다.

private fun buildFakeManittoRoomModel(
    roomId: String,
    createdOffsetDays: Int,
    expirationOffsetDays: Int,
    matchingOffsetDays: Int?,
    deletedOffsetDays: Int?,
    missionCount: Int = 5,
    memberCount: Int = 5,
): ManittoRoomModel {
    val createdDate = TimeUtil.getDateWithOffsetFromNow(createdOffsetDays)
    val expirationDate = TimeUtil.getDateWithOffsetFromNow(expirationOffsetDays)
    val matchingDate = matchingOffsetDays?.let { TimeUtil.getDateWithOffsetFromNow(it) }
    val deletedDate = deletedOffsetDays?.let { TimeUtil.getDateWithOffsetFromNow(it) }

    return ManittoRoomModel(
        roomId = roomId,
        roomName = "Fake Room $roomId",
        createdAt = createdDate,
        expirationDate = expirationDate,
        matchingDate = matchingDate,
        deletedByCreatorDate = deletedDate,
        missions = List(missionCount) { index ->
            ManittoRoomMission(index.toString(), "Fake Mission $index")
        },
        members = List(memberCount) { index ->
            val santaId = (index + 1).toString()
            val manittoId = ((index + 1) % memberCount + 1).toString()
            ManittoRoomMember(
                santa = SantaRoomInfo(
                    userId = santaId,
                    userName = "Fake User $santaId"
                ),
                manitto = ManittoRoomInfo(
                    userId = manittoId,
                    userName = "Fake User $manittoId"
                )
            )
        }
    )
}

 

2. 각 방 종류에 해당하는 가짜 ManittoRoomModel 맵을 제작했습니다.

private val fakeRoomModelMap: Map<String, ManittoRoomModel> = mapOf(
    // 삭제된 방
    "1" to buildFakeManittoRoomModel("1", -3, 4, null, -1),
    // 진행중인 방
    "2" to buildFakeManittoRoomModel("2", -3, 4, -1, null),
    // 대기중인 방
    "3" to buildFakeManittoRoomModel("3", -1, 6, null, null),
    // 종료된 방
    "4" to buildFakeManittoRoomModel("4", -7, -1, -3, null),
    // 만료된 방
    "5" to buildFakeManittoRoomModel("5", -7, -1, null, null)
)

 

3. 이후 생성된 가짜 리스트를 반환하도록 설정했습니다.

fun getMyManittoList(): List<MyManittoModel> =
    fakeRoomModelMap.values.map { it.toMyManittoModel() }

 

4. 마지막으로 가짜 리스트를 반환하는 구현체를 생성하여, 모듈이 인식할 수 있도록 구현했습니다.

class FakeRoomRequest : RoomRequest {

    override suspend fun getRooms(): List<MyManittoModel> {
        return FakeRoomItems.getMyManittoList()
    }
    
    //...
}

 

(3) MockDebug 결과

다음과 같은 과정을 구현한 후, mockDebug 환경으로 앱을 실행하게 되면, 앱 UI에 가짜 아이템들의 모습을 확인할 수 있습니다.

 

 


참고 자료 :

빌드 변형 구성  |  Android Studio  |  Android Developers

Android Flavor(Product Flavor) - chattymin

 

Marchbreeze
Marchbreeze

안드로이드 개발자, 김상호입니다.

https://github.com/Marchbreeze

목차