Backend ๐Ÿ’ป

๋ฉฑ๋“ฑ์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ์‹œ์Šคํ…œ ๊ฐœ๋ฐœํ•˜๊ธฐ

minjiwoo 2024. 10. 13. 23:02
728x90

 
GPT4o ๋ชจ๋ธ ์‚ฌ์šฉ - LLM ์„ ๊ธฐ๋ฐ˜์œผ๋กœ Resume ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ๋Š” ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜, Resumait

Intro 

 ํ˜„์žฌ ๊ธ€๋˜ 9๊ธฐ์—์„œ ๋งŒ๋‚œ ํŒ€์›๋“ค๊ณผ ๊ฐ™์ด Resumait ๋ผ๋Š” ์ œํ’ˆ์„ ๋งŒ๋“ค๊ณ  ์žˆ๋‹ค. CS ์Šคํ„ฐ๋””๋ฅผ ์ง„ํ–‰ํ•˜๋‹ค๊ฐ€ ๋œป์ด ๋งž์•„์„œ ์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ๊นŒ์ง€ ๊ฐœ๋ฐœ์„ ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค. Resumait ์€ LLM ์„ ๊ธฐ๋ฐ˜์œผ๋กœ, ์‚ฌ์šฉ์ž์˜ ์ด๋ ฅ์„œ ์ž‘์„ฑ์„ ๋„์™€์ฃผ๋Š” ์„œ๋น„์Šค์ด๋‹ค. Resumait๋Š” ํŠนํžˆ ์ฃผ๋‹ˆ์–ด ๋ฐ ๊ฒฝ๋ ฅ ๊ฐœ๋ฐœ์ž๋ฅผ ์ฃผ์š” ํƒ€๊ฒŸ์œผ๋กœํ•˜๊ณ  ์žˆ๋‹ค. ๊ฐœ๋ฐœ์ž ์ฑ„์šฉ ์‹œ์žฅ ํŠน์„ฑ์ƒ ํ•˜๋‚˜์˜ ์ด๋ ฅ์„œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์—ฌ๋Ÿฌ ํšŒ์‚ฌ์˜ ๊ณต๊ณ ์— ์ง€์›ํ•˜๊ฒŒ ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ์ข…์ข… ๊ฐœ๋ฐœ์ž์—๊ฒŒ๋„ ์ž๊ธฐ์†Œ๊ฐœ์„œ๋ฅผ ์š”๊ตฌํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ๋‹ค. 

Resumait ์—์„œ๋Š” Credit ์ด ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋‹จ์œ„์ด๋‹ค. ์‚ฌ์šฉ์ž๋Š” ๋ณธ์ธ์˜ ์ด๋ ฅ์„œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž๊ธฐ์†Œ๊ฐœ์„œ ๊ธ€์„ ์ž‘์„ฑํ•  ๋•Œ Credit ์„ ์†Œ๋น„ํ•˜๊ฒŒ ๋œ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์œ ์˜ํ•  ์ ์€ ์ด Credit์€ ์‚ฌ์šฉ์ž๊ฐ€ Resumait์—์„œ ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํ• ๋‹น๋Ÿ‰๊ณผ๋„ ๊ฐ™๊ธฐ ๋•Œ๋ฌธ์—, 
Credit ์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์ถฉ์ „ํ•˜๋Š” ๊ณผ์ •์—์„œ Credit ๊ฐ’์— ํ•ญ์ƒ ๋ฌธ์ œ๊ฐ€ ์—†์–ด์•ผ ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด์„œ, Credit ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ณผ์ •์—์„œ API ํ†ต์‹ ์— ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒจ ๋‘๋ฒˆ ์‚ฌ์šฉ๋˜๊ฒŒ ํ•˜๋ฉด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ์ฆ‰, Credit ์„ ์‚ฌ์šฉํ•œ ๊ฒฐ์ œ API ์—์„œ๋Š” '๋ฉฑ๋“ฑ์„ฑ'์„ ๋ณด์žฅํ•ด์•ผ ํ•œ๋‹ค. ๋ฉฑ๋“ฑ์„ฑ์„ ์–ด๋–ป๊ฒŒ ๋ณด์žฅํ•˜๋ฉฐ, ๋ฉฑ๋“ฑ์„ฑ์ด๋ผ๋Š” ๊ฐœ๋…์ด ์ •ํ™•ํ•˜๊ฒŒ ๋ฌด์—‡์ธ์ง€ ์ •๋ฆฌํ•ด๋ณด์•˜๋‹ค!

 

๋ฉฑ๋“ฑ์„ฑ(Idempotency)์ด๋ž€? 

๋ฉฑ๋“ฑ์„ฑ(Idempotency)์ด๋ž€, ์—ฐ์‚ฐ์„ ์—ฌ๋Ÿฌ๋ฒˆ ์‹คํ–‰ํ•˜๋”๋ผ๋„ ๊ฒฐ๊ณผ๊ฐ€ ๋‹ฌ๋ผ์ง€์ง€ ์•Š๋Š” ์„ฑ์งˆ์„ ์˜๋ฏธํ•œ๋‹ค. ๋ฉฑ๋“ฑ์„ฑ์€ ๊ฐœ๋ฐœํ•  ๋•Œ์—๋„ ์ข…์ข… ๋“ฑ์žฅํ•˜๊ฒŒ ๋˜๋Š” ๊ฐœ๋…์ธ๋ฐ, ๊ฐœ๋… ์ž์ฒด๋Š” ๋™์ผํ•˜์ง€๋งŒ ๋ฉฑ๋“ฑ์„ฑ์„ ๋ณด์žฅํ•ด์•ผ ํ•˜๋Š” ๋‹ค์–‘ํ•œ ์ผ€์ด์Šค๊ฐ€ ์žˆ๋‹ค.

 

1. HTTPS ๋ฉ”์„œ๋“œ์—์„œ์˜ ๋ฉฑ๋“ฑ์„ฑ

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

HTTP ๋ฉ”์„œ๋“œ ์ข…๋ฅ˜ ๋ฉฑ๋“ฑ์„ฑ ์œ ๋ฌด
GET O
POST X
PUT O
PATCH X
DELETE O

GET ๋ฉ”์„œ๋“œ์•ผ ๋‹น์—ฐํžˆ ๊ฐ’์— ๋Œ€ํ•ด '์กฐํšŒ' ๋งŒํ•˜๋‹ˆ๊นŒ ๋ฉฑ๋“ฑ์„ฑ์„ ๋ณด์žฅํ•œ๋‹ค๋Š” ๊ฒƒ์ด ์‰ฝ๊ฒŒ ์ดํ•ด๋˜๋Š”๋ฐ ๋‹ค๋ฅธ ๋ฉ”์†Œ๋“œ์˜ ๊ฒฝ์šฐ ํ—ท๊ฐˆ๋ฆฌ๊ธฐ ๋„ํ•œ๋‹ค. PUT์˜ ๊ฒฝ์šฐ ๊ฐ’์— ๋Œ€ํ•ด ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฉ”์„œ๋“œ์ด๋‹ค. ๊ทธ๋ ‡์ง€๋งŒ ์—ฌ๋Ÿฌ๋ฒˆ ํ˜ธ์ถœํ•˜๊ฒŒ ๋˜์–ด๋„ ํ•ญ์ƒ ๊ฐ™์€ ๊ฐ’์— ๋Œ€ํ•ด์„œ ์—…๋ฐ์ดํŠธ ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ด ๊ฒฝ์šฐ์—๋„ ๋ฉฑ๋“ฑ์„ฑ์„ ๋ณด์žฅํ•œ๋‹ค๊ณ  ํ•  ์ˆ˜ ์žˆ๋‹ค. 

DELETE์˜ ๊ฒฝ์šฐ์—๋„ ๊ฐ’์„ ์‚ญ์ œํ•œ ํ›„, ๋‹ค์‹œ ์š”์ฒญ์„ ๋ณด๋‚ด๋”๋ผ๋„ ๊ฐ’์ด ์ด๋ฏธ ์‚ญ์ œ๋œ ์ƒํƒœ๋Š” ๋™์ผํ•  ๊ฒƒ์ด๋‹ค. ๋”ฐ๋ผ์„œ DELETE๋„ ๋ฉฑ๋“ฑํ•˜๋‹ค. 

 

2. ETL ํŒŒ์ดํ”„๋ผ์ธ์—์„œ์˜ ๋ฉฑ๋“ฑ์„ฑ

๋ฐ์ดํ„ฐ ETL ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์ถ•ํ•  ๋•Œ์—๋„ ๋ฉฑ๋“ฑ์„ฑ์€ ์ค‘์š”ํ•˜๋‹ค. ๋‚ด๊ฐ€ ๋ฐ์ดํ„ฐ์—”์ง€๋‹ˆ์–ด๋ง ์—…๋ฌด๋ฅผ ํ•˜๋ฉฐ ๊ฒฝํ—˜ํ•œ ETL ํŒŒ์ดํ”„๋ผ์ธ์˜ ์„ฑ๊ฒฉ์€ ํฌ๊ฒŒ 2๊ฐ€์ง€์˜€๋‹ค. 

  1. ๋งˆ์Šคํ„ฐ ์„ฑ ํ…Œ์ด๋ธ”์— ๋Œ€ํ•œ ETL ์ž‘์—…: ์ผ์ •ํ•œ ์ฃผ๊ธฐ (ex. ์ผ ๋‹จ์œ„) ๋งˆ๋‹ค ํ…Œ์ด๋ธ”์˜ ์ „์ฒด ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•ด์„œ ๋ชจ๋“  ๊ฐ’์ด update ํ•˜๋Š” ETL ์ž‘์—… 
  2. ์ฆ๋ถ„ ์ ์žฌ๊ฐ€ ํ•„์š”ํ•œ ํ…Œ์ด๋ธ”์— ๋Œ€ํ•œ ETL ์ž‘์—…: ์ผ์ •ํ•œ ์ฃผ๊ธฐ๋งˆ๋‹ค ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๋ฅผ ๋ˆ„์ ํ•˜์—ฌ ์Œ“์•„๊ฐ€๋Š” ETL ์ž‘์—…

 

ํŠนํžˆ, 2๋ฒˆ์˜ ์ฆ๋ถ„์ ์žฌ๊ฐ€ ํ•„์š”ํ•œ ํ…Œ์ด๋ธ”์ธ ๊ฒฝ์šฐ์— table row์˜ ์‹๋ณ„๊ฐ’์ด ๋˜๋Š” key ์ปฌ๋Ÿผ๋“ค์„ ๊ธฐ์ค€์œผ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ์ฆ๋ถ„์ ์žฌ ๋  ๊ฒƒ์ด๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ํŒŒ์ดํ”„๋ผ์ธ์— ์žฅ์• ๊ฐ€ ์ผ์–ด๋‚˜์„œ ๊ทธ๋‚  ํŒŒ์ดํ”„๋ผ์ธ์„ 2๋ฒˆ ์‹คํ–‰์‹œ์ผฐ์„ ๋•Œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ค‘๋ณต๋˜์–ด์„œ 2๋ฒˆ ๋“ค์–ด๊ฐ€๊ฒŒ ๋˜๋ฉด ๋ฌธ์ œ๊ฐ€ ๋œ๋‹ค. ์ฆ‰, ETL ํŒŒ์ดํ”„๋ผ์ธ์—์„œ๋„ ๋ฉฑ๋“ฑ์„ฑ์„ ๋ณด์žฅํ•ด ์ฃผ์–ด์•ผ ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ์ฃผ๋กœ ETL ํŒŒ์ดํ”„๋ผ์ธ์—์„œ ๋ฉฑ๋“ฑ์„ฑ์„ ๋ณด์žฅํ•ด์ฃผ๊ธฐ ์œ„ํ•ด์„œ UPSERT ํŠธ๋žœ์žญ์…˜์„ ์ง€์›ํ•œ๋‹ค๋ฉด UPSERT ์—ฐ์‚ฐ์„ ์‚ฌ์šฉํ•œ๋‹ค.

์•„๋ž˜๋Š” DataLake์—์„œ ACID ํŠธ๋žœ์žญ์…˜์„ ์ง€์›ํ•˜๋Š” DeltaTable ์—์„œ pyspark ๋ฅผ ์‚ฌ์šฉํ•œ UPSERT ์˜ˆ์‹œ์ด๋‹ค. key ์ปฌ๋Ÿผ์„ ๊ธฐ์ค€์œผ๋กœ ๋™์ผํ•œ ๊ฐ’์— ๋Œ€ํ•ด์„œ ๋ชจ๋‘ ์—…๋ฐ์ดํŠธํ•˜๊ณ , ์—†๋Š” ๊ฒฝ์šฐ์—๋Š” insert ํ•˜๊ฒŒ ๋œ๋‹ค. 

์•„๋ž˜์˜ ์ฝ”๋“œ์—์„œ๋Š” target_table์ด ๋ฐ์ดํ„ฐ๋ฅผ ์ฆ๋ถ„์ ์žฌํ•˜๋Š” ๋Œ€์ƒ ํ…Œ์ด๋ธ”์ด๋ฉฐ, sourceDF๊ฐ€ ์ƒˆ๋กœ ๋“ค์–ด์˜จ ๋ฐ์ดํ„ฐ์— ํ•ด๋‹นํ•œ๋‹ค. 

from delta.tables import DeltaTable 

target_table = DeltaTable.forName(spark, "existing delta table name")

(target_table.alias("t")
	.merge(sourceDF.alias("s"), "s.key = t.key")
	.whenMatchedUpdateAll() 
	.whenNotMatchedInsertAll()
	.execute())

 

์—ฐ์‚ฐ์„ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ์—๋Š” key ์ปฌ๋Ÿผ ๊ฐ’๋“ค์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฏธ ํƒ€๊ฒŸ ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๊ฐ€ ์ ์žฌ๊ฐ€ ๋˜์–ด์žˆ์–ด์„œ ์ค‘๋ณต๋˜๋Š” ๊ฐ’๋“ค์„ DELETE ํ•œ ํ›„์—, ํƒ€๊ฒŸ ํ…Œ์ด๋ธ”์— ๊ทธ๋‚  ์ฆ๋ถ„ ์ ์žฌํ•  ๋ฐ์ดํ„ฐ๋ฅผ INSERT ํ•˜๊ฒŒ ๋œ๋‹ค. 

-- 1. ๋‚ ์งœ ๋ฒ”์œ„์— ํ•ด๋‹นํ•˜๋Š” ์ค‘๋ณต๋˜๋Š” ๋ฐ์ดํ„ฐ ์‚ญ์ œ
DELETE FROM target_table
WHERE key_column IN (
    SELECT key_column
    FROM source_table
    WHERE 1=1
    AND key_column >= '2024-10-12'
    AND key_column < '2024-10-13'
);

-- 2. ์ƒˆ๋กœ์šด ์ฆ๋ถ„ ๋ฐ์ดํ„ฐ๋ฅผ ํƒ€๊ฒŸ ํ…Œ์ด๋ธ”์— ์‚ฝ์ž…
INSERT INTO target_table
SELECT *
FROM source_table
WHERE 1=1 
AND key_column >= '2024-10-12'
AND key_column < '2024-10-13';

 

3. ๊ฒฐ์ œ (Credit) ์‹œ์Šคํ…œ์—์„œ ๋ฉฑ๋“ฑ์„ฑ

๊ฒฐ์ œ ์‹œ์Šคํ…œ์—์„œ ๋ฉฑ๋“ฑ์„ฑ์„ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•ด์„œ ํ† ์Šค ํŽ˜์ด๋จผ์ธ  ๊ฒฐ์ œ ์‹œ์Šคํ…œ์˜ ๊ธ€์„ ๋งŽ์ด ์ฐธ๊ณ ํ•˜์˜€๋‹ค. ๋ฉฑ๋“ฑ์„ฑ key API์— ํฌํ•จ์‹œ์ผœ์„œ, ๋ฉฑ๋“ฑ์„ฑ์„ ๋ณด์žฅํ•˜๊ณ  ์•ˆ์ „ํ•œ ๊ฒฐ์ œ ์š”์ฒญ์„ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์˜€๋‹ค. 

์ด์— ๋”ฐ๋ผ์„œ Litestar Application์˜ Credit API ๋ฅผ ๊ฐœ๋ฐœํ•  ๋•Œ์—๋„ ๋ฉฑ๋“ฑํ‚ค (idemponency_key) ๋ผ๋Š” ๊ฐ’์„ ๋งŒ๋“ค๊ฒŒ ๋˜์—ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  API ์š”์ฒญ์ด ๋“ค์–ด์˜ฌ ๋•Œ, ๋ฉฑ๋“ฑํ‚ค๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค. ์ด ๋ฉฑ๋“ฑํ‚ค๋Š” ๋ฉฑ๋“ฑํ‚ค๋งŒ์„ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ table์„ ๋งŒ๋“ค์—ˆ๋‹ค. ๋ฉฑ๋“ฑํ‚ค๊ฐ€ ํฌํ•จ๋œ ์š”์ฒญ์ด ๋“ค์–ด์™”์„ ๋•Œ, ๋ฉฑ๋“ฑํ‚ค table์— ํ•ด๋‹น ๋ฉฑ๋“ฑํ‚ค์™€ ๋งค์นญ๋˜๋Š” ์š”์ฒญ ๊ธฐ๋ก์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด์„œ ์ค‘๋ณต๋˜๋Š” ์ฒ˜๋ฆฌ๋ฅผ ๋ฐฉ์ง€ํ•˜์˜€๋‹ค. 

Resumait์—์„œ LLM ๊ณผ ๊ด€๋ จ๋œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ฐœ๋ฐœํ•˜๊ธฐ ์œ„ํ•œ ์›น ๊ฐœ๋ฐœ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ๋Š” Litestar ๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค. ์šฐ์„ , Credit API ์ž‘์—…์„ ์œ„ํ•œ entity ์ •์˜๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ํ–ˆ๋‹ค. Wallet ์ด๋ผ๋Š” ๊ฐœ๋…์ด ์žˆ์œผ๋ฉฐ, Wallet Entity ์— credit ๊ฐ’์„ ์ €์žฅํ•œ๋‹ค. CreditHistory๋Š” credit์„ ์‚ฌ์šฉํ•œ ๋‚ด์šฉ์„ ๋กœ๊น…ํ•˜๊ธฐ ์œ„ํ•œ Entity์ด๋‹ค. ๋งˆ์ง€๋ง‰์œผ๋กœ IdempotencyKey๊ฐ€ ๋ฉฑ๋“ฑ์„ฑ ํ‚ค๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ ํ…Œ์ด๋ธ”๊ณผ ์—ฐ๋™๋œ๋‹ค. 

from uuid import UUID
from advanced_alchemy.base import UUIDAuditBase
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import UUID as psql_UUID
from typing import Literal

class CreditWallet(UUIDAuditBase):
    user_id: Mapped[UUID] = mapped_column(psql_UUID(as_uuid=True), unique=True, nullable=False)
    credit: Mapped[int] = mapped_column(Integer, nullable=False)

class CreditHistory(UUIDAuditBase):
    user_id: Mapped[UUID] = mapped_column(psql_UUID(as_uuid=True), nullable=False)
    current_credit: Mapped[int] = mapped_column(Integer, nullable=False)
    transaction_type: Mapped[Literal['use', 'charge']] = mapped_column(String, nullable=False)
    amount: Mapped[str] = mapped_column(Integer, nullable=False)

class IdempotencyKey(UUIDAuditBase):
    idempotency_key: Mapped[UUID] = mapped_column(psql_UUID(as_uuid=True), unique=True, nullable=False)

Credit ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ถฉ์ „, ์‚ฌ์šฉํ•˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค. 

class CreditWalletImpl(SQLAlchemyAsyncRepositoryService[CreditWallet]):
    repository_type = CreditWalletRepo

    async def check(self
                , user_id: UUID) -> CreditWallet | None:
        """check current credit"""

        credit = await self.repository.get_one_or_none(user_id=user_id)
        if credit is None:
            raise HTTPException(detail="Not Found", status_code=404)
        return credit
        

    async def create_wallet(self
                        , user_id:UUID) -> CreditWallet:
        """create wallet entity"""
        wallet = CreditWallet(user_id=user_id, credit=1500)
        await self.repository.add(wallet)
        await self.repository.session.commit()
        await self.repository.session.refresh(wallet)
        return wallet

    async def charge(self, user_id: UUID, amount: int) -> bool:
        """charge credit"""
        wallet = await self.repository.get_one_or_none(user_id=user_id)
        if wallet is None: 
            raise HTTPException(detail="Not Found", status_code=404)

        wallet.credit += amount
        await self.repository.update(wallet)
        await self.repository.session.commit()
        return True 

    
    async def use(self, user_id: UUID, amount: int) -> bool:
        """use credit"""
        wallet = await self.repository.get_one_or_none(user_id=user_id)
        if wallet and wallet.credit >= amount:
            wallet.credit -= amount 
            await self.repository.update(wallet)
            return True
        return False
    
    async def delete_wallet(self, user_id:UUID) -> bool:
        """delete wallet entity when the user account deleted"""
        wallet = await self.repository.get_one_or_none(user_id=user_id)
        if wallet is None:
            raise HTTPException(detail="Not Found", status_code=404)
        await self.repository.delete(wallet.id)
        await self.repository.session.commit()
        return True

 

๋‹ค์Œ์œผ๋กœ, ๋ฉฑ๋“ฑ์„ฑํ‚ค๋ฅผ ์กฐํšŒํ•˜๊ณ  ์ถ”๊ฐ€ํ•˜๋Š” ๋กœ์ง์ด๋‹ค. 

class IdempotencyKeyImpl(SQLAlchemyAsyncRepositoryService[IdempotencyKey]):
    repository_type = IdempotencyKeyRepo

    async def get_by_key(self, idempotency_key:UUID) -> IdempotencyKey | None:
        """search by idempotency key"""
        return await self.repository.get_one_or_none(idempotency_key=idempotency_key)
    
    async def add_new_key(self, idempotency_key:UUID) -> IdempotencyKey:
        """add a new idempotency key"""
        key = IdempotencyKey(idempotency_key=idempotency_key)
        await self.repository.add(key)
        await self.repository.session.commit() 
        await self.repository.session.refresh(key)
        return key

 

๋ฉฑ๋“ฑ์„ฑ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ, ์ค‘๋ณต๋˜๋Š” ์š”์ฒญ ์ฒ˜๋ฆฌ๋ฅผ ๋ฐฉ์ง€ํ•˜๋Š” ์„œ๋น„์Šค ๋ ˆ์ด์–ด์˜ ๋กœ์ง์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค. charge_credit ํ•จ์ˆ˜(credit ์ถฉ์ „)์™€ use_credit ํ•จ์ˆ˜(credit ์‚ฌ์šฉ)์—์„œ ๋ฉฑ๋“ฑ์„ฑ ํ‚ค๋ฅผ parameter๋กœ ๋ฐ›๊ณ  ์žˆ๋‹ค. 

