55일차 같은 56일차,,ㅎㅎ
오늘은 코드카타 SQL 70번 문제와 알고리즘 62번 문제를 풀고, Spring 플러스 프로젝트의 Lv.3-1과 3-2를 마무리했다!
오늘은 1개의 SQL 문제를 풀었는데, 지난 번에 풀었던 것과 비슷하게 리뷰를 가장 많이 쓴 사람들의 리뷰 목록을 출력하는 것이었다.
max()와 count()를 함께 사용할 수 있을 줄 알았지만, max(count())와 같은 형식으로는 사용하지 못한다고 한다.
그래서 각 멤버의 리뷰 수를 계산하는 서브쿼리, max를 계산하는 서브쿼리, max와 리뷰 합계가 같은 멤버의 id 목록을 출력하는 서브쿼리로 총 3개의 서브쿼리를 만들었고,
메인 쿼리에서는 멤버 id가 계산한 멤버 id 목록에 포함된 경우만 출력하도록 쿼리문을 만들었다.
쿼리 안에 서브쿼리, 그 안에 서브쿼리, 그 안에 서브쿼리를 만들어야 했기에 조금 까다로웠는데, 그래도 많은 시간이 걸리지는 않았다.
오늘 드디어 알고리즘 문제를 풀었는데, 계속 해결하지 못했던 문제이다.
아기가 발음할 수 있는 옹알이 4가지가 주어졌는데, 아기는 한 단어를 반복해서 말하지는 못하지만, 서로 다른 단어는 연속해서 발음할 수 있다.
여러 단어의 배열이 주어졌을 때, 이 중 아기가 발음할 수 있는 단어가 몇개인지를 구하는 문제였다.
나는 while문과 for문을 섞어가며 같은 문자가 반복되는지 확인하고, 단어 중 가능한 문자 배열에 해당하지 않는 문자가 있는지를 확인했었다.
그런데 이렇게 풀었을 때 문제 자체를 해결할 수는 있었지만, 대부분의 테스트에서 시간 초과가 되어 테스트에 통과할 수 없었다.
이 한 문제를 너무 오래 고민했었기에 결국 그냥 다른 사람의 풀이를 참고하기로 했는데, 다른 사람의 풀이를 보니 정말 간단하더라.
일단 단어가 연속된다면 무조건 2번 이상일 것이기에, 2번 연속된 단어는 무조건 이후의 연산을 진행하지 않도록 제한하고, 가능한 단어가 있다면 무조건 " " 공백으로 바꾸었다.
최종 연산 결과가 공백이 아니라면 불가능한 단어가 있는 것이고, 최종 결과가 공백 뿐이라면 모두 가능한 단어만 존재한다는 뜻이다.
나는 가능하지 않은 단어를 제거하려고 생각했는데, 가능한 단어를 제거하려고 생각한다면 굉장히 간단하게 해결할 수 있는 문제였다.
언제나 발상의 전환이 문제 풀이의 핵심이던데.. 문제를 풀 때 다른 접근 방법이 없는지 생각해봐야겠다.
오늘 푼 문제와 풀이는 깃허브를 통해 업로드해두었다.
GitHub 보러가기
오늘부터는 Spring 플러스 프로젝트의 도전과제를 시작했다!
도전과제는 Lv.3과 Lv.4로 나뉘어져 있는데, Lv.4는 코틀린을 적용해보는 내용이기에 이 부분은 나중에 코틀린 강의를 다 듣고 진행할 예정이다.
Lv.3-1 : QueryDSL을 활용한 검색 API 구현
Lv.3-2 : Transaction 옵션을 활용하여 독립된 로그 생성
Lv.3-3 : AWS를 사용한 프로젝트 관리와 배포
Lv.3-4 : 대용량 데이터 처리를 위한 쿼리 성능 개선
Lv.3은 이렇게 총 4가지 단계로 구성되어 있는데, 오늘은 이 중 Lv.3-1, 3-2를 마무리했다.
먼저, QueryDSL을 활용한 검색 API 구현은 사실 강의 실습을 통해 몇번 진행했던 적이 있어서 쿼리문을 만드는 것은 어렵지 않았다.
하지만, 요구사항 중 반드시 DTO 프로젝션을 사용해야 하는데, DTO에 일정에 등록된 매니저 수와 댓글 수를 반환하라는 내용이 있었다.
이전에는 todo.getManagers.size()를 활용하면 손쉽게 List의 사이즈를 알아낼 수 있었는데, QueryDSL에서는 Q타입 클래스를 사용하기도 하고, 왜인지 모르겠지만, size()를 사용할 수가 없었다.
찾아보니, QueryDSL에서 리스트의 사이즈를 사용하려면, 별도의 서브쿼리를 만들어야 한다고 하더라.
List<TodoSearchResponse> responseList = queryFactory
.select(Projections.constructor(TodoSearchResponse.class,
todo.title,
ExpressionUtils.as(
JPAExpressions.select(manager.count())
.from(manager)
.where(manager.todo.eq(todo)),
"managers"
),
ExpressionUtils.as(
JPAExpressions.select(comment.count())
.from(comment)
.where(comment.todo.eq(todo)),
"comments"
)
))
.from(todo)
...
}
그래서 결국은 필요한 매니저 수와 댓글 수를 구하기 위해 각각의 개수를 계산하는 서브쿼리를 만들었다.
그래도 이렇게 서브쿼리를 만들고 나니 다행히 모든 값이 정확히 계산되어 출력되는 것을 확인할 수 있었다.
왜 size()를 사용할 수 없는지에 대해서 조금 더 자세히 알아봐야겠다.
Lv.3-2에서는 Transaction의 옵션을 사용해 매니저 등록에 관한 내용을 로깅하는 것을 요구했다.
사실 이전 팀프로젝트에서 로그 부분을 내가 맡았었기 때문에, 이 문제는 굉장히 쉽게 해결할 수 있었다.
문제에서는 매니저 등록과 로그 기록이 완전히 독립되어 동작할 수 있게 구현하라고 하였다.
즉, 매니저 등록에 성공하든 실패하든 로그는 기록되어야 한다는 뜻이다.
그래서 일단 AOP를 활용해 매니저 등록 메서드를 포인트컷으로 잡아두었고,
try {
result = joinPoint.proceed();
} catch (Throwable throwable) {
success = false;
description = throwable.getMessage();
throw throwable;
} finally {
Log log = new Log(loginUserId, targetUserId, todoId, success, description);
logService.save(log);
}
try-catch-finally를 통해 예외가 발생하든, 발생하지 않든 로그를 남길 수 있도록 구현하였다.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(Log log) {
logRepository.save(log);
}
그리고, 로그 서비스의 save 메서드는 매번 새로운 트랜잭션을 생성하여 작업을 진행하도록 설정해주었다.
다만, 내가 구현한 방법의 문제점이 하나 있었는데, 매니저 등록의 성공 여부와 상관없이 로그는 기록되지만, 만약 로그 기록에 실패한다면 매니저도 등록되지 않는다는 것이다.
처음에는 이걸 해결하기 위해 logRepository.save() 메서드를 try-catch문으로 감싸야할까 생각했다.
하지만, 내가 내린 결론은 지금 상황을 유지하여 로그 기록 실패 시, 매니저 등록도 중단하도록 하는 것이었다.
왜냐하면 매니저 등록 성공 여부와 상관없이 로그를 남기라는 것은 로그 기록이 매니저 등록보다 우선이라는 뜻일 것이다.
그렇기에 만약 시스템 어딘가에 문제가 생겨 로그가 기록되지 않는다면, 다른 작업 또한 중단되어야 한다고 판단했다.
로그 엔티티에 어떤 내용들을 담을지 고민하다가 시간을 조금 쓰기는 했지만, 그래도 그 외에는 큰 어려움 없이 문제를 해결할 수 있었다.
이제 다음 단계인 AWS를 통한 관리와 배포를 진행하기 위해 AWS 강의를 듣고 있다..
빨리 강의를 듣고 내일 다음 단계를 시작해봐야겠다.
내가 작성한 코드는 깃허브에 업로드해두었다.
GitHub 보러가기
지난주 금요일부터 지난 주말까지 너무 신나게 놀았더니 오늘 너무 너무 공부하기가 싫더라..
아까 낮에 잠깐 쉬었다 오려고 침대에 누웠는데, 개꿀잠 잤다..
연말이라 계속 약속도 많고, 놀 일이 많아서 계속 해이해지고 있는데, 공휴일에 놀려면 빨리 과제 끝내버려야겠다..