240621 JPA 심화 - 진행한 부분까지 과제하기

노재원·2024년 6월 21일
0

내일배움캠프

목록 보기
66/90

Spring scheduler 과제하기

챌린지반 과제로 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에 업데이트 한다. 유저가 탈퇴했더라도 댓글의 닉네임 정보는 쭉 남아있다. 댓글이 수백만개가 되면 당연히 문제가 생길 거지만 일단 정책의 목표를 다뤘으니 넘어가기로 했다.
실무에서 보면 싫어할 코드일 것이다.

Soft delete 상태의 댓글중 오래된 댓글 삭제하기

@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로 초기 값 설정하기

테스트 데이터를 매번 만들기 귀찮아서 강의에서 나온다는 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일까지라서 앞으로 배우는 내용을 새 프로젝트에 담는다고 보면 될 것 같다.

Github repo

Http-only Cookie에 Refresh token 담기

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) 가 적용되는 수 입니다.

예를들어

  • F(2) = F(0) + F(1) = 0 + 1 = 1
  • F(3) = F(1) + F(2) = 1 + 1 = 2
  • F(4) = F(2) + F(3) = 1 + 2 = 3
  • F(5) = F(3) + F(4) = 2 + 3 = 5

와 같이 이어집니다.

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줄은 갈색으로 칠해져 있는 격자 모양 카펫을 봤습니다.

carpet.png

Leo는 집으로 돌아와서 아까 본 카펫의 노란색과 갈색으로 색칠된 격자의 개수는 기억했지만, 전체 카펫의 크기는 기억하지 못했습니다.

Leo가 본 카펫에서 갈색 격자의 수 brown, 노란색 격자의 수 yellow가 매개변수로 주어질 때 카펫의 가로, 세로 크기를 순서대로 배열에 담아 return 하도록 solution 함수를 작성해주세요.

제한사항
  • 갈색 격자의 수 brown은 8 이상 5,000 이하인 자연수입니다.
  • 노란색 격자의 수 yellow는 1 이상 2,000,000 이하인 자연수입니다.
  • 카펫의 가로 길이는 세로 길이와 같거나, 세로 길이보다 깁니다.

문제 링크

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로 표기되어 있다.)
수식 자체가 어려운 편은 아니었으니 위처럼 풀어도 되는 것 같다.

0개의 댓글