* 진행중인 사이드 프로젝트를 인수인계 받기 전에 구현되어 있던 기능을 개선하다 생긴 사례에 대한 내용입니다.
현재 개발중인 이 앱에는 러너들을 위한 "유사 채팅 기능"이 존재한다.
소켓 통신을 통한 실시간 채팅 기능은 아니며, 메시지 전송 시마다 리스트를 갱신하는 방식으로 구현되어 있었다.
✔️ 기능 개요
- 최대 8명까지 참여가 가능하다.
- 메시지의 진영은 (내가 보낸), (나머지 사람들이 보낸)으로 나누어진다.(카카오톡처럼)
- 메시지는 텍스트, 이미지를 전송할 수 있다.
✔️ 메시지 진영 구분
- 클래스 간의 계층 구조를 만들고, ListAdapter에서 뷰홀더에 bind할 때 sealed class를
- (내가 보낸) 메시지와 (나머지 사람들이 보낸) 메시지로 나눈 뒤
- (텍스트)와 (메시지)를 나누는 방식으로 구현되어 있었다.
뷰홀더는 (내가 보낸 메시지 컨테이너)와 (나머지 사람들이 보낸) 컨테이너로 나뉘며,
그 안에서 (텍스트 메시지)와 (이미지 메시지)가 순차적으로 bind된다.
sealed class RunningTalkUiState {
data class MyRunningTalkUiState(
val items: List<RunningTalkItem>,
val createTime: String,
val isPostWriter: Boolean
): RunningTalkUiState()
data class OtherRunningTalkUiState(
val writerName: String,
val writerProfileImgUrl: String?,
val items: List<RunningTalkItem>,
val createTime: String,
val isPostWriter: Boolean,
var isChecked: Boolean = false,
val isReportMode: Boolean
): RunningTalkUiState()
}
✔️ 기존 로직
코틀린이라기보다는 자바에 가까운 명령형 코드이다.
중첩된 반복문의 사용으로 가독성이 낮고, 가변 컬렉션을 사용하여 상태 관리가 까다롭다.
🎈 그룹화 로직 문제
전체 톡 내용을 조회할 때 문제가 발생했다.
내부의 while 문에서 메시지를 (같은 시간에 보내진)이라는 조건만으로 메시지를 그룹화하고 있어 문제가 발생한다.
- 메시지의 발신자와 상관 없이 (내가 보낸/상대방이 보낸)
- 같은 시간에 보내진 메시지라면 그룹화된다
-> 내가 보낸 메시지가 상대방 진영에서 보이거나, 그 반대의 경우가 발생한다
private val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.KOREA)
fun messagesToRunningTalkUiState(messages: List<Messages>): List<RunningTalkUiState> {
val result: ArrayList<RunningTalkUiState> = arrayListOf()
var index = 0
while (index < messages.size) {
val target = messages[index]
val items: ArrayList<RunningTalkItem> = arrayListOf()
messageToRunningTalkItem(target)?.let { addedItem ->
items.add(addedItem)
}
val targetDate = dateStringToString(target.createAt, formatter)
index++
if (items.isNotEmpty()) {
while (index < messages.size) {
val item = messages[index]
val itemDate = dateStringToString(item.createAt, formatter)
if(targetDate == itemDate) { <<< WRONG
messageToRunningTalkItem(item)?.let {
items.add(it)
}
index++
} else break
}
result.add(
if(target.from.lowercase() == "me") RunningTalkUiState.MyRunningTalkUiState(
createTime = timeHourAndMinute(target.createAt),
isPostWriter = target.whetherPostUser.uppercase() == "Y",
items = items
) else RunningTalkUiState.OtherRunningTalkUiState(
createTime = timeHourAndMinute(target.createAt),
isPostWriter = target.whetherPostUser.uppercase() == "Y",
isReportMode = false,
writerName = target.nickName,
writerProfileImgUrl = target.profileImageUrl,
items = items
))
}
}
return result
}
✔️ 개선된 로직
- groupBy 함수를 사용해서 Pair(발신 시간, 발신자)를 key로 메시지들을 그룹화한다
- mapNotNull 함수를 사용해서 그룹화된 메시지들을 (내가 보낸)/(상대방이 보낸) 객체로 매핑한다
키워드: 가변성 제한
다만 데이터 계층의 모델을 프레젠테이션 계층의 모델로 매핑할 때 매퍼에서 from이라는 문자열 값을 enum 객체로 변환해주는 로직이 없어,
when문이 exhaustive하지 않다는 점은 아쉬움으로 남는다.
추후 유지보수 단계에서 일괄적으로 모든 매직 넘버/매직 리터럴을 변환하는 과정을 추가할 예정이다.
private val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.KOREA)
fun parseMessagesToRunningTalkUiState(messages: List<Messages>): List<RunningTalkUiState> {
val groupedMessages = messages.groupBy {
// ex) Pair("2024-11-18 14:00", "me")
Pair(dateStringToString(it.createAt, formatter), it.from.lowercase())
}
return groupedMessages.mapNotNull { (key, groupedMessages) ->
val targetFrom = key.second
val items = groupedMessages.mapNotNull { messageToRunningTalkItem(it) }
if (items.isEmpty()) return@mapNotNull null
val target = groupedMessages.first()
when (targetFrom) {
MessageFrom.ME.key -> RunningTalkUiState.MyRunningTalkUiState(
createTime = timeHourAndMinute(target.createAt),
isPostWriter = target.whetherPostUser.uppercase() == "Y",
items = items
)
MessageFrom.OTHER.key -> RunningTalkUiState.OtherRunningTalkUiState(
createTime = timeHourAndMinute(target.createAt),
isPostWriter = target.whetherPostUser.uppercase() == "Y",
isReportMode = false,
writerName = target.nickName,
writerProfileImgUrl = target.profileImageUrl,
items = items
)
else -> {
LogUtil.errorLog("Unexpected from value: $targetFrom")
null
}
}
}
}
추가적으로 개선이 가능한 부분은, 발신자 여부 데이터인 me/other 문자열 처리 정도이다.
response 객체를 매핑할 때 문자열 값을 enum 클래스로 변환하여 when문을 exhaustive하게 만들어야 한다.
기존 개발자분께 인수인계를 받고 스프린트를 바로 진행하다 보니 위와 같은 개선점이 많은데,
스프린트3까지 종료된 이후에 일괄적으로 개선 작업을 진행해야겠다.
안드로이드 진영이 코틀린을 메인으로 사용한다는 것은 정말 매력적이며, 서버 개발자들 사이에서도 코프링이 떠오르고 있는게 어찌 보면 당연한 수순이라 생각된다.
나도 나중에 친구들이랑 프로젝트하면 코프링을 해보고 싶기도 하고..
요즘 운동하랴 사이드 프로젝트 팀원들과 바쁘게 작업하랴 포스팅을 작성할 시간이 없었는데, 트러블 슈팅 과정만이라도 꾸준히 적으려 노력은 해봐야겠다!
'개발 > 트러블슈팅' 카테고리의 다른 글
[안드로이드] 릴리즈 빌드 시 난독화 설정 문제(카카오, 네이버 소셜 로그인) (1) | 2024.11.18 |
---|---|
[안드로이드] 안드로이드 스튜디오 PluginException (0) | 2023.11.10 |
[안드로이드] Jetpack compose dependency 설정 (0) | 2023.07.11 |
[안드로이드] FragmentContainerView NullPointerException 해결 (0) | 2023.05.27 |
[안드로이드] is not abstract and does not implement abstract member public abstract fun getActionId() 오류 (0) | 2023.04.17 |