Skip to content

Conversation

@chanho0908
Copy link
Member

@chanho0908 chanho0908 commented Dec 18, 2025

이슈 번호

#1

리뷰/머지 희망 기한 (선택)

  • 2025.12.21

작업내용

멀티모듈 아키텍처 구조를 설정하고 Convention Plugin 기반 빌드 시스템을 구축했습니다.

왜 수정했는지?

  • 확장 가능한 멀티모듈 아키텍처를 구축하기 위해
  • Convention Plugin을 활용한 일관된 빌드 설정을 위해
  • Feature, Domain, Data, Core 레이어를 분리하여 관심사를 명확히 하기 위해
  • 빌드 로직의 재사용성과 유지보수성을 높이기 위해

무엇을 구현했는지?

  1. build-logic (Convention Plugin)

    • AndroidApplicationConventionPlugin
    • AndroidLibraryConventionPlugin
    • AndroidComposeConventionPlugin
    • FeatureConventionPlugin
    • DataConventionPlugin
    • JvmLibraryConventionPlugin
    • KoinConventionPlugin
    • 빌드 로직 재사용을 위한 확장 함수들
  2. Feature 레이어

    • feature/login: 로그인 Feature 모듈 예제
  3. Domain 레이어

    • 비즈니스 로직을 담당할 모듈
  4. Data 레이어

    • 데이터 소스를 담당할 모듈
  5. Core 레이어 (공통 모듈)

    • ui: State/SideEffect 기반 UI 아키텍처
    • util: 유틸리티
    • navigation: 네비게이션
    • design-system: 디자인 시스템
    • network: 네트워크
  6. 프로젝트 설정

    • TYPESAFE_PROJECT_ACCESSORS 활성화
    • .editorconfig (Ktlint 14.0.1)

결과물

프로젝트 구조

Twix/
├── app/                      # 메인 앱
├── feature/
│   └── login/               # Feature 모듈
├── domain/                  # 도메인 레이어
├── data/                    # 데이터 레이어
├── core/
│   ├── ui/                  # 공통 UI
│   ├── util/                # 유틸리티
│   ├── navigation/          # 네비게이션
│   ├── design-system/       # 디자인 시스템
│   └── network/             # 네트워크
└── build-logic/             # Convention Plugin
    └── convention/

리뷰어에게 추가로 요구하는 사항 (선택)

  • Convention Plugin의 구조와 설정이 적절한지 확인 부탁해 !
  • BaseViewModel은 사용하기로 해서 StateHolderSideEffectHolder를 제거할지 고민해봤어 !
    저 두개를 유지하면 만약 상태나 사이드 이펙트 둘 중 하나만 사용하는 ViewModel에서도
    StateHolderSideEffectHolder를 그대로 사용할 수 있어 중복 코드를 줄일 수 있을 것 같아서
    유지해도 괜찮지 않을까라고 생각하는데 현수 생각은 어때 ?

그럼 일단 어제 보여준 코드 기준으로 BaseViewModel은 이렇게 될 것 같아 !

abstract class BaseViewModel<
    I : UiIntent,
    S : State,
    E : SideEffect
>(
    initialState: S,
) : ViewModel() {

    protected val stateHolder = StateHolder(initialState)
    val state = stateHolder.state

    protected val sideEffectHolder = SideEffectHolder<E>()
    val sideEffect = sideEffectHolder.flow

    protected val currentState: S
        get() = stateHolder.current

    abstract fun onIntent(intent: I)

    protected fun reduce(reducer: S.() -> S) {
        stateHolder.reduce(reducer)
    }

    protected fun postSideEffect(effect: E) {
        viewModelScope.launch {
            sideEffectHolder.emit(effect)
        }
    }

    protected fun postSideEffect(vararg effects: E) {
        viewModelScope.launch {
            effects.forEach { sideEffectHolder.emit(it) }
        }
    }
}

