글을 작성하게 된 계기
데이터 엔지니어링 학습을 하며 텀블링 윈도우(Tumbling Windows), 슬라이딩 윈도우(Sliding Windows)에 대해 학습하게 되었고, 이를 정리하기 위해 글을 작성하게 되었습니다.
1. Tumbling Windows
텀블링 윈도우(Tumbling Windows)는 일정 시간 간격으로 데이터를 처리하는 방식 입니다. 이는 일정 시간 간격으로 데이터를 처리하고, 그 시간 간격이 지나면 다음 시간 간격으로 넘어갑니다. 즉, 일정한 시간 간격으로 데이터를 처리하는 방식으로, 각 윈도우는 서로 겹치지 않으며 고정된 크기를 가집니다. 예를 들어, 1분 단위의 윈도우를 설정하면 12:00:00 ~ 12:00:59, 12:01:00 ~ 12: 01:59처럼 구분됩니다. 각 이벤트는 특정 윈도우에만 포함되며, 동일한 데이터가 여러 윈도우에서 중복 처리되지 않습니다.
텀블링 윈도우의 특징은 다음과 같습니다.
윈도우가 겹치지 않음: 각 이벤트는 하나의 윈도우에만 포함됨.고정된 크기: 예를 들어, 1분 윈도우라면 12:00:00 ~ 12:00:59, 12:01:00 ~ 12:01:59처럼 설정됨.이벤트의 경계 문제: 경계에서 발생한 이벤트가 윈도우에 포함되지 않거나, 다른 윈도우로 넘어가면서 데이터 손실이 발생할 수 있음.
예를 들어, User1이 12:00:05, 12:00:20, 12:00:30, 12:00:50에 이벤트를 발생시켰다면, 이는 12:00:00 ~ 12:00:59 윈도우에 포함됩니다. 하지만 12:01:00.000에 발생한 이벤트는 이전 윈도우에 포함되지 않고 새로운 12:01:00 ~ 12:01:59 윈도우에서 처리됩니다. 이처럼 경계에서 발생한 이벤트는 다음 윈도우로 넘어가거나 포함되지 않을 가능성이 있습니다. 이를 통해, 하나의 이벤트가 하나의 윈도우에서만 처리되므로 관리가 쉬우며, 윈도우가 종료되면 데이터를 폐기할 수 있어 메모리 사용을 최소화할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
+----------------+--------+-------------------------------+
| 시간 (HH:MM:SS) | User | 포함된 윈도우 구간 |
+----------------+--------+-------------------------------+
| 12:00:05 | User1 | [12:00:00 ~ 12:00:59] |
| 12:00:20 | User1 | [12:00:00 ~ 12:00:59] |
| 12:00:30 | User1 | [12:00:00 ~ 12:00:59] |
| 12:00:50 | User1 | [12:00:00 ~ 12:00:59] |
| 12:00:59.999 | User1 | [12:00:00 ~ 12:00:59] (경계)|
| 12:01:00.000 | User1 | [12:01:00 ~ 12:01:59] ❌ |
| 12:01:10 | User1 | [12:01:00 ~ 12:01:59] |
| 12:01:30 | User1 | [12:01:00 ~ 12:01:59] |
+----------------+--------+-------------------------------+
텀블링 윈도우의 장점은 다음과 같습니다.
단순하고 빠른 계산: 하나의 이벤트는 오직 하나의 윈도우에서만 처리되기 때문에 윈도우 관리가 간단하며, 중복 계산이 발생하지 않습니다.효율적 메모리 사용: 윈도우가 종료되면 해당 데이터를 폐기할 수 있으므로 불필요한 메모리 사용을 줄일 수 있어 메모리 사용이 효율적입니다.
반면, 단점은 다음과 같습니다.
경계 데이터 미 포함: 예를 들어, 12:00:59.999에 발생한 이벤트와 12:01:00.000에 발생한 이벤트는 불과 1밀리초 차이밖에 없지만 서로 다른 윈도우에 속하게 되어 분석 결과가 단절될 수 있습니다.어려운 세밀한 시간 조절: 텀블링 윈도우는 윈도우 크기가 고정되어 있어 분석 시점을 세밀하게 조정하기 어렵습니다. 따라서 보다 정밀한 실시간 분석이 필요한 경우에는 텀블링 윈도우만으로는 한계가 있을 수 있습니다.
이를 코드로 보면 다음과 같습니다.
1
2
3
4
data class Event(
val timestamp: Long,
val user: String
)
1
2
3
4
5
6
7
8
9
10
11
12
13
fun tumblingWindow(
events: List<Event>,
windowSizeMillis: Long
): Map<Long, List<Event>> {
val groupedEvents = mutableMapOf<Long, MutableList<Event>>()
for (event in events) {
val windowStart = (event.timestamp / windowSizeMillis) * windowSizeMillis
groupedEvents.computeIfAbsent(windowStart) { mutableListOf() }.add(event)
Log.info("🪟 Tumbling Event at ${Instant.ofEpochMilli(event.timestamp)} -> Window Start: ${Instant.ofEpochMilli(windowStart)}")
}
return groupedEvents
}
2. Sliding Windows
슬라이딩 윈도우(Sliding Windows)는 일정한 간격으로 계속 이동하면서 데이터를 처리하는 방식 입니다. 텀블링 윈도우와 달리 슬라이딩 윈도우는 일정 간격으로 겹치는 부분이 있어, 이벤트가 여러 윈도우에서 중복 처리될 수 있습니다.
슬라이딩 윈도우의 특징은 다음과 같습니다.
윈도우가 겹침: 동일한 이벤트가 여러 개의 윈도우에서 포함될 수 있음.고정된 크기, 유동적인 이동: 윈도우 크기는 고정되어 있지만, 일정 간격으로 새로운 윈도우가 생성됨.세밀한 시간 조정 가능: 텀블링 윈도우와 달리, 짧은 간격으로 분석 시점을 조정할 수 있어 더 정밀한 분석이 가능함.
예를 들어, User1이 12:00:05, 12:00:20, 12:00:30, 12:00:50에 이벤트를 발생시켰다면, 이는 12:00:00 ~ 12:00:59 윈도우뿐만 아니라, 12:00:30 ~ 12:01:29 윈도우에도 포함됩니다. 12:01:00.000에 발생한 이벤트 역시 두 개의 윈도우(12:00:30 ~ 12:01:29, 12:01:00 ~ 12:01:59)에 포함될 수 있습니다. 즉, 이벤트가 여러 윈도우에서 중복 처리될 수 있어 분석 연속성이 향상됩니다. 즉, 12:00:00 ~ 12:00:59 윈도우에 포함된 이벤트는 다음 구간에도 포함되며, 이를 통해 세밀한 시간 조정이 가능합니다.
1
2
3
4
5
6
7
8
9
10
11
12
+----------------+--------+--------------------------------------------------+
| 시간 (HH:MM:SS) | User | 포함된 윈도우 구간 |
+----------------+--------+--------------------------------------------------+
| 12:00:05 | User1 | [12:00:00 ~ 12:00:59], [12:00:30 ~ 12:01:29] v |
| 12:00:20 | User1 | [12:00:00 ~ 12:00:59], [12:00:30 ~ 12:01:29] v |
| 12:00:30 | User1 | [12:00:00 ~ 12:00:59], [12:00:30 ~ 12:01:29] v |
| 12:00:50 | User1 | [12:00:00 ~ 12:00:59], [12:00:30 ~ 12:01:29] v |
| 12:00:59.999 | User1 | [12:00:00 ~ 12:00:59], [12:00:30 ~ 12:01:29] → |
| 12:01:00.000 | User1 | [12:00:30 ~ 12:01:29], [12:01:00 ~ 12:01:59] v |
| 12:01:10 | User1 | [12:00:30 ~ 12:01:29], [12:01:00 ~ 12:01:59] v |
| 12:01:30 | User1 | [12:01:00 ~ 12:01:59], [12:01:30 ~ 12:02:29] v |
+----------------+--------+-------------------------------------------------+
슬라이딩 윈도우의 장점은 다음과 같습니다.
분석의 연속성 보장: 윈도우가 겹쳐 있으므로 특정 순간의 이벤트가 다른 윈도우에서도 반영될 수 있어 보다 자연스러운 흐름을 유지할 수 있음.세밀한 실시간 분석 가능: 짧은 간격으로 윈도우가 이동하기 때문에 텀블링 윈도우보다 정밀한 분석이 가능함.
반면, 단점은 다음과 같습니다.
중복 계산 발생 가능: 동일한 이벤트가 여러 개의 윈도우에서 포함되기 때문에 중복 계산을 피하기 위한 추가적인 처리가 필요함.더 많은 메모리 사용: 여러 윈도우가 동시에 유지되므로 텀블링 윈도우보다 메모리 사용량이 증가할 수 있음.
이를 코드로 보면 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fun slidingWindow(
events: List<Event>,
windowSizeMillis: Long,
slideSizeMillis: Long
): Map<Long, List<Event>> {
val groupedEvents = mutableMapOf<Long, MutableList<Event>>()
for (event in events) {
val eventTime = event.timestamp
val firstWindowStart = eventTime / slideSizeMillis * slideSizeMillis - (windowSizeMillis - slideSizeMillis)
// 이벤트가 포함될 모든 윈도우 계산
for (windowStart in firstWindowStart..eventTime step slideSizeMillis) {
if (windowStart >= 0) {
groupedEvents.computeIfAbsent(windowStart) { mutableListOf() }.add(event)
Logger.info("🪟 Sliding Event at ${Instant.ofEpochMilli(eventTime)} -> Window Start: ${Instant.ofEpochMilli(windowStart)}")
}
}
}
return groupedEvents
}
3. Two Pointer vs Sliding Window
투 포인터(Two Pointer)와 슬라이딩 윈도우(Sliding Window)는 유사한 개념이지만, 차이점이 있습니다.
Two Pointer: 배열이나 문자열에서 두 개의 포인터를 사용하여 특정 조건을 만족하는 구간을 찾는 기법입니다.Sliding Window: 일정한 크기의 윈도우를 유지하며, 데이터를 처리하는 방식입니다. 연속된 데이터를 다룰 때 유용합니다.
투 포인터는 포인터 이동에 따라 유동적으로 윈도우 크기가 변할 수 있지만, 슬라이딩 윈도우는 고정된 크기로 데이터를 분석합니다. 배열이나 문자열에서 두 개의 포인터를 이동시키면서 특정 조건을 만족하는 부분을 찾 습니다.
1
2
3
4
+------------------------------+
| 배열: [1, 3, 5, 7, 9, 11, 13, 15] |
| [1, 3, 5] → [3, 5, 7] → [5, 7, 9] ... |
+------------------------------+
이를 코드로 보면 다음과 같은데, 두 개의 포인터를 동적으로 움직여가며 범위를 좁힙니다. 이를 통해 원하는 타겟을 찾는 것이죠.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun twoPointerSum(
arr: IntArray,
target: Int
): Pair<Int, Int>? {
var left = 0
var right = arr.size - 1
while (left < right) {
val sum = arr[left] + arr[right]
when {
sum == target -> return Pair(arr[left], arr[right])
sum < target -> left++
else -> right--
}
}
return null
}
반면, 슬라이딩 윈도우(Sliding Window)는 일정한 크기의 윈도우를 유지하며 데이터를 처리하는 방식입니다. 투 포인터는 포인터를 유동적으로 이동시키면서 범위를 좁혀가는 반면, 슬라이딩 윈도우는 고정된 크기의 윈도우를 유지하면서 한 칸씩 이동하는 특징이 있습니다. 따라서 투 포인터는 특정 조건을 만족하는 구간을 찾는 데 유용하고, 슬라이딩 윈도우는 연속된 데이터를 일정 크기로 유지하며 처리하는 데 적합합니다.
예를 들어, 배열에서 연속된 K개의 요소 중 최대 합을 찾는 문제에서는 슬라이딩 윈도우를 사용하는 것이 효과적입니다. 초기 윈도우의 합을 구한 후, 새로운 요소를 추가하고 가장 오래된 요소를 제거하는 방식으로 윈도우를 유지하면서 이동합니다. 이를 통해 불필요한 연산을 줄이고, 효율적으로 최대 합을 구할 수 있습니다. 이처럼 투 포인터는 포인터를 조정하며 특정 값을 만족하는 구간을 찾을 때 유용하고, 슬라이딩 윈도우는 연속된 데이터의 일정 구간을 분석할 때 적합합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun maxSumSlidingWindow(arr: IntArray, k: Int): Int {
if (arr.size < k) return -1
var maxSum = 0
var windowSum = 0
for (i in 0 until k) {
windowSum += arr[i]
}
maxSum = windowSum
for (i in k until arr.size) {
windowSum += arr[i] - arr[i - k]
maxSum = maxOf(maxSum, windowSum)
}
return maxSum
}
4. Session Window
세션 윈도우(Session Windows)는 이벤트의 발생 간격을 기준으로 동적으로 생성되는 윈도우 입니다. 슬라이딩 윈도우나 텀블링 윈도우와 달리, 고정된 크기가 없으며 이벤트 간 간격(Gap Duration)에 따라 윈도우가 결정 됩니다.
윈도우 크기가 일정하지 않음: 이벤트가 연속적으로 발생하면 하나의 윈도우에 포함되지만, 일정 시간(gap) 동안 이벤트가 없으면 새로운 윈도우가 생성됨.데이터가 몰리는 시점에서 하나의 윈도우로 합쳐짐: 사용자의 행동을 기반으로 데이터 그룹을 형성할 수 있음.유동적인 경계 설정: 특정 시간이 지나야 새로운 윈도우가 생성되므로, 실시간 분석에 적합.
예를 들어, sessionGapMillis이 5초 라고 가정하고 이벤트가 아래와 같이 발생했다고 가정해보겠습니다. 위 예제에서, 12:00:01 ~ 12:00:02 사이에는 간격이 짧아 같은 세션에 포함되지만, 12:00:02 이후 5초 이상 이벤트가 없다가 12:00:08에 새로운 이벤트가 발생하므로 새로운 세션이 시작됩니다.
1
2
3
4
5
6
7
8
+----------------+--------+---------------------------+
| 시간 (HH:MM:SS) | User | 포함된 세션 윈도우 구간 |
+----------------+--------+---------------------------+
| 12:00:01 | User1 | [12:00:01 ~ 12:00:04] |
| 12:00:02 | User1 | [12:00:01 ~ 12:00:04] |
| 12:00:08 | User1 | [12:00:08 ~ 12:00:09] |
| 12:00:09 | User1 | [12:00:08 ~ 12:00:09] |
+----------------+--------+---------------------------+
세션 윈도우의 장점은 다음과 같습니다.
- 사용자 행동 분석에 최적화: 일정한 시간 간격이 아닌, 사용자 이벤트 흐름을 기준으로 윈도우를 형성하므로 클릭 스트림 분석, 사용자 세션 분석 등에 유리함.
- 유동적인 윈도우 크기: 이벤트가 몰릴 때는 긴 윈도우를, 이벤트가 뜸할 때는 짧은 윈도우를 자동으로 조정하여 생성할 수 있음.
- 불필요한 데이터 처리 최소화: 이벤트가 없는 기간 동안 별도로 빈 윈도우를 유지할 필요가 없어 메모리 사용이 효율적임.
반면 단점은 다음과 같습니다.
- 윈도우 크기 예측이 어려움: 사전에 윈도우 크기를 설정할 수 없으므로, 데이터 흐름이 변할 경우 분석 시 고려해야 할 사항이 많아짐.
- 이벤트 간격 설정에 따라 결과가 달라질 수 있음: 예를 들어 sessionGapMillis = 5초 가 적절한지 판단하는 것이 중요함.
이를 코드로 보면 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fun sessionWindow(
events: List<Event>,
sessionGapMillis: Long
): Map<Long, List<Event>> {
val sortedEvents = events.sortedBy { it.timestamp }
val groupedSessions = mutableMapOf<Long, MutableList<Event>>()
var currentSessionStart: Long? = null
for (event in sortedEvents) {
val eventTime = event.timestamp
if (currentSessionStart == null || eventTime - groupedSessions[currentSessionStart]!!.last().timestamp > sessionGapMillis) {
// 새로운 세션 시작
currentSessionStart = eventTime
groupedSessions[currentSessionStart] = mutableListOf()
}
groupedSessions[currentSessionStart]!!.add(event)
Logger.info("🪟 Session Event at ${Instant.ofEpochMilli(eventTime)} -> Session Start: ${Instant.ofEpochMilli(currentSessionStart)}")
}
return groupedSessions
}
5. 정리
각 윈도우에 대한 특징을 살펴보았는데요, 이를 통해 데이터를 효율적으로 처리하고 분석할 수 있습니다.
- 텀블링 윈도우: 고정된 크기의 윈도우를 유지하며 데이터를 처리하는 방식으로, 각 윈도우는 서로 겹치지 않고 고정된 크기를 가집니다.
- 슬라이딩 윈도우: 일정한 간격으로 계속 이동하면서 데이터를 처리하는 방식으로, 윈도우가 겹치며 고정된 크기를 가집니다.
- 세션 윈도는 이벤트의 발생 간격을 기준으로 동적으로 생성되는 윈도우로, 윈도우 크기가 일정하지 않고 데이터가 몰리는 시점에서 하나의 윈도우로 합쳐집니다.