챌린지반 과제로 Spring scheduler를 이용한 어플리케이션을 자유 주제로 만들어오는 과제가 나왔다. 나는 Soft delete 정책을 세울 때마다 고민했던 댓글 관련 정책을 Scheduler로 만들어보기로 했다.
사실 이런 정책은 없을 것 같기도 한데 Comment가 User에 직접적인 연관을 맺지 않고 user_id만 기록해두고 있다가 nickname을 주기적으로 갱신하는 걸 진행해봤다.
@Scheduled(cron = "0 0/10 * * * *")
fun updateNicknames() {
commentService.updateNicknames()
}
// repository
override fun updateUserNicknames() {
queryFactory.update(comment)
.set(
comment.nickname,
JPAExpressions.select(user.nickname)
.from(user)
.where(user.id.eq(comment.userId))
)
.where(
JPAExpressions.selectOne()
.from(user)
.where(user.id.eq(comment.userId))
.exists()
)
.execute()
}
대충 10분마다 갱신하게 해봤고 user_id로 유저를 조회해 해당 닉네임을 comment의 nickname에 업데이트 한다. 유저가 탈퇴했더라도 댓글의 닉네임 정보는 쭉 남아있다. 댓글이 수백만개가 되면 당연히 문제가 생길 거지만 일단 정책의 목표를 다뤘으니 넘어가기로 했다.
실무에서 보면 싫어할 코드일 것이다.
@Scheduled(cron = "0 0 9 * * *")
fun deleteOldSoftDeletedComments() {
commentService.deleteOldSoftDeletedComments()
}
// repository
@Modifying
@Query(value = "DELETE FROM comment WHERE status = 'DELETED' AND deleted_at < :expireDate", nativeQuery = true)
fun deleteOldSoftDeletedComments(expireDate: LocalDateTime)
여태 Soft delete 해놓고 비워주진 않았는데 드디어 스케쥴러를 만들어봤다. 매일 오전 9시에 작동한다고 생각하고 특정 expireDate 이전이 되면 삭제해주는 방식이다.
묘하게 틀렸었는데 우선 QueryDSL로 작성했다가 @SQLRestriction 설정때문에 nativeQuery를 사용해야 해서 JpaRepository를 쓰게 만들었다.
그리고 @Modifying
을 설정해주지 않으면 @Query
로 발생하는 쿼리를 DML이라고 인식하질 않아서 붙여줘야 한다.
테스트 데이터를 매번 만들기 귀찮아서 강의에서 나온다는 Application runner로 Data initializer를 만들어봤다.
@Configuration
class CommentDataInitializer(
private val commentRepository: CommentRepository,
private val userRepository: UserRepository
) {
@Bean
fun run() = ApplicationRunner {
val user = User(
username = "testuser",
password = "password123",
nickname = "TestUser",
)
userRepository.save(user)
val comments = (1..10).map {
Comment(
content = "comment $it",
nickname = user.nickname,
userId = user.id
)
}
commentRepository.saveAll(comments)
}
}
이걸 해서 테스트 과정이 귀찮지 않았다.
챌린지반 과제는 짧게 끝냈고 복습 과제, 개선 과제라고 여러 주제를 담아둔 과제가 나왔는데 여태 배운 기술들을 응용하고 설정하는 느낌이라 아예 새 프로젝트로 진행중이다. 제출도 7월 1일까지라서 앞으로 배우는 내용을 새 프로젝트에 담는다고 보면 될 것 같다.
fun signin(@RequestBody request: SignInRequest, response: HttpServletResponse): ResponseEntity<SignInResponse> {
val signInResponse = userService.signIn(request)
val cookie = ResponseCookie.from("refreshToken", signInResponse.refreshToken)
.path("/")
.httpOnly(true)
.maxAge(7 * 24 * 60 * 60)
.sameSite("None")
.build()
response.addHeader("Set-Cookie", cookie.toString())
return ResponseEntity.ok(signInResponse.copy(refreshToken = "http-only"))
}
복습 과제에 로그인 성공 시, 로그인에 성공한 유저의 정보를 JWT를 활용하여 클라이언트에게 Cookie로 전달하기
라는 내용이 있길래 response에 cookie를 설정해서 넣어봤다. 보안상의 이유로 Token을 Http-only로 전달하는 건 자주 있는 경우같고 Https에만 전달하는 secured 옵션은 로컬에서만 테스트 하니까 켜지 않았다.
일반 Cookie
객체도 만들어 봤었는데 Swagger에서 안뜨길래 이걸로 바꿔보니 이것도 안떴다.
여러번 찾아봐도 코드 자체는 다 유사했고 아마 Swagger 테스트가 http-only 설정때문에 제대로 못읽나 싶어 일단 이대로 마무리했다.
피보나치 수는 F(0) = 0, F(1) = 1일 때, 1 이상의 n에 대하여 F(n) = F(n-1) + F(n-2) 가 적용되는 수 입니다.
예를들어
와 같이 이어집니다.
2 이상의 n이 입력되었을 때, n번째 피보나치 수를 1234567으로 나눈 나머지를 리턴하는 함수, solution을 완성해 주세요.
fun solution(n: Int) = fastFibonacci(n)
fun fastFibonacci(n: Int): Int {
if (n == 0) return 0
if (n == 1) return 1
var current = 1
var prev = 0
for (i in 2..n) {
val temp = current
current = (current + prev) % 1234567
prev = temp
}
return current
}
처음에는 당연히 가장 익숙한 재귀함수 방식을 골랐고 n의 최대 제한이 10만이라 그런지 틀렸다. 재귀함수 방식은 재귀함수를 설명하기 위한 예시인거지 효율적인 거랑은 거리가 먼 건가 생각이 들었다.
그래서 정직하게 O(n)으로 처리하기 위해 순회를 한 번만 돌리게 해서 이전 숫자랑 더하게 반환했는데 틀리는 경우가 나왔다. 여기선 Int 범위때문에 오버플로우 나는 걸테니 Long으로 바꿔줬는데 에러가 났다.
큰 숫자의 피보나치 숫자가 얼만큼 큰지는 몰라도 범위 초과 말고는 생각할 게 없으니 좀 고민하다가 문제에 맞게 원래 fastFibonacci(n) % 1234567
하던 부분을 current = (current + prev) % 1234567
으로 옮겨주니 제출에 성공했다.
수학을 잘 못하니 증명할 수식을 적을 수는 없겠지만 최종 값에 mod 한거랑 중간에 mod 한거랑 결국 같은 mod 범위 내의 숫자로 계속 갱신될 것이기에 문제가 원하는 답이 나올 거라고 얼추 떠올려볼 수 있었다.
여담으로 트러블 슈팅을 보면 알수있듯 mod를 잘못한게 문제였고 재귀함수는 문제가 아니었다. 다른 분이 푸신 tailrec 형태의 제출은 다음과 같다. 나는 tailrec을 쓰지 않고 일반 fib(n-1) + fib(n-2) 형태로 했는데 tailrec을 쓰려면 매개변수가 많아져서 관뒀다.
fun solution(n: Int): Int = if(n == 1 || n == 2) 1 else fib(n, 1, 1)
tailrec fun fib(n: Int, a: Int, b: Int): Int = if(n == 1) a else fib(n - 1, b % 1234567, (a + b) % 1234567)
Leo는 카펫을 사러 갔다가 아래 그림과 같이 중앙에는 노란색으로 칠해져 있고 테두리 1줄은 갈색으로 칠해져 있는 격자 모양 카펫을 봤습니다.
Leo는 집으로 돌아와서 아까 본 카펫의 노란색과 갈색으로 색칠된 격자의 개수는 기억했지만, 전체 카펫의 크기는 기억하지 못했습니다.
Leo가 본 카펫에서 갈색 격자의 수 brown, 노란색 격자의 수 yellow가 매개변수로 주어질 때 카펫의 가로, 세로 크기를 순서대로 배열에 담아 return 하도록 solution 함수를 작성해주세요.
fun solution(brown: Int, yellow: Int): IntArray {
var answer = intArrayOf()
val sum = brown + yellow
for (i in 1..sum) {
if (sum % i == 0) {
val width = sum / i
val height = i
if (width < height) {
continue
}
if ((width - 2) * (height - 2) == yellow) {
answer = intArrayOf(width, height)
break
}
}
}
return answer
}
이번 문제는 일단 sum과 가로 세로의 곱이 같은 것부터 생각해봤고 이제 노란색, 갈색의 구분이 필요했다. 그러면 테두리와 내부의 개수에 대한 공식도 있을 것 같은데 이건 수수께끼 문제 풀듯 상상력을 발휘하기가 좀 머리 아프긴 했다.
그래도 대충 생각해본 건 좌우 양끝 하나씩 해서 갈색 2개, 상하 양끝 하나씩 해서 갈색 2개 해서 width - 2, height - 2를 구해 곱하면 yellow의 값이 나오길래 이건가보다 싶어 제출하니 성공했다. 천만다행히도 공식 자체는 잘 구한 것 같다.
다만 코드가 못생겼는데 다른 코드를 보니 깔끔했다.
fun solution(brown: Int, red: Int): IntArray {
return (1..red)
.filter { red % it == 0 }
.first { brown == (red / it * 2) + (it * 2) + 4 }
.let { intArrayOf(red / it + 2, it + 2) }
}
(문제가 중간에 바뀌어 red로 표기되어 있다.)
수식 자체가 어려운 편은 아니었으니 위처럼 풀어도 되는 것 같다.