Skip to content

Commit 1a8e813

Browse files
committed
prepare week2
1 parent adbc14a commit 1a8e813

File tree

7 files changed

+185
-9
lines changed

7 files changed

+185
-9
lines changed

src/main/kotlin/CacheConfig.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.wafflestudio.seminar.spring2023
2+
3+
import com.github.benmanes.caffeine.cache.Caffeine
4+
import org.springframework.boot.context.properties.ConfigurationProperties
5+
import org.springframework.boot.context.properties.EnableConfigurationProperties
6+
import org.springframework.context.annotation.Bean
7+
import org.springframework.context.annotation.Configuration
8+
import java.time.Duration
9+
10+
@EnableConfigurationProperties(CacheProperties::class)
11+
@Configuration
12+
class CacheConfig(
13+
private val cacheProperties: CacheProperties,
14+
) {
15+
16+
@Bean
17+
fun cache(): Caffeine<Any, Any> {
18+
return Caffeine.newBuilder()
19+
.maximumSize(cacheProperties.size)
20+
.expireAfterWrite(cacheProperties.ttl)
21+
}
22+
}
23+
24+
@ConfigurationProperties("cache")
25+
data class CacheProperties(
26+
val ttl: Duration,
27+
val size: Long,
28+
)

src/main/kotlin/playlist/service/PlaylistLikeService.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ package com.wafflestudio.seminar.spring2023.playlist.service
33
interface PlaylistLikeService {
44
fun exists(playlistId: Long, userId: Long): Boolean
55
fun create(playlistId: Long, userId: Long)
6+
fun createSynchronized(playlistId: Long, userId: Long)
67
fun delete(playlistId: Long, userId: Long)
78
}

src/main/kotlin/playlist/service/PlaylistLikeServiceImpl.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ class PlaylistLikeServiceImpl(
3232
)
3333
}
3434

