본문 바로가기

Android/랜덤리즘

[랜덤리즘] 로그인 구현 대작전 (수정) -3

지난번 포스팅

 

[랜덤리즘] 로그인 구현 대작전(구현) -2

일단 로그인을 구현하기 위해서는 아래의 2가지 상황이 있기 때문에 온보딩 페이지가 필요하다고 생각이 들었다. 1. 로그인을 하는 경우 2. 로그인을 하지 않는 경우 -> 둘러보기 로그인을 하지

w36495.tistory.com

 

로그인을 구현하는 포스팅 2편의 내용이 잘못되었음을 .. 알게되었다.

잘못되었다고 생각한 부분

LoginFragment 에서 HomeFragment 로 넘어올 때 UserRepository 에 데이터를 캐싱해보자! 해서 UserRepository 의 LiveData 에 데이터를 넣어주었었다.

HomeFragment 에서 해당 LiveData 를 관찰자로 설정해주면, 그 값을 사용할 수 있지 않을까? 싶은 마음이었다.

그런데, HomeFragment 가 Attach 되기도 전에 LiveData 로 값이 전송되었다면? (관찰자를 등록하기 전에)

HomeFragment 에서 관찰자를 등록한다해도 이 전에 값이 전송되었으니 관찰할 값이 없다 .. 그 이후에는 변경되지 않기때문에 ..

 

User 의 값을 관찰하다가 값이 변경되면 사용자가 풀었던 문제리스트를 가져오도록 작성했었다.

그런데 User 의 값을 관찰할 수 없으니 사용자가 풀었던 문제리스트도 가져올 수 없다.. .. 넘 당연한 것 ..

이 부분을 간과했던 것 같다.

 

 

그리고 그 외에도 기존 기능(로그인 상관없이 문제를 서버로부터 가져옴)에 새로운 기능(사용자가 푼 문제 목록)을 추가하려니 코드가 꼬여버리는 문제가 발생했다.

 

이번에는 그 꼬여버린 매듭을 풀기 위해서 객사오에서 배웠던 내용을 적용해보았다! 

 


마주한 문제

기존 기능(로그인 상관없이 문제를 서버로부터 가져옴)에 새로운 기능(사용자가 푼 문제 목록)을 추가하려니 코드가 꼬여버렸다.. ...

로그인 여부를 어떻게 알아낼지? 사용자가 이전에 풀었던 문제 목록을 어떻게 가져올지? 에 대한 코드를 작성하려니 어디서부터 손을 대야할지 막막했다 ..

 

유지보수가 어려운 코드를 작성했구나 싶어서 문제를 가져오는 부분의 코드를 다시 설계해보려고 했다.

 


객체지향적으로 설계하기

최근에 객체지향의 사실과 오해를 공부하다보니 내가 만들고 있는 프로그램에도 적용해보고 싶은 마음이 들었다.

그래서 객사오에서 집중하는 '협력' 그리고 '메시지'를 중심에 두고 설계해보았다!

1️⃣ 애플리케이션이 수행할 기능 -> 시스템의 책임

랜덤리즘에서 애플리케이션이 중점적으로 수행할 기능 : 사용자에게 랜덤으로 1문제를 보여주는 것 -> 시스템의 책임

 

2️⃣ 시스템의 책임을 구현하기 위해 협력 관계를 시작할 적절한 객체 찾기 (시스템의 책임 -> 객체의 책임)

가장 먼저 사용자로부터 요청을 받으면 ProblemViewModel 이 요청에 대한 응답을 받아 UI 를 업데이트해야하므로 ProblemViewModel 이 적절한 시작 객체라고 생각하였다.

3️⃣ 객체의 책임을 완수하기 위해 다른 객체의 도움이 필요하다면, 메시지를 결정하기

사용자가 '문자열' 알고리즘을 클릭했을 때 -> '문자열' 알고리즘에 해당되는 1개의 문제가 화면에 보여야 한다.

 

 

그렇기 때문에 적절한 메시지는 '문제를 보여줘' 라고 설정하였다.

4️⃣ 메시지를 수신하기에 적합한 객체 찾기

레이어드 아키텍처를 사용하고 있었으므로, Server 로 요청하고 응답을 받기 위한 객체는 UseCase 와 Repository 이다.

로그인 여부에 따라 2가지의 경우로 작성하였다.

 

 

 

 

이렇게 설계한 대로 코드로 작성하였고, 수정 전과 후를 클래스 다이어그램을 통해서도 비교해보았다.

 


수정 전의 클래스 다이어그램

 

수정 후의 클래스 다이어그램

ViewModel 과 UseCase 의 클래스 다이어그램

UseCase 와 Repository 의 클래스 다이어그램

 

전/후를 비교해보며 느낀 점

수정 전의 VIewModel 을 제외한 UseCase 와 Repository 는 따로 가공하지 않고 데이터를 전달만 하는 역할만 하고있었다.

수정 후의 ViewModel 은 문제를 가져오는 일에 집중을 하고, UseCase 에서 데이터를 가공하는 등 지난 뷰모델에서 했던 일들을 수행하고 있다!

 

가령 사용자가 풀지 않은 문제를 보여준다고 한다면,

GetSolvableProblemsUseCase() 안에서는 가장 먼저 UserRepository 를 통해 사용자의 정보를 가져온 후, GetSolvedProblemsUseCase() 를 통해 사용자가 풀었던 문제의 리스트를 가져온다.

그 이후에 GetProblemsUseCase() 를 통해 사용자가 선택한 타입의 문제들을 가져왔다면, isSolvableProblem() 을 통해 2개의 문제리스트를 비교하여 풀지 않은 문제들만 반환하도록 해주었다!

 


문제 타입(ProblemType) 리팩토링

그리고 나에게 골칫덩어리였던 문제 타입(ProblemType)을 판별하는 부분을 수정하고 싶었다.

 

data class ProblemType(
    val tag: String? = null,
    val level: Int? = null,
    val source: String? = null
)

 

수정 전의 ProblemType 은 위와 같이 사용자가 선택한 타입을 저장하고, 이에 맞추어 판별하도록 했었다.

 

private fun judgeCurrentProblemType() {
    _problemType.value?.run {
        tag?.let { tag ->
            level?.let { level ->
                // 알고리즘만 선택했을 때
                if (level == ALL_LEVEL) getProblemsByTag(tag)
                // 특정 알고리즘 + 특정 레벨을 선택했을 때
                else getProblemByTagAndLevel(tag, level)
                // 알고리즘만 선택했을 때
            } ?: getProblemsByTag(tag)
        } ?: level?.let { level ->
            // 레벨(클래스)만 선택했을 때
            getProblemsByLevel(level)
        }
        // 문제 출처를 선택했을 때
        source?.let { source -> getProblemsBySourceOfProblem(source) }
    }
}

 

그런데 너무 코드도 한 눈에 파악이 되지 않고, 새로운 ProblemType 을 추가한다면 너무 막막해서 좋지 않은 코드라고 느끼게 되었다.

경우의 수가 많아지는 셈이니까 ..

 

사실 이때 당시에도 많이 막막했었는데 ProblemType 으로 데이터 클래스를 만들자! 하며 신나게 코드를 작성했던 기억이 난다..!

그래서 나중에 꼭 수정해주어야겠다고 생각했는데, 바로 오늘이 되었다!

 

수정된 코드는 아래와 같다.

 

sealed class ProblemType: java.io.Serializable

data class TagType(
    val tag: String,
) : ProblemType()


data class LevelType(
    val level: Int,
) : ProblemType()


data class SourceType(
    val source: String,
) : ProblemType()


data class TagAndLevelType(
    val tag: String,
    val level: Int,
) : ProblemType()

 

sealed class 를 사용해주었다.

sealed class 를 사용한 이유는 각 타입들을 처리할 때, when 을 통해 타입을 제한할 수 있기 때문이다.

 

val result = when (problemType) {
    is TagType -> getProblemsByTag(problemType.tag)
    is LevelType -> getProblemsByLevel(problemType.level)
    is TagAndLevelType -> getProblemsByTagAndLevel(problemType.tag, problemType.level)
    is SourceType -> getProblemsBySource(problemType.source)
}

 

sealed class 는 컴파일러가 자식 클래스들을 알고 있기 때문에 else 를 따로 작성해주지 않는다!

새로운 문제의 타입이 생긴다면 data class 를 정의해주고, 위의 코드에서 is 만 작성해주면 된다 !!

이전에 사용했던 방법보다 유지보수측면에서 좋아진 것같아 너무 좋았다 !!

 


가장 중심이 되는 기능을 수정할 수 있어서 너무 좋았다.

그리고 공부했던 내용들을 적용할 수 있어서도 좋았다.

 

 

기존에는 코드를 작성하며 이곳 저곳 왔다갔다했는데, 메시지를 작성하고 순서를 정해주니 그대로 따라 코드를 치기만하면 되어서 좋았다 ...

뭐지 .. 딱 집중할 수 있어서 좋았다 ..

 

ProblemType 데이터 클래스를 작성했을 때만해도 좋은데?!!??! 하며 작성했었는데, 좋지 않은 방법임을 알게되었으니 성장한 것 같기도 ..

나머지 부분도, UserRepository 도 수정해주어야 하는데, 이번에 설계한 방법대로 작성해보면 좋을 것 같다!