@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()));
// 나머지 설정
저는 RedisTemplate<String, ReidsValue>의 타입으로 RedisTemplate를 사용하고 있었습니다.
RedisValue는 제가 만든 인터페이스이며, 값으로 RedisValue를 조상 클래스로 갖고 있는 RefreshToken 클래스를 넣고 있었습니다.
문제는 Redis에 저장된 RefreshToken를 가져올 때, java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class 에러가 찍히는 문제가 있었습니다.
문제 파악
해당 에러는 Redis에서 json 데이터를 가져올 때, LinkedHashMap 타입으로 가져오는데, 이 LinkedHashMap을 RefreshToken 객체 타입으로 캐스팅할 수 없어서 발생했습니다.
문제 해결
구글 서칭과 Chat GPT로 알아본 결과 3가지 해결 방법을 생각할 수 있었습니다.
redisValueSerializer는 StringRedisSerializer를 사용하고, redis에 value를 저장하고, 읽어들이는 로직에서 직적 ObjectMapper를 사용하여 writeValueAsString와 covertValue를 구현하는 방법
redisValueSerializer는 GenericJackson2JsonRedisSerializer 를 사용하고, json 데이터에 클래스 정보(@class 필드)를 포함시키는 ObjectMapper를 GenericJackson2JsonRedisSerializer에 포함시키는 방법
redisValueSerializer는 Jackson2JsonRedisSerializer 를 사용하고, json 데이터 type 필드에 이름을 넣는 방법
저는 이 중에서 3번 방법을 선택했습니다.
1번 방법은 성능이 제일 좋을 것 같지만, 코드 구현 및 유지보수 측면에서 좋지 않다고 판단되어 제외했습니다.
2번 방법은 1번 방법보다 유지보수가 좋지만, 성능도 제일 떨어질 것 같고, 클래스 정보가 변경된 후 배포되면 기존에 redis에 저장되어 있던 value들은 역직렬화가 안되는 문제가 발생하고 또한, 다른 서버에서 해당 value를 가져오려면 value 객체의 위치도 동일하게 해야 한다는 종속성 문제도 있기 때문에 제외했습니다.
query가 맨 앞이 "?"인지 확인하여 시작위치를 파악하고, query 길이와 word 길이가 같은지, "?"를 제외한 문자열이 word이 시작위치 기준으로 문자열 길이만큼 동일한지 확인하면 된다고 생각했습니다.
def solution(words, queries):
answer = []
for query in queries:
cnt = 0
N = len(query)
if query[0] == "?":
front = False
else:
front = True
for word in words:
if N != len(word):
continue
search_word = query.replace("?", "")
search_N = len(search_word)
if front:
if word[:search_N] == search_word:
cnt += 1
else:
if word[-search_N:] == search_word:
cnt += 1
answer.append(cnt)
return answer
정확성: 25.0
효율성: 30.0
합계: 55.0 / 100.0
2차 시도
1차 시도보다 시간 효율성을 좋게하기 위해 딕셔너리를 생각했습니다.
딕셔너리 키는 word에 들어갈 수 있는 모든 query의 수이고, 값은 해당 쿼리로 검색할 수 있는 word의 수입니다.
def solution(words, queries):
answer = []
word_dict = {}
for word in words:
string = ""
N = len(word)
for token in word[:-1]:
string += token
keyword = string.ljust(N, "?")
if keyword in word_dict:
word_dict[keyword] += 1
else:
word_dict[keyword] = 1
string = ""
for token in word[::-1][:-1]:
string += token
keyword = string.ljust(N, "?")[::-1]
if keyword in word_dict:
word_dict[keyword] += 1
else:
word_dict[keyword] = 1
for query in queries:
answer.append(word_dict[query] if query in word_dict else 0)
return answer
정확성: 25.0
효율성: 30.0
합계: 55.0 / 100.0
3차 시도
마지막으로 트리에 알고리즘을 적용했습니다.
class Node(object):
def __init__(self, key = None):
self.key = key
self.length = []
self.child = {}
class Trie():
def __init__(self):
self.head = Node()
def insertTree(self, word):
cur = self.head
word_length = len(word)
cur.length.append(word_length)
for token in word:
if token not in cur.child:
cur.child[token] = Node(token)
cur = cur.child[token]
cur.length.append(word_length)
def searchTree(self, word):
cur = self.head
word_length = len(word)
for token in word:
if token == "?":
return cur.length.count(word_length)
elif token not in cur.child:
break
cur = cur.child[token]
return 0
def solution(words, queries):
answer = []
tree = Trie()
reverse_tree = Trie()
for word in words:
tree.insertTree(word)
reverse_tree.insertTree(word[::-1])
for query in queries:
if query[0] == "?":
answer.append(reverse_tree.searchTree(query[::-1]))
else:
answer.append(tree.searchTree(query))
return answer
정확성: 25.0
효율성: 75.0
합계: 100.0 / 100.0
코드 설명
Node class는 문자열의 token를 key로 갖고, 해당 노드를 거치는 word들의 길이를 나타내는 리스트를 length로 갖고, 자식노드를 나타내는 딕셔너리를 child로 갖습니다.
Trie class는 트리에 알고리즘을 적용시키기 위한 클래스입니다.
insertTree는 word의 token들을 노드에 차례로 넣고 함께 그 노드를 거치는 word들의 길이를 length 리스트에 넣어줍니다.
searchTree는 query의 token이 "?"가 나올 때까지 들어가서 "?"를 만나면 전 노드의 length에 찾고자하는 query 길이를 카운트하고 그 카운터 값을 리턴합니다.
solution function에서 "????o"와 같이 뒤에서 부터 검색하기 위한 reverse_tree를 따로 만들어 줍니다.
그리고 words에 대하여 모든 tree와 reverse_tree를 만듭니다.
그리고 queries에 대하여 query가 "?"로 시작하면 revers_tree에서 찾고 아니라면 tree에서 찾아서 answer 리스트에 추가합니다.
딕셔너리를 만든 후 words를 돌아서 특정 word가 만들어 지는 경우의 수 key에 대해 특정 word value를 가지거나 완전한 word만 가능할 때 answer에 key의 길이를 더해주면 될 것 같다고 생각했습니다.
from collections import defaultdict
def solution(words):
answer = 0
word_dict = defaultdict(list)
for word in words:
N = len(word)
for i in range(1, N+1):
word_dict[word[:i]] += [word]
for word in words:
N = len(word)
for i in range(1, N+1):
if len(word_dict[word[:i]]) == 1:
answer += i
break
else:
answer += N
return answer
정확성: 81.8
합계: 81.8 / 100.0
2차 시도
value에 가능한 word 리스트가 아닌 개수를 넣어서 len(word_dict[word[:i])의 시간을 줄여보려했습니다.
또한 먼저 word_dict[word]가 1가 아니라면 바로 answer에 word 길이를 더하여 word에 대해 for문이 도는 횟수를 줄이고자 했습니다.
from collections import defaultdict
def solution(words):
answer = 0
word_dict = defaultdict(int)
for word in words:
N = len(word)
for i in range(1, N+1):
word_dict[word[:i]] += 1
for word in words:
N = len(word)
if word_dict[word] != 1:
answer += N
continue
for i in range(1, N+1):
if word_dict[word[:i]] == 1:
answer += i
break
return answer
class Node(object):
def __init__(self, key, cnt = 0):
self.key = key
self.cnt = cnt
self.child = {}
class Trie():
def __init__(self):
self.head = Node(None)
def insertTree(self, string):
cur = self.head
for token in string:
if token not in cur.child:
cur.child[token] = Node(token)
cur = cur.child[token]
cur.cnt += 1
def searchTree(self, string):
cur = self.head
cnt = 0
for token in string:
cnt += 1
cur = cur.child[token]
if cur.cnt == 1:
return cnt
return len(string)
def solution(words):
answer = 0
wordTree = Trie()
for word in words:
wordTree.insertTree(word)
for word in words:
answer += wordTree.searchTree(word)
return answer