Upgrade to Pro — share decks privately, control downloads, hide ads and more …

어려운 상태관리 쉽게 관리하기!

어려운 상태관리 쉽게 관리하기!

- DroidKnights 2020.

omjoonkim

April 01, 2021
Tweet

More Decks by omjoonkim

Other Decks in Programming

Transcript

  1. Speaker l 부제 : 유지보수성을 높이는 개발 방법 ӣߧળ @omjoonkim

    w Coupang 어려운 상태관리! 쉽게 관리하기.
  2. Situation - 함수 하나가 몇백 라인을 넘긴다. - 함수 내부에서

    다양한 멤버 or 전역변수들을 사용하고 있다. - 한 화면이 담당하고 있는 기능, 위젯이 많다.
  3. - 함수 하나가 몇백 라인을 넘긴다. - 함수 내부에서 다양한

    멤버 or 전역변수들을 사용하고 있다. - 한 화면이 담당하고 있는 기능, 위젯이 많다. Situation Is it Easy to improve an existing Feature or add a New Feature?
  4. - 함수 하나가 몇백 라인을 넘긴다. - 함수 내부에서 다양한

    멤버 or 전역변수들을 사용하고 있다. - 한 화면이 담당하고 있는 기능, 위젯이 많다. Situation Is it good Maintainability?
  5. Pain point - 함수 하나가 몇백 라인을 넘긴다. - 함수

    내부에서 다양한 멤버 or 전역변수들을 사용하고 있다. - 한 화면이 담당하고 있는 기능, 위젯이 많다. - 하나의 함수에 너무 많은 역할이 존재한다. - 멤버 or 전역변수를 사용할 수록 Side Effect 가 발생할 확률이 높다. - 존재하는 기능, 위젯만큼 복잡도가 높아진다. 👇
  6. - 함수 하나가 몇백 라인을 넘긴다. - 함수 내부에서 다양한

    멤버 or 전역변수들을 사용하고 있다. - 한 화면이 담당하고 있는 기능, 위젯이 많다. - 하나의 함수에 너무 많은 역할이 존재한다. - 멤버 or 전역변수를 사용할 수록 Side Effect 가 발생할 확률이 높다. - 존재하는 기능, 위젯만큼 복잡도가 높아진다. Pain point 👇 High Complexity
  7. - 함수 하나가 몇백 라인을 넘긴다. - 함수 내부에서 다양한

    멤버 or 전역변수들을 사용하고 있다. - 한 화면이 담당하고 있는 기능, 위젯이 많다. - 하나의 함수에 너무 많은 역할이 존재한다. - 멤버 or 전역변수를 사용할 수록 Side Effect 가 발생할 확률이 높다. - 존재하는 기능, 위젯만큼 복잡도가 높아진다. Pain point 👇 High Complexity Row Maintainability =
  8. - 함수 하나가 몇백 라인을 넘긴다. - 함수 내부에서 다양한

    멤버 or 전역변수들을 사용하고 있다. - 한 화면이 담당하고 있는 기능, 위젯이 많다. - 하나의 함수에 너무 많은 역할이 존재한다. - 멤버 or 전역변수를 사용할 수록 Side Effect 가 발생할 확률이 높다. - 존재하는 기능, 위젯만큼 복잡도가 높아진다. Pain point 👇 Complexity = State
  9. - 함수 하나가 몇백 라인을 넘긴다. - 함수 내부에서 다양한

    멤버 or 전역변수들을 사용하고 있다. - 한 화면이 담당하고 있는 기능, 위젯이 많다. - 하나의 함수에 너무 많은 역할이 존재한다. - 멤버 or 전역변수를 사용할 수록 Side Effect 가 발생할 확률이 높다. - 존재하는 기능, 위젯만큼 복잡도가 높아진다. Pain point 👇 Complexity Management = State Management Maintainability ↕
  10. Solution - 함수의 역할을 잘게 나눈다. - Side Effect를 발생시키지

    않도록 개발한다. - 기능, 위젯별로 책임을 명확하게 나누고 의존성을 제거한다. - 하나의 함수에 너무 많은 역할이 존재한다. - 멤버 or 전역변수를 사용할 수록 Side Effect가 많아진다. - 존재하는 기능, 위젯만큼 복잡도가 높아진다. 👇
  11. Solution - 함수의 역할을 잘게 나눈다. - 함수를 분해, 합성이

    쉽게 개발한다. - Side Effect를 발생시키지 않도록 개발한다. - Immutable, 참조 투명하게 개발한다. - 기능, 위젯별로 책임을 명확하게 나누고 의존성을 제거한다 - 좋은 컨벤션과 아키텍쳐 설계.
  12. Practice - Android Architecture Blueprints - todo-mvp-kotlin 👇 - dev-rx

    https://github.com/android/architecture-samples https://github.com/omjoonkim/android-architecture
  13. UI

  14. Code - start() override fun start() { if (taskId !=

    null && isDataLoading) { populateTask() } }
  15. Code - side effect, referencial transparaency override fun start() {

    if (taskId != null && isDataLoading) { populateTask() } } private val taskId: String? override var isDataLoading: Boolean
  16. Code - side effect, referencial transparaency override fun start() {

    if (taskId != null && isDataLoading) { populateTask() } } private val taskId: String? override var isDataLoading: Boolean taskId는 immutable하기 때문에 Side Effect를 발생시키지 않는다. 하지만 가독성을 해칠 수 있다.
  17. Code - side effect, referencial transparaency override fun start() {

    if (taskId != null && isDataLoading) { populateTask() } } private val taskId: String? override var isDataLoading: Boolean taskId는 immutable하기 때문에 Side Effect를 발생시키지 않는다. 하지만 가독성을 해칠 수 있다. isDataLoading은 함수 스코프 밖에서 변경될 수 있음으로 Side Effect를 만든다.
  18. Code - side effect, referencial transparaency override fun start() {

    if (taskId != null && isDataLoading) { populateTask() } } private val taskId: String? override var isDataLoading: Boolean taskId는 immutable하기 때문에 Side Effect를 발생시키지 않는다. 하지만 가독성을 해칠 수 있다. isDataLoading은 함수 스코프 밖에서 변경될 수 있음으로 Side Effect를 만든다. 그럼으로 start()는 참조투명하지 않다.
  19. Code - function composition, decomposing override fun populateTask() { if

    (taskId == null) { throw RuntimeException("populateTask() was called but task is new.") } isDataLoading = true tasksRepository.getTask(taskId, this) }
  20. Code - function composition, decomposing override fun populateTask() { if

    (taskId == null) { throw RuntimeException("populateTask() was called but task is new.") } isDataLoading = true tasksRepository.getTask(taskId, this) } RuntimeException을 throw 한다
  21. Code - function composition, decomposing override fun populateTask() { if

    (taskId == null) { throw RuntimeException("populateTask() was called but task is new.") } isDataLoading = true tasksRepository.getTask(taskId, this) } RuntimeException을 throw 한다 isDataLoading 을 true 로 변경한다.
  22. Code - function composition, decomposing override fun populateTask() { if

    (taskId == null) { throw RuntimeException("populateTask() was called but task is new.") } isDataLoading = true tasksRepository.getTask(taskId, this) } RuntimeException을 throw 한다 Repository를 통해 Task를 가져온다. isDataLoading 을 true 로 변경한다.
  23. Code - function composition, decomposing override fun onTaskLoaded(task: Task) {

    if (addTaskView.isActive) { addTaskView.setTitle(task.title) addTaskView.setDescription(task.description) } isDataLoading = false }
  24. Code - function composition, decomposing override fun onTaskLoaded(task: Task) {

    if (addTaskView.isActive) { addTaskView.setTitle(task.title) addTaskView.setDescription(task.description) } isDataLoading = false } addTaskView.isActive 일 때 View를 업데이트 한다.
  25. Code - function composition, decomposing override fun onTaskLoaded(task: Task) {

    if (addTaskView.isActive) { addTaskView.setTitle(task.title) addTaskView.setDescription(task.description) } isDataLoading = false } addTaskView.isActive 일 때 View를 업데이트 한다. isDataLoading 을 false 로 변경한다.
  26. if add new features Old Function Function 1 Function 2

    Function 3 New Function Decomposing
  27. function composition, decomposing - 어떤 방식을 택할지 고민해야 한다. 🤯

    - 처음부터 미래를 걱정하고 코드를 작성하는 것은 어렵다. - 그렇기 때문에 추후에도 분해와 합성을 쉽게 만드는 것이 더욱 효율적이다.
  28. Code - start() override fun start() { if (taskId !=

    null && isDataLoading) { populateTask() } }
  29. Code - start() override fun start() { if (taskId !=

    null && isDataLoading) { populateTask() } } val taskIdIsExist = taskId.filter { it.isNotEmpty() } val isEditTask = Observables.combineLatest( dataLoading, taskIdIsExist ).filter { (isDataLoading, _) -> isDataLoading.not() } .distinctUntilChanged() .map { (_, taskId) -> taskId } .share()
  30. Code - start() override fun start() { if (taskId !=

    null && isDataLoading) { populateTask() } } val taskIdIsExist = taskId.filter { it.isNotEmpty() } val isEditTask = Observables.combineLatest( dataLoading, taskIdIsExist ).filter { (isDataLoading, _) -> isDataLoading.not() } .distinctUntilChanged() .map { (_, taskId) -> taskId } .share() private val taskId = PublishSubject.create<String>() private val dataLoading = BehaviorSubject.createDefault(false)
  31. Code - start() override fun start() { if (taskId !=

    null && isDataLoading) { populateTask() } } val taskIdIsExist = taskId.filter { it.isNotEmpty() } val isEditTask = Observables.combineLatest( dataLoading, taskIdIsExist ).filter { (isDataLoading, _) -> isDataLoading.not() } .distinctUntilChanged() .map { (_, taskId) -> taskId } .share() private val taskId = PublishSubject.create<String>() private val dataLoading = BehaviorSubject.createDefault(false) fun start(taskId: String?) { this.taskId.onNext(taskId ?: "") }
  32. Code - start() override fun start() { if (taskId !=

    null && isDataLoading) { populateTask() } } val taskIdIsExist = taskId.filter { it.isNotEmpty() } val isEditTask = Observables.combineLatest( dataLoading, taskIdIsExist ).filter { (isDataLoading, _) -> isDataLoading.not() } .distinctUntilChanged() .map { (_, taskId) -> taskId } .share() taskId 값이 빈값이 아닐 때 흐르는 스트림을 생성.
  33. Code - start() override fun start() { if (taskId !=

    null && isDataLoading) { populateTask() } } val taskIdIsExist = taskId.filter { it.isNotEmpty() } val isEditTask = Observables.combineLatest( dataLoading, taskIdIsExist ).filter { (isDataLoading, _) -> isDataLoading.not() } .distinctUntilChanged() .map { (_, taskId) -> taskId } .share() taskId 값이 빈값이 아닐 때 흐르는 스트림을 생성. 위의 스트림과 dataLoading 을 조합하고 filter Operator를 통해 if (taskId != null && isDataLoading) 와 같게 동작하는 스트림을 생성
  34. Code - start() override fun start() { if (taskId !=

    null && isDataLoading) { populateTask() } } val taskIdIsExist = taskId.filter { it.isNotEmpty() } val isEditTask = Observables.combineLatest( dataLoading, taskIdIsExist ).filter { (isDataLoading, _) -> isDataLoading.not() } .distinctUntilChanged() .map { (_, taskId) -> taskId } .share() taskId 값이 빈값이 아닐 때 흐르는 스트림을 생성. 위의 스트림과 dataLoading 을 조합하고 filter Operator를 통해 if (taskId != null && isDataLoading) 와 같게 동작하는 스트림을 생성 모든 스트림은 서로 합성 될 수 있으며 또한 분해 될 수 있다.
  35. Code - populateTask() override fun populateTask() { if (taskId ==

    null) { throw RuntimeException(“populateTask()….”) } isDataLoading = true tasksRepository.getTask(taskId, this) }
  36. Code - populateTask() override fun populateTask() { if (taskId ==

    null) { throw RuntimeException(“populateTask()….”) } isDataLoading = true tasksRepository.getTask(taskId, this) } isEditTask는 이미 앞에서 taskId == null 에 대한 검사를 완료한 스트림이다. isEditTask
  37. Code - populateTask() override fun populateTask() { if (taskId ==

    null) { throw RuntimeException(“populateTask()….”) } isDataLoading = true tasksRepository.getTask(taskId, this) } isEditTask는 이미 앞에서 taskId == null 에 대한 검사를 완료한 스트림이다. isEditTask.map { true } .subscribe(dataLoading::onNext, ::printStackTrace) isEditTask를 true 로 매핑한 뒤 dataLoading을 변경하는 스트림. override fun populateTask() { if (taskId == null) { throw RuntimeException(“populateTask()….”) } isDataLoading = true tasksRepository.getTask(taskId, this) }
  38. Code - populateTask() isEditTask는 이미 앞에서 taskId == null 에

    대한 검사를 완료한 스트림이다. isEditTask.map { true } .subscribe(dataLoading::onNext, ::printStackTrace) val getTask = onGetTask.flatMapSingle(::getTask) .share() isEditTask를 true 로 매핑한 뒤 dataLoading을 변경하는 스트림. Task를 불러오고 캐싱한 스트림. override fun populateTask() { if (taskId == null) { throw RuntimeException(“populateTask()….”) } isDataLoading = true tasksRepository.getTask(taskId, this) }
  39. Code - onTaskLoaded override fun onTaskLoaded(task: Task) { if (addTaskView.isActive)

    { addTaskView.setTitle(task.title) addTaskView.setDescription(task.description) } isDataLoading = false } override fun onTaskLoaded(task: Task) { if (addTaskView.isActive) { addTaskView.setTitle(task.title) addTaskView.setDescription(task.description) } isDataLoading = false }
  40. Code - onTaskLoaded override fun onTaskLoaded(task: Task) { if (addTaskView.isActive)

    { addTaskView.setTitle(task.title) addTaskView.setDescription(task.description) } isDataLoading = false } val task = getTask.ofType(Success::class.java) .map { result -> result.data as Task } addTaskView.isActive 대신 Success 조건이 추가 됨
  41. Code - onTaskLoaded override fun onTaskLoaded(task: Task) { if (addTaskView.isActive)

    { addTaskView.setTitle(task.title) addTaskView.setDescription(task.description) } isDataLoading = false } val task = getTask.ofType(Success::class.java) .map { result -> result.data as Task } task.subscribe(::updateView, ::printStackTrace) addTaskView.isActive 대신 Success 조건이 추가 됨 Success일 경우 Task로 매핑하고 updateView를 호출하여 View를 갱신한다.
  42. Code - onTaskLoaded override fun onTaskLoaded(task: Task) { if (addTaskView.isActive)

    { addTaskView.setTitle(task.title) addTaskView.setDescription(task.description) } isDataLoading = false } val task = getTask.ofType(Success::class.java) .map { result -> result.data as Task } task.subscribe(::updateView, ::printStackTrace) getTask.map { false } .subscribe(dataLoading::onNext, ::printStackTrace) addTaskView.isActive 대신 Success 조건이 추가 됨 Success일 경우 Task로 매핑하고 updateView를 호출하여 View를 갱신한다. Success, Fail 상관없이 Task 로드가 완료 되었을 때 dataLoading을 변경한다.
  43. if add new features val isEditTask = Observables.combineLatest( dataLoading, taskIdIsExist

    ).filter { (isDataLoading, _) -> isDataLoading.not() } .distinctUntilChanged() .map { (_, taskId) -> taskId } .share()
  44. if add new features val taskIdAndDataLoading = Observables.combineLatest( dataLoading, taskIdIsExist

    ) val isEditTask = taskIdAndDataLoading .filter { (isDataLoading, _) -> isDataLoading.not() } .distinctUntilChanged() .map { (_, taskId) -> taskId } .share()
  45. if add new features val taskIdAndDataLoading = Observables.combineLatest( dataLoading, taskIdIsExist

    ) val isEditTask = taskIdAndDataLoading .filter { (isDataLoading, _) -> isDataLoading.not() } .distinctUntilChanged() .map { (_, taskId) -> taskId } .share() taskIdAndDataLoading.something
  46. Refactoring with Rx ✨ - 함수 = Stream. - Stream은

    분해와 합성에 용이하다. - 중복코드를 최소화 할 수 있다. - 다른 코드에 영향을 덜 주면서 개선, 새로운 기능을 추가 할 수 있다. - Side Effect를 최소화하여 개발이 가능하다. - 모든 상태를 Stream으로 관리 할 수 있다. - operator function은 참조 투명하다. - Rx의 강력한 Operator 들로 인해 가독성이 좋다.
  47. Refactoring with Rx ✨ - 함수 = Stream. - Stream은

    분해와 합성에 용이하다. - 중복코드를 최소화 할 수 있다. - 다른 코드에 영향을 덜 주면서 개선, 새로운 기능을 추가 할 수 있다. - Side Effect를 최소화하여 개발이 가능하다. - 모든 상태를 Stream으로 관리 할 수 있다. - operator function은 참조 투명하다. - Rx의 강력한 Operator 들로 인해 가독성이 좋다. 유지보수성이 높은 코드를 작성할 수 있다.
  48. Concern 😇 - 잘 써야지 가독성이 좋다. - 높은 학습

    비용이 존재한다. - 충분히 익숙해지기 전 작성하는 코드는 오히려 가독성이 훨씬 떨어진다. - Stream은 참조 투명하지 않다. - 생명주기(Disposable), 쓰레드 관리 등. - 언어의 특성상 Immutable을 잘 관리해야 한다. - 복잡한 상황에서 발생하는 여러 예외 케이스에 대하여 컨벤션이 잘 만들어져있어야 한다.
  49. Concern 😇 - 잘 써야지 가독성이 좋다. - 높은 학습

    비용이 존재한다. - 충분히 익숙해지기 전 작성하는 코드는 오히려 가독성이 훨씬 떨어진다. - Stream은 참조 투명하지 않다. - 생명주기(Disposable), 쓰레드 관리 등. - 언어의 특성상 Immutable을 잘 관리해야 한다. - 복잡한 상황에서 발생하는 여러 예외 케이스에 대하여 컨벤션이 잘 만들어져있어야 한다. 높은 수준을 요구하지만 그 만큼의 가치가 있다.