- Convention Plugin 기반 빌드 로직 구성 (build-logic)
- Feature 레이어: login 모듈
- Domain 레이어: 비즈니스 로직 모듈
- Data 레이어: 데이터 소스 모듈
- Core 레이어: 공통 모듈
  - ui: State/SideEffect 기반 UI 구조
  - util: 유틸리티
  - navigation: 네비게이션
  - design-system: 디자인 시스템
  - network: 네트워크
- TYPESAFE_PROJECT_ACCESSORS 활성화
- Ktlint 14.0.1 전역 적용

🤖 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <help@firebender.com>
Base automatically changed from feat/#1-basic-project to develop December 18, 2025 07:03
@chanho0908 chanho0908 changed the title 🏗️ 멀티모듈 아키텍처 구조 설정 멀티모듈 아키텍처 구조 설정 Dec 18, 2025
@chanho0908 chanho0908 self-assigned this Dec 18, 2025
@chanho0908 chanho0908 marked this pull request as draft December 18, 2025 07:13
@chanho0908 chanho0908 requested a review from dogmania December 18, 2025 07:14
@chanho0908 chanho0908 added the Feature Extra attention is needed label Dec 18, 2025
Copy link
Member

@dogmania dogmania left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다! 수정사항 다 반영되면 디코로 알려주세요!

