๋ชฉ์ฐจ
๋ฐฐ๊ฒฝ
์๋น์ค๋ฅผ ๊ฐ๋ฐํ๊ณ ๊ณ ๋ํ๊ฐ ์งํ๋๋ฉด ํ์ฐ์ ์ผ๋ก ๋์์ฑ ๋ฌธ์ ๋ฅผ ๋ง์ฃผํ๊ฒ ๋๋๋ฐ, ์ ํฌ ๋ชจ๋ฝ๋ ์์ธ๋ ์๋์์ต๋๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก ์น ํ๊ฒฝ์์๋ ๊ฐ์ ์๊ฐ์ ์ฌ๋ฌ ๊ฐ์ ์์ฒญ์ด ๋ค์ด์ฌ ์ ์๊ณ , ์คํ๋ง๊ฐ์ ๋ฉํฐ์ฐ๋ ๋ ํ๊ฒฝ์์๋ ์ฌ๋ฌ ์ฐ๋ ๋๊ฐ ํ ์์์ ๊ณต์ ํ ์ ์์ด, ๋์์ฑ ๋ฌธ์ ๊ฐ ์ฌํ ๋๋ ๋ฐ๋๋ฝ์ด ๋ฐ์ํ ์ ์์ต๋๋ค. ์ ํฌ ๋ชจ๋ฝ์์๋ ์ฟผ๋ฆฌ ์ฑ๋ฅ์ ์ํด ๋ฐ์ ๊ทํ๋ฅผ ํ์ฌ ์
๋ฐ์ดํธ๋ฅผ ํ๋ ์ฟผ๋ฆฌ๊ฐ ์๊ฒผ๊ณ , ์ฌ๋ฌ ์ฐ๋ ๋์์ ํ ๋ ์ฝ๋์ ๋ํด ๊ณต์ ๋ฝ์ ํ๋ํ ๋ค ๋ฐฐํ๋ฝ ํ๋์ ์๋ํ์ฌ ๋ฐ๋๋ฝ์ด ๋ฐ์ํ์์ต๋๋ค.ย ์ด์ ์ด์ ๊ฒ์๋ฌผ์์๋ ๋ฐ๋๋ฝ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋น๊ด์ ๋ฝ, ๋๊ด์ ๋ฝ ๋ฑ์ ๋ํ ๊ณ ๋ฏผ๊ณผ ๊ฒฐ๋ก ์ ์ผ๋ก ๋น์ฆ๋์ค ๋ก์ง์ ๋๋ ๋ฐ๋๋ฝ์ ํํผํ๋ฉด์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์์ต๋๋ค. ํ์ง๋ง ์ด๋ ๊ฒ ๋น์ฆ๋์ค ๋ก์ง์ผ๋ก๋ ์์ ํ์ง ์์ ์ ์์ต๋๋ค. ์ง๋ ๋ก์ง์ ๋จ์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์
ํ๋ ๋ก์ง์ด์์ง๋ง ์ ์ฐฉ์ ํฌํ์ ๊ฐ์ด ์์์ ์ ํ์ด ์์ ๋์๋ ์ ํฉ์ฑ ๋ฌธ์ ๊ฐ ์ฌ์ ํ ๋ฐ์ํ ์ ์์ต๋๋ค. ๊ทธ๋์ ๋์์ฑ ๋ฌธ์ ๋ฅผ ์กฐ๊ธ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ผ๋ก ์ ๊ทผํด๋ณด๋ ค๊ณ ํฉ๋๋ค.
๋น์ฆ๋์ค ์ฝ๋
์ ์ฐฉ์์ผ๋ก ์ฝ์์ก๊ธฐ ์ ํ์ 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 ๋ ์ธ์
์ ๋ณด๋ฅผ ๋ ๋์ค์ ์บ์ฑ ์๋ฒ๋ก ์ด์ฉํ ์ ์์ด ํ์ฅ์ฑ์ ๋์ฑ ์ฉ์ดํฉ๋๋ค. ์ข
ํฉ์ ์ผ๋ก ๋ดค์ ๋, ๋น์ฆ๋์ค์ ์๋ฒ ์์์ ๊ฐ์ฉ์ฑ ๋ฑ์ ์ํฉ์ ๋ง๊ฒ ์ ํํด์ ์ฌ์ฉํด์ผ ํฉ๋๋ค.