Android

클린 아키텍처와 안드로이드 권장 멀티 모듈 적용하기 (2)

배준형 2023. 10. 5. 20:03

 

이 게시글은 아래의 게시글에서 이어지는 내용입니다.

 

클린 아키텍처와 안드로이드 권장 멀티 모듈 적용하기 (1)

도대체 이놈의 클린 아키텍처가 뭐길래 이렇게 저를 괴롭히는지 모르겠습니다. 클린 아키텍처를 처음 접하고, 이것이 뭔지 이해하기 까지도 시간이 많이 걸렸습니다. 그럼에도 아직 잘 모르는

everyday-develop-myself.tistory.com

 

Core


이제부터는 Core 폴더에 들어갈 모듈 5개를 만들어볼 예정입니다. 3개의 게시글을 전체 다 따라오시면 아래와 같은 멀티 모듈 프로젝트를 만들 수 있습니다. 

 

 

다만 이 게시글에서는 안드로이드에 멀티 모듈을 적용하는데 중점이 맞춰져 있습니다. 그렇기에 각각의 모듈의 기능에 대한 설명은 조금 부족할 수 있습니다. 그렇기에 아직 익숙하지지 않은 부분이 있다면 기능을 제대로 알고 돌아오는 것이 좋을것 같습니다.

 

 

 

 

Core 폴더의 모듈 생성

Core 폴더의 모듈은 Android Library 템플릿을 사용합니다.

 

 

 

 

Model 모듈


가장 먼저 할 것은 model 모듈을 만드는 것입니다. model 모듈은 대부분의 모듈들이 의존성을 가지고 있기에 먼저 설계하는 것이 편합니다.

 

Model 모듈의 구조는 아래와 같습니다.

 

 

 

`build.gradle` 파일

저번과 다르게 이제부터는 각 모듈에 대한 Gradle을 설정해 줍니다. 빌드 로직에서 잘 구현해 놨기 때문에 아래와 같이 간단하게 모든 Gradle을 설정할 수 있습니다.

plugins {
    id("jun.android.library")
    id("jun.android.room")
}

android {
    namespace = "com.jun.model"
}

 

 

`User` 파일

여기에서는 데이터 클래스를 사용해 Room 라이브러리에서 생성할 데이터베이스를 정의합니다. 또한 커스텀 예외 클래스를 생성해 잘못된 사용자 데이터가 들어왔을 경우를 처리합니다.

@Entity(tableName = "users")
data class User(
    @PrimaryKey val email: String,
    val password: String
)

class InvalidUserException(message: String): Exception(message)

 

 

 

Database 모듈


우리가 만들 앱은 간단한 로그인 기능을 가지고 있기에 간단하게 Room 라이브러리를 사용해서 로컬 데이터베이스를 구축하려고 합니다. 그렇기에 Database 모듈에서 관련 설정을 해주면 됩니다.

 

Database 모듈의 전체 구조는 아래와 같습니다.

 

 

 

`build.gradle` 파일

Database 모듈은 방금전에 만든 Model 모듈에 의존성을 가지고 있습니다. 그렇기에 이를 아래와 같이 설정해줍니다.

plugins {
    id("jun.android.library")
    id("jun.android.hilt")
    id("jun.android.room")
}

android {
    namespace = "com.jun.database"
}
dependencies {
    implementation(projects.core.model)
}

 

 

`userDao` 파일

데이터베이스를 구축하기 위해서는 DAO를 만들어야 합니다. 해당 로그인 앱에서는 회원가입 기능과 로그인 기능을 구현하기 위해 유저를 insert하는 메서드Email로 유저 정보를 가져오는 메서드가 있습니다.

@Dao
interface UserDao {
    @Insert
    suspend fun insertUser(user: User)

    @Query("SELECT * FROM users WHERE email = :email")
    suspend fun getUserByEmail(email: String): User?
}

 

 

`AppDatabase` 파일

그리고 아래와 같이 데이터베이스를 생성하고 관리하기 위한 클래스를 정의합니다.

 

여기서는 데이터베이스의 인스턴스를 싱글턴으로 관리하기 위해 아래와 같이 작성하였습니다.

@Database(
    entities = [User::class],
    version = 1
)
abstract class AppDatabase : RoomDatabase() {

    abstract val userDao: UserDao

    companion object {
        @Volatile
        private var database: AppDatabase? = null

        fun getDatabase(context: Context): AppDatabase {
            return database ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database"
                ).build()
                database = instance
                instance
            }
        }
    }
}

 

 

`AppModule` 파일

멀티 모듈 프로젝트에서는 주로 Hilt를 사용해서 의존성을 주입합니다. 나중에 data 모듈에서 정의할 Repository에서 필요한 userDao 객체를 여기서 바로 제공합니다.

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Singleton
    @Provides
    fun provideDatabase(
        @ApplicationContext context: Context
    ): AppDatabase = AppDatabase.getDatabase(context)


    @Singleton
    @Provides
    fun provideRepository(database: AppDatabase
    ): UserDao = database.userDao
}

 

 

 

 

Data 모듈


Data 모듈에서는 RepositoryData Source 등이 포함됩니다. 여기서는 다루고 있지 않지만 서버와의 통신을 하거나 Open API로부터 Retrofit2을 사용해 데이터를 가져온다면, Data 모듈에 정의하는 것이 좋습니다.

 

Data 모듈의 전체 구조는 아래와 같습니다.

 

 

 

`build.gradle` 파일

이제는 각 모듈에 맞는 gradle을 정의하는 것이 크게 어렵지 않을 것이라 감히 예상해봅니다. Data 모듈에 필요한 플러그인과 의존성을 정의하면 됩니다.

 

Data 모듈은 저희가 정의했던 Model 그리고 이를 사용해 구축했던 데이터베이스를 사용합니다. 그럼 해당하는 모듈의 의존성 또한 필요하겠죠

plugins {
    id("jun.android.library")
    id("jun.android.hilt")
    id("jun.kotlin.hilt")
}

android {
    namespace = "com.jun.data"
}
dependencies {
    implementation(projects.core.model)
    implementation(projects.core.database)
}

 

그 다음은 크게 어렵지 않습니다. Repository를 정의하고, Usecase에 의존성을 주입해주면 됩니다.

 

 

`UserRepository` 파일

interface UserRepository {

    suspend fun insertUser(user: User)

    suspend fun getUserByEmail(email: String): User?
}

 

 

`DefaultUserRepository` 파일

여기서 `AppModule` 파일에서 제공해줬던 userDao 객체를 주입받습니다.  

class DefaultUserRepository @Inject constructor(
    private val userDao: UserDao
) : UserRepository {

    override suspend fun insertUser(user: User) {
        userDao.insertUser(user)
    }

    override suspend fun getUserByEmail(email: String): User? {
        return userDao.getUserByEmail(email)
    }
}

 

 

`DataModule` 파일

여기서 다시 한번 안드로이드에서 권장하는 아키텍처를 보겠습니다. 

 

Damain Layer 옆에 (optional) 이란 단어가 보이시나요??

 

 

맞습니다. Domain Layer는 없어도 괜찮습니다. 다만 아직은 비즈니스 로직이 복잡합니다. 그렇기에 이를 기능으로 나눠서 복잡한 비즈니스 로직을 캡슐화해서 관심사를 분리하는 것이 좋습니다.

 

 

이를 위한 것이 바로 Usecase입니다. Usecase를 사용하므로써 UI 계층에서 데이터에 대한 의존성을 가질 때, 필요한 기능만 가져올 수 있게 됩니다.

 

그렇기에 Usecase는 이름만 봐도 어떤 기능인지 알 수 있도록 이름을 정의하고 `invoke() operator`를 재정의해서 클래스 이름으로 사용할 수 있게 정의합니다.


 

 

Domain 모듈


서론이 좀 길어졌는데 저희가 구현할 Domain 모듈의 구조는 아래와 같습니다.

 

 

 

`build.gradle` 파일

Domain은 Model과 Data 모듈에 대한 의존성을 가지고 있습니다.

plugins {
    id("jun.android.library")
}

android {
    namespace = "com.jun.domain"
}
dependencies {
    implementation(projects.core.data)
    implementation(projects.core.model)
}

 

 

`SignInUsecase` 파일

`SignInUsecase`는 이름 그대로 회원가입을 진행합니다. 내부 로직을 간단하게 설명하겠습니다.

 

  1. Repository를 통해 데이터베이스에 접근해 중복된 이메일이 있는지 체크합니다.
  2. 중복된 이메일이 없다면 유효성 검사를 실시합니다. 
  3. 유효성 검사를 통과하였다면 Repository를 통해 User 정보를 데이터베이스에 저장합니다.
  4. 각각의 단계에서 오류가 발생한다면 InvaildUserException을 던집니다.
class SignInUsecase  @Inject constructor(
    private val repository: UserRepository
) {

    @Throws(InvalidUserException::class)
    suspend operator fun invoke(user: User) {
        if (repository.getUserByEmail(user.email) != null)
            throw InvalidUserException("이메일이 존재합니다.")
        else {
            isUserInformationValid(user).let { message ->
                if (message != "성공") throw InvalidUserException(message)
                repository.insertUser(user)
            }
        }
    }

    private fun isUserInformationValid(user: User): String =
        when {
            TextUtils.isEmpty(user.email) -> {
                "이메일을 입력해 주세요."
            }
            !Patterns.EMAIL_ADDRESS.matcher(user.email).matches() -> {
                "유효한 이메일을 입력해 주세요."
            }
            TextUtils.isEmpty(user.password) -> {
                "비밀번호를 입력해 주세요."
            }
            user.password.length <= 6 -> {
                "비밀번호는 최소 길이는 6입니다."
            }
            else -> "성공"
        }
}

 

 

`LoginUsecase` 파일

`LoginUsecase`는 이름 그대로 로그인을 진행합니다. 내부 로직을 간단하게 설명하겠습니다.

 

  1. Repository를 통해 데이터베이스에 접근해 중복된 이메일이 있는지 체크합니다.
  2. 중복된 이메일이 있다면 비밀번호를 비교합니다.
  3. 비밀번호가 일치하지 않는다면 오류를 발생시킵니다.
  4. 각각의 단계에서 오류가 발생한다면 InvaildUserException을 던집니다.
class LoginUsecase @Inject constructor(
    private val repository: UserRepository
) {

    @Throws(InvalidUserException::class)
    suspend operator fun invoke(user: User) {
        repository.getUserByEmail(user.email)?.let { userByEmail ->
            if (userByEmail.password != user.password)
                throw InvalidUserException("비밀번호가 일치하지 않습니다.")
        } ?: throw InvalidUserException("존재하지 않는 이메일입니다.")
    }
}

 

 

 

 

Designsystem 모듈


Designsystem 모듈은 compose 기반 디자인 시스템입니다. 이 모듈에서는 애니메이션을 포함하거나, 내부 UI 활용을 위한 매핑을 합니다. 이 경우 Font 적용이나 내부의 theme 적용 등을 할 수 있습니다.

 

당장 디자인 시스템이 없더라도, 컴포즈 활용 시에는 개발의 편의성을 위해 이러한 매핑 함수를 미리 만들어 놓는 것이 좋습니다.

 

저는 간단한 로그인 앱을 구현했기에 Designsystem 모듈에서는 theme만 적용했습니다.

 

전체 구조는 아래와 같습니다.

 

 

 

`build.gradle` 파일

plugins {
    id("jun.android.library")
    id("jun.android.compose")
}

android {
    namespace = "com.jun.designsystem"
}

dependencies {
    implementation(libs.androidx.appcompat)
}

 

 

`themes.xml` 파일

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <style name="Theme.loginCAApp" parent="Theme.AppCompat.DayNight.NoActionBar" />

    <style name="Theme.loginCAApp.TransparentSystemBar">
        <item name="android:statusBarColor">@android:color/transparent</item>
        <item name="android:navigationBarColor">@android:color/transparent</item>
    </style>

</resources>

 

 

 

2편 마무리


오늘 게시글에서는 베이스 코드들을 담고 있는 core 폴더를 구현해 봤습니다. 다음 게시글에서는 이제 이렇게구현한 것들을 실제로 UI에서 사용하는 feature를 구현해 보려고 합니다.

 

 

다음 게시글

 

클린 아키텍처와 안드로이드 권장 멀티 모듈 적용하기 (3)

이 게시글은 아래의 게시글에서 이어지는 내용입니다. 클린 아키텍처와 안드로이드 권장 멀티 모듈 적용하기 (2) 이 게시글은 아래의 게시글에서 이어지는 내용입니다. 클린 아키텍처와 안드로

everyday-develop-myself.tistory.com

 

전체 코드

 

GitHub - Iwillbeagood/Login-CleanArchitecture: CleanArchitecture Multi Module Login App

CleanArchitecture Multi Module Login App. Contribute to Iwillbeagood/Login-CleanArchitecture development by creating an account on GitHub.

github.com