2026-01-20
1일 1아티클
LY
고르디우스 변수
상황
- 로컬과 원격, 두 저장소에
FooData저장, 이 데이터의 동기화를 위해FooSynchronizer를 구현하는 상황 - 원본인 원격
FooData를 사용해 로컬FooData업데이트 - 원격 및 로컬 데이터는 각각
FooLocalDao와FooRemoteClient에서 가져옴 FooSynchronizer의synchronizeWithRemoteEntries함수 : 로컬과 원격 간FooData ID목록 비교, 그 결과에 따라 CUD 수행- C: ID가 원격에만 존재할 때 실행
- U: ID가 로컬과 원격에 모두 존재할 때 실행
- D: ID가 로컬에만 존재할 때 실행
class FooSynchronizer(
private val fooRemoteClient: FooRemoteClient,
private val fooLocalDao: FooLocalDao
) {
fun synchronizeWithRemoteEntries() {
val remoteEntries: List<FooModel> = fooRemoteClient.fetch()
val remoteEntryIds = remoteEntries.map(FooModel::id).toSet()
val localEntries: List<FooModel> = fooLocalDao.getAllEntries()
val localEntryMap = localEntries.associateBy(FooModel::id)
val localEntryIds = localEntryMap.keys
val createdEntryIds = remoteEntryIds.subtract(localEntryIds)
val deletedEntryIds = localEntryIds.subtract(remoteEntryIds)
remoteEntries.forEach { remoteEntry ->
// 항목 추가
if (remoteEntry.id in createdEntryIds) {
... // `remoteEntry`를 사용해 '추가'하는 로직
return@forEach
}
// 항목 업데이트
val localEntry = localEntryMap[remoteEntry.id]
?: error("This must not happen.")
... // `remoteEntry`와 `localEntry`를 사용해 '업데이트'하는 로직
}
localEntries.asSequence()
.filter { it.id in deletedEntryIds }
.forEach { deletedEntry ->
... // `deletedEntry`를 사용해 '삭제'하는 로직
}
}
}
문제점
- 코드의 흐름을 따라가기 위해, 상당한 노력 필요
- 원인 : 데이터의 의존성이 복잡하게 얽힘
- C 문제 :
createdEntryIds를 구하려면,remoteEntries에서remoteEntryIds를 구하기 →localEntries에서localEntryMap를 구하고 거기서 다시localEntryIds를 구하기 →remoteEntryIds와localEntryIds를 비교해서createdEntryIds를 구하기. 이때,remoteEntries데이터의 반복 사용 발생 및 처리 흐름 복잡도 증가 - U 문제:
localEntryMap에 대한 의존성 때문에 원래라면 발생할 수 없는 런타임 에러 정의 필요 - D 문제: CU 코드와 일관성 불일치
해결 방법
- 이상적인 중간 데이터가 어떤 것인지 상상하고, 거기서부터 함수의 구성을 역으로 설계
- 위 문제에서는, 데이터 CUD를 수행하므로
createdEntries,updatedEntries,deletedEntries세 쌍을 통해 코드 단순화 가능 - 장점 : 파일을 직접 읽어서 CPU 시간 파싱하는 방식은 커널의 무거운 락을 건드리지 않고 데이터 가져오기 가능
개선 코드
fun synchronizeWithRemoteEntries() {
val remoteEntries: List<FooModel> = fooRemoteClient.fetch()
val remoteEntryMap = remoteEntries.associateBy(FooModel::id)
val localEntries: List<FooModel> = fooLocalDao.getAllEntries()
val localEntryMap = localEntries.associateBy(FooModel::id)
val allEntryIds = remoteEntryMap.keys + localEntryMap.keys
val (createdEntries, updatedEntries, deletedEntries) = allEntryIds.asSequence()
.map { id -> remoteEntryMap[id] to localEntryMap[id] }
.partitionByNullity()
createdEntries.forEach { createdEntry ->
... // `createdEntry`를 사용해 '추가'하는 로직
}
updatedEntries.forEach { (remoteEntry, localEntry) ->
... // `remoteEntry`와 `localEntry`를 사용해 '업데이트'하는 로직
}
deletedEntries.forEach { deletedEntry ->
... // `deletedEntry`를 사용해 '삭제'하는 로직
}
}
companion object {
/** ... */
private fun <S : Any, T : Any> Sequence<Pair<S?, T?>>.partitionByNullity():
Triple<List<S>, List<Pair<S, T>>, List<T>> {
val leftEntries: MutableList<S> = mutableListOf()
val bothEntries: MutableList<Pair<S, T>> = mutableListOf()
val rightEntries: MutableList<T> = mutableListOf()
forEach { (left, right) ->
when {
left != null && right == null -> leftEntries += left
left != null && right != null -> bothEntries += left to right
left == null && right != null -> rightEntries += right
else /* left == null && right == null */ -> Unit
}
}
return Triple(leftEntries, bothEntries, rightEntries)
}
}
synchronizeWithRemoteEntries내에서 가장 중요한 코드인 CUD의forEach가 부각됨
데이터의 의존성이 복잡할 때 → 이상적인 중간 데이터를 생성해서 정리
오늘 배운 것
- WebSocket
트러블슈팅
STOMP 로컬 테스트 문제
문제 상황
- Postman으로 웹소켓 통신 테스트 시도 → CONNECTION 불가 문제 발생 (로그 X)
- Gemini Code Assist, Antigravity 진단 원인 : 유효한 STOMP 프레임으로 인식 X (로그가 없기에)
- 특히, NULL 문자(^@) 인식 불가 가능성 제기 → 없애는 등의 제안 방식 시도했으나 증상 동일
- Postman 이외의 다른 STOMP 테스트 도구 사용 시도 : 운영 중단된 도구들, 진입 장벽 존재
해결 방법
- 웹소켓을 Postman으로 테스트한 다른 사례 탐색
- Postman에서는, 한 API를 호출한 뒤의 동작을 지정할 수 있는 스크립트가 존재 → 이를 활용, NULL 문자에 대한 환경 변수를 인식할 수 있는 형태로 세팅 진행
- 이후 시도 시, 정상적으로 연결됨을 확인
내일 할 일
- 사용자 정보 RUD