[Kotlin] Jetpack, Koin, 클린 아키텍처

Boknami·2024년 2월 27일
0

코틀린

목록 보기
19/19

디자인 패턴

🔍 MVI패턴을 알아보자!


UI 프레임 워크

📑 Jetpack Compose

지금까지의 개발은 xml 기반의 레이아웃 시스템으로 진행해왔다. 그렇기에 이번엔 젯팩 컴포즈를 이용해 개발을 진행해볼 예정!

Jetpack Compose를 설명할 때 선언전 UI 프레임워크 라는 말이 등장한다.


🔍장점1:🤷‍♂️ 선언적?

UI를 정의하는 코드가 UI의 최종 모습을 직접 나타내므로 코드의 가독성과 유지보수성을 향상시킨다..라고 되어있는데 이것도 뭔 말인지 와닿지가 않았다..

일단 선언적과 대비되는 명령형 프로그래밍을 보면 쉽게 이해가 가능했다.
명령형 프로그래밍은 기존의 방식. 그렇니까 xml에는 UI모습, .kt 파일에는 UI의 동작 이렇게 분리해서 구현을 했다. 왜나하면 명령형은 동작을 명시적으로 지시해야하는 프로그래밍이기 때문이다.

그런데 선언적 UI는 코드 자체가 무엇(what)을 표현하며, 상태나 동작을 명시적으로 제어하진 않는다! Jetpack Compose는 UI를 작성하는 코드 자체가 UI 그 자체의 모습을 직접적으로 나타낸다. 무슨 말이냐 하면 함수 또는 composable이라고 불리는 구성 요소 안에 UI의 모습과 동작이 모두 정의되어 있기에 코드를 읽으면서 UI의 모습을 파악하는 것이 더욱 쉽고 직관적라는 의미이다!


🔍장점2: 상태 관리의 단순화

Jetpack Compose는 상태 관리를 위한 기능을 내장하고 있다!
상태 관리를 이 전 게시글인 MVI에서 어느 정도 알고 와서 이 부분은 어렵지 않았다.

아무튼 Jetpack Compose는 상태 관리 기능을 내장하고 있기 때문에 UI와 상태를 간단하게 연결할 수 있다. 이를 통해 우리는 UI의 상태 변화에 따라 쉽게 대응이 가능하다!

이러한 이유에서 MVI와 Jetpack Compose는 궁합이 참 잘 맞다!


🔍장점3: Android 생태계와의 통합

Jetpack Compose는 Android 생태계의 다양한 라이브러리와 툴과의 통합을 지원한다! 이러한 점에서 기존의 Android 개발자들이 Jetpack Compose를 쉽게 도입하여 기존 앱을 Jetpack Compose로 변경하는데 겪는 어려움을 최소화했다! 또한 새롭게 개발을 할 때도 필요한 걸 여러가지 추가할 필요없이 바로 사용이 가능!


의존성 주입(DI)

을 살펴보기 전에..의존성 역전 원칙에 대해서 먼저 복습해야했다!

🤷‍♂️의존성 역전

의존성 역전 원칙의 핵심은 의존하는 하위 레벨 모듈이 변하더라도 상위 레벨의 모듈은 변하지 않아야 한다는 원칙(고수준 모듈이 저수준 모듈에 의존하면 안 되며, 둘 모두 추상화에 의존해야 한다는 원칙)

예를 들어 주문하는 기계 로직을 만들고 있다고 하자.
총 3개의 클래스를 만들었다 -> Service, DB, OrderInfo

이 때 Service는 주문을 처리하기 위해서 DB에 '의존'하고 있는 상황이다.
쉽게 말해 Service의 주문 처리를 위해서는 DB클래스가 필연적으로 필요하단 말이다.
이 상황 자체가 지금 의존성 역전을 위반하고 있는 것이다. 

이럴 때 해결을 하기 위해 DB에 직접적으로 의존하지 않고 추상화한 한 단계를 더 추가하는 것이 좋다. 가장 쉽게 할 수 있는 방법이 interface를 만드는 것이다. 예를 들어 OrderRepo라는 인터페이스를 만들어서 DB클래스에 있던 주문을 save,get하는 함수들을 인터페이스에 두고 실제적 구현은 이 인터페이스를 상속 받아 만든 클래스 안에서 구현하는 것이다!

📌고수준?저수준?

의존성 역전을 공부하다 보면 저수준의 모듈. 고수준의 모듈. 이런 말들이 자주 나온다. 이게 이해가 안되서 깊게 들어가보니 저수준의 모듈은 시스템의 기능적인 부분을 직접 처리하는 느낌이다. 즉 데이터베이스 엔진이나 네트워크 프레임워크 같은 하부 기술을 직접 다루는 클래스는 저수준 모듈로 간주된다!

고수준 모듈은 보통 기능적인 부분에 직접적으로 관여하고 있지 않고 저수준의 모듈들을 사용하고 있다.

🔍 Koin

위에서 말한 의존성 역전 원칙을 구현하는 방법 중 하나가 DI이다.
DI는 객체가 의존성을 직접 생성하지 않고 "외부에서 주입받아" 사용하는 것
예를 들어, 자동차가 있을 때, 엔진을 자동차가 직접 만들지 않고 외부에서 받아서 장착하는 것이 DI인 것이다!!

Koin은 경량DI 프레임워크로 코드를 간결하게 유지하며 쉽게 DI를 구현할 수 있도록 해준다.

아래의 예시를 직접 보면서 이해하자면

  • 의존성을 가지는 클래스
// HelloService.kt -> 아래 클래스 헬로레포가 필요함!(의존성)
class HelloService(private val repository: HelloRepository) {
    fun sayHello(): String {
        return "Hello, ${repository.getData()}!"
    }
}

