2026-01-20

1일 1아티클

LY

고르디우스 변수

상황

  • 로컬과 원격, 두 저장소에 FooData 저장, 이 데이터의 동기화를 위해 FooSynchronizer를 구현하는 상황
  • 원본인 원격 FooData를 사용해 로컬 FooData 업데이트
  • 원격 및 로컬 데이터는 각각 FooLocalDaoFooRemoteClient에서 가져옴
  • FooSynchronizersynchronizeWithRemoteEntries 함수 : 로컬과 원격 간 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를 구하기 → remoteEntryIdslocalEntryIds를 비교해서 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가 부각됨

데이터의 의존성이 복잡할 때 → 이상적인 중간 데이터를 생성해서 정리

오늘 배운 것

  1. WebSocket

트러블슈팅

STOMP 로컬 테스트 문제

문제 상황

  • Postman으로 웹소켓 통신 테스트 시도 → CONNECTION 불가 문제 발생 (로그 X)
  • Gemini Code Assist, Antigravity 진단 원인 : 유효한 STOMP 프레임으로 인식 X (로그가 없기에)
    • 특히, NULL 문자(^@) 인식 불가 가능성 제기 → 없애는 등의 제안 방식 시도했으나 증상 동일
  • Postman 이외의 다른 STOMP 테스트 도구 사용 시도 : 운영 중단된 도구들, 진입 장벽 존재

해결 방법

  • 웹소켓을 Postman으로 테스트한 다른 사례 탐색
  • Postman에서는, 한 API를 호출한 뒤의 동작을 지정할 수 있는 스크립트가 존재 → 이를 활용, NULL 문자에 대한 환경 변수를 인식할 수 있는 형태로 세팅 진행
  • 이후 시도 시, 정상적으로 연결됨을 확인

내일 할 일

  1. 사용자 정보 RUD

참고자료

results matching ""

    No results matching ""