Today's Codekata
// 문자열 나누기
class Solution {
public int solution(String s) {
int answer = 0;
int i = 0;
while (i < s.length()) {
int num1 = 1, num2 = 0;
for (int j = i + 1; j < s.length(); j++) {
if (s.charAt(i) == s.charAt(j)) {
num1++;
} else {
num2++;
}
if (num1 == num2) {
break;
}
}
answer++;
i += num1 + num2;
}
return answer;
}
}
// 대충 만든 자판
class Solution {
public int[] solution(String[] keymap, String[] targets) {
int[] answer = new int[targets.length];
Map<Character, Integer> minPress = new HashMap<>();
for (String key : keymap) {
for (int i = 0; i < key.length(); i++) {
minPress.put(key.charAt(i), Math.min(minPress.getOrDefault(key.charAt(i), 101), i + 1));
}
}
for (int i = 0; i < targets.length; i++) {
for (char c : targets[i].toCharArray()) {
if (!minPress.containsKey(c)) {
answer[i] = -1;
break;
}
answer[i] += minPress.get(c);
}
}
return answer;
}
}
이번 문제를 풀면서 처음으로 Map 자료구조를 직접 활용해봤다. 그동안 개념적으로만 알고 있었던 Map이 실제로 얼마나 유용한지 몸소 느낄 수 있었던 경험이었다. 처음엔 Map이 복잡하게 느껴졌지만, 막상 써보니 코드가 훨씬 깔끔해지고 로직도 명확해졌다. 앞으로는 조건 기반 계산이나 누적 처리할 때 Map을 적극적으로 활용해볼 생각이다.
-- 상품을 구매한 회원 비율 구하기
SELECT YEAR(O.SALES_DATE) AS YEAR, MONTH(O.SALES_DATE) AS MONTH,
COUNT(DISTINCT O.USER_ID) AS PURCHASED_USERS,
ROUND(COUNT(DISTINCT O.USER_ID) / T.TOTAL_USERS, 1) AS PURCHASED_RATIO
FROM ONLINE_SALE O
JOIN USER_INFO U ON O.USER_ID = U.USER_ID
JOIN (
SELECT COUNT(*) AS TOTAL_USERS
FROM USER_INFO
WHERE YEAR(JOINED) = 2021
) T
WHERE YEAR(U.JOINED) = 2021
GROUP BY YEAR, MONTH, T.TOTAL_USERS
ORDER BY YEAR, MONTH;
# Recyclable and Low Fat Products
SELECT product_id
FROM Products
WHERE low_fats = 'Y' AND recyclable = 'Y';
# Find Customer Referee
select name
from Customer
where referee_id != 2
or referee_id is null;
일정 관리 앱
리팩토링
프로젝트를 구현하며 배웠던 것들을 토대로 같은 프로젝트를 하나 더 만들어보며 되새기는 시간을 가졌다. 그 과정 중에 내가 놓쳤던 부분들을 되짚고 개선하는 시간을 가졌다.
return new ResponseEntity<>(schedule, HttpStatus.CREATED);
return new ResponseEntity<>(schedule, HttpStatus.OK);
↓↓↓
return ResponseEntity.status(HttpStatus.CREATED).body(userResponse);
return ResponseEntity.ok(schedule);
먼저 두 방식은 같은 HTTP 응답을 반환하지만, 변경해준 방식이 더 간결하고 직관적으로 보여 전체적으로 변경해줬다. 사용하기에도 더 편하게 느껴졌고, 전체적으로 코드를 일관되게 유지하기도 쉬워졌다.
@Transactional(readOnly = true)
public User login(LoginRequest request) {
User user = userRepository.findByEmail(request.email()).orElseThrow(
() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 이메일입니다."));
if (!passwordEncoder.matches(request.password(), user.getPassword())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다.");
}
return user;
}
처음엔 이메일과 비밀번호 중 어떤 부분이 틀렸는지를 명확하게 알려주는 것이 사용자에게 더 친절하다고 생각했다. 하지만 테스트를 거치며 보안적인 관점에서 봤을 때는 이 방식이 오히려 위험할 수 있다는 사실을 알게 되었다. 그래서 아래처럼 리팩토링 해봤다.
@Transactional(readOnly = true)
public User login(LoginRequest request) {
User user = userRepository.findByEmail(request.email());
if (user == null || !passwordEncoder.matches(request.password(), user.getPassword())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "이메일 또는 비밀번호가 일치하지 않습니다.");
}
return user;
}
왜 수많은 서비스들이 로그인 실패 시 "아이디 또는 비밀번호가 올바르지 않습니다."라고 안내하는지, 이제야 이해할 수 있는 순간이었다. 불필요한 정보 유출을 막는 것이 중요한다는 걸 배웠다. 그리고 코드도 더 간결해져서 가독성까지 좋아졌다.
마치며
외에도 변수명을 더 명확하게 수정해줬고, 불필요한 중간 변수들을 제거하면서 코드 흐름이 더 간결해지고, 읽는 시간이 줄어들었다. 이런 사소한 리팩토링이 쌓이니 전체적인 가독성과 유지보수성이 좋아졌다. 또 로그인 로직을 리팩토링하면서, 단순히 사용자에게 친절한 메시지보다 정보를 감춰 보안을 지키는 것이 더 친절한 방식일 수도 있다는 생각을 했다. 이메일 존재 여부를 노출하지 않는 방식으로 바꾸면서, 실제 서비스들이 왜 그렇게 만들어져 있는지 잘 이해할 수 있었다. 그리고 컨트롤러와 서비스의 책임을 더 명확하게 나눠줬고, 응답 구조를 일관되게 만들어서 완성도가 더 높아짐을 체감했다. 오늘 리팩토링은 개발자로서의 사고방식을 조금 더 성숙하게 만들어준 시간이었다.