Comment on lines +6 to +10
abstract class BuildLogicConventionPlugin(private val block: Project.() -> Unit) : Plugin<Project> {
final override fun apply(target: Project) {
with(target, block = block)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 이렇게 추상화해서 빼는 거 되게 좋을 거 같아요

Comment on lines +7 to +11
applyPlugins(
"twix.android.library",
"org.jetbrains.kotlin.plugin.serialization",
"twix.koin"
)
Copy link
Member

@dogmania dogmania Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저희가 직접 구현한 컨벤션 플러그인을 적용할 때는 apply를 사용해서 넣는 게 나중에 플러그인 코드 수정할 때나 무슨 내용이 들어있는지 파악할 때 더 편할 거 같아요

예를 들어서

class DataConventionPlugin : BuildLogicConventionPlugin({
    pluginManager.apply<AndroidLibraryConventionPlugin>()
    applyPlugins("org.jetbrains.kotlin.plugin.serialization")
    pluginManager.apply<KoinConventionPlugin>()

이런식으로 수정하게 되면 커스텀 플러그인 내부에 어떤 내용이 들어있는지 IDE 네비게이션으로 바로 들어가서 확인할 수 있고 문자열로 하드코딩하면서 발생하는 실수도 방지할 수 있을 거 같아요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0ec91a4

리뷰 반영 완료했어 !

제네릭을 사용할 수 도 있었구나 몰랐넹 ㅎㅎ
이렇게 하면 타입 안정성이 확실히 생길 것 같아 !

Comment on lines 8 to 12
applyPlugins(
"twix.android.library",
"twix.android.compose",
"twix.koin",
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 마찬가지로 apply + 클래스로 쓰는 게 유지보수할 때 더 편할 것 같습니다!

Comment on lines +7 to +25
fun DependencyHandlerScope.implementation(project: Project) {
"implementation"(project)
}

fun DependencyHandlerScope.implementation(provider: Provider<*>) {
"implementation"(provider)
}

fun DependencyHandlerScope.debugImplementation(provider: Provider<*>) {
"debugImplementation"(provider)
}

fun DependencyHandlerScope.androidTestImplementation(provider: Provider<*>) {
"androidTestImplementation"(provider)
}

fun DependencyHandlerScope.testImplementation(provider: Provider<*>) {
"testImplementation"(provider)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 함수로 뺄 생각은 못해봤는데 좋네요

Comment on lines 1 to 7
plugins {
alias(libs.plugins.twix.android.library)
}

android {
namespace = "com.twix.navigation"
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네비게이션에서는 Compose를 사용해야 해서 Compose 플러그인 추가해주세요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 반영 커밋 : 6e59559

Comment on lines +6 to +10
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 저번에 얘기했던 네비게이션 구조 반영해서 수정해야 할 거 같아요. :app에 MainActivity를 단일 액티비티로 두고 진입점을 :app으로 통합하는 식으로 바꾸면 좋을 거 같아요 이 부분은 제가 작업해서 pr 올릴게요!

@dogmania
Copy link
Member

BaseViewModel 여부와 상관없이 StateHolder, SideEffectHolder는 필요한 것들이라서 남겨두는 게 맞다고 생각해요

)

dependencies {
implementation(libs.library("kotlinx-coroutines-core"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 AndroidLibraryConvention에 이미 이 의존성을 정의하고 있어서 중복되는 의존성이에요! 제거해주세요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 반영 커밋 : 6e59559

Comment on lines 15 to 22
extensions.configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

extensions.configure<KotlinProjectExtension> {
jvmToolchain(libs.version("java").requiredVersion.toInt())
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 자바 버전을 하드코딩 + 버전 카탈로그에서 읽어오기가 섞여있는데 통일하면 좋을 거 같아요

class JvmLibraryConventionPlugin : BuildLogicConventionPlugin({
    applyPlugins("org.jetbrains.kotlin.jvm")

    val javaVersionInt = libs.version("java").requiredVersion.toInt()
    val javaVersion = JavaVersion.toVersion(javaVersionInt)

    extensions.configure<JavaPluginExtension> {
        sourceCompatibility = javaVersion
        targetCompatibility = javaVersion
    }

    extensions.configure<KotlinProjectExtension> {
        jvmToolchain(javaVersionInt)
    }

이런식으로 버전 카탈로그 기준으로 버전 통일 가능합니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 반영 커밋 : 5323bd0

Base automatically changed from develop to main December 20, 2025 09:21
@chanho0908 chanho0908 changed the base branch from main to develop December 20, 2025 09:23
@chanho0908 chanho0908 marked this pull request as ready for review December 20, 2025 09:24
Data 모듈의 의존성을 DataConventionPlugin으로 이동하고, FeatureConventionPlugin의 플러그인 적용 방식을 개선하여 빌드 로직을 더욱 체계적으로 관리
Comment on lines 8 to 21
class AndroidComposeConventionPlugin : BuildLogicConventionPlugin({
applyPlugins("org.jetbrains.kotlin.plugin.compose")

extensions.configure<LibraryExtension> {
configureCompose(this)
}

dependencies {
val bom = platform(libs.library("compose-bom"))
implementation(bom)
implementation(libs.bundle("compose"))
debugImplementation(libs.bundle("compose-debug"))
}
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MainActivity를 :app에 두려면 :app에서도 Compose를 사용해야 해요. 지금 플러그인 설정으로는 android.application을 적용하고 있는 ApplicatonConvention에는 이 컨벤션을 적용할 수가 없어서 다음과 같이 수정하면 좋을 거 같아요

class AndroidComposeConventionPlugin : BuildLogicConventionPlugin({
    applyPlugins("org.jetbrains.kotlin.plugin.compose")

    pluginManager.withPlugin("com.android.application") {
        extensions.configure<ApplicationExtension> {
            configureCompose(this)
        }
    }

    pluginManager.withPlugin("com.android.library") {
        extensions.configure<LibraryExtension> {
            configureCompose(this)
        }
    }

withPlugin이 해당 플러그인이 있을 때 블록을 실행하는 거라 저렇게 하면 feature에는 android.application이 들어가지 않고 분기처리가 가능합니다

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app 모듈은 컴포즈 의존성 없이 만들고
앱 시작점을 LoginActivity로 하면 되는 줄 알고
app 모듈에 컴포즈 의존성을 제거했는데 app 모듈에서 하는게 맞았구나 ! 😓

리뷰 반영 커밋 : 71d95d2

@chanho0908 chanho0908 merged commit 23562b4 into develop Dec 22, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature Extra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants