๐ค ์๋ก
๋ค์ค ์ฌ์ฉ์๋ฅผ ์ํ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค๋ค ๋ณด๋ฉด ํ ๋ฒ์ฏค์ ๋์์ฑ ๋ฌธ์ ์ ์ ํ๊ฒ ๋ฉ๋๋ค.
๋์์ฑ ๋ฌธ์ ๋ ํ๋์ ์์์ ๋ํด์ ์ฌ๋ฌ ์ฐ๋ ๋๊ฐ ๋์์ ์ ๊ทผํ์ฌ ์์ ํ๋ ๊ฒฝ์ฐ์ ๋ฐ์ํ๋ ๋ฌธ์ ์ ๋๋ค.
๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ๊ฐ๋จํ ์์๋ก๋ ํน์ ๊ฒ์๋ฌผ์ ์กฐํ ์๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ฆ๊ฐ๋์ง ์๋ ๋ฌธ์ ๊ฐ ์์ ์ ์์ต๋๋ค.
ํน์ ๊ฒ์๋ฌผ์ 100๋ช ์ ์ฌ์ฉ์๊ฐ ๋์์ ์ ๊ทผํ๋ ๊ฒฝ์ฐ ์กฐํ์๋ 100์ด ๋์ด์ผ ์ณ๊ฒ ์ง๋ง, ๋๋ถ๋ถ์ ๊ฒฝ์ฐ ์กฐํ์๋ 100๋ณด๋ค ์์ ๊ฐ์ผ๋ก ์ค์ ๋ ๊ฒ์ ๋๋ค.
์กฐํ์ ๊ฐ์ ๊ฒฝ์ฐ์๋ ๋์ฒด๋ก ๋ฐ์ดํฐ์ ์ ํฉ์ฑ์ด ์ค์ํ์ง ์๊ธฐ ๋๋ฌธ์ ์ด๋ฌํ ๋ถ๋ถ์ด ๋ฌธ์ ๊ฐ ๋์ง ์์ ์ ์์ง๋ง, ์ํ์ ์ฌ๊ณ ์๋๊ณผ ๊ฐ์ ๊ฒฝ์ฐ์ ์ด๋ฌํ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค๋ฉด ์น๋ช ์ ์ผ ์ ์์ต๋๋ค.
์ด๋ฌํ ๋์์ฑ ๋ฌธ์ ๋ฅผ ๋ง๊ธฐ ์ํด์๋ ๋ค์ํ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ ์ ์๋๋ฐ์, ์ด๋ฒ ๊ธ ๋ถํฐ ์ฐจ๋ก๋๋ก ์ฌ๋ฌ๊ฐ์ง ๋ฐฉ๋ฒ๋ค์ ๋ํด ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
๐ค ํ๋ก์ ํธ ์ธํ
๊ฐ๋จํ๊ฒ๋ h2๋ฅผ ์ฌ์ฉํด์ ๋ณ๋์ ์ค์ ์์ด ์งํํ ์ ์์ผ๋, ๋ค์ ๊ธ์์ ์งํ๋๋ ๋ด์ฉ๋ค๊น์ง ๊ณ ๋ คํ์ฌ docker ์์ MySQL์ ๋์์ ์งํํ๋๋ก ํ๊ฒ ์ต๋๋ค.
๐ณ docker์ MySQL ์ค์น
docker๋ฅผ ์ค์นํ๊ณ , ๋ค์ ๋ช ๋ น์ด๋ฅผ ํตํด Mysql์ ์คํํด ์ฃผ์๋ฉด ๋ฉ๋๋ค.
docker pull mysql
docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=1234 --name mysql mysql
docker ps๋ฅผ ์ ๋ ฅํ์ ๋, ๋ค์๊ณผ ๊ฐ์ด ๋ณด์ธ๋ค๋ฉด ์ฑ๊ณต์ ๋๋ค.
๐ณ MySQL ๋ด๋ถ ์ ์ & ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ฑ
์ดํ ๋ค์ ๋ช ๋ น์ด๋ค์ ํตํด docker ๋ด๋ถ์ mysql์ ์ ์ํฉ๋๋ค.
docker exec -it mysql bash
mysql -u root -p1234
์ดํ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๋ง๋ค์ด์ฃผ๋๋ก ํ๊ฒ ์ต๋๋ค.
create database stock;
use stock;
๐ณ ํ๋ก์ ํธ ์์ฑ
๋ฒ์ ์ ๊ธฐ๋ณธ๊ฐ ๊ทธ๋๋ก ์ฌ์ฉํด ์ฃผ์๋ฉด ๋ฉ๋๋ค.
์์กด์ฑ์ ์ ์ฌ์ง๊ณผ ๊ฐ์ด, Spring Data JPA, Spring Web, MySQL Driver, Lombok(์ ํ)์ ์ ํ ํ ์์ฑํด์ฃผ์๋ฉด ๋ฉ๋๋ค.
์ดํ application.yml ํ์ผ์ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํฉ๋๋ค.
spring:
jpa:
hibernate:
ddl-auto: create
show-sql: true
properties:
hibernate:
format_sql: true
use_sql_comments: true
highlight_sql: true
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/stock
username: root
password: 1234
logging:
level:
org:
hibernate:
SQL: DEBUG
type:
descriptor:
sql:
BasicBinder: TRACE
๐ณ ๋ก์ง ์์ฑ
๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ์์๋ก๋ ์ฌ๊ณ ์์คํ ์ ๊ฐ์ ํ๋๋ก ํ๊ฒ ์ต๋๋ค.
ํ๋์ ์ฐฝ๊ณ ์ ํน์ ์ํ์ ๋ํ ์ฌ๊ณ ๊ฐ 100๊ฐ์ธ ๊ฒฝ์ฐ, ๋์์ 100๊ฐ์ ์ฌ๊ณ ๊ฐ์ ์์ฒญ์ด ๋ค์ด์์ ๋ ์ต์ข ์ ์ผ๋ก ์ฌ๊ณ ๊ฐ 0๊ฐ๊ฐ ๋๋๋ก ํ๋ ๊ฒ์ด ๋ชฉํ์ ๋๋ค.
์ด์ ๋ถํฐ ํด๋์ค๋ค์ ์์ฑํ ๊ฒ์ธ๋ฐ, ์ต์ข ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Stock {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long productId;
private int quantity;
public Stock(Long productId, int quantity) {
this.productId = productId;
this.quantity = quantity;
}
public void decrease() {
if (this.quantity == 0) {
throw new RuntimeException("์ฌ๊ณ ๋ 0๊ฐ ๋ฏธ๋ง์ด ๋ ์ ์์ต๋๋ค.");
}
this.quantity --;
}
}
import org.springframework.data.jpa.repository.JpaRepository;
public interface StockRepository extends JpaRepository<Stock, Long> {
default Stock getById(Long id) {
return findById(id).orElseThrow(NoSuchElementException::new);
}
}
@RequiredArgsConstructor
@Service
public class StockService {
private final StockRepository stockRepository;
@Transactional
public void decrease(Long id) {
Stock stock = stockRepository.getById(id);
stock.decrease();
stockRepository.saveAndFlush(stock);
}
}
์ด์ ํ ์คํธ์ฝ๋๋ฅผ ์์ฑํฉ๋๋ค.
@SpringBootTest
class StockServiceTest {
@Autowired
private StockService stockService;
@Autowired
private StockRepository stockRepository;
private Long stockId;
@BeforeEach
void setUp() {
stockId = stockRepository.saveAndFlush(new Stock(1L, 100))
.getId();
}
@AfterEach
void tearDown() {
stockRepository.deleteAll();
}
@Test
void decreaseStock() {
// given
stockService.decrease(stockId);
// when
Stock stock = stockRepository.getById(stockId);
// then
assertThat(stock.getQuantity()).isEqualTo(99);
}
}
๊ธฐ๋ณธ์ ์ธ ์ธํ ์ด ์๋ฃ๋์์ต๋๋ค.
์ด์ ์ง๊ธ์ ์ฝ๋๊ฐ ์ด๋ค ๋์์ฑ ๋ฌธ์ ๋ฅผ ์ ๋ฐ์ํฌ ์ ์๋์ง์ ๋ํด ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
๐ค ํ์ฌ ์ฝ๋์ ๋ฌธ์ ์
์ ํ ์คํธ ์ฝ๋์ ์ํฉ์ ์์ฒญ์ด 1๊ฐ๋ง ๋ค์ด์จ ๊ฒฝ์ฐ์ ๋ํ ํ ์คํธ ์ฝ๋๋ก์จ, ์์ฒญ์ด 1๊ฐ์ธ ๊ฒฝ์ฐ์๋ ์ฌ๊ณ ๊ฐ์๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ด๋ฃจ์ด์ง๋ ๊ฒ์ ํ์ธํ ์ ์์์ต๋๋ค.
๊ทธ๋ฌ๋ ์์ฒญ์ด ๋์์ 100๊ฐ๊ฐ ๋ค์ด์จ๋ค๋ฉด ์ด๋ป๊ฒ ๋์ํ๊ฒ ๋ ๊น์?
์ด๋ฌํ ์ํฉ์ ํ ์คํธ ์ฝ๋๋ฅผ ํตํด์ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
@Test
void decrease_with_100_request() throws InterruptedException {
// given
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
// when
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
stockService.decrease(stockId);
} catch (Exception e) {
latch.countDown();
}
});
}
latch.await();
// then
Stock stock = stockRepository.getById(stockId);
assertThat(stock.getQuantity()).isEqualTo(0);
}
์ ์ฝ๋์ ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ์ด์ ๋ ๋ฐ๋ก Race Condition ๋๋ฌธ์ ๋๋ค.
๐ก Race Condition์ด๋ ๋ ๊ฐ ์ด์์ ํ๋ก์ธ์ค(ํน์ ์ค๋ ๋)๋ค์ด ํ๋์ ์์์ ์ ๊ทผํ์ฌ ๋ณ๊ฒฝํ๋ ๊ฒฝ์ฐ, ์ ๊ทผ ์์์ ๋ฐ๋ผ ์คํ ๊ฒฐ๊ณผ๊ฐ ๋ฌ๋ผ์ง๋ ํ์์ ์๋ฏธํฉ๋๋ค.
๋ฌธ์ ์ํฉ์ ๊ฐ๋ตํ ๋ณด๊ธฐ ์ํด ๊ทธ๋ฆผ์ ํตํด ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
์ ํฌ๊ฐ ๊ธฐ๋ํ๋ ์ ์์ ์ธ ๋์์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
๊ทธ๋ฌ๋ ์ค์ ๋ก๋ ์๋์ ๊ฐ์ด ๋์ํจ์ผ๋ก์จ ์์์น ๋ชปํ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.
์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์๋ ํ๋์ ์ฐ๋ ๋์ ์์ ์ด ์๋ฃ๋ ์ดํ์๋ง ๋ค๋ฅธ ์ฐ๋ ๋๊ฐ ๊ณต์ ์์์ ์ ๊ทผํ ์ ์๋๋ก ํด์ฃผ์ด์ผ ํฉ๋๋ค.
๐ค ํด๊ฒฐ - synchronized ์ฌ์ฉ
์๋ฐ์์ ์ ๊ณตํ๋ synchronized ํค์๋๋ฅผ ํตํด, ๊ฐ๋จํ๊ฒ ํ ๊ฐ์ ์ฐ๋ ๋๋ง ์ ๊ทผ์ด ๊ฐ๋ฅํ๋๋ก ์ค์ ํด์ค ์ ์์ต๋๋ค.
๊ธฐ์กด ์ฝ๋๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ๋ณ๊ฒฝํฉ๋๋ค.
@RequiredArgsConstructor
@Service
public class StockService {
private final StockRepository stockRepository;
@Transactional
public synchronized void decrease(Long id) {
Stock stock = stockRepository.getById(id);
stock.decrease();
stockRepository.saveAndFlush(stock);
}
}
์ด์ ๋ค์ ํ ์คํธ๋ฅผ ์คํํ๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์คํจํ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
์ด๋ ์คํ๋ง์ @Transactional ์ด๋ ธํ ์ด์ ์ผ๋ก ์ธํด ๋ฐ์ํ๋ ๋ฌธ์ ๋ก์จ, ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ์ด์ ๋ฅผ ๊ฐ๋ตํํ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
@Transactional์ด ๋ถ์ ๋ฉ์๋๋ ์คํ๋ง์์ ๋ง๋ค์ด์ฃผ๋ ์ด๋ ํ ๊ฐ์ฒด๋ก ๊ฐ์ธ์ง๊ฒ ๋๋๋ฐ์, ์ด๋ ๋์์ ๊ฐ๋จํ ํํํ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์ด๋ ์๋์ ๊ฐ์ด stockService.decrease()๊ฐ ํธ์ถ๋ ํ ํธ๋์ญ์ ์ด ๋๋๊ธฐ ์ ์ ๋ค๋ฅธ ์ฐ๋ ๋์์ decrease() ๋ฉ์๋๋ฅผ ํธ์ถํ ์ ์์ต๋๋ค.
์ด๋ก ์ธํด ๋์์ฑ ๋ฌธ์ ๊ฐ ํด๊ฒฐ๋์ง ์์ ๊ฒ์ ๋๋ค.
@Transactional ์ด๋ ธํ ์ด์ ์ ์ ๊ฑฐํจ์ผ๋ก์จ ์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ต๋๋ค.
@RequiredArgsConstructor
@Service
public class StockService {
private final StockRepository stockRepository;
public synchronized void decrease(Long id) {
Stock stock = stockRepository.getById(id);
stock.decrease();
stockRepository.saveAndFlush(stock);
}
}
๐ค synchronized์ ๋ฌธ์ ์
synchronized๋ฅผ ํตํด ๊ฐ๋จํ๊ฒ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ผ๋, ์ด๋ ๋ ๊ฐ์ง ๋ฌธ์ ์ ์ ๊ฐ์ง๋๋ค.
1. ํธ๋์ญ์ ์ด ๋ณด์ฅ๋์ง ์๋๋ค
์ ๋ก์ง์ ๊ฐ๋จํ๊ธฐ ๋๋ฌธ์ @Transactional์ด ๋ถ์ด์์ง ์์๋ ํฐ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ง ์์ง๋ง, ํ๋์ ํธ๋์ญ์ ์ผ๋ก ์ฒ๋ฆฌํด์ผ ํ๋ ์์ ์ด ๋ง์์ง๋ ๊ฒฝ์ฐ ์ด๋ค์ ํ๋์ ํธ๋์ญ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์์ด์ง๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.
2. ๋ค์ค ์น ์๋ฒ ํ๊ฒฝ์์๋ ์ ์ฉํ ์ ์๋ค.
synchronized ํค์๋๋ ํ๋์ ํ๋ก์ธ์ค ๋ด์์๋ง ๋ณด์ฅ์ด ๋ฉ๋๋ค.
์๋ฒ๊ฐ ํ ๋์ผ ๊ฒฝ์ฐ์๋ ๊ด์ฐฎ๊ฒ ์ง๋ง Scale Out์ ํตํด ์๋ฒ๊ฐ 2๋ ์ด์์ผ๋ก ๋์ด๋๋ ๊ฒฝ์ฐ์๋ ์ฌ๋ฌ ์๋ฒ์์ ๋ฐ์ดํฐ์ ์ ๊ทผ์ด ๊ฐ๋ฅํ๊ฒ ๋๋ฏ๋ก ์ฌ์ ํ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.
๐ค ๋ค์ค ์น ์๋ฒ ํ๊ฒฝ์์์ ๋์ ํ ์คํธํ๊ธฐ
์ ๋ง ๋ค์ค ์น ์๋ฒ ํ๊ฒฝ์์๋ synchronized๋ฅผ ํตํด์ ๋ฌธ์ ๊ฐ ํด๊ฒฐ๋์ง ์๋์ง ํ ์คํธํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
๋ค์ ์ฝ๋๋ฅผ ์ถ๊ฐํฉ๋๋ค.
@RequiredArgsConstructor
@RestController
public class StockController {
private final StockService stockService;
private final StockRepository stockRepository;
@GetMapping("/stocks")
public void createStock() {
stockRepository.save(new Stock(1L, 100));
}
@GetMapping("/stocks/{id}/decrease")
public void decreaseStock(
@PathVariable Long id
) {
stockService.decrease(id);
}
}
์ดํ ํฌํธ๋ฅผ ๋ฌ๋ฆฌ ํ์ฌ ์๋ฒ 2๋๋ฅผ ์คํ์ํค๋๋ก ํ๊ฒ ์ต๋๋ค.
(์ธํ ๋ฆฌ์์ง์์ ์ด๋ฅผ ์ํด์๋ allow multiple instances ์ต์ ์ด ์ผ์ ธ ์์ด์ผ ํ๋ฉฐ, ๋ค์ ์ฌ์ดํธ๋ฅผ ์ฐธ๊ณ ํ์ฌ ์ค์ ํฉ๋๋ค.)
ํ ๋์ ์๋ฒ๋ ๋ฐ๋ก ์คํ์ํจ ๋ค, ๋ ๋ฒ์งธ ์๋ฒ๋ฅผ ์คํ์ํฌ ๋๋ application.yml์ ๋ค์๊ณผ ๊ฐ์ด ๋ณ๊ฒฝํ๊ณ ์คํ์์ผ์ค๋๋ค.
spring:
jpa:
hibernate:
ddl-auto: validate # ์์
show-sql: true
properties:
hibernate:
format_sql: true
use_sql_comments: true
highlight_sql: true
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/stock
username: root
password: 1234
logging:
level:
org:
hibernate:
SQL: DEBUG
type:
descriptor:
sql:
BasicBinder: TRACE
server:
port: 8081 # ์ถ๊ฐ
์๋ฒ๋ฅผ ์คํํ์๋ค๋ฉด Postman๋ฑ์ ์ฌ์ฉํ์ฌ http://localhost:8080/stocks๋ก get ์์ฒญ์ ํ ๋ฒ ๋ณด๋ด์ค๋๋ค.
์ดํ MySQL ๋ด๋ถ์์ ์ฌ๋๋ก ์ํ์ด ์์ฑ๋์๋์ง ํ์ธํฉ๋๋ค.
์ด์ ๋ค์ ์ฝ๋๋ฅผ ํตํด ๋จผ์ ํ ๋์ ์๋ฒ์๋ง ๋์์ 100๊ฐ์ ์์ฒญ์ ๋ณด๋ด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
class MultiServerStockServiceTest {
@Test
void decrease_with_100_request_to_multi_server() throws InterruptedException {
// given
int threadCount = 100;
RestTemplate restTemplate = new RestTemplate();
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
// when
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
int port = 8080;
ResponseEntity<Void> forEntity = restTemplate.getForEntity(
"http://localhost:" + port + "/stocks/1/decrease",
Void.class);
} finally {
latch.countDown();
}
});
}
latch.await();
}
}
์ต์ข ์ ์ผ๋ก stock์ ์๋์ ํ์ธํ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์๋ฅผ ํตํด ๋จ์ผ ์๋ฒ์์๋ synchronized๋ฅผ ํตํด ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐฉ์ง๋จ์ ํ์ธํ ์ ์์์ต๋๋ค.
์ด์ด์ 2๋์ ์๋ฒ์ ๋ํด ๋์ผํ๊ฒ 100๊ฐ์ ์์ฒญ์ ๋ณด๋ด๋๋ก ํ๊ฒ ์ต๋๋ค.
(์ด์ ํ ์คํธ์ ๋ง์ฐฌ๊ฐ์ง๋ก, ๋ ์๋ฒ๋ฅผ ๋ชจ๋ ์ฌ์คํ ํด์ค ๋ค, http://localhost:8080/stocks๋ก get ์์ฒญ์ ๋ณด๋ด์ค๋๋ค.)
์ฝ๋๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.jupiter.api.Test;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
class MultiServerStockServiceTest {
@Test
void decrease_with_100_request_to_multi_server() throws InterruptedException {
// given
int threadCount = 100;
RestTemplate restTemplate = new RestTemplate();
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
// when
for (int i = 0; i < threadCount; i++) {
final int ii = i;
executorService.submit(() -> {
try {
int port = (ii % 2 == 0) ? 8080 : 8081;
ResponseEntity<Void> forEntity = restTemplate.getForEntity(
"http://localhost:" + port + "/stocks/1/decrease",
Void.class);
} finally {
latch.countDown();
}
});
}
latch.await();
}
}
๊ฒฐ๊ณผ๋ฅผ ํ์ธํ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์๋ฅผ ํตํด 2๊ฐ ์ด์์ ๋ค์ค ์๋ฒ ํ๊ฒฝ์์๋ synchronized ํค์๋๋ฅผ ํตํด ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์์ ํ์ธํ ์ ์์์ต๋๋ค.
๐ค ํด๊ฒฐ - Lock
synchronized ์ด์ธ์๋ java์์ ์ ๊ณตํด์ฃผ๋ Lock์ ํตํด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ต๋๋ค.
StockService ์ฝ๋๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํฉ๋๋ค.
@RequiredArgsConstructor
@Service
public class StockService {
private final StockRepository stockRepository;
@Transactional
public void decrease(Long id) {
Stock stock = stockRepository.getById(id);
stock.decrease();
stockRepository.saveAndFlush(stock);
}
}
์ดํ Lock์ ํตํด ๋์์ฑ ์ ์ด๋ฅผ ๋ด๋นํ๋ Facade๋ค ๋ง๋ค์ด ์ฃผ๊ฒ ์ต๋๋ค.
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class LockStockFacade {
private ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();
private final StockService stockService;
public void decrease(Long id) throws InterruptedException {
Lock lock = locks.computeIfAbsent(String.valueOf(id), key -> new ReentrantLock());
boolean acquiredLock = lock.tryLock(3, TimeUnit.SECONDS);
if (!acquiredLock) {
throw new RuntimeException("Lock ํ๋ ์คํจ");
}
try {
stockService.decrease(id);
} finally {
lock.unlock();
}
}
}
์ ์ฝ๋๋ฅผ ์คํํด๋ณด๊ธฐ ์ํด ๋ค์ ํ ์คํธ์ฝ๋๋ฅผ ์์ฑํฉ๋๋ค.
@SpringBootTest
class LockStockFacadeTest {
@Autowired
private LockStockFacade lockStockFacade;
@Autowired
private StockRepository stockRepository;
private Long stockId;
@BeforeEach
void setUp() {
stockId = stockRepository.saveAndFlush(new Stock(1L, 100))
.getId();
}
@AfterEach
void tearDown() {
stockRepository.deleteAll();
}
@Test
void decrease_with_100_request_lock_facade() throws InterruptedException {
// given
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
// when
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
lockStockFacade.decrease(stockId);
} catch (Exception e) {
System.out.println(e);
} finally {
latch.countDown();
}
});
}
latch.await();
// then
Stock stock = stockRepository.getById(stockId);
assertThat(stock.getQuantity()).isEqualTo(0);
}
}
์ฑ๊ณตํ๋ ๊ฒ์ ์ ์ ์์ต๋๋ค.
๊ทธ๋ฌ๋ Lock์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ ์ญ์๋ synchronized์ ๊ฐ์ด ๋ค์ค ์๋ฒ ํ๊ฒฝ์์๋ ์ ๋๋ก ์๋ํ์ง ์์ต๋๋ค.
๐ณ ์ฐธ๊ณ : ๋ถํธ 3.2 ์ด์ ๋ฒ์ - LockRegistry
์คํ๋ง๋ถํธ 3.2 ๋ฒ์ ์ด์๋ถํฐ๋ Spring Integration ์์กด์ฑ์ ์ถ๊ฐํ ๋ค, LockRegistry๋ฅผ ํตํด ๋ค์๊ณผ ๊ฐ์ด ๊ฐํธํ๊ฒ ์ฌ์ฉํ ์๋ ์์ต๋๋ค.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.support.locks.DefaultLockRegistry;
import org.springframework.integration.support.locks.LockRegistry;
@Configuration
public class LockConfig {
@Bean
public LockRegistry lockRegistry() {
return new DefaultLockRegistry();
}
}
@RequiredArgsConstructor
@Service
public class LockStockFacade {
private final LockRegistry lockRegistry;
private final StockService stockService;
public void decrease(Long id) throws InterruptedException {
lockRegistry.executeLocked(String.valueOf(id), () -> {
stockService.decrease(id);
});
}
}
๐ค ๋ง๋ฌด๋ฆฌ
์ด๋ฒ ๊ธ์์๋ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฌด์์ธ์ง์, synchronized, Lock๋ฅผ ํตํด ๋จ์ผ ์น ์๋ฒ ํ๊ฒฝ์์ ์ด๋ฅผ ์ด๋ป๊ฒ ํด๊ฒฐํ ์ ์๋์ง์ ๋ํด ์์๋ณด์์ต๋๋ค.
๊ทธ๋ฌ๋ synchronized๋ ์ฌ๋ฌ ๋์ ์๋ฒ์์ ๋์ผ ์์์ ์ ๊ทผํ๋ ๊ฒฝ์ฐ์๋ ์ฌ์ฉํ ์ ์๋ค๋ ๋ฌธ์ ์ ์ด ์กด์ฌํ์ต๋๋ค.
๋ค์ ๊ธ์์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋๊ด์ ๋ฝ๊ณผ ๋น๊ด์ ๋ฝ์ ํตํด ๋ค์ค ์๋ฒ ํ๊ฒฝ์์ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์๋ณด๊ฒ ์ต๋๋ค.
๐ Reference
[์ฌ๊ณ ์์คํ ์ผ๋ก ์์๋ณด๋ ๋์์ฑ์ด์ ํด๊ฒฐ๋ฐฉ๋ฒ]
https://www.youtube.com/watch?v=F-KDcb3FBuw
https://docs.spring.io/spring-integration/reference/distributed-locks.html