디스코드 클론코딩 서비스를 개발하면서
채팅방 입장 링크를 발급해준 후 한시간 후 만료시키고
만료되지 않은 코드라면 참여가 가능하게 해야하는 기능을 개발해야했다
Key:Value 형태로 간단하게 저장할 수 있을 것 같고 만료되는 기능도 TTL로 하기 쉬운 Redis를 쓰기로 했는데,
코드 발급 시에는 채팅방id, userId -> 코드 이렇게 접근해야 했고
코드로 참여 시에는 코드 -> 채팅방 id 이렇게 접근해야 했다
그러나 Redis는 Key로 Value를 찾는 것은 효율적인 반면, Value로 Key를 찾는 것은 효율적이지 않아서
처음에는 채팅방id, userId -> 코드, 코드 -> 채팅방 id 이렇게 DB를 두개로 나눠서 저장했다
@Service
class ChatRoomService(
private val chatRoomRepository: ChatRoomRepository,
private val invitationRepository: InvitationRepository,
) {
fun getChatRoomInvitation(
chatRoomId: String,
userId: String,
): InvitationResponse {
verifyChatRoomAccess(chatRoomId, userId)
val code = invitationRepository.getInvitationCode(userId, chatRoomId)
return InvitationResponse(code)
}
}
@Component
class InvitationChatRoomRepository(
private val redisTemplate: RedisTemplate<String, String>,
private val propertyConfig: PropertyConfig,
) {
fun setInvitationCode(
invitationLink: String,
chatRoomId: String,
) {
val ops: ValueOperations<String, String> = redisTemplate.opsForValue()
val result = ops.setIfAbsent(invitationLink, chatRoomId, propertyConfig.getExpiration())
if (result == null || !result) {
throw GlobalException(ErrorCode.INVITATION_LINK_SAVE_FAILURE)
}
}
}
@Component
class UserChatRoomInvitationRepository(
private val redisTemplate: RedisTemplate<String, String>,
private val propertyConfig: PropertyConfig,
private val invitationChatRoomRepository: InvitationChatRoomRepository,
) {
fun getInvitation(
userId: String,
chatRoomId: String,
): InvitationResponse {
val key = generateKey(userId, chatRoomId)
var value = redisTemplate.opsForValue().get(key)
if (value == null) {
value = setInvitation(key, chatRoomId)
}
val expiration = redisTemplate.getExpire(key)
return InvitationResponse(value, LocalDateTime.now().plusSeconds(expiration).toString())
}
fun setInvitation(
key: String,
chatRoomId: String,
): String {
val invitationCode = generateCode()
val ops: ValueOperations<String, String> = redisTemplate.opsForValue()
ops.set(key, invitationCode, propertyConfig.getExpiration())
invitationChatRoomRepository.setInvitationCode(invitationCode, chatRoomId)
return invitationCode
}
private fun generateKey(
userId: String,
chatRoomId: String,
): String {
return "$userId:$chatRoomId"
}
private fun generateCode(): String {
return UUID.randomUUID().toString()
}
}
1차 개선 : Repository 2개 -> 1개
그랬다가
chatroom id : user id -> code만 저장한 후
그 정보를 링크로 암호화해서 내보내고
chatroom id와 user id로 code를 찾았을때
저장되어있는 code가 암호화된 링크를 decode했을 때 나온 code와 일치하면(만료되지 않은 code이면)
해당 chatroom에 추가하는 방식으로 쓰면
채팅방id, userId -> 코드 Repository와 코드 -> 채팅방 id Repository 두개를 쓸 필요가 없이
채팅방id, userId -> 코드 Repository 만 있으면 되어서 하나를 삭제했다
그 후 Redis 의 여러 자료구조를 찾아보고 chatRoomId를 key, userId를 subkey로 쓰는 Hash를 쓰고싶었지만
userId마다 expiration을 적용할 수 없어서 사용할 수 없었다.
(userId가 필요한 이유 : 채팅방에 참여중인 사람 한 명당 한 시간동안 유효한 링크 하나를 발급하고 싶었다, 채팅방 전체가 링크를 공용으로 사용할 수 있다면 다른 사람이 59분전에 발급해서 1분 남은 링크를 뿌리게 되는 일이 있을 수도 있으니까...)
@Component
class InvitationRepository(
private val redisTemplate: RedisTemplate<String, String>,
private val chatRoomProperty: ChatRoomProperty,
) {
fun getInvitationCode(
userId: String,
chatRoomId: String,
): String {
val key = generateKey(userId, chatRoomId)
var value = redisTemplate.opsForValue().get(key)
if (value == null) {
value = setInvitation(key, chatRoomId)
}
return generateCode(key, value)
}
fun setInvitation(
key: String,
chatRoomId: String,
): String {
val value = generateValue()
val ops: ValueOperations<String, String> = redisTemplate.opsForValue()
ops.set(key, value, chatRoomProperty.getExpiration())
return value
}
fun getExpiration(
userId: String,
chatRoomId: String,
): Long {
val key = generateKey(userId, chatRoomId)
val expiration = redisTemplate.getExpire(key)
return expiration
}
fun generateCode(
key: String,
value: String,
): String {
val combine = "$key,$value"
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(combine.toByteArray(StandardCharsets.UTF_8))
return Base64.getEncoder().encodeToString(hash)
}
private fun generateKey(
userId: String,
chatRoomId: String,
): String {
return "$userId:$chatRoomId"
}
private fun generateValue(): String {
return UUID.randomUUID().toString()
}
}
그래서 이 Repository 만 남겼다
이 이후 Repository에 있는 로직들을 Service에 의존하는 Service Layer를 추가적으로 만들어서 분리했다
https://greedydeveloper.tistory.com/377
'Journal' 카테고리의 다른 글
QueryDSL 설정 (0) | 2024.09.05 |
---|---|
Spatial Index 전환기 (0) | 2024.08.01 |
Service에서 다른 Service 를 의존하게 하기 (0) | 2024.06.20 |
깃허브 프로필 꾸며보기!! (0) | 2024.02.09 |
[팀프로젝트][모아요이츠] Post domain 개발일지 (1) | 2024.01.09 |