Skip to content

Commit 82f1bd6

Browse files
authored
Merge pull request #70 from soma-baekgu/feature/BG-332-register-ws-session-in-redis
[BG-332]: ws 접속 정보를 레디스에 기록한다 (4h / 3h)
2 parents 19b05e4 + 936811f commit 82f1bd6

File tree

18 files changed

+306
-93
lines changed

18 files changed

+306
-93
lines changed

api/src/test/resources/application.yaml

-5
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,6 @@ spring:
2828
kafka:
2929
bootstrap-servers: [ "localhost:9092" ]
3030

31-
data:
32-
redis:
33-
host: localhost
34-
port: 6379
35-
3631
oauth:
3732
google:
3833
client-id: a

common/src/main/kotlin/com/backgu/amaker/common/status/StatusCode.kt

+3
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,7 @@ enum class StatusCode(
5252

5353
// eventAssignedUser
5454
EVENT_ASSIGNED_USER_NOT_FOUND("4000", "접근할 수 없는 이벤트입니다."),
55+
56+
// realtime
57+
REALTIME_NOT_FOUND("4000", "세션정보를 찾을 수 없습니다."),
5558
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.backgu.amaker.domain.workspace
2+
3+
data class WorkspaceSession(
4+
val id: String,
5+
val userId: String,
6+
val workspaceId: Long,
7+
val realtimeId: String,
8+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.backgu.amaker.infra.redis.workspace.dto
2+
3+
import com.backgu.amaker.domain.workspace.WorkspaceSession
4+
import com.fasterxml.jackson.annotation.JsonCreator
5+
import com.fasterxml.jackson.annotation.JsonProperty
6+
7+
data class WorkspaceSessionRedisData
8+
@JsonCreator
9+
constructor(
10+
@JsonProperty("id") val id: String,
11+
@JsonProperty("userId") val userId: String,
12+
@JsonProperty("workspaceId") val workspaceId: Long,
13+
@JsonProperty("realtimeId") val realtimeId: String,
14+
) {
15+
companion object {
16+
fun of(workspaceSession: WorkspaceSession) =
17+
WorkspaceSessionRedisData(
18+
workspaceSession.id,
19+
workspaceSession.userId,
20+
workspaceSession.workspaceId,
21+
workspaceSession.realtimeId,
22+
)
23+
}
24+
25+
fun toDomain() = WorkspaceSession(id, userId, workspaceId, realtimeId)
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.backgu.amaker.infra.redis.workspace.repository
2+
3+
import com.backgu.amaker.domain.workspace.WorkspaceSession
4+
import com.backgu.amaker.infra.redis.workspace.dto.WorkspaceSessionRedisData
5+
import org.springframework.data.redis.core.ListOperations
6+
import org.springframework.data.redis.core.RedisTemplate
7+
import org.springframework.stereotype.Repository
8+
9+
@Repository
10+
class WorkspaceSessionRepository(
11+
private val redisTemplate: RedisTemplate<String, WorkspaceSessionRedisData>,
12+
private val listOps: ListOperations<String, WorkspaceSessionRedisData> = redisTemplate.opsForList(),
13+
) {
14+
companion object {
15+
const val PREFIX = "workspace:"
16+
17+
fun key(workspaceId: Long) = "$PREFIX$workspaceId"
18+
}
19+
20+
fun addWorkspaceSession(
21+
workspaceId: Long,
22+
workspaceSession: WorkspaceSession,
23+
) {
24+
listOps.leftPush(key(workspaceId), WorkspaceSessionRedisData.of(workspaceSession))
25+
}
26+
}

realtime/build.gradle.kts

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ dependencies {
2121
implementation("org.springframework.boot:spring-boot-starter-data-redis")
2222

2323
testImplementation(kotlin("test"))
24+
testImplementation("com.redis.testcontainers:testcontainers-redis-junit:1.6.4")
25+
testImplementation("org.testcontainers:testcontainers:1.19.0")
26+
testImplementation("org.testcontainers:mysql:1.16.0")
2427
}
2528

2629
tasks.withType<KotlinCompile> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.backgu.amaker.realtime.config
2+
3+
import com.backgu.amaker.infra.redis.workspace.dto.WorkspaceSessionRedisData
4+
import com.backgu.amaker.infra.redis.workspace.repository.WorkspaceSessionRepository
5+
import org.springframework.boot.context.properties.ConfigurationProperties
6+
import org.springframework.context.annotation.Bean
7+
import org.springframework.context.annotation.Configuration
8+
import org.springframework.data.redis.connection.RedisConnectionFactory
9+
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
10+
import org.springframework.data.redis.core.RedisTemplate
11+
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
12+
import org.springframework.data.redis.serializer.StringRedisSerializer
13+
14+
@Configuration
15+
@ConfigurationProperties(prefix = "spring.data.redis")
16+
class RedisConfig {
17+
lateinit var host: String
18+
var port: Int = 0
19+
20+
@Bean
21+
fun redisConnectionFactory(): RedisConnectionFactory {
22+
val factory = LettuceConnectionFactory(host, port)
23+
factory.afterPropertiesSet()
24+
return factory
25+
}
26+
27+
@Bean
28+
fun redisTemplate(): RedisTemplate<String, WorkspaceSessionRedisData> {
29+
val template = RedisTemplate<String, WorkspaceSessionRedisData>()
30+
template.connectionFactory = redisConnectionFactory()
31+
template.keySerializer = StringRedisSerializer()
32+
template.valueSerializer = GenericJackson2JsonRedisSerializer()
33+
return template
34+
}
35+
36+
@Bean
37+
fun workspaceSessionRepository(redisTemplate: RedisTemplate<String, WorkspaceSessionRedisData>): WorkspaceSessionRepository =
38+
WorkspaceSessionRepository(redisTemplate)
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.backgu.amaker.realtime.workspace.service
2+
3+
import com.backgu.amaker.realtime.workspace.session.WorkspaceWebSocketSession
4+
import org.springframework.stereotype.Service
5+
import org.springframework.web.socket.WebSocketSession
6+
7+
@Service
8+
class WorkspaceSessionFacadeService(
9+
private val workspaceUserService: WorkspaceUserService,
10+
private val workspaceSessionService: WorkspaceSessionService,
11+
) {
12+
fun enrollUserToWorkspaceSession(
13+
userId: String,
14+
workspaceId: Long,
15+
workspaceSession: WorkspaceWebSocketSession<WebSocketSession>,
16+
) {
17+
workspaceUserService.checkUserBelongToWorkspace(userId, workspaceId)
18+
workspaceSessionService.enrollUserToWorkspaceSession(userId, workspaceId, workspaceSession)
19+
}
20+
21+
fun dropOutWorkspaceSession(
22+
userId: String,
23+
workspaceId: Long,
24+
workspaceSession: WorkspaceWebSocketSession<WebSocketSession>,
25+
) {
26+
workspaceSessionService.dropOut(userId, workspaceId, workspaceSession)
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.backgu.amaker.realtime.workspace.service
2+
3+
import com.backgu.amaker.infra.redis.workspace.repository.WorkspaceSessionRepository
4+
import com.backgu.amaker.realtime.workspace.session.WorkspaceWebSocketSession
5+
import com.backgu.amaker.realtime.workspace.storage.SessionStorage
6+
import org.springframework.stereotype.Service
7+
import org.springframework.web.socket.WebSocketSession
8+
9+
@Service
10+
class WorkspaceSessionService(
11+
private val sessionStorage: SessionStorage,
12+
private val workspaceSessionRepository: WorkspaceSessionRepository,
13+
) {
14+
fun enrollUserToWorkspaceSession(
15+
userId: String,
16+
workspaceId: Long,
17+
session: WorkspaceWebSocketSession<WebSocketSession>,
18+
) {
19+
sessionStorage.addSession(session)
20+
workspaceSessionRepository.addWorkspaceSession(
21+
workspaceId,
22+
session.toDomain(),
23+
)
24+
}
25+
26+
fun dropOut(
27+
userId: String,
28+
workspaceId: Long,
29+
session: WorkspaceWebSocketSession<WebSocketSession>,
30+
) {
31+
sessionStorage.removeSession(session.id)
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.backgu.amaker.realtime.workspace.session
2+
3+
import com.backgu.amaker.domain.workspace.WorkspaceSession
4+
5+
class WorkspaceWebSocketSession<T>(
6+
val id: String,
7+
val userId: String,
8+
val workspaceId: Long,
9+
val realTimeId: String,
10+
val session: T,
11+
) {
12+
fun toDomain() = WorkspaceSession(id, userId, workspaceId, realTimeId)
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.backgu.amaker.realtime.workspace.storage
2+
3+
import com.backgu.amaker.common.status.StatusCode
4+
import com.backgu.amaker.realtime.common.excpetion.RealTimeException
5+
import com.backgu.amaker.realtime.workspace.session.WorkspaceWebSocketSession
6+
import org.springframework.stereotype.Component
7+
import org.springframework.web.socket.WebSocketSession
8+
import java.util.concurrent.ConcurrentHashMap
9+
10+
@Component
11+
class SessionStorage {
12+
private val sessionsMap = ConcurrentHashMap<String, WorkspaceWebSocketSession<WebSocketSession>>()
13+
14+
fun addSession(session: WorkspaceWebSocketSession<WebSocketSession>) {
15+
sessionsMap[session.id] = session
16+
}
17+
18+
fun removeSession(id: String) {
19+
sessionsMap.remove(id)
20+
}
21+
22+
fun getSession(id: String): WorkspaceWebSocketSession<WebSocketSession> =
23+
sessionsMap[id] ?: throw RealTimeException(StatusCode.INTERNAL_SERVER_ERROR)
24+
}
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
11
package com.backgu.amaker.realtime.ws.handler
22

3+
import com.backgu.amaker.realtime.server.config.ServerConfig
34
import com.backgu.amaker.realtime.utils.WebSocketSessionUtils
4-
import com.backgu.amaker.realtime.workspace.service.WorkspaceUserService
5+
import com.backgu.amaker.realtime.workspace.service.WorkspaceSessionFacadeService
6+
import com.backgu.amaker.realtime.workspace.session.WorkspaceWebSocketSession
57
import com.backgu.amaker.realtime.ws.constants.WebSocketConstants.Companion.USER_ID
68
import com.backgu.amaker.realtime.ws.constants.WebSocketConstants.Companion.WORKSPACE_ID
7-
import com.backgu.amaker.realtime.ws.session.SessionInfo
8-
import com.backgu.amaker.realtime.ws.session.storage.SessionStorage
99
import org.springframework.stereotype.Component
1010
import org.springframework.web.socket.CloseStatus
1111
import org.springframework.web.socket.WebSocketSession
1212
import org.springframework.web.socket.handler.TextWebSocketHandler
1313

1414
@Component
1515
class WebSocketSessionHandler(
16-
private val storage: SessionStorage<Long, String, SessionInfo>,
17-
private val workspaceUserService: WorkspaceUserService,
16+
private val workspaceSessionFacadeService: WorkspaceSessionFacadeService,
17+
private val serverConfig: ServerConfig,
1818
) : TextWebSocketHandler() {
1919
override fun afterConnectionEstablished(session: WebSocketSession) {
2020
val userId: String = WebSocketSessionUtils.extractAttribute<String>(session, USER_ID)
2121
val workspaceId = WebSocketSessionUtils.extractAttribute<Long>(session, WORKSPACE_ID)
22-
23-
workspaceUserService.checkUserBelongToWorkspace(userId, workspaceId)
24-
25-
storage.addSession(workspaceId, userId, SessionInfo(userId, workspaceId, session))
22+
workspaceSessionFacadeService.enrollUserToWorkspaceSession(
23+
userId,
24+
workspaceId,
25+
WorkspaceWebSocketSession(session.id, userId, workspaceId, serverConfig.id, session),
26+
)
2627
}
2728

2829
override fun afterConnectionClosed(
@@ -32,6 +33,10 @@ class WebSocketSessionHandler(
3233
val userId: String = WebSocketSessionUtils.extractAttribute<String>(session, USER_ID)
3334
val workspaceId = WebSocketSessionUtils.extractAttribute<Long>(session, WORKSPACE_ID)
3435

35-
storage.removeSession(workspaceId, userId)
36+
workspaceSessionFacadeService.dropOutWorkspaceSession(
37+
userId,
38+
workspaceId,
39+
WorkspaceWebSocketSession(session.id, userId, workspaceId, serverConfig.id, session),
40+
)
3641
}
3742
}

realtime/src/main/kotlin/com/backgu/amaker/realtime/ws/session/SessionInfo.kt

-20
This file was deleted.

realtime/src/main/kotlin/com/backgu/amaker/realtime/ws/session/storage/InMemorySessionStorage.kt

-37
This file was deleted.

realtime/src/main/kotlin/com/backgu/amaker/realtime/ws/session/storage/SessionStorage.kt

-21
This file was deleted.

0 commit comments

Comments
 (0)