반응형

데이터의 Total 개수와 해당 페이지에 해당하는 요소를 데이터베이스로부터 추출해 응답해야 하는 API를 개발해야 했다.

보통은 이런 것을 JPA의 페이징을 이용해서 만든다.

근데, 이것을 JDBC로 만들어보고 JPA보다 빠른지 궁금해져서 만들며 비교를 해보려고 한다.

각각 파이썬으로 데이터를 1000개 정도 넣고 테스트 해보았다.

 

  • JDBC

우선 JDBC이다.

사용한 코드는 아래와 같다.

총 개수와 해당 요소들을 가져오는 쿼리문을 2번 날리게 된다.

 

Service

override fun professorGetWritingList(
        assignmentId: Int,
        page: Int,
        pageSize: Int
    ): ResponseEntity<CoBoResponseDto<ProfessorGetWritingListRes>> {
        val startTime = System.currentTimeMillis()
        val totalElements = writingRepository.countByAssignmentIdWithJDBC(assignmentId)
        val writings =
            writingRepository.findByAssignmentIdOrderByStatePagingWithJDBC(
                assignmentId = assignmentId,
                page = page,
                pageSize = pageSize
            ).map{
                ProfessorGetWritingLisElementRes(
                    studentId = it.studentId,
                    createdAt = it.createdAt!!,
                    updatedAt = it.updatedAt!!,
                    writingState = it.state.value
                )
            }

        println("TIME: ${System.currentTimeMillis() - startTime}ms")

        return CoBoResponse(
            data = ProfessorGetWritingListRes(
                totalElements = totalElements,
                writings = writings
            ),
            coBoResponseStatus = CoBoResponseStatus.SUCCESS
        ).getResponseEntity()
    }

 

Repository

    override fun findByAssignmentIdOrderByStatePagingWithJDBC(assignmentId: Int, page: Int, pageSize: Int): List<Writing> {
        val sql = "SELECT writing.id, writing.student_id, writing.assignment_id, writing.content, writing.state, writing.created_at, writing.updated_at, writing.submitted_at " +
                "FROM writing WHERE writing.assignment_id = ? ORDER BY writing.state LIMIT ?, ?"
        return jdbcTemplate.query(
            sql, {rs, _ -> writingRowMapper(rs)}, assignmentId, page, pageSize
        )
    }

    override fun countByAssignmentIdWithJDBC(assignmentId: Int): Long {
        return jdbcTemplate.queryForObject("SELECT count(*) FROM writing WHERE assignment_id = ?", Long::class.java, assignmentId)!!

    }

    private fun writingRowMapper(resultSet: ResultSet): Writing {
        return Writing(
            id = resultSet.getInt("id"),
            studentId = resultSet.getString("student_id"),
            assignment = Assignment(resultSet.getInt("assignment_id")),
            content = resultSet.getString("content"),
            state = WritingStateEnum.from(resultSet.getShort("state"))!!,
            createdAt = resultSet.getTimestamp("created_at").toLocalDateTime(),
            updatedAt = resultSet.getTimestamp("updated_at").toLocalDateTime(),
            submittedAt = resultSet.getTimestamp("submitted_at").toLocalDateTime()
        )
    }

 

0페이지에 요소를 20개로 속도를 측정해보면

요소 20개 요소 100개
39MS 47MS
41MS 46MS
44MS 48MS
38MS 40MS

 

이 정도의 속도가 나왔다, 역시 요소가 많아질수록 시간이 오래 걸리기는 한다.

 

  • JPA

이번에는 간편한 JPA이다.

간단하게 PageRequest로 페이징해서 거기에서 totalElements와 요소들을 가져왔다.

override fun professorGetWritingList(
        assignmentId: Int,
        page: Int,
        pageSize: Int
    ): ResponseEntity<CoBoResponseDto<ProfessorGetWritingListRes>> {
        val startTime = System.currentTimeMillis()
        val jpaList = writingRepository.findByAssignment(Assignment(assignmentId), PageRequest.of(page, pageSize))

        val totalElements = jpaList.totalElements
        val writings = jpaList.content.map {
            ProfessorGetWritingListElementRes(
                it.studentId, it.createdAt!!, it.updatedAt!!, it.state.value
            )
        }

        println("TIME: ${System.currentTimeMillis() - startTime}ms")

        return CoBoResponse(
            data = ProfessorGetWritingListRes(
                totalElements = totalElements,
                writings = writings
            ),
            coBoResponseStatus = CoBoResponseStatus.SUCCESS
        ).getResponseEntity()
    }

 