// HelloRepository.kt
class HelloRepository {
    fun getData(): String {
        return "Kotlin"
    }
}

  • Koin모듈
// MyAppModule.kt
import org.koin.dsl.module

val myAppModule = module {
    single { HelloService(get()) } // HelloRepository를 주입받음
    single { HelloRepository() }
}

module { } 안에 객체들을 생성한다.
1. single -> 싱글톤 객체
2. factory -> 요청 시마다 새로운 객체가 생성된다


  • 메인
fun main() {
    // Koin 모듈 로드
    loadKoinModules(myAppModule)
    
    // Koin을 시작
    startKoin {}

    // HelloService 가져오기
    val helloService: HelloService = get()

    // HelloService 사용
    println(helloService.sayHello())
    
    // Koin 정리 (스캐너 쓰고 close하는 것처럼!)
    stopKoin()
    unloadKoinModules(myAppModule)
}

짧은 예지만 이러한 방식으로 koin을 사용할 수 있다!

🤔 음..좋은데 이렇게까지 해야해?

그런데 사실 단순히 토이,사이드 프로젝트? 정도 수준에서 이러한 의존성 역전을 사용할 일이 있을까? 라는 생각이 들었다.

깊게 한 번 생각해보자!
애초에 의존성을 주입한다는 것 자체가 어느 정도 클린한 구조로 앱을 만들려고 하고 있다는거다! 왜냐하면 의존성 주입(DI)이 필요한 상황이라면 현재 한 클래스 내에 너무 많은 기능(책임)을 부여하지 않으려고 노력중이라는 의미이다!

이쯤 생각하면 SRP(Single Responsibility Principle)이 떠오른다!
한 객체가 한 책임만을 담당해야한다! 우리가 좋은 앱 구조를 만들기 위해서는 결국 이 SOLID를 준수해야하고 그 안에 의존성을 쉽게 컨트롤 할 수 있는 DI를 사용하는 것이 좋다!


아키텍처

🔍 clean architecture

계층

클린 아키텍처는 소프트웨어 시스템을 여러 가지 서로 다른 레이어로 나누어 구조화 시킨다. 이러한 레이어들은 서로 다른 수준의 추상화를 가지며, 의존성이 바깥쪽에서 안쪽으로 향하도록 설계된다.

..무슨 말인가 바로 와닿지 않아서 설명을 읽으며 그림을 그려봄!
그림을 보면 외부에서 안쪽으로 의존성이 향하도록 설계된다는 게 무슨 의미인지 알 수 있다!

  • External Interfaces (외부 인터페이스)
    데이터베이스나 외부 데이터 소스와의 상호 작용을 담당하는 부분이다.
    데이터를 가져오고 저장하는 작업을 수행한다. 일반적으로 데이터 소스에 접근하는 인터페이스와 해당 인터페이스를 구현하는 클래스를 포함!
    Repository패턴을 사용하여 외부 인터페이스를 추상화하고 Usecase에서 이를 사용한다. Usecase는 Repository를 통해 데이터를 얻어오고 저장한다.

  • Presenters (프레젠터)
    UI와 비즈니스 로직 간의 중간 계층으로, UI에서 발생한 이벤트를 처리하고 필요한 데이터를 비즈니스 로직에 전달한다.
    뷰모델은 UI와 비즈니스 로직 간의 중간 계층을 담당하며, 데이터를 UI에 표시하기 위해 가공하거나 UI에서 발생한 이벤트를 처리합니다.

  • Use Case (유스케이스)
    애플리케이션의 실제 비즈니스 로직을 포함하는 부분이다.
    프레젠터나 뷰모델로부터 전달된 요청을 처리하고 데이터를 가공하여 반환한다. Usecase는 Entity 협력하여 비즈니스 규칙을 구현한다.

  • Entity (엔티티)
    애플리케이션의 핵심 데이터 구조를 나타내는 부분이다.
    데이터베이스나 외부 데이터 소스에서 가져온 데이터를 객체로 변환한 형태이다. 엔티티는 일반적으로 도메인 모델을 나타내게 된다.

이러한 식으로 레이어가 존재하는데 이게 정답은 아니다!
클린 아키텍처에서는 레이어를 나누는 것은 권장하나 "클린 아키텍처라면 이 레이어를 반드시 써라!" 이런 건 없는 것 같다.

해당 프로젝트에 맞추어서 자신들이 필요한 레이어를 구성하고 레이어의 역할을 명확히 한다면 문제 없는 듯!

카운터 앱을 이용한 클린 아키텍처 예시

  • 프레젠트 레이어
    사용자 인터페이스와 사용자 액션을 처리한다!
    사용자 인터페이스: 화면에 숫자를 표시하고, 버튼을 통해 카운터를 증가시키거나 감소시킵니다.
class MainActivity : AppCompatActivity() {

    private var counter = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        updateCounterText()

        btnIncrement.setOnClickListener {
            counter++
            updateCounterText()
        }

        btnDecrement.setOnClickListener {
            counter--
            updateCounterText()
        }
    }

    private fun updateCounterText() {
        txtCounter.text = counter.toString()
    }
}
  • 도메인 레이어
    비즈니스 로직을 처리하는 부분
    현재 카운트 값을 유지하고, 증가 또는 감소하는 등의 동작
class CounterUseCase {

    private var counter = 0

    fun getCurrentCount(): Int {
        return counter
    }

    fun incrementCount() {
        counter++
    }

    fun decrementCount() {
        counter--
    }
}
  • 데이터 레이어
    현재 카운트 값을 저장하거나 로드하는 등의 데이터 관리를 담당
object CounterRepository {

    private var counter = 0

    fun getCurrentCount(): Int {
        return counter
    }
}

0개의 댓글