๐ ์ข์์ ๋ฒํผ ๋๋ฅด๋ ๊ฑด 1์ด, ์ค๊ณํ๋ ๊ฑด ํ๋ฃจ
TL;DR ๐ก ์ข์์ ๊ธฐ๋ฅ์ ์ค๊ณํ๋ฉด์ ์ญ์ ์ ๋ต, ์ค๋ณต ์์ฒญ(๋ฐ๋ฅ) ์ฒ๋ฆฌ, ์ํ ์ํ ๋ถ๋ฆฌ๊น์ง ๊ณ ๋ฏผ์ด ๊ผฌ๋ฆฌ๋ฅผ ๋ฌผ์๋ค. โ๋จ์ํ ๊ธฐ๋ฅโ์ ์์๋ค.
๐ ์ข์์ ํ๋์ฏค์ด์ผ
์ด์ปค๋จธ์ค ์ค๊ณ ๊ณผ์ ์์ ์ข์์ ๊ธฐ๋ฅ์ ์ ํ๋ค. ์ฒ์์ likes ํ
์ด๋ธ ํ๋ ๋ง๋ค๊ณ INSERT/DELETE ํ๋ฉด ๋๋ ๊ฑฐ ์๋๊ฐ?โ๋ผ๊ณ ์๊ฐํ๋ค.
๊ทผ๋ฐ ๋ง์ ์ค๊ณ๋ฅผ ์์ํ๋๊น ์ง๋ฌธ์ด ๋๋ ์์ด ๋์๋ค.
- ์ทจ์ํ๋ฉด ์ง์ง ์ง์? ๋จ๊ฒจ๋ฌ?
- ๊ฐ์ ๋ฒํผ ๋ ๋ฒ ๋๋ฆฌ๋ฉด?
- ์ข์์ ๋๋ฅธ ์ํ์ด ํ์ ์ด๋ฉด ์ด๋ป๊ฒ ๋ณด์ฌ์ค?
ํ๋์ฉ ์ ๋ฆฌํ๋ค ๋ณด๋ ํ๋ฃจ๊ฐ ๊ฐ๋ค.
1. ๐๏ธ ์ง์ธ ๊ฑฐ์ผ, ๋จ๊ธธ ๊ฑฐ์ผ
์ข์์ ์ทจ์๋ฅผ ๋๋ฅด๋ฉด DB์์ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ง. ๋ ๊ฐ์ง ์ ํ์ง๊ฐ ์์๋ค.
Soft Delete โ deleted_at์ ์๊ฐ๋ง ์ฐ๊ณ ํ์ ๋จ๊ฒจ๋ . ์ด๋ ฅ์ด ๋ณด์กด๋๋๊น ๋์ค์ ์ถ์ฒ/๋ญํน ๋ฐ์ดํฐ๋ก ํ์ฉ ๊ฐ๋ฅ.
Hard Delete โ ๋ฌผ๋ฆฌ ์ญ์ . ํ ์ด๋ธ์ด ๊น๋ํ์ง๋ง ์ทจ์ ์ด๋ ฅ์ด ์ฌ๋ผ์ง.
์ข์์ ํน์ฑ์ ๋ฑ๋ก/์ทจ์๊ฐ ๋น๋ฒํ๋ค. Soft Delete๋ก ๊ฐ๋ฉด UK(Unique Key) ์ค๊ณ๊ฐ ๊ท์ฐฎ์์ง๋ค.
UNIQUE (user_id, product_id)๋ก ์ก์ผ๋ฉด deleted_at์ด ์ฐํ ํ ๋๋ฌธ์ ์ฌ๋ฑ๋ก์ด ๋งํ๋ค. ๊ทธ๋ ๋ค๊ณ UNIQUE (user_id, product_id, deleted_at)์ผ๋ก ์ก์ผ๋ฉด? MySQL์์ NULL์ UK ์ค๋ณต ์ฒดํฌ์์ ๋น ์ง๊ธฐ ๋๋ฌธ์ ์๋๋๋ก ๋์ํ์ง ์์ ์ ์๋ค.
๊ฑฐ๊ธฐ๋ค ๋ชจ๋ ์กฐํ ์ฟผ๋ฆฌ์ WHERE deleted_at IS NULL์ ๋นผ๋จน์ผ๋ฉด ์ญ์ ๋ ๋ฐ์ดํฐ๊ฐ ์์ฌ ๋์จ๋ค.
์๊ตฌ์ฌํญ์ โ์ข์์ ๋ฐ์ดํฐ๋ ํฅํ ์ถ์ฒ/๋ญํน ๊ธฐ์ด ๋ฐ์ดํฐ๋ก ํ์ฉ ๊ฐ๋ฅโ์ด๋ผ๋ ๋ฌธ์ฅ์ด ์์ด์ ์ข ๊ฑธ๋ ธ๋ค. ์ด๋ ฅ์ด ์์ผ๋ฉด ํ๋ ํจํด ๋ถ์์ ๋ชป ํ๋๊น.
๊ทผ๋ฐ ๋ค์ ์๊ฐํด๋ณด๋, ์ถ์ฒ/๋ญํน์ ํ์ํ ๊ฑด โ์ง๊ธ ๋๊ฐ ๋ญ ์ข์์ ํ๊ณ ์๋์งโ์ด์ง, โ3์ผ ์ ์ ๋๋ ๋ค ์ทจ์ํ๋คโ ๊ฐ์ ์ด๋ ฅ์ ์๋ ๊ฒ ๊ฐ๋ค. ํ๋ ํจํด ๋ถ์์ด ์ ๋ง ํ์ํด์ง๋ฉด, likes ํ ์ด๋ธ์ ๊ฑด๋๋ฆฌ๋ ๊ฒ ์๋๋ผ ๋ณ๋ ์ด๋ฒคํธ ํ ์ด๋ธ์ ์ถ๊ฐํ๋ ๊ฒ ๋ง์ง ์์๊น.
ํ์ฌ ์ฝ๋(favorite_goods)๋ ํ์ธํด๋ดค๋๋ฐ ๋ฌผ๋ฆฌ ์ญ์ ๋ฐฉ์์ด์๋ค.
๊ฒฐ๋ก : Hard Delete. ์ข์์์ ํต์ฌ ๊ฐ์น๋ โํ์ฌ ์ํโ์ด์ง โ์ด๋ ฅโ์ด ์๋๋ผ๊ณ ํ๋จํ๋ค.
2. ๐ฑ๏ธ ๋ฐ๋ฅ โ ๊ฐ์ ๋ฒํผ ๋ ๋ฒ ๋๋ ธ์ ๋
์ข์์ ๋ฒํผ์ ๋น ๋ฅด๊ฒ ๋ ๋ฒ ๋๋ฅด๋ ๊ฒฝ์ฐ. ํ๋ก ํธ์์ ๋ง์์ผ ํ๋ ๊ฑฐ ์๋๋๊ณ ํ ์ ์๋๋ฐ, ๋ฐฑ์๋๋ โํ๋ก ํธ๊ฐ ์ ๋ง์์ ๋โ๋ ๋๋นํด์ผ ํ๋ค๊ณ ์๊ฐํ๋ค.
์ผ๋จ ์ค๋ณต์ด๋ฉด ๋ญ ๋ฐํํ ์ง
์ด๋ฏธ ์ข์์ํ ์ํ์ ๋ค์ POST๊ฐ ์ค๋ฉด?
- A: ํ ๊ธ โ ์์ผ๋ฉด ์ญ์ , ์์ผ๋ฉด ์์ฑ. UX๋ ๊ฐ๋จํ๋ฐ POST/DELETE ์๋ฏธ๊ฐ ๋ชจํธํด์ง
- B: 200 OK โ ์ด๋ฏธ ์์ด๋ ์ฑ๊ณต ๋ฐํ. ๋ฉฑ๋ฑ์ ์ด๋ผ ์ฌ์๋์ ์์ . ๊ทผ๋ฐ ์ ๊ท ์์ฑ์ด๋ ๊ตฌ๋ถ์ด ์ ๋จ
- C: 409 CONFLICT โ ์ด๋ฏธ ์กด์ฌํ๋ฉด ์๋ฌ. HTTP ์คํ์ ์ถฉ์ค
C์(409)์ ์ ํํ๋ค. POST๋ โ์์ฑโ์ด๋๊น ์ด๋ฏธ ์์ผ๋ฉด ์ถฉ๋์ด๋ผ๊ณ ์๊ฐํ๋ค. ๋ค๋ง B์(200)๋ ํ๋ฆฐ ๊ฑด ์๋ ๊ฒ ๊ฐ๊ณ , ์ด๊ฑด ์ข ์ ๋ต์ด ์๋ ์์ญ์ด๋ผ๊ณ ๋๊ผ๋ค.
409๋ก ๋์ด ์๋๋ค
๋ฌธ์ ๋ ํ์ด๋ฐ์ด๋ค. ๊ฑฐ์ ๋์์ ๋ ์์ฒญ์ด ๋ค์ด์ค๋ฉด:
1
2
์์ฒญ A: SELECT โ ์์ โ INSERT โ ์ฑ๊ณต
์์ฒญ B: SELECT โ ์์ โ INSERT โ ???
์์ฒญ B๊ฐ SELECTํ๋ ์์ ์ A์ INSERT๊ฐ ์์ง ์ปค๋ฐ ์ ๋์ผ๋ฉด, B๋ โ์์โ์ผ๋ก ํ๋จํ๊ณ INSERT๋ฅผ ์๋ํ๋ค.
์ฌ๊ธฐ์ DB์ Unique Key๊ฐ ์ต์ข ๋ฐฉ์ด์ ์ญํ ์ ํ๋ค:
1
ALTER TABLE likes ADD CONSTRAINT uk_likes UNIQUE (user_id, product_id);
B์ INSERT๊ฐ UK ์๋ฐ์ผ๋ก DataIntegrityViolationException์ด ํฐ์ง๋ฉด, ์ด๊ฑธ ์ก์์ 409 CONFLICT๋ก ๋ณํํ๋ฉด ๋๋ค.
๊ทธ๋ฐ๋ฐ ๋งค๋ฒ DB๊น์ง ๊ฐ์ผ ํด?
ํ์ฌ์์ ๋น์ทํ ๋ฌธ์ ๋ฅผ Redis๋ก ํด๊ฒฐํ ๊ฒฝํ์ด ์๋ค. @Cooldown์ด๋ผ๋ ์ปค์คํ
์ด๋
ธํ
์ด์
์ ๋ง๋ค์ด์ ์ผ๋ค.
Cooldown์ ๊ฒ์์์ ์คํฌ ์ด ๋ค์ ์ผ์ ์๊ฐ ๋์ ์ฌ์ฌ์ฉ์ด ์ ๋๋ ๊ทธ โ์ฟจํ์โ์์ ๋ฐ์จ ์ด๋ฆ์ด๋ค. ๊ฐ์ ์์ฒญ์ด ์งง์ ์๊ฐ ์์ ๋ฐ๋ณต๋๋ฉด, ์ฟจํ์์ด ์ ๋๋ฌ์ผ๋๊น ๋ฌด์ํ๋ ๊ฐ๋ .
๊ตฌ์กฐ๋ AOP + Redis + SpEL ์กฐํฉ์ด๋ค:
1
2
3
4
@Cooldown(key = RECENT_GOODS, value = "'target:' + #goodsId + ':buyer:' + #buyerId", ttl = 3)
public void addRecentGoods(Long goodsId, Long buyerId) {
// ...
}
@Cooldown์ ๋ฉ์๋์ ๋ถ์ด๋ฉด AOP(CooldownAspect)๊ฐ ๊ฐ๋ก์ฑ๋ค. value์ SpEL ํํ์์ ์ธ ์ ์์ด์, ๋ฉ์๋ ํ๋ผ๋ฏธํฐ๋ฅผ ์กฐํฉํด ๋์ ์ผ๋ก ํค๋ฅผ ๋ง๋ ๋ค. ์ ์์์์๋ RECENT_GOODS:target:123:buyer:456 ๊ฐ์ ํค๊ฐ ์์ฑ๋๋ค.
ํต์ฌ์ CooldownService.acquire():
1
2
3
4
5
public boolean acquire(String key, Duration ttl) {
return Boolean.TRUE.equals(
redisTemplate.opsForValue().setIfAbsent(key, "1", ttl)
);
}
Redis์ setIfAbsent๋ SETNX ๋ช
๋ น์ด์ ๋์ํ๋ค. ํค๊ฐ ์์ผ๋ฉด ์ธํ
ํ๊ณ true, ์ด๋ฏธ ์์ผ๋ฉด ์๋ฌด๊ฒ๋ ์ ํ๊ณ false. ์ด๊ฒ ์์์ (atomic)์ผ๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ ๋์ ์์ฒญ์ด ์๋ ๋ฑ ํ๋๋ง ํต๊ณผํ๋ค. TTL์ ๊ฐ์ด ๊ฑธ์ด์, 3์ด๊ฐ ์ง๋๋ฉด ํค๊ฐ ์๋ ์ญ์ ๋๊ณ ๋ค์ ์์ฒญํ ์ ์๋ค.
AOP์์๋ acquire()์ ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ๋ถ๊ธฐํ๋ค:
1
2
3
4
5
6
7
8
9
10
11
12
13
// CooldownAspect.java (ํต์ฌ๋ง)
boolean firstHit;
try {
firstHit = cooldownService.acquire(key, Duration.ofSeconds(cooldown.ttl()));
} catch (Exception e) {
log.warn("Redis ์ฅ์ ๋ฐ์ โ ์ค๋ณต ์์ฒญ ์ ์ด ๋ถ๊ฐ. key={}", key);
firstHit = true; // Redis ์ฅ์ ์ โ ๊ทธ๋ฅ ํต๊ณผ (๊ฐ์ฉ์ฑ ์ฐ์ )
}
if (!firstHit) {
return null; // ์ฟจํ์ ์ ๋๋จ โ ๋น์ฆ๋์ค ๋ก์ง ์คํ ์ ํจ
}
return pjp.proceed(); // ์ฒซ ์์ฒญ โ ์ ์ ์คํ
Redis๊ฐ ์ฃฝ์ด๋ ์๋น์ค๊ฐ ๋ฉ์ถ๋ฉด ์ ๋๋๊น catch์์ firstHit = true๋ก ๋น ์ง๋ค. ๋ฐ๋ฅ ๋ฐฉ์ง๋ณด๋ค ๊ฐ์ฉ์ฑ์ด ๋ ์ค์ํ๋ค๋ ํ๋จ์ด์๋ค.
์ข์์์ ์ ์ฉํ๋ฉด ์ด๋ฐ 3์ค ๋ฐฉ์ด๊ฐ ๋๋ค:
| ๋ ์ด์ด | ๋ฐฉ์ด ์๋จ | ์ญํ |
|---|---|---|
| 1์ฐจ | Redis SETNX like:{userId}:{productId} TTL 3์ด | ๋ฐ๋ฅ(์ฐํ) ์ฐจ๋จ โ DB๊น์ง ์ ๊ฐ |
| 2์ฐจ | Service์์ SELECT EXISTS | ์ ์์ ์ธ ์ค๋ณต ์์ฒญ 409 ๋ฐํ |
| 3์ฐจ | DB UNIQUE (user_id, product_id) | ์ต์ข ๋ฐฉ์ด์ โ Race Condition ๋๋น |
1์ฐจ๊ฐ ์์ผ๋ฉด ๋ฐ๋ฅ ์์ฒญ์ด ์ ๋ถ DB๊น์ง ๋ด๋ ค๊ฐ๋ค. 3์ฐจ๊ฐ ์์ผ๋ฉด Redis ์ฅ์ ์ ์ค๋ณต ์ฝ์ ์ด ๋ฐ์ํ ์ ์๋ค. ํ ๊ฒน๋ง์ผ๋ก๋ ๋ถ์ํ๋ค.
3. ๐ท๏ธ ํ์ ์ธ๋ฐ ๋ณด์ฌ์ค์ผ ํด?
์ข์์ ๋๋ฅธ ์ํ์ด ํ์ ์ด ๋๋ค. ์ข์์ ๋ชฉ๋ก์์ ๊ทธ ์ํ์ ์ด๋ป๊ฒ ๋ณด์ฌ์ค์ผ ํ ๊น?
์ฒ์์ โํ์ ์ด๋ฉด ๋ชฉ๋ก์์ ๋นผ๋ฉด ๋์งโ๋ผ๊ณ ์๊ฐํ๋ค. ๊ทผ๋ฐ ๋ด๊ฐ ์ฐํด๋ ์ํ์ด ์ด๋ ๋ ๊ฐ์๊ธฐ ์ฌ๋ผ์ ธ ์์ผ๋ฉด? ์ฌ์ฉ์ ์ ์ฅ์์๋ ๋นํฉ์ค๋ฝ๋ค. ๊ทธ๋ฐ๋ฐ ์ผํ๋ชฐ์์ ์ค์ ๋ก ์ด๋ป๊ฒ ํ๋์ง ๋ ์ฌ๋ ค๋ณด๋ฉด:
๋ฌด์ ์ฌ โ ํ์ ์ํ์ ํ์ ์ค๋ฒ๋ ์ด + โํ์ โ ๋ฑ์ง๋ก ํ์๋๋ค
ํ์ ์ด์ด๋ ๋ชฉ๋ก์๋ ๋ณด์ฌ์ค๋ค. ๋ค๋ง ์ด๋ฏธ์ง๊ฐ ์ด๋ก๊ฒ(๋ค ์ฒ๋ฆฌ) ๋๊ณ ํ์ ๋ฑ์ง๊ฐ ๋ถ๋๋ค.
์ด๊ฑธ DB ์ค๊ณ๋ก ์ฎ๊ธฐ๋ฉด, ์ํ์ ๋ ๊ฐ์ง ์์ฌ๊ฒฐ์ ์ด ํ์ํ๋ค:
- ํ๋งค ๊ฐ๋ฅ ์ฌ๋ถ โ ๊ตฌ๋งค ๋ฒํผ์ด ํ์ฑํ๋๋๊ฐ?
- ๋ชฉ๋ก ๋ ธ์ถ ์ฌ๋ถ โ ์ํ ๋ชฉ๋ก์ ๋ณด์ด๋๊ฐ?
์ด ๋ ๊ฐ๋ฅผ status ํ๋๋ก ํ์น๋ฉด โ๋ณด์ด์ง๋ง ๊ตฌ๋งค ๋ถ๊ฐโ ์ํ๋ฅผ ํํํ ์๊ฐ ์๋ค.
1
2
status = ACTIVE โ ๊ตฌ๋งค ๊ฐ๋ฅ + ๋
ธ์ถ
status = INACTIVE โ ๊ตฌ๋งค ๋ถ๊ฐ + ???
INACTIVE์ธ๋ฐ ๋ณด์ฌ์ค์ผ ํ๋, ์จ๊ฒจ์ผ ํ๋? status๋ง์ผ๋ก๋ ๋ต์ด ์ ๋์จ๋ค.
๊ทธ๋์ status + displayYn์ ๋ถ๋ฆฌํ๋ค:
| status | displayYn | ๋ชฉ๋ก ๋ ธ์ถ | ๊ตฌ๋งค ๊ฐ๋ฅ | UI |
|---|---|---|---|---|
| ACTIVE | true | โ | โ | ์ผ๋ฐ ์ํ ์นด๋ |
| ACTIVE | false | โ | โ (URL ์ง์ ) | ๊ด๋ฆฌ์๊ฐ ์๋์ ์ผ๋ก ์จ๊ธด ์ํ |
| INACTIVE | true | โ | โ | ๋ค ์ฒ๋ฆฌ + โํ๋งค์ค์งโ ๋ฑ์ง |
| INACTIVE | false | โ | โ | ์์ ๋ฏธ๋ ธ์ถ |
ํ๋ก ํธ์์๋ ์ด ๋ ํ๋๋ฅผ ์กฐํฉํด์ UI๋ฅผ ๋ถ๊ธฐํ ์ ์๋ค:
1
2
3
displayYn = false โ ๋ชฉ๋ก์์ ์ ์ธ
displayYn = true, ACTIVE โ ์ ์ ์ํ ์นด๋
displayYn = true, INACTIVE โ ๋ค์ฒ๋ฆฌ + 'ํ๋งค์ค์ง' ๋ผ๋ฒจ
โ์ด๊ฑฐ ์ค๋ฒ์์ง๋์ด๋ง ์๋์ผ?โ๋ผ๋ ์๊ฐ๋ ๋ค์๋ค. ๊ณผ์ ๋ฒ์์์ ๋ค ์ฒ๋ฆฌ๊ฐ ์ค์ ๋ก ํ์ํ ๊ฑด ์๋๋๊น.
๊ทผ๋ฐ Shopify, Amazon, ๋ค์ด๋ฒ, ์ฟ ํก ์ ๋ถ ์ฐพ์๋ดค๋๋ฐ, ์ฑ์ํ ํ๋ซํผ์ ๋๋ถ๋ถ status โ visibility๋ฅผ ๋ถ๋ฆฌํด์ ๊ด๋ฆฌํ๊ณ ์์๋ค. ๋ฐฉํฅ์ฑ์ ํ๋ฆฌ์ง ์์๋ค๊ณ ์๊ฐํ๋ค.
displayYn ๋ค์ด๋ฐ๋ ๊ณ ๋ฏผ์ด ์์๋ค. Kotlin์์
isDisplay๋ก ํ๋ฉด Jackson ์ง๋ ฌํ ์display๋ก ๋ณํ๋๋ ์๋ ค์ง ๋ฒ๊ทธ(jackson-module-kotlin #80)๊ฐ ์๋ค. ํ๊ตญ ์ด์ปค๋จธ์ค ๊ด๋ก๋ฅผ ๋ฐ๋ผYn์ ๋ฏธ์ฌ๋ก ๊ฐ๋ค.
๐ ๋์๋ณด๋ฉด
์ข์์ ๊ธฐ๋ฅ ํ๋์์ ๋์จ ๊ณ ๋ฏผ๋ค:
- ์ญ์ ์ ๋ต โ Hard Delete vs Soft Delete โ ํ์ฌ ์ํ vs ์ด๋ ฅ ๋ณด์กด
- ์ค๋ณต ์ฒ๋ฆฌ โ 409 vs 200 โ HTTP ์๋ฏธ๋ก vs ํด๋ผ์ด์ธํธ ํธ์์ฑ
- ๋ฐ๋ฅ ๋ฐฉ์ง โ Redis SETNX โ Service ์ฒดํฌ โ DB UK 3์ค ๋ฐฉ์ด
- ์ํ ๋ถ๋ฆฌ โ status ร displayYn โ โ๋ณด์ด์ง๋ง ๊ตฌ๋งค ๋ถ๊ฐโ๋ฅผ ํํ
์ฒ์์ โINSERT/DELETE๋ฉด ๋์งโ๋ผ๊ณ ์๊ฐํ ๊ฒ ๋ถ๋๋ฌ์ด ๊ฑด ์๋๋ฐ, ํ๊ณ ๋ค์๋ก ํ๋จ์ ์ฐ์์ด์๋ค. ๊ฐ๊ฐ์ โ์ ๋ตโ์ด ์๋ค๊ธฐ๋ณด๋ค๋ ์ํฉ์ ๋ฐ๋ผ ํธ๋ ์ด๋์คํ๊ฐ ๋ฌ๋ผ์ง๋ ๊ฑฐ๊ณ .
์ค๊ณ ์ฃผ์ฐจ๋ผ ์ฝ๋๋ ์ ์งฐ์ง๋ง, ์ด๋ฐ ๊ณ ๋ฏผ๋ค์ ๋จผ์ ํด๋๋๊น ๊ตฌํํ ๋ ๋ ํค๋งฌ ๊ฒ ๊ฐ๋ค. ์๋ง.
์ข์์ ๋ฒํผ ๋๋ฅด๋ ๊ฑด 1์ด. ๊ทธ ๋ค๋ฅผ ์ค๊ณํ๋ ๊ฑด ํ๋ฃจ๊ฐ ๋๊ฒ ๊ฑธ๋ ธ๋ค.