전과 동일하게 측정해보자

요소 20개 요소 100개
128MS 81MS
63MS 76MS
61MS 68MS
61MS 69MS

 

확실히 JDBC로 호출하는 것이 빠른 것을 볼 수 있다.

 

사용자가 얼마 없으면 JPA로 개발하고, 사용자가 많아지면 JDBC로 개발할 것 같다.

이번은 이미 JDBC 코드로 만들어놔서 JPA를 사용하지는 않을 것 같다.

 

  • JDBC 비동기

마지막으로 JDBC에서 호출하던 2개의 쿼리문을 비동기로 동시에 호출해보자.

 

JDBC 서비스 코드에서 아래같이 수정했다.

override fun professorGetWritingList(
        assignmentId: Int,
        page: Int,
        pageSize: Int
    ): ResponseEntity<CoBoResponseDto<ProfessorGetWritingListRes>> {

        val startTime = System.currentTimeMillis()

        val totalElementsCompletableFuture = CompletableFuture.supplyAsync {
            writingRepository.countByAssignmentIdWithJDBC(assignmentId)
        }

        val writingsCompletableFuture = CompletableFuture.supplyAsync {
            writingRepository.findByAssignmentIdOrderByStatePagingWithJDBC(
                assignmentId = assignmentId,
                page = page,
                pageSize = pageSize
            ).map{
                ProfessorGetWritingListElementRes(
                    studentId = it.studentId,
                    createdAt = it.createdAt!!,
                    updatedAt = it.updatedAt!!,
                    writingState = it.state.value
                )
            }
        }

        val coboResponse = CoBoResponse(
            data = ProfessorGetWritingListRes(
                totalElements = totalElementsCompletableFuture.get(),
                writings = writingsCompletableFuture.get()
            ),
            coBoResponseStatus = CoBoResponseStatus.SUCCESS
        )

        println("TIME: ${System.currentTimeMillis() - startTime}ms")

        return coboResponse.getResponseEntity()
    }

 

실행해보면

요소 20개 요소 100개
35MS 46MS
37MS 31MS
29MS 21MS
30MS 26MS

 

이렇게 말도 안되는 속도가 나온다.

물론 가장 빠르기는 하지만, 그냥 JDBC를 사용했을 때와 비슷한 거 같기도....?

 

확실히 JPA가 편하고 비동기가 강력한 것 같다.

반응형

DB를 사용하다보면 이런 경우를 많이 보게 될 것이다.

 

만약 해당 PK를 가지는 데이터가 있다면 해당 데이터를 update, 해당 PK를 가지는 데이터가 없다면 해당 데이터를 update

 

이 로직을 JPA로 처리하려면 findById로 해당 데이터를 찾고, if-else를 사용해서 데이터를 저장하는 알고리즘을 만들게 된다.

 

 이 알고리즘은 굉장히 간단하다.

 

하지만 데이터베이스를 2번 접근해야 한다는 문제가 있다.

비록 아직 주니어이지만, 데이터베이스를 많이 접근하게 된다면 응답속도에 굉장한 문제가 있다는 것을 알고 있다.

그리고 데이터베이스의 접근을 최소한으로 줄이고 싶었다.

 

그래서 JDBC로 쿼리문을 한줄로 작성하여 요청하는 방법을 사용하였다.

 

    override fun ifExistUpdateElseInsert(chatRoom: ChatRoom) {
        jdbcTemplate.update(
            "INSERT INTO chat_room (id, chat_state_enum) " +
                    "VALUES (?, ?) " +
                    "ON DUPLICATE KEY UPDATE chat_state_enum = VALUES(chat_state_enum)",
            chatRoom.id, chatRoom.chatStateEnum.value)
    }

 

위와 같은 코드를 작성했다.

DUPLICATE KEY UPDATE를 사용하는 방법이다.

 

이러면 해당 PK가 있을 경우, PK를 제외한 나머지 데이터를 업데이트하게 된다.

 

이 방법을 사용하면 데이터베이스에 한번만 접근하기 때문에 JPA로 두번 접근하는 방법보다는 훨씬 빠를 것이다.

 

하지만 DUPLICATE KEY UPDATE 방법이 데이터베이스에 부하를 주기 때문에, 때로는 JPA로 2번 접근하는 것이 더 좋은 방법일 수도 있다고 한다.

운영중인 서버 환경을 잘 파악해 이 방법 중 적절한 방법을 사용해야 할 것 같다.

+ Recent posts