@dataclass
class CreditService: 
    wallet_impl: CreditWalletImpl
    history_impl: CreditHistoryImpl
    idempotency_impl: IdempotencyKeyImpl

    async def check_credit_wallet(self, user_id:UUID) -> CreditWalletSchema: 
        wallet = await self.wallet_impl.check(user_id=user_id)
        return self.wallet_impl.to_schema(wallet, schema_type=CreditWalletSchema)
    
    async def create_credit_wallet(self, user_id:UUID) -> CreditWalletSchema:
        wallet = await self.wallet_impl.create_wallet(user_id=user_id)
        return self.wallet_impl.to_schema(wallet, schema_type=CreditWalletSchema)
    
    async def charge_credit(self
                                , user_id:UUID
                                , amount: int
                                , idempotency_key:UUID) -> CreditHistorySchema | dict[str:str]:
        existing_key = await self.idempotency_impl.get_by_key(idempotency_key=idempotency_key)
        if existing_key:
            """์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ์š”์ฒญ"""
            return {"message": "request was already processed"}
        await self.idempotency_impl.add_new_key(idempotency_key=idempotency_key)
        result = await self.wallet_impl.charge(user_id=user_id, amount=amount)
        if result: 
            obj = await self.wallet_impl.check(user_id=user_id)
            history = await self.history_impl.record_history(user_id=user_id
                                                                        , amount=amount
                                                                        , current=obj.credit
                                                                        , transaction_type="charge")
            return self.history_impl.to_schema(history, schema_type=CreditHistorySchema)
        else: 
            return {"message": "user_id is not valid !"} 
    
    async def use_credit(self
                            , user_id:UUID
                            , amount: int
                            , idempotency_key:UUID) -> CreditHistorySchema | dict[str:str]: 
        
        existing_key = await self.idempotency_impl.get_by_key(idempotency_key=idempotency_key)
        if existing_key:
            """์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ์š”์ฒญ"""
            return {"message": "request was already processed"}
        await self.idempotency_impl.add_new_key(idempotency_key=idempotency_key)
        result = await self.wallet_impl.use(user_id=user_id, amount=amount)
        if result: 
            obj = await self.wallet_impl.check(user_id=user_id)
            history = await self.history_impl.record_history(user_id=user_id
                                                                        , amount=amount
                                                                        , current=obj.credit
                                                                        , transaction_type="use")
            return self.history_impl.to_schema(history, schema_type=CreditHistorySchema)
        else: 
            obj = await self.wallet_impl.check(user_id=user_id)
            if obj:
                return {"message": "insufficient credit !"} 
            return {"message": "user_id is not valid !"}
        
    async def delete_credit_wallet(self, user_id:UUID) -> bool: 
        return await self.wallet_impl.delete_wallet(user_id=user_id)

 

๋ฉฑ๋“ฑ์„ฑ์— ๊ด€ํ•œ ๊ฐœ๋…๊ณผ ์˜ˆ์‹œ, ๊ทธ๋ฆฌ๊ณ  ์šฐ๋ฆฌ ์„œ๋น„์Šค Resumait์˜ Credit API ์— ๋ฉฑ๋“ฑ์„ฑ์„ ๋ณด์žฅํ•˜๊ณ  ์žˆ๋Š” ์˜ˆ์‹œ์— ๋Œ€ํ•ด์„œ ์†Œ๊ฐœํ–ˆ๋‹ค. Resumait์˜ ์œ ์ €๋“ค์ด ์ƒ์„ฑ๋œ ์ž๊ธฐ์†Œ๊ฐœ์„œ๋ฅผ ์ž˜ ๋ฐ›์•„๋ณด๊ณ , Credit์ด ์ž˜๋ชป ์“ฐ์ด๋Š” ๋ฌธ์ œ๋“ค์ด ์—†์—ˆ์œผ๋ฉด ์ข‹๊ฒ ๋‹ค


Reference

https://docs.tosspayments.com/blog/what-is-idempotency

 

๋ฉฑ๋“ฑ์„ฑ์ด ๋ญ”๊ฐ€์š”? | ํ† ์ŠคํŽ˜์ด๋จผ์ธ  ๊ฐœ๋ฐœ์ž์„ผํ„ฐ

์ƒ์†Œํ•œ ํ‘œํ˜„์ด์ง€๋งŒ ์•Œ๊ณ  ๋ณด๋ฉด ์‰ฌ์›Œ์š”. ๋ฉฑ๋“ฑ์„ฑ์— ๋Œ€ํ•ด ์ดํ•ดํ•˜๊ณ  API๋ฅผ ๋ฉฑ๋“ฑํ•˜๊ฒŒ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ•๋„ ํ•จ๊ป˜ ์•Œ์•„๋ด์š”.

docs.tosspayments.com

 

728x90