35+
@Synchronized
36+
override fun createSynchronized(playlistId: Long, userId: Long) {
37+
create(playlistId, userId)
38+
}
39+
3540
override fun delete(playlistId: Long, userId: Long) {
3641
val like = playlistLikeRepository.findByPlaylistIdAndUserId(playlistId = playlistId, userId = userId)
3742
?: throw PlaylistNeverLikedException()

src/main/kotlin/playlist/service/PlaylistServiceCacheImpl.kt

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,16 @@ package com.wafflestudio.seminar.spring2023.playlist.service
22

33
import com.github.benmanes.caffeine.cache.Caffeine
44
import org.springframework.stereotype.Service
5-
import java.time.Duration
65

76
@Service
87
class PlaylistServiceCacheImpl(
98
private val impl: PlaylistServiceImpl,
9+
cacheBuilder: Caffeine<Any, Any>,
1010
) : PlaylistService {
1111

12-
private val playlistGroupsCache = Caffeine.newBuilder()
13-
.maximumSize(1)
14-
.expireAfterWrite(Duration.ofSeconds(10))
15-
.build<Unit, List<PlaylistGroup>>()
12+
private val playlistGroupsCache = cacheBuilder.build<Unit, List<PlaylistGroup>>()
1613

17-
private val playlistCache = Caffeine.newBuilder()
18-
.maximumSize(1000)
19-
.expireAfterWrite(Duration.ofSeconds(10))
20-
.build<Long, Playlist>()
14+
private val playlistCache = cacheBuilder.build<Long, Playlist>()
2115

2216
override fun getGroups(): List<PlaylistGroup> {
2317
val cached = playlistGroupsCache.getIfPresent(Unit)

src/main/resources/application.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,8 @@ spring:
1414
jpa:
1515
defer-datasource-initialization: true
1616
show-sql: true
17+
open-in-view: false
18+
19+
cache:
20+
ttl: 10s
21+
size: 100
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.wafflestudio.seminar.spring2023.week2
2+
3+
import com.wafflestudio.seminar.spring2023.QueryCounter
4+
import com.wafflestudio.seminar.spring2023.QueryCounter.Result
5+
import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistRepository
6+
import com.wafflestudio.seminar.spring2023.song.repository.AlbumRepository
7+
import org.assertj.core.api.Assertions.assertThat
8+
import org.hibernate.LazyInitializationException
9+
import org.junit.jupiter.api.Test
10+
import org.junit.jupiter.api.assertDoesNotThrow
11+
import org.junit.jupiter.api.assertThrows
12+
import org.springframework.beans.factory.annotation.Autowired
13+
import org.springframework.boot.test.context.SpringBootTest
14+
import org.springframework.transaction.annotation.Transactional
15+
import java.util.concurrent.Executors
16+
17+
@SpringBootTest
18+
class PersistenceContextTest @Autowired constructor(
19+
private val albumRepository: AlbumRepository,
20+
private val playlistRepository: PlaylistRepository,
21+
private val queryCounter: QueryCounter,
22+
) {
23+
private val threadPool = Executors.newFixedThreadPool(4)
24+
25+
@Test
26+
fun `영속성 컨텍스트 없이 지연로딩`() {
27+
val album = albumRepository.findById(1L).get()
28+
29+
assertThrows<LazyInitializationException> {
30+
println(album.artist.name)
31+
}
32+
33+
val (_, queryCount) = queryCounter.count {
34+
albumRepository.findById(1L).get()
35+
}
36+
37+
// 영속성 컨텍스트가 없기 때문에 캐싱 되지 않음.
38+
assertThat(queryCount).isEqualTo(1)
39+
}
40+
41+
@Transactional // 기본적으로 Transactional이 영속성 컨텍스트 생존 범위
42+
@Test
43+
fun `영속성 컨텍스트 안에서 지연로딩`() {
44+
val album = albumRepository.findById(1L).get()
45+
46+
assertDoesNotThrow {
47+
println(album.artist.name)
48+
}
49+
50+
val (_, queryCount) = queryCounter.count {
51+
albumRepository.findById(1L).get()
52+
}
53+
54+
// 영속성 컨텍스트가 있기 때문에 캐싱됨.
55+
assertThat(queryCount).isEqualTo(0)
56+
}
57+
58+
@Transactional // 기본적으로 Transactional이 영속성 컨텍스트 생존 범위, 그러나 스레드 간에 영속성 컨텍스트를 공유하지 않는다.
59+
@Test
60+
fun `스레드 단위의 엔티티 캐싱`() {
61+
// 첫 번째 쿼리: DB 조회 후 영속성 컨텍스트에 캐시
62+
val (threadName, queryCount) = queryCounter.count {
63+
playlistRepository.findById(1L).get()
64+
65+
Thread.currentThread().name
66+
}
67+
68+
println("First query executed by Thread($threadName)")
69+
70+
assertThat(queryCount).isEqualTo(1)
71+
72+
// 두 번째 쿼리: 첫 번째 쿼리와 같은 스레드에서 발생. 캐싱된 데이터 조회
73+
val (threadName2, queryCount2) = queryCounter.count {
74+
playlistRepository.findById(1L).get()
75+
76+
Thread.currentThread().name
77+
}
78+
79+
println("Second query executed by Thread($threadName2)")
80+
81+
assertThat(queryCount2).isEqualTo(0)
82+
83+
// 세 번째 쿼리: 이전 쿼리들과 다른 스레드에서 발생. 즉, 다른 영속성 컨텍스트를 갖고 있기 때문에 DB 조회.
84+
val taskByOtherThread = threadPool.submit<Result<String>> {
85+
queryCounter.count {
86+
playlistRepository.findById(1L).get()
87+
88+
Thread.currentThread().name
89+
}
90+
}
91+
92+
val (threadName3, queryCount3) = taskByOtherThread.get()
93+
94+
println("Third query executed by Thread($threadName3)")
95+
96+
assertThat(queryCount3).isEqualTo(1)
97+
}
98+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.wafflestudio.seminar.spring2023.week2
2+
3+
import com.wafflestudio.seminar.spring2023.playlist.service.PlaylistAlreadyLikedException
4+
import com.wafflestudio.seminar.spring2023.playlist.service.PlaylistLikeService
5+
import org.assertj.core.api.Assertions.assertThat
6+
import org.junit.jupiter.api.Test
7+
import org.junit.jupiter.api.assertDoesNotThrow
8+
import org.junit.jupiter.api.assertThrows
9+
import org.springframework.beans.factory.annotation.Autowired
10+
import org.springframework.boot.test.context.SpringBootTest
11+
import org.springframework.transaction.annotation.Transactional
12+
import java.util.concurrent.ExecutionException
13+
import java.util.concurrent.Executors
14+
15+
@Transactional
16+
@SpringBootTest
17+
class SynchronizationTest @Autowired constructor(
18+
private val playlistLikeService: PlaylistLikeService,
19+
) {
20+
private val threadPool = Executors.newFixedThreadPool(4)
21+
22+
@Test
23+
fun `동기화 없이 좋아요 따닥 생성`() {
24+
val taskA = threadPool.submit { playlistLikeService.create(playlistId = 1L, userId = 1L) }
25+
val taskB = threadPool.submit { playlistLikeService.create(playlistId = 1L, userId = 1L) }
26+
27+
assertDoesNotThrow {
28+
taskA.get()
29+
taskB.get()
30+
}
31+
}
32+
33+
@Test
34+
fun `동기화 사용하여 좋아요 따닥 생성`() {
35+
val taskA = threadPool.submit { playlistLikeService.createSynchronized(playlistId = 2L, userId = 1L) }
36+
val taskB = threadPool.submit { playlistLikeService.createSynchronized(playlistId = 2L, userId = 1L) }
37+
38+
val exception = assertThrows<ExecutionException> {
39+
taskA.get()
40+
taskB.get()
41+
}
42+
43+
assertThat(exception.cause).isInstanceOf(PlaylistAlreadyLikedException::class.java)
44+
}
45+
}

0 commit comments

Comments
 (0)