์๋ ํ์ธ์ ๐
์ค๋์ ์ง๋ ํฌ์คํ ์ ์ด์ด ์๋๋ก์ด๋ ํ๋ก์ ํธ์์
์ด๋ป๊ฒ Clean Architecture๋ฅผ ์ ์ฉ์ํค๋์ง์ ๋ํด์ ํฌ์คํ ํด๋ณด๋ ค ํฉ๋๋ค.

Clean Architecture์ ๊ฐ๋ ์ ๋ํด์๋ ์๋ ํฌ์คํ ์ ์ฐธ๊ณ ๋ถํ๋๋ฆฝ๋๋ค ๐ค
[Android] ์๋๋ก์ด๋ Clean Architecture์ ๋ํด์ ( 1 ) - Clean Architecture์ ๊ฐ๋
์๋ ํ์ธ์ ๐ ์ค๋์ Clean Architecture์ ๋ํด์ ๊ณต๋ถํ ๋ด์ฉ์ ํฌ์คํ ํด๋ณด๋ ค ํฉ๋๋ค. ํ๋ก์ ํธ์ ์ ์ฉํ๋ ๋ฐฉ๋ฒ๊น์ง ํฌ์คํ ํ๊ธฐ์ ๋ด์ฉ์ด ๊ธธ์ด์ง ๊ฒ ๊ฐ์ ์ค๋์ ๊ฐ๋ ์ ๋ํด์๋ง ๊ฐ๋ตํ๊ฒ ํฌ
devyo-111commit.tistory.com
Android์์ Clean Architecture๋ฅผ ์ ์ฉํ๊ธฐ ์ํด์๋ ์์ ๋งํ๋ฏ
3๊ฐ์ง layer๋ก ๋๋๋ ๊ฒ๋ถํฐ ์์ํฉ๋๋ค.
์ ๋ ์ด์ ์ ํฌ์คํ ํ๋ Hilt ์์ ์ํ์ ์ ์ฉํด ๋ณด์์ต๋๋ค. ๐
Domain Layer
[Beach.kt] ( Data์์ ์ ๋ฌ๋ฐ์ BeachDTO์์ UI์์ ์ฌ์ฉํ ํด๋ณ์ ์ ๋ณด๋ง์ ์ ์ํ class )
data class Beach(
val poiNm : String = "",
val congestion : String = ""
)
[BeachCongestionList.kt] ( ์ ํด๋ณ์ ์ ๋ณด๋ฅผ ๋ชจ์๋์ List๋ฅผ ์ ์ํด๋์ class )
data class BeachCongestionList(
val beachList : ArrayList<Beach>
)
[BeachCongestionList.kt] ( Repository interface )
interface MainRepository {
suspend fun getBeachCongestion() : BeachCongestionList
}
[GetBeachCongestionList.kt] ( ํด๋ณ ํผ์ก๋ ์ ๋ณด๋ฅผ ํฌํจํ ๋ฆฌ์คํธ๋ฅผ ๊ฐ์ ธ์ค๋ UseCase )
class GetBeachCongestionList @Inject constructor(
private val repository: MainRepository
) {
suspend fun invoke() : BeachCongestionList{
return repository.getBeachCongestion()
}
}
[build.gradle]
module๊ฐ ์์กด์ฑ ์ฃผ์ X
Data Layer
[MyApi.kt] ( ํด์์์ฅ ํผ์ก๋๋ฅผ ๊ฐ์ ธ์ค๋ API )
interface MyApi {
@GET(DataConst.GET_CONGESTION_URL)
suspend fun getBeachCongestion() : BeachCongestionListDTO
}
[DataConst.kt] ( Data Layer์์ ์ฌ์ฉํ common ํ ์์๋ฅผ ์ ์ํ class )
class DataConst {
companion object{
const val GET_CONGESTION_URL = "/seantour_map/travel/getBeachCongestionApi.do" // ํด์์์ฅ ํผ์ก๋ ๊ตฌํ๋ URL
}
}
[DTO classes] ( ํด์์์ฅ ํผ์ก๋ API์ DTO class๋ค๋ก API์ ๋ณด๋ ๊ณต๊ณต๋ฐ์ดํฐ ํฌํธ์ ํตํด ํ์ธ ๋ฐ๋๋๋ค. )
data class BeachDTO(
val etlDt : String = "",
val seqId : Int = -1,
val poiNm : String = "",
val uniqPop : Int = -1,
val congestion : String = ""
)
class BeachCongestionListDTO {
private lateinit var beachListDTO : MutableList<BeachDTO>
@SerializedName("Beach0")
val beachDTO0 : BeachDTO? = null
@SerializedName("Beach1")
val beachDTO1 : BeachDTO? = null
@SerializedName("Beach2")
val beachDTO2 : BeachDTO? = null
@SerializedName("Beach3")
val beachDTO3 : BeachDTO? = null
@SerializedName("Beach4")
val beachDTO4 : BeachDTO? = null
@SerializedName("Beach5")
val beachDTO5 : BeachDTO? = null
@SerializedName("Beach6")
val beachDTO6 : BeachDTO? = null
@SerializedName("Beach7")
val beachDTO7 : BeachDTO? = null
@SerializedName("Beach8")
val beachDTO8 : BeachDTO? = null
@SerializedName("Beach9") // ์ดํ ์๋ต
fun getAllBeachList() : MutableList<BeachDTO> {
if(!::mBeachListDTO.isInitialized){
mBeachListDTO = mutableListOf()
mBeachListDTO.add(beachDTO0 as BeachDTO)
mBeachListDTO.add(beachDTO1 as BeachDTO)
mBeachListDTO.add(beachDTO2 as BeachDTO)
mBeachListDTO.add(beachDTO3 as BeachDTO)
mBeachListDTO.add(beachDTO4 as BeachDTO)
mBeachListDTO.add(beachDTO5 as BeachDTO)
mBeachListDTO.add(beachDTO6 as BeachDTO)
mBeachListDTO.add(beachDTO7 as BeachDTO)
mBeachListDTO.add(beachDTO8 as BeachDTO)
mBeachListDTO.add(beachDTO9 as BeachDTO)
mBeachListDTO.add(beachDTO10 as BeachDTO) // ์ดํ ์๋ต
}
return beachListDTO
}
}
[BeachMapper.kt] ( Data์ DTO๊ฐ์ฒด๋ฅผ Domain์ Model๋ค๋ก mapping ์ํค๋ class )
object BeachMapper {
private fun mapperToBeach(beachDTO: BeachDTO) : Beach {
val beach = beachDTO.run {
Beach(poiNm, congestion)
}
return beach
}
fun mapperToBeachCongestionList(beachCongestionListDTO: BeachCongestionListDTO) : BeachCongestionList {
val list = arrayListOf<Beach>()
beachCongestionListDTO.getAllBeachList().forEach {
val beach = mapperToBeach(it)
list.add(beach)
}
return BeachCongestionList(list)
}
}
[MainRepositoryImpl.kt] ( MainRepository์ ๊ตฌํ์ฒด ํด๋น ๊ตฌํ์ฒด๋ฅผ ํตํด Data์ ๊ฐ์ Domain์ผ๋ก ์ ๋ฌ )
class MainRepositoryImpl @Inject constructor(
private val beachDataSource: BeachDataSource
) : MainRepository {
override suspend fun getBeachCongestion(): BeachCongestionList {
return beachDataSource.getCongestionList()
}
}
[build.gradle]
Domain Layer ์์กด์ฑ ์ฃผ์
dependencies {
// ... ์๋ต
implementation project(':domain')
}
Presenatation Layer
[MainApplication.kt]
@HiltAndroidApp
class MainApplication : Application()
[NetworkConst.kt]
const val BASE_URL = "https://www.tournmaster.com"
[AppModule.kt] ( Hilt์์ Provide๋ฅผ ์ ๊ณตํ๊ธฐ ์ํ Module )
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideMyApi() : MyApi {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(MyApi::class.java)
}
}
[BindModule.kt] ( Hilt์์ Bind๋ฅผ ์ ๊ณตํ๊ธฐ ์ํ Module )
@Module
@InstallIn(SingletonComponent::class)
abstract class BindModule {
@Binds
@Singleton
abstract fun bindMainRepository(mainRepositoryImpl: MainRepositoryImpl) : MainRepository
@Binds
@Singleton
abstract fun bindBeachDataSource(beachDataSourceImpl: BeachDataSourceImpl) : BeachDataSource
}
[MyViewModel.kt] ( MainActivity์ ViewModel class )
@HiltViewModel
class MyViewModel @Inject constructor(
private val getBeachCongestionList: GetBeachCongestionList
) : ViewModel(){
private val _beachCongestionList = MutableLiveData<BeachCongestionList>()
val beachCongestionList : LiveData<BeachCongestionList>
get() = _beachCongestionList
private val _toastMessage = MutableLiveData<String>()
val toastMessage : LiveData<String>
get() = _toastMessage
fun getBeachInfo(){
val exceptionHandler = CoroutineExceptionHandler{ _ , exception ->
when(exception){
is SocketTimeoutException -> {onError("ํต์ ์๊ฐ ์ด๊ณผ!")}
}
}
viewModelScope.launch(exceptionHandler) {
_beachCongestionList.value = getBeachCongestionList.invoke()
}
}
private fun onError(message : String) {
_toastMessage.value = message
}
}
[MainActivity.kt]
private const val TAG = "MainActivity"
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding : ActivityMainBinding
private val viewModel : MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater).also {
setContentView(it.root)
}
observeData()
}
override fun onResume() {
super.onResume()
viewModel.getBeachInfo()
}
private fun observeData() {
with(viewModel){
beachCongestionList.observe(this@MainActivity){
Log.d(TAG, "observeData: $it")
}
toastMessage.observe(this@MainActivity){
Toast.makeText(this@MainActivity , it , Toast.LENGTH_SHORT).show()
}
}
}
}
[build.gradle]
Domain , Data Layer ์์กด์ฑ ์ฃผ์
dependencies {
implementation project(':domain')
implementation project(':data')
// ... ์๋ต
}
Clean Architecture๋ ์ ๋ต์ด ์๊ณ ๊ฐ ๊ณ์ธต์ ์กฐ๊ฑด๋ค์ด ๋ถํฉํ๋ค๋ฉด ๊ฐ์ธ์ ๋ฐ๋ผ์ ํจํค์ง์ ๊ตฌ์กฐ ๋ฑ์ ๋ฐ๋ ์ ์๋ค๊ณ ํฉ๋๋ค.
์ ๋ํ ๊ณต๋ถ๋ฅผ ํ๋ฉฐ ์ ์ฉํด ๋ณธ ๊ฒ์ด๊ธฐ์ ์๋ชป๋ ๋ถ๋ถ์ด ์๋ค๋ฉด ๋ง์ ํผ๋๋ฐฑ ๋ถํ๋๋ฆฌ๋ฉฐ
๋ง์ ๋ถ๋ค์๊ฒ ์ฐธ๊ณ ๊ฐ ๋์์ผ๋ฉด ์ข๊ฒ ์ต๋๋ค ๐๐ปโ๏ธ
์ด์์ผ๋ก Clean Architecture์ ๋ํ ํฌ์คํ ์ ๋ชจ๋ ๋ง์น๋๋ก ํ๊ฒ ์ต๋๋ค.
์ค๋๋ ์ฆ์ฝํ์ธ์ :)