-
week1 과제에 대한 세미나장님/조교님들의 코멘트들을 읽어보던 중 궁금한 점이 있어 질문드립니다. 먼저, 이 질문의 핵심은 spring 프레임워크에 국한되는 것이 아니기에 최대한 여러 라벨을 달아두었습니다. 백엔드나 서버 라벨이 있어도 좋았을 듯 하네요. 질문에 들어가기에 앞서, 본 질문은 spring 세미나의 week1 과제를 구체적인 예시로 들고 있습니다. spring 세미나를 듣지 않는 분들을 위해 spring 세미나장님이 만들어주신 ER 다이어그램을 공유합니다. 여기서, 특정 id를 갖는 playlist 조회 api를 구현하는 게 과제의 일부였으며, 해당 api의 response는 다음과 같습니다. {
"playlist": {
"id": number,
"title": string,
"subtitle": string,
"image": string,
"songs": [
{
"id": number,
"title": string,
"artists": [
{
"id": number,
"name": string
},
...
],
"album": string,
"image": string,
"duration": string
},
...
]
},
"isLiked": boolean
} 이때, 데이터를 빠르게 가져오기 위해 데이터베이스의 JOIN 기능을 써야한다는 것은 명확합니다. 하지만, 때때로 JOIN을 애플리케이션 레벨에서 진행하는 것이 효율적일 수도 있다고 말씀해주셨습니다. 좀 더 구체적으로 설명해보자면, Spring과 JPA 기준으로 이 두 방법의 구현은 다음과 같습니다. 구현하나, server-side JOIN만 사용하기먼저, 다음과 같이 interface PlaylistRepository : JpaRepository<PlaylistEntity, Long> {
@Query(
"""
SELECT p FROM playlists p
LEFT JOIN FETCH p.playlistSongs ps
LEFT JOIN FETCH ps.song s
LEFT JOIN FETCH s.songArtists sa
LEFT JOIN FETCH sa.artist
LEFT JOIN FETCH s.album
LEFT JOIN FETCH s.album.artist
WHERE p.id = :id
"""
)
fun findByIdOrNull(id: Long): PlaylistEntity?
} service layer에서는 단순히 갖다 쓰기만 합니다. class PlaylistServiceImpl(
private val playlistGroupRepository: PlaylistGroupRepository,
private val playlistRepository: PlaylistRepository,
) : PlaylistService {
..
override fun get(id: Long): Playlist =
playlistRepository.findByIdOrNull(id)?.toPlaylist()
?: throw PlaylistNotFoundException()
}
이 방법은 client-side JOIN은 전혀 사용하지 않으며, database에게 모든 데이터의 구성을 위임합니다. 두번째 방법은 세미나장님께서 위와 같이 구현한 코드에 조언해주신 방법으로, 질의를 쪼개서 클라이언트에서 JOIN을 수행하는 방법입니다. 둘, client-side JOIN 섞어쓰기먼저, playlist repository를 단순히 정의하고, song repository를 별도로 정의합니다. interface PlaylistRepository : JpaRepository<PlaylistEntity, Long> {
@Query(
"""
SELECT p FROM playlists p
LEFT JOIN FETCH p.playlistSongs ps
WHERE p.id = :id
"""
)
fun findByIdOrNull(id: Long): PlaylistEntity?
}
interface SongRepository : JpaRepository<SongEntity, Long> {
..
@Query(
"""SELECT s FROM songs s
LEFT JOIN FETCH s.album
LEFT JOIN FETCH s.songArtists sa
LEFT JOIN FETCH sa.artist
WHERE s.id IN :ids"""
)
fun findAllByIdInWithJoinFetch(ids: List<Long>): List<SongEntity>
} 그리고, service layer 에서 둘을 조합합니다. class PlaylistServiceImpl(
private val playlistGroupRepository: PlaylistGroupRepository,
private val playlistRepository: PlaylistRepository,
private val songRepository: SongRepository,
) : PlaylistService {
..
override fun get(id: Long): Playlist {
val playlist = playlistRepository.findByIdOrNull(id) ?: throw PlaylistNotFoundException()
val songs = songRepository.findAllByIdInWithJoinFetch(playlist.playlistSongs.map { it.song.id })
return playlist.toPlaylist(songs)
}
} 실험 방법데이터의 종류와 양, 코드 퀄리티, 부하 정도, 등등 환경에 따라 결과가 천차만별일 수 있지만 어떤 방법이 빠른지에 대해 간단하게 실험을 진행해보았습니다. 세미나장님이 만들어주신 데이터를 기준으로, 가장 많은 song이 포함되어있는 15번 플레이리스트를 초당 100번의 요청을 1분동안 진행시켰습니다. 데이터베이스는 H2와 MySQL을 사용해보았습니다. 실험 결과server-side JOIN (H2)client-side JOIN (H2)server-side JOIN (MySQL)client-side JOIN (MySQL)결론 및 질문간단하게 위와 같이 실험을 진행해보았는데, 조잡하기 짝이 없는 실험이지만 그럼에도 결과가 비슷하거나 오히려 서버 사이드 조인을 사용하는 것이 소폭 빠른 모습을 보여주었습니다. 데이터베이스 측의 부하는 따로 측정해보진 않았지만 쿼리 수가 2배인 만큼 클라이언트 사이드의 부하가 더 클 것이라고 예상됩니다. 게다가 현재는 한 플레이리스트 당 곡의 수가 30~40 개 정도밖에 안 되지만, 만약 곡의 수가 수백, 수천 개라면 질의를 날릴 때 그리고 현대적인 데이터베이스에서는 우리가 작성하는 SQL이 복잡하거나 비효율적으로 보일지라도, 실제로는 내부적으로 optimizer를 거치면서 execution plan이 최적화되기 때문에 cartesian product와 같은 큰 비용의 작업은 거의 발생하지 않는다고 알고 있습니다. 그래서 제 짧은 생각으로는 클라이언트 사이드가 아니라 서버 사이드 JOIN을 사용하는 것이 대부분의 경우에 좋다고 생각되는데요, 그럼에도 클라이언트 사이드의 JOIN을 사용하는 것이 좋을 수 있다면 어떤 경우가 있을지가 궁금합니다. |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 4 replies
-
일반적으로는 DB에서 처리하는 게 성능상 좋은 건 맞습니다. 저도 대부분 서버에서 조인해서 사용합니다.
세미나장님이 어떤 맥락으로 말했는지 몰라서 생각나는거 다 썼습니다. 추가적으로 저도 DB JOIN을 쓰는 것을 선호하고요. JPA와 같은 ORM을 싫어합니다ㅋㅋ |
Beta Was this translation helpful? Give feedback.
일반적으로는 DB에서 처리하는 게 성능상 좋은 건 맞습니다. 저도 대부분 서버에서 조인해서 사용합니다.
다만 클라이언트 상에서 조인하는 경우가 몇가지 있을 수 있는데요
코드 작성의 편의 + 모듈화 가능
지금 세미나에선 Spring data jpa 쓰는 걸로 아는데요
JPA 등의 ORM 에서 중시하는 건 개발자가 SQL 단을 크게 신경 안써도 쓸 수 있도록 되어있습니다. 이로 인해 생산성이 높아질 수 있습니다.
Mybatis등에서는 데이터 접근시마다 대응하는 sql을 작성해야하는 것이 문제였습니다. 이로 인해 모든 DB 접근마다 다른 메소드를 사용하게 되고 코드의 모듈화가 매우 힘들어지는 결과가 생겼습니다.
이를 해결하기 위해 최적화를 포기하고 ORM을 쓰기 시작했고요 (저도 TB 단위의 데이터가 저장되어 있는 게 아니라면 DB 최적화는 크게 필요없다고 생각합니다. )
다만 최적화까지 신경쓰면 ORM이랑 섞어쓸 때 너무 깊게 알아야해서 최적화까지 신경쓰신다면 다른거 쓰는 것 추천합니다ㅋㅋ
RDB 리소스 문제
위와는 정반대의 이유로 엄청난 크기의 데이터가 저장된 DB에서는 JOIN 이 부담스러운 경우도 존재합니다.
RDB는 scale out이 안되지만 (vitess등 어느정도의 scale out을 지원하는 RDB도 존재하긴 합니다.) 서버는 scale out이 가능해서 DB CPU 사용량이 높은 경우에는 JOIN을 안하고 가…