//
Search
Duplicate
๐ŸŽ

๋ถ„์‚ฐ๋ฝ์œผ๋กœ ๋™์‹œ์„ฑ ๋ฌธ์ œ ํ•ด๊ฒฐํ•˜๊ธฐ(Redis, ๋„ค์ž„๋“œ ๋ฝ)

์ƒ์„ฑ์ผ
2023/03/23
ํƒœ๊ทธ
Redis
Spring
Transaction
Aop
MySQL
๋ชฉ์ฐจ

๋ฐฐ๊ฒฝ

์„œ๋น„์Šค๋ฅผ ๊ฐœ๋ฐœํ•˜๊ณ  ๊ณ ๋„ํ™”๊ฐ€ ์ง„ํ–‰๋˜๋ฉด ํ•„์—ฐ์ ์œผ๋กœ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ๋งˆ์ฃผํ•˜๊ฒŒ ๋˜๋Š”๋ฐ, ์ €ํฌ ๋ชจ๋ฝ๋„ ์˜ˆ์™ธ๋Š” ์•„๋‹ˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ ์›น ํ™˜๊ฒฝ์—์„œ๋Š” ๊ฐ™์€ ์‹œ๊ฐ„์— ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์š”์ฒญ์ด ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ๊ณ , ์Šคํ”„๋ง๊ฐ™์€ ๋ฉ€ํ‹ฐ์“ฐ๋ ˆ๋“œ ํ™˜๊ฒฝ์—์„œ๋Š” ์—ฌ๋Ÿฌ ์“ฐ๋ ˆ๋“œ๊ฐ€ ํ•œ ์ž์›์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์–ด, ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ์‹ฌํ•  ๋•Œ๋Š” ๋ฐ๋“œ๋ฝ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ €ํฌ ๋ชจ๋ฝ์—์„œ๋Š” ์ฟผ๋ฆฌ ์„ฑ๋Šฅ์„ ์œ„ํ•ด ๋ฐ˜์ •๊ทœํ™”๋ฅผ ํ•˜์—ฌ ์—…๋ฐ์ดํŠธ๋ฅผ ํ•˜๋Š” ์ฟผ๋ฆฌ๊ฐ€ ์ƒ๊ฒผ๊ณ , ์—ฌ๋Ÿฌ ์“ฐ๋ ˆ๋“œ์—์„œ ํ•œ ๋ ˆ์ฝ”๋“œ์— ๋Œ€ํ•ด ๊ณต์œ ๋ฝ์„ ํš๋“ํ•œ ๋’ค ๋ฐฐํƒ€๋ฝ ํš๋“์„ ์‹œ๋„ํ•˜์—ฌ ๋ฐ๋“œ๋ฝ์ด ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.ย ์ด์— ์ด์ „ ๊ฒŒ์‹œ๋ฌผ์—์„œ๋Š” ๋ฐ๋“œ๋ฝ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋น„๊ด€์  ๋ฝ, ๋‚™๊ด€์  ๋ฝ ๋“ฑ์— ๋Œ€ํ•œ ๊ณ ๋ฏผ๊ณผ ๊ฒฐ๋ก ์ ์œผ๋กœ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋‚˜๋ˆ  ๋ฐ๋“œ๋ฝ์„ ํšŒํ”ผํ•˜๋ฉด์„œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜์˜€์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์œผ๋กœ๋„ ์™„์ „ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ง€๋‚œ ๋กœ์ง์€ ๋‹จ์ˆœ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฝ์ž…ํ•˜๋Š” ๋กœ์ง์ด์—ˆ์ง€๋งŒ ์„ ์ฐฉ์ˆœ ํˆฌํ‘œ์™€ ๊ฐ™์ด ์ž์›์— ์ œํ•œ์ด ์žˆ์„ ๋•Œ์—๋Š” ์ •ํ•ฉ์„ฑ ๋ฌธ์ œ๊ฐ€ ์—ฌ์ „ํžˆ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ์กฐ๊ธˆ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์œผ๋กœ ์ ‘๊ทผํ•ด๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

๋น„์ฆˆ๋‹ˆ์Šค ์ฝ”๋“œ

์„ ์ฐฉ์ˆœ์œผ๋กœ ์•ฝ์†์žก๊ธฐ ์„ ํƒ์„ 10๋ช…๋งŒ ํ•  ์ˆ˜ ์žˆ๋Š” ๋กœ์ง์ž…๋‹ˆ๋‹ค.
public class AppointmentService { @Transactional public void selectAvailableTimesWithFirstCome(String teamCode, long memberId, String appointmentCode, List<AvailableTimeRequest> requests) { Appointment appointment = findAppointmentInTeam(teamCode, appointmentCode); if (appointment.isLimit()) { return; } appointment.selectAvailableTimesWithFirstCome( toStartDateTime(requests), memberId); appointmentRepository.updateSelectedCount(appointmentCode); } }
Java
๋ณต์‚ฌ
@Entity public class Appointment { // ... private long selectedCount; private long limitCount; private Set<AvailableTime> availableTimes = new HashSet<>(); public boolean isLimit() { return this.limitCount <= this.selectedCount; } public void selectWithFirstCome(Set<LocalDateTime> localDateTimes, long memberId) { final List<AvailableTime> availableTimes = toAvailableTimes(localDateTimes, memberId); this.availableTimes.addAll(availableTimes); } }
Java
๋ณต์‚ฌ
์œ„์™€ ๊ฐ™์€ ์ฝ”๋“œ์—์„œ ์ค‘์š”ํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.
โ€ข
์„ ์ฐฉ์ˆœ ์ˆ˜(limitCount)๋ฅผ ๋ณด๊ณ  ์„ ์ฐฉ์ˆœ ์ˆ˜๊ฐ€ ๋‹ค ์ฐผ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค(isLimit()).
โ€ข
์„ ์ฐฉ์ˆœ ์ˆ˜๊ฐ€ ์•„์ง ๋‹ค ์ฐจ์ง€ ์•Š์•˜๋‹ค๋ฉด ์•ฝ์†์žก๊ธฐ ์„ ํƒ ์ธ์›์„ ์ถ”๊ฐ€ํ•œ๋‹ค(addAll()).
โ€ข
์„ ํƒ ์ธ์›์„ +1ํ•œ๋‹ค(updateSelectedCount()).
์ด๋Ÿฌํ•œ ๋กœ์ง์—์„œ ๋งŒ์•ฝ 100๋ช…์˜ ๋ฉค๋ฒ„๊ฐ€ ๋™์‹œ์— ์„ ํƒ์žก๊ธฐ๋ฅผ ์š”์ฒญํ•œ๋‹ค๋ฉด ๊ฒฐ๊ณผ๊ฐ€ ์–ด๋–ป๊ฒŒ ๋˜๋Š”์ง€ ํ™•์ธํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ

@Test void ์„ ์ฐฉ์ˆœ_10๋ช…์˜_์•ฝ์†์žก๊ธฐ์—_100๋ช…์ด_๋™์‹œ์—_์„ ํƒํ•œ๋‹ค() throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(100); CountDownLatch countDownLatch = new CountDownLatch(100); for (long i = 1; i < 101; i++) { long memberId = i; executorService.submit(() -> { try { appointmentService.selectAvailableTimesWithFirstCome(teamCode, memberId, appointmentCode, requests); } finally { countDownLatch.countDown(); } }); } countDownLatch.await(); Appointment appointment = appointmentRepository.findByCode(appointmentCode).orElseThrow(); assertThat(appointment.getSelectedCount()).isEqualTo(appointment.getLimitCount()); }
Java
๋ณต์‚ฌ
๊ธฐ์กด์˜ ๋™์‹œ์„ฑ์„ ํšŒํ”ผํ•˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์œผ๋กœ ์ง„ํ–‰ํ–ˆ์„ ๋•Œ, ๊ฒฐ๊ณผ๋ฅผ ํ•œ๋ฒˆ ํ™•์ธํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.
limitCount ๋Š” 10์ธ๋ฐ, ์ถ”๊ฐ€์ ์œผ๋กœ 7๊ฐœ์˜ ์š”์ฒญ์ด ์„ฑ๊ณตํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ ์ด์œ ๋Š” ๋ฌด์—‡์ผ๊นŒ์š”?
์กฐํšŒํ•˜๋Š” ์‹œ์ ๊ณผ ๊ฒ€์ฆํ•˜๋Š” ์‹œ์ ์— ์‹œ๊ฐ„์  ์ฐจ์ด๊ฐ€ ์žˆ๋Š” ๋กœ์ง(Check-Then-Act ํŒจํ„ด)์ด๊ธฐ๋•Œ๋ฌธ์— ๋‘ ํŠธ๋žœ์žญ์…˜์˜ ๋กœ์ง์ด ๋ชจ๋‘ ์„ฑ๊ณตํ•˜๊ฒŒ ๋œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ€์žฅ ๋จผ์ € ์ƒ๊ฐ๋‚œ ๋ฐฉ๋ฒ•์€ ๋น„๊ด€๋ฝ์ž…๋‹ˆ๋‹ค. ์กฐํšŒํ•˜๋Š” ์‹œ์ ๋ถ€ํ„ฐ ๋ฐฐํƒ€๋ฝ์„ ํš๋“ํ•˜๊ธฐ๋•Œ๋ฌธ์— ๊ฐ„๋‹จํ•˜๊ฒŒ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๊ณ , ์•ฑ ์„œ๋ฒ„๊ฐ€ ์—ฌ๋Ÿฌ ๋Œ€์ธ ๋ถ„์‚ฐ ํ™˜๊ฒฝ์—์„œ๋„ ์ ์šฉ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
ํ•˜์ง€๋งŒ ๋น„๊ด€๋ฝ์€ ํ•ด๋‹น ๋ ˆ์ฝ”๋“œ์— ๋ฝ์„ ๊ฑธ๊ธฐ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ์“ฐ๋ ˆ๋“œ๋Š” ๋ฐฐํƒ€๋ฝ์„ ๋ฐ˜๋‚ฉํ•˜๋Š” ๋™์•ˆ DB ์„œ๋ฒ„ ๋‚ด๋ถ€์—์„œ ๊ณ„์† ๋Œ€๊ธฐํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค. ๋ฌด์—‡๋ณด๋‹ค ์„ ์ฐฉ์ˆœ์ด ์•„๋‹Œ ๋‹ค๋ฅธ ๋กœ์ง์—์„œ ํ•ด๋‹น ๋ ˆ์ฝ”๋“œ์— ๊ณต์œ ๋ฝ ๋˜๋Š” ๋ฐฐํƒ€๋ฝ์„ ํš๋“ํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์—์„œ๋„ ๋Œ€๊ธฐํ•˜๊ธฐ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ๋กœ์ง์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด์— ๋”ฐ๋ผ ๋น„๊ด€๋ฝ์€ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ธฐ๋กœ ๊ฒฐ์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋ถ„์‚ฐ๋ฝ

๊ธฐ์กด์˜ ๋ฐฉ์‹์ด ์•„๋‹Œ ์ƒˆ๋กœ์šด ๋ฐฉ์‹์œผ๋กœ ํ•ด๊ฒฐํ•  ๋ฐฉ์‹์„ ์ฐพ๊ณ  ์žˆ์—ˆ๊ณ , MySQL 8 ๊ต์žฌ๋ฅผ ๊ณต๋ถ€ํ•˜๋‹ค๊ฐ€ ๋ถ„์‚ฐ๋ฝ์ธ ๋„ค์ž„๋“œ๋ฝ ์„ ์•Œ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๋ถ„์‚ฐ๋ฝ์— ๋Œ€ํ•ด ์กฐ๊ธˆ ํ•™์Šตํ•œ ๋’ค ์ ์šฉํ•ด ๋ณด๊ณ ์ž ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
๋ถ„์‚ฐ ๋ฝ์€ DB์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ฝ์˜ ์ข…๋ฅ˜(๊ณต์œ ๋ฝ, ๋ฐฐํƒ€๋ฝ, ๋ ˆ์ฝ”๋“œ๋ฝ ๋“ฑ)์™€๋Š” ๊ฐœ๋…์ ์œผ๋กœ ๋‹ค์†Œ ์ฐจ์ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. DB์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ฝ์€ ๋ณดํ†ต ๋ ˆ์ฝ”๋“œ๋‚˜ ํ…Œ์ด๋ธ”๊ณผ ๊ฐ™์€ ์ž์›์— ๋Œ€ํ•ด ๋ฝ์„ ๊ฒ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ถ„์‚ฐ๋ฝ์—์„œ๋Š” ๋กœ์ง, API ๋“ฑ๊ณผ ๊ฐ™์€ ์ž์›์— ์ ‘๊ทผํ•˜๋ ค๋Š” ๋Œ€์ƒ์— ๋Œ€ํ•ด ๋ฝ์„ ๊ฒ๋‹ˆ๋‹ค.
์˜ˆ๋ฅผ ๋“ค์–ด, ์••๊ตฌ์ • ํ˜„๋Œ€ ๋ฐฑํ™”์ ์˜ ๋‚˜์ดํ‚ค ๋งค์žฅ์—์„œ ์—ด ๋ช…๋งŒ ์ž…์žฅ์ด ๊ฐ€๋Šฅํ•œ ์ œํ•œ์ด ์žˆ๋Š” ์ด๋ฒคํŠธ๊ฐ€ ์žˆ๋‹ค๊ณ  ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ธฐ์กด์˜ ๋น„๊ด€๋ฝ์ด๋ผ๋ฉด ๋งˆ์ง€๋ง‰ ์—ด ๋ฒˆ์งธ๋กœ ์ž…์žฅํ•œ ์ธ์›์ด ๋ฌธ์„ ์ž ๊ทธ๊ณ  ๋“ค์–ด๊ฐ€, ์•ˆ์—์„œ ์ธ์›์ด ๋ฌธ์„ ์—ด๊ณ  ๋‚˜์˜ฌ ๋•Œ๊นŒ์ง€ ์ž์›์„ ์ ์œ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ดํ›„์— ๋„์ฐฉํ•œ ์ธ์›์€ ๋ฌธ์ด ์—ด๋ฆด ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ถ„์‚ฐ ๋ฝ์ธ ๊ฒฝ์šฐ์—๋Š” ๋ฌธ์„ ์ž ๊ทผ๋‹ค๊ธฐ ๋ณด๋‹ค๋Š” ์ž…์žฅํ•  ์ธ์›์ด ๋‚˜์ดํ‚ค ๋งค์žฅ ์ด๋ฒคํŠธ๋ฅผ ์œ„ํ•ด ์™”๋Š”์ง€, ์•„๋‹ˆ๋ฉด ํ‘ธ๋“œ์ฝ”ํŠธ ์ด์šฉ์„ ์œ„ํ•ด ์™”๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํ‘ธ๋“œ์ฝ”ํŠธ ์ด์šฉ์ž๋ผ๋ฉด ๊ทธ๋Œ€๋กœ ๋“ค์–ด๊ฐ€๊ณ , ๋‚˜์ดํ‚ค ๋งค์žฅ ์ด๋ฒคํŠธ๋ฅผ ์œ„ํ•ด ์™”๋‹ค๋ฉด ์—ด ๋ช…์ด ์ด๋ฏธ ์ž…์žฅํ–ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒ€์ˆ˜์ž๊ฐ€ ์กด์žฌํ•˜๊ณ  ์ด ๊ฒ€์ˆ˜์ž๊ฐ€ ์ž…์žฅ์„ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค.
โ€˜๊ฒ€์ˆ˜์žโ€™๋กœ ํ™œ์šฉ๋˜๋Š” ์ด๋Ÿฌํ•œ ๋ถ„์‚ฐ๋ฝ์œผ๋กœ๋Š” MySQL์˜ ๋„ค์ž„๋“œ ๋ฝ, Redis, Zookeeper ๋“ฑ์ด ์žˆ์œผ๋ฉฐ ์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” ๋„ค์ž„๋“œ ๋ฝ๊ณผ Redis ๋ฅผ ์ ์šฉํ•ด ๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๋„ค์ž„๋“œ ๋ฝ

๊ฐ€์žฅ ๋จผ์ € ์ ์šฉํ•  ๋ถ„์‚ฐ๋ฝ์€ MySQL ์—์„œ ์ง€์›ํ•˜๋Š” ๋„ค์ž„๋“œ ๋ฝ์ž…๋‹ˆ๋‹ค. select get_lock(key, time_out) ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ๋„ค์ž„๋“œ ๋ฝ์„ ์ด์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. key ์— ํ•ด๋‹นํ•˜๋Š” ์ธ์ž๋Š” ์•ž์„œ ์˜ˆ์‹œ์—์„œ ์–ธ๊ธ‰ํ•œ ๋ฐฑํ™”์ ์— ์ด์šฉํ•˜๋ ค๋Š” ๋ชฉ์ ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  time_out ์€ ๋Œ€๊ธฐํ•  ์ตœ๋Œ€ ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค. ๋ฝ์— ๋Œ€ํ•œ ์ •๋ณด๋Š” ๊ฒ€์ˆ˜์ž ์—ญํ• ์„ ํ•˜๋Š” ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
public interface AppointmentRepository extends Repository<Appointment, Long> { @Query(value = "select get_lock(:key, 1000)", nativeQuery = true) void getLock(String key); @Query(value = "select release_lock(:key)", nativeQuery = true) void releaseLock(String key); }
Java
๋ณต์‚ฌ
๋„ค์ž„๋“œ ๋ฝ์€ Spring Data JPA ๋‚˜ JPQL ์—์„œ ์ง€์›ํ•˜์ง€ ์•Š๊ธฐ๋•Œ๋ฌธ์— nativeQuery ๋กœ ์ง์ ‘ ์ž‘์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ nativeQuery ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋‚ด๋ถ€ ํŠธ๋žœ์žญ์…˜์„ ์ด์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด์— ๋”ฐ๋ผ ๋งŒ์•ฝ ํŠธ๋žœ์žญ์…˜์ด ํ•„์š”ํ•˜๋‹ค๋ฉด @Transactional ์–ด๋…ธํ…Œ์ด์…˜์„ ๋‹ฌ์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ฝ์„ ํš๋“ํ•˜๊ณ  ํ•ด์ œํ•˜๋Š” ๋กœ์ง์€ ์˜์†์„ฑ์„ ์ด์šฉํ•  ์ด์œ ๊ฐ€ ์—†๊ณ , ์ดํ›„์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๊ฐ™์€ ํŠธ๋žœ์žญ์…˜์„ ํƒˆ ํ•„์š”๊ฐ€ ์—†๊ธฐ๋•Œ๋ฌธ์— ์—ฌ๊ธฐ์„œ๋Š” ํŠธ๋žœ์žญ์…˜์„ ์ด์šฉํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
@Transactional public void selectAvailableTimesWithFirstComeNamedLock(String teamCode, long memberId, String appointmentCode, List<AvailableTimeRequest> requests) { try { appointmentRepository.getLock(appointmentCode); // ํš๋“! Appointment appointment = findAppointmentInTeam(teamCode, appointmentCode); if (appointment.isLimit()) { return null; } appointment.selectAvailableTimesWithFirstCome( toStartDateTime(requests), memberId, systemTime.now()); appointmentRepository.updateSelectedCount(appointmentCode); return null; } finally { appointmentRepository.releaseLock(appointmentCode); // ํ•ด์ œ! } }
Java
๋ณต์‚ฌ
key ๋กœ๋Š” ์•ฝ์†์žก๊ธฐ ์ฝ”๋“œ๋ฅผ ๋„ฃ์–ด์ฃผ์–ด ํ•ด๋‹น ๋กœ์ง์„ ์ด์šฉํ•˜๋Š” ์•ฝ์†์žก๊ธฐ ์ฝ”๋“œ๊ฐ€ ๊ฐ™๋‹ค๋ฉด ๋Œ€๊ธฐํ•˜๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ธฐ์กด์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์˜ ์•ž, ๋’ค์— ๋„ค์ž„๋“œ ๋ฝ์„ ํš๋“ํ•˜๊ณ  ํ•ด์ œํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•ด ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ํš๋“/ํ•ด์ œ ๋กœ์ง์„ ํ†ตํ•ด ์กฐํšŒํ•˜๊ธฐ ์ „ ๋ฝ ํš๋“์„ ํ•˜๊ณ  ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜๋งŒ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ๋ ค ๋ณด๋ฉด!
์ž˜๋ชป ์ถ”๊ฐ€๋œ ๋ฐ์ดํ„ฐ์˜ ์ˆ˜๊ฐ€ 7๊ฐœ์—์„œ 1๊ฐœ๋กœ ์ค„์–ด๋“ค๊ธด ํ–ˆ์ง€๋งŒ ์ด๋ฒˆ์—๋„ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ์— ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. ์‹คํŒจํ•œ ์ด์œ ๋Š” ๋ถ„์‚ฐ๋ฝ ํ•ด์ œ ์‹œ์ ๊ณผ @Transactional ์„ ์ด์šฉํ•œ ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์‹œ์ ์˜ ๋ถˆ์ผ์น˜ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
์Šคํ”„๋ง AOP๋ฅผ ํ†ตํ•ด ์„ ์ฐฉ์ˆœ ๋ฉ”์„œ๋“œ ์‹œ์ž‘ํ•˜๊ธฐ ์ „๊ณผ ๋๋‚˜๊ณ  ๋‚˜์„œ ํŠธ๋žœ์žญ์…˜์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํ”„๋ก์‹œ๊ฐ€ ๋™์ž‘ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด ๋ฝ ํš๋“๊ณผ ํ•ด์ œ๋Š” ์„ ์ฐฉ์ˆœ ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€์—์„œ ์ผ์–ด๋‚ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์ด์œ ๋กœ ํŠธ๋žœ์žญ์…˜ A ๊ฐ€ ์ปค๋ฐ‹๋˜๊ธฐ ์ „ ๋ฝ ํ•ด์ œ๊ฐ€ ๋˜๊ณ  ์ด ๋ฝ์„ ๊ธฐ๋‹ค๋ฆฌ๋˜ ํŠธ๋žœ์žญ์…˜ B ์—์„œ๋Š” ๋ฝ์„ ๋ฐ”๋กœ ํš๋“ํ•œ ํ›„ ์กฐํšŒ๋ฅผ ํ•˜๊ธฐ๋•Œ๋ฌธ์— ํŠธ๋žœ์žญ์…˜ A ๊ฐ€ ์ปค๋ฐ‹๋˜๊ธฐ ์ด์ „์˜ ๊ฐ’์„ ์กฐํšŒํ•˜๊ฒŒ ๋œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๋ฝ์„ ํš๋“ํ•˜๊ณ  ํ•ด์ œํ•˜๋Š” ๋กœ์ง์ด ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„์˜ ๋ฐ–์— ์žˆ๋„๋ก facade ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
@Service public class AppointmentFacade { public void selectAvailableTimesWithNamedLock(String teamCode, long memberId, String appointmentCode, List<AvailableTimeRequest> requests) { try { appointmentRepository.getLock(teamCode); appointmentService.selectAvailableTimesWithFirstCome(teamCode, memberId, appointmentCode, requests); } finally { appointmentRepository.releaseLock(teamCode); } } }
Java
๋ณต์‚ฌ
๊ทธ๋ฆฌ๊ณ  ์„œ๋น„์Šค ๊ณ„์ธต์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์—์„œ๋Š” ๋ฝ์„ ํš๋“ํ•˜๊ณ  ํ•ด์ œํ•˜๋Š” ๋กœ์ง์„ ์ œ๊ฑฐํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ๋„ ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ ๋Œ€์‹  Facade ํด๋ž˜์Šค์˜ ๋ฉ”์„œ๋“œ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ๋ ค ๋ณด๋ฉด,
๋“œ๋””์–ด ์„ ์ฐฉ์ˆœ ์ˆ˜์— ๋งž๊ฒŒ ๋ฐ์ดํ„ฐ๊ฐ€ ์ž˜ ์‚ฝ์ž…๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

๋„ค์ž„๋“œ ๋ฝ์˜ ๋ฌธ์ œ์ 

๋กœ์ง์— ๋งž๊ฒŒ ์ž˜ ์‚ฝ์ž…๋˜๊ณ  DB ์˜ ๊ธฐ๋Šฅ์„ ์ž˜ ์ด์šฉํ•˜์˜€์ง€๋งŒ, ๋„ค์ž„๋“œ ๋ฝ์€ ์—ฌ์ „ํžˆ ๋ฌธ์ œ์ ์„ ์•ˆ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ฒซ์งธ๋กœ ์ผ์‹œ์ ์ธ ๋ฝ์— ๋Œ€ํ•œ ์ •๋ณด๊ฐ€ DB์— ์ €์žฅ๋˜๊ณ , ๋ฝ์„ ํš๋“ํ•˜๊ณ  ์ œ๊ฑฐํ•˜๋Š” ์ฟผ๋ฆฌ๊ฐ€ ๋งค๋ฒˆ ๋ฐœ์ƒํ•˜์—ฌ DB์— ๋ถˆํ•„์š”ํ•œ ๋ถ€ํ•˜๋ฅผ ์ค„ ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‘˜์งธ๋กœ ๋งŒ์•ฝ facade ๊ณ„์ธต์˜ ๋ฉ”์„œ๋“œ์— ๋กœ์ง์ด ์ถ”๊ฐ€๋˜์–ด ํŠธ๋žœ์žญ์…˜์ด ํ•„์š”ํ•˜๋‹ค๋ฉด, ์„œ๋น„์Šค ๊ณ„์ธต์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์—์„œ๋Š” ์ƒˆ๋กœ์šด ํŠธ๋žœ์žญ์…˜์„ ์œ„ํ•ด REQUIRES_NEW ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
@Service public class AppointmentFacade { @Transactional // ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ public void selectAvailableTimesWithNamedLock() { try { appointmentRepository.getLock(teamCode); appointmentService.selectAvailableTimesWithFirstCome(teamCode, memberId, appointmentCode, requests); // ์ถ”๊ฐ€ ๋กœ์ง } finally { appointmentRepository.releaseLock(teamCode); } } @Service public class AppointmentService { @Transactional(propagation = Propagation.REQUIRES_NEW) // requires new ์ถ”๊ฐ€ public void selectAvailableTimesWithFirstCome() { // ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ˆ˜ํ–‰ } }
Java
๋ณต์‚ฌ
์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ ๋™์‹œ์— 10 ๊ฐœ์˜ ์š”์ฒญ์ด ๋™์‹œ์— ๋ฐœ์ƒํ•œ๋‹ค๋ฉด HikariCP Maximum Pool Size ๋กœ ์ธํ•ด ๋ฐ๋“œ๋ฝ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. HikariCP Maximum Pool Size ์˜ ๊ธฐ๋ณธ๊ฐ’์€ 10์ž…๋‹ˆ๋‹ค. ๋ถ„์‚ฐ๋ฝ์„ ํš๋“ํ•œ ์“ฐ๋ ˆ๋“œ๊ฐ€ requires new ๋ฅผ ๋งŒ๋‚˜ ์ƒˆ๋กœ์šด ํŠธ๋žœ์žญ์…˜์„ ์–ป๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ปค๋„ฅ์…˜์„ ์ƒˆ๋กœ ์ƒ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด ๋•Œ ๋ถ„์‚ฐ๋ฝ์„ ํš๋“ํ•˜์ง€ ๋ชปํ•œ ์“ฐ๋ ˆ๋“œ 9๊ฐœ๋Š” ์ด๋ฏธ ์ปค๋„ฅ์…˜์„ ๋ฌผ๊ณ  ๋ฝ์„ ํš๋“ํ•˜๊ธฐ ์œ„ํ•ด ๋Œ€๊ธฐ์ค‘์ž…๋‹ˆ๋‹ค. ์ด ์‹œ์ ์—์„œ ์ด๋ฏธ ์ปค๋„ฅ์…˜์ด ๋ชจ๋‘ ์ฐผ๊ธฐ ๋•Œ๋ฌธ์— ๋ถ„์‚ฐ๋ฝ์„ ํš๋“ํ•œ ์“ฐ๋ ˆ๋“œ๋Š” ์ƒˆ๋กœ ์ปค๋„ฅ์…˜์„ ์ƒ์„ฑํ•˜์ง€ ๋ชปํ•ด ๋Œ€๊ธฐํ•˜๊ฒŒ ๋˜๋ฉด์„œ ๋ฐ๋“œ๋ฝ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ 2๊ฐœ์˜ ์š”์ฒญ๊ณผ ์ปค๋„ฅ์…˜์œผ๋กœ ๋‹จ์ˆœํ™”ํ•œ๋‹ค๋ฉด ๊ทธ๋ฆผ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.
๊ทธ๋ ‡๋‹ค๊ณ  HikariCP Maximum Pool Size ๋ฅผ ๋Š˜๋ฆฐ๋‹ค๋ฉด ๋†€๊ณ  ์žˆ๋Š” ์ปค๋„ฅ์…˜์ด ๋งŽ๊ฒŒ ๋˜์–ด ๋น„ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์ด์œ ๋“ค๋กœ ์ธํ•ด ๋„ค์ž„๋“œ ๋ฝ ๋Œ€์‹  Redis ๋ฅผ ์•Œ์•„๋ณด์•˜์Šต๋‹ˆ๋‹ค.

๋ ˆ๋””์Šค ๋ถ„์‚ฐ๋ฝ

Redis๋Š”ย "ํ‚ค-๊ฐ’" ๊ตฌ์กฐ์˜ ๋น„์ •ํ˜• ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ์˜คํ”ˆ ์†Œ์Šค ๊ธฐ๋ฐ˜์˜ ๋น„๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค. ์ €์žฅ์†Œ, ์บ์‹œ, ๋ฉ”์„ธ์ง€ ๋ธŒ๋กœ์ปค ๋“ฑ์œผ๋กœ ์‚ฌ์šฉ๋˜๋ฉฐ, ๋ณดํ†ต ๋ฉ”๋ชจ๋ฆฌ ์บ์‹ฑ ์ €์žฅ์†Œ๋กœ ํ™œ์šฉ๋ฉ๋‹ˆ๋‹ค.
Redis๋ฅผ ํ™œ์šฉํ•œย ๋ถ„์‚ฐ ๋ฝ(Distributed Lock)์„ ํ™œ์šฉํ•˜์—ฌ ๋™์‹œ์„ฑ์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ ๋””์Šคํฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” DB๋ณด๋‹ค ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” Redis๊ฐ€ ๋” ๋น ๋ฅด๊ฒŒ ๋ฝ์„ ํš๋“ ๋ฐ ํ•ด์ œํ•  ์ˆ˜ ์žˆ๊ณ  ํœ˜๋ฐœ๋˜๊ธฐ๋•Œ๋ฌธ์—, ์•ž์„œ ์–ธ๊ธ‰๋“œ๋ ธ๋˜ ๋„ค์ž„๋“œ ๋ฝ์ด ๊ฐ€์ง€๋Š” ์ฒซ๋ฒˆ์งธ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์ ์šฉํ•˜๊ธฐ ์ „์— ํ•œ ๊ฐ€์ง€ ๊ณ ๋ คํ•ด์•ผ ํ•  ๊ฒƒ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ž๋ฐ”์˜ Redis ํด๋ผ์ด์–ธํŠธ๋กœ๋Š” Lettuce์™€ Redisson์ด ์žˆ๋Š”๋ฐ Spring Data Redis๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ธฐ๋ณธ์ ์œผ๋กœ ์ง€์›ํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ๋Š” Lettuce์ž…๋‹ˆ๋‹ค.
ํ•˜์ง€๋งŒ Lettuce๋กœ ๋ถ„์‚ฐ ๋ฝ์„ ๊ตฌํ˜„ํ•˜๋ ค๋ฉด ๋ฐ˜๋“œ์‹œ ์Šคํ•€ ๋ฝ์˜ ํ˜•ํƒœ๋กœ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์Šคํ•€ ๋ฝ์€ ๋ฝ์„ ํš๋“ํ•˜๊ธฐ ์œ„ํ•ดย SETNX๋ผ๋Š” ๋ช…๋ น์–ด๋กœ ๊ณ„์†ํ•ด์„œ Redis์— ๋ฝ ํš๋“ ์š”์ฒญ์„ ๋ณด๋‚ด์•ผ ํ•˜๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. ๋•Œ๋ฌธ์— ํ•„์—ฐ์ ์œผ๋กœ Redis์— ๋งŽ์€ ๋ถ€ํ•˜๋ฅผ ๊ฐ€ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋ฝ ํš๋“ ์š”์ฒญ ์‚ฌ์ด ์‚ฌ์ด๋งˆ๋‹คย Thread.sleep์„ ํ†ตํ•ด ๋ถ€ํ•˜๋ฅผ ์ค„์—ฌ์ค˜์•ผ ํ•˜๊ณ , sleep ์„ ์ค€๋‹ค๋ฉด ์—ฐ์†์ ์ธ ๋ฝ ํ•ด์ œ/ํš๋“์ด ๋˜์ง€ ์•Š์•„ ๋น„ํšจ์œจ์ ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๋ฐ˜๋ฉด Redisson์„ ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ๊ฐ€์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Redisson์€ Lettuce์ฒ˜๋Ÿผ ์Šคํ•€ ๋ฝ์œผ๋กœ ๋ฝ ํš๋“ ์š”์ฒญ์„ ๋ณด๋‚ด์ง€ ์•Š๊ณ  ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ๋ฝ์„ ํš๋“ํ•˜๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ 10๊ฐœ์˜ ์“ฐ๋ ˆ๋“œ ์ค‘ ํ•˜๋‚˜์˜ ์“ฐ๋ ˆ๋“œ๊ฐ€ ๋ฝ์„ ํš๋“ํ•˜๋ฉด ๋Œ€๊ธฐํ•˜๊ณ  ์žˆ๋Š” 9๊ฐœ์˜ ์“ฐ๋ ˆ๋“œ๋Š” ๋ฝ ํš๋“์„ ์œ„ํ•ด ํŠน์ • ์ฑ„๋„์„ ๊ตฌ๋…(subscribe)ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ฝ์„ ํš๋“ํ•˜๊ณ  ์žˆ๋˜ ์“ฐ๋ ˆ๋“œ์˜ ๋กœ์ง์ด ์™„๋ฃŒ๋˜๋ฉด ๋ฝ์„ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค. ๋ฝ์ด ํ•ด์ œ๋˜๋ฉด ๋ฝ์ด ํ•ด์ œ๋˜์—ˆ๋‹ค๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ๋Œ€๊ธฐ ์“ฐ๋ ˆ๋“œ๋“ค์ด ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋Š” ์ฑ„๋„์— ๋ฐœํ–‰(publish)ํ•ฉ๋‹ˆ๋‹ค. ์ด์–ด์„œ ๋Œ€๊ธฐ ์“ฐ๋ ˆ๋“œ ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋‹ค์‹œ ๋ฝ์„ ํš๋“ํ•˜๊ณ , ์ด ๊ณผ์ •์„ ๋ฐ˜๋ณตํ•ฉ๋‹ˆ๋‹ค. ์ด์— ๋”ฐ๋ผ Lettuce ๋Œ€์‹ , Redisson ์„ ํ™œ์šฉํ•˜์—ฌ ๋ถ„์‚ฐ๋ฝ์„ ๊ตฌํ˜„ํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

Redisson์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ถ„์‚ฐ๋ฝ ๊ตฌํ˜„

์šฐ์„  Redisson์— ๋Œ€ํ•œ ์˜์กด์„ฑ์„ ์„ค์ •ํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ Redis๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” Spring Data Redis๋Š” ๊ธฐ๋ณธ ํด๋ผ์ด์–ธํŠธ๋กœ Lettuce๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์—, Redisson์€ ์ถ”๊ฐ€์ ์ธ ์˜์กด์„ฑ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
implementation 'org.redisson:redisson-spring-boot-starter:3.18.0'
Plain Text
๋ณต์‚ฌ
redisson-spring-boot-starter๋Š” Spring Data Redis์˜ ๊ธฐ๋Šฅ๋“ค์„ ํฌํ•จํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ๊ตณ์ด spring-boot-starter-data-redis๋ฅผ implementation ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
Redisson์—๋Š” RLock์ด๋ผ๋Š” ๊ฐ์ฒด๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ๋ฝ์„ ์ปจํŠธ๋กคํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
@Service @RequiredArgsConstructor public class AppointmentFacade { private final RedissonClient redissonClient; public void selectAvailableTimesWithFirstComeRedis(String teamCode, long memberId, String appointmentCode, List<AvailableTimeRequest> requests) { RLock lock = redissonClient.getLock(String.format("firstCome:%s", appointmentCode)); try { if (!lock.tryLock(10, 1, TimeUnit.SECONDS)) { System.out.println("๋ฝ ํš๋“ ๋Œ€๊ธฐ ์‹œ๊ฐ„์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); } appointmentService.selectAvailableTimesWithFirstCome(teamCode, memberId, appointmentCode, requests); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } }
Java
๋ณต์‚ฌ
์•ž์„  ๋„ค์ž„๋“œ ๋ฝ ๋กœ์ง๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์„œ๋น„์Šค ๊ณ„์ธต ๋Œ€์‹  facade ๊ณ„์ธต์˜ ๋ฉ”์„œ๋“œ์— ๋ฝ์„ ํš๋“ํ•˜๊ณ  ํ•ด์ œํ•˜๋Š” ๋กœ์ง์„ ๋งŒ๋“ค์–ด ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. getLock ์„ ํ†ตํ•ด ํš๋“ํ•  ๋ฝ ํ‚ค๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๊ณ , RLock ์˜ tryLock ๋ฉ”์„œ๋“œ๋กœ ๋ฝ์„ ํš๋“ํ•˜๊ฑฐ๋‚˜ ๋Œ€๊ธฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ๋ฝ ํš๋“์„ ๋Œ€๊ธฐํ•  ํƒ€์ž„์•„์›ƒ์ด๊ณ , ๋‘ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ๋ฝ์ด ๋งŒ๋ฃŒ๋˜๋Š” ์‹œ๊ฐ„์„ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. ์•ž์„œ ์–ธ๊ธ‰ํ•œ ๋„ค์ž„๋“œ ๋ฝ๊ณผ ์œ ์‚ฌํ•˜๊ธฐ๋•Œ๋ฌธ์— ํ๋ฆ„ ์„ค๋ช…์€ ๋„˜์–ด๊ฐ€๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ฐ”๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ๋ ค ๋ณด๋ฉด!
ํ…Œ์ŠคํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ํ†ต๊ณผํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ฐœ์„ ํ•  ๋ถ€๋ถ„์„ ์กฐ๊ธˆ ๋” ์ƒ๊ฐํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„œ๋น„์Šค๊ฐ€ ํ™•์žฅ๋˜์–ด ์œ„์˜ ๋ถ„์‚ฐ ๋ฝ์„ ์ง€์†์ ์œผ๋กœ ์ถ”๊ฐ€ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ์—, facade ํด๋ž˜์Šค์˜ ์ค‘๋ณต ๋กœ์ง์ด ๊ณ„์†ํ•ด์„œ ์ถ”๊ฐ€์ ์œผ๋กœ ์ƒ๊ธธ ์ˆ˜ ๋ฐ–์— ์—†์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์ค‘๋ณต ๋กœ์ง์„ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด์„œ AOP๋ฅผ ์ ์šฉํ•ด ๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

Annotation ๊ธฐ๋ฐ˜์˜ ๋ถ„์‚ฐ๋ฝ AOP ๊ตฌํ˜„ํ•˜๊ธฐ

facade ๋ฉ”์„œ๋“œ ๋‚ด์˜ ๋ถ„์‚ฐ๋ฝ์„ ํš๋“ํ•˜๊ณ  ํ•ด์ œํ•˜๋Š” ๋กœ์ง์„ ์Šคํ”„๋ง AOP๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ„๋‹จํ•˜๊ฒŒ ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ถ„์‚ฐ๋ฝ์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.ย ๋ถ„์‚ฐ๋ฝ ์ ์šฉ ๋กœ์ง์€@Transactional ์˜ ํ๋ฆ„๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์‹œ์ž‘๊ณผ ๋์— ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ด์™€ ๊ฐ™์ด AOP ๋กœ ํ•ด๋‹น ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๋ฉด ์–ด๋…ธํ…Œ์ด์…˜๋งŒ์œผ๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ถ„์‚ฐ๋ฝ์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๊ณ , ๋ฐ˜๋ณต ๋กœ์ง์„ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.
๋จผ์ € ์ ์šฉํ•˜๋ ค๋Š” ์–ด๋…ธํ…Œ์ด์…˜์„ ๋งŒ๋“ค์–ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DistributedLock { TimeUnit timeUnit() default TimeUnit.SECONDS; long waitTime() default 10L; long leaseTime() default 1L; }
Java
๋ณต์‚ฌ
์•ž์„œ tryLock() ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„ฃ์—ˆ๋˜ ๊ฐ’์„ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋„ฃ์—ˆ๊ณ  ์‚ฌ์šฉํ•˜๋Š” ๋ฉ”์„œ๋“œ์—์„œ ์ด ๊ฐ’์„ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
@Component @RequiredArgsConstructor @Aspect @Order(value = 1) public class DistributedLockAspect { private final RedissonClient redissonClient; @Around("@annotation(com.morak.back.core.support.DistributedLock)") public Object lock(final ProceedingJoinPoint joinPoint) throws Throwable { String appointmentCode = (String) joinPoint.getArgs()[2]; RLock lock = redissonClient.getLock(String.format("firstCome:%s", appointmentCode)); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); DistributedLock lock = method.getAnnotation(DistributedLock.class); try { if (!rLock.tryLock(lock.waitTime(), lock.leaseTime(), lock.timeUnit())) { return false; } Object returnValue = joinPoint.proceed(); rLock.unlock(); return returnValue; } catch (final Exception e) { Thread.currentThread().interrupt(); throw new InterruptedException(); } } }
Java
๋ณต์‚ฌ
@DistributedLock public void selectAvailableTimesWithFirstCome() { }
Java
๋ณต์‚ฌ
facade ์— ์žˆ๋˜ ๋ฝ ํš๋“/ํ•ด์ œ ๋กœ์ง์„ Aspect ๋‚ด๋ถ€๋กœ ์˜ฎ๊ฒผ์Šต๋‹ˆ๋‹ค. ์ด์™ธ์—๋„ ํ˜ธ์ถœ๋œ ๋ฉ”์„œ๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ key ๋กœ ์‚ฌ์šฉํ•  ์•ฝ์†์žก๊ธฐ ์ฝ”๋“œ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ , ์–ด๋…ธํ…Œ์ด์…˜์— ์ ์šฉ๋œ ๊ฐ’์„ ๊ฐ€์ ธ์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์€ @Order ๋ถ€๋ถ„์ธ๋ฐ์š”, ๋นˆ ๋“ฑ๋กํ•˜๋Š” ์ˆœ์„œ๋ฅผ ํŠธ๋žœ์žญ์…˜ ์ธํ„ฐ์…‰ํ„ฐ๋ณด๋‹ค ๋จผ์ € ๋“ฑ๋ก๋˜๊ฒŒ ํ•˜์—ฌ @Transactional ์— ์˜ํ•œ ํŠธ๋žœ์žญ์…˜์„ ์ƒ์„ฑํ•˜๊ณ  ์ปค๋ฐ‹ํ•˜๋Š” ๋กœ์ง๋ณด๋‹ค ๋ฒ”์œ„๊ฐ€ ํฌ๊ฒŒ ๋งŒ๋“ค์–ด์ค˜์•ผํ•ฉ๋‹ˆ๋‹ค.
@Order ์—†์ด ๋นˆ์„ ๋“ฑ๋กํ•˜๋ฉด ์œ„์˜ ์ด๋ฏธ์ง€์ฒ˜๋Ÿผ ํŠธ๋žœ์žญ์…˜ ์ธํ„ฐ์…‰ํ„ฐ๊ฐ€ ๋จผ์ € ๋“ฑ๋ก๋˜์–ด ํŠธ๋žœ์žญ์…˜์ด ์—ด๋ฆฌ๊ณ  ๋‚˜์„œ ๋ฝ์„ ํš๋“ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ํ๋ฆ„์€ ์•ž์„œ ์–ธ๊ธ‰ํ•œ ๋ฌธ์ œ์ธ, ์ปค๋ฐ‹ ์ด์ „ ๋ถ„์‚ฐ๋ฝ ํ•ด์ œ ๋ฌธ์ œ๋ฅผ ๋งˆ์ฃผํ•˜๊ธฐ๋•Œ๋ฌธ์— @Order ๋กœ ๊ฐ€์žฅ ๋จผ์ € ๋นˆ ๋“ฑ๋ก์ด ๋˜๊ฒŒ ์„ค์ •ํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋งˆ๋ฌด๋ฆฌ

ํ•˜๋‚˜์˜ ์„œ๋ฒ„์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ์—ฌ๋Ÿฌ ๋Œ€์˜ ์„œ๋ฒ„์ธ ๋ถ„์‚ฐ ํ™˜๊ฒฝ์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ™•์ธํ•˜๊ณ  ์ด๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ๋ถ„์‚ฐ๋ฝ์„ ์ ์šฉํ•ด ๋ณด์•˜์Šต๋‹ˆ๋‹ค. ์ ์šฉํ•ด๋ณธ ๋ถ„์‚ฐ๋ฝ์œผ๋กœ๋Š” MySQL ์˜ ๋„ค์ž„๋“œ ๋ฝ๊ณผ ๋ ˆ๋””์Šค์˜ ๋ฉ”์„ธ์ง€ ๋ธŒ๋กœ์ปค๋ฅผ ์ด์šฉํ•œ ๋ถ„์‚ฐ๋ฝ์ž…๋‹ˆ๋‹ค.
๋„ค์ž„๋“œ ๋ฝ์˜ ์žฅ์ ์œผ๋กœ๋Š” ์ถ”๊ฐ€์ ์ธ ๋ฆฌ์†Œ์Šค๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์†Œํ”„ํŠธ์›จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ ๋„ ๊ธฐ์กด์˜ DB ์ธ MySQL ๋กœ ๋ถ„์‚ฐ๋ฝ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋‹จ์ ์œผ๋กœ๋Š” ๋ฝ์— ๋Œ€ํ•œ ์ •๋ณด๊ฐ€ ํ…Œ์ด๋ธ”์— ๋”ฐ๋กœ ์ €์žฅ๋˜์–ด ๋ฌด๊ฑฐ์›Œ์งˆ ์ˆ˜ ์žˆ๊ณ , ์‹ค์ œ DB ์— ๋ฝ์œผ๋กœ ์ธํ•œ ์ปค๋„ฅ์…˜ ๋Œ€๊ธฐ๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ๋•Œ๋ฌธ์— ์„ฑ๋Šฅ์ƒ ๋‹จ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.
๋ฐ˜๋ฉด ๋ ˆ๋””์Šค์˜ ๋ฉ”์„ธ์ง€ ๋ธŒ๋กœ์ปค๋ฅผ ์ด์šฉํ•œ ๋ถ„์‚ฐ๋ฝ์€ ๋ฝ์— ๋Œ€ํ•œ ์ •๋ณด๋Š” ํœ˜๋ฐœ์„ฑ์ด ์žˆ๊ณ , ๋ฉ”๋ชจ๋ฆฌ์—์„œ ๋ฝ์„ ํš๋“ํ•˜๊ณ  ํ•ด์ œํ•˜๊ธฐ๋•Œ๋ฌธ์— ๊ฐ€๋ณ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ ์ƒˆ๋กœ์šด ๊ธฐ์ˆ ์„ ์œ„ํ•œ ํ•™์Šต๊ณผ ๋ฆฌ์†Œ์Šค๊ฐ€ ํ•„์š”๋กœ ํ•˜๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ ˆ๋””์Šค์— ๋Œ€ํ•œ ๊ธฐ๋ณธ์ ์ธ ์ •๋ณด๊ฐ€ ์žˆ๋‹ค๋ฉด ๋”์šฑ ํ™œ์šฉ๋„๊ฐ€ ํฝ๋‹ˆ๋‹ค. ๋ ˆ๋””์Šค์˜ ๊ฐ€์žฅ ์ž˜ ์•Œ๋ ค์ง„ ์šฉ๋„๋Š” ์บ์‹ฑ ์„œ๋ฒ„์ž…๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์„œ๋น„์Šค๊ฐ€ ํ™•์žฅ๋˜๊ณ  ์š”์ฒญ ์ˆ˜๊ฐ€ ๋”์šฑ ๋งŽ์•„์ง€๋ฉด select count ๋‚˜ ์„ธ์…˜ ์ •๋ณด๋ฅผ ๋ ˆ๋””์Šค์˜ ์บ์‹ฑ ์„œ๋ฒ„๋กœ ์ด์šฉํ•  ์ˆ˜ ์žˆ์–ด ํ™•์žฅ์„ฑ์— ๋”์šฑ ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค. ์ข…ํ•ฉ์ ์œผ๋กœ ๋ดค์„ ๋•Œ, ๋น„์ฆˆ๋‹ˆ์Šค์™€ ์„œ๋ฒ„ ์ž์›์˜ ๊ฐ€์šฉ์„ฑ ๋“ฑ์˜ ์ƒํ™ฉ์— ๋งž๊ฒŒ ์„ ํƒํ•ด์„œ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.