๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ–ฅ๏ธ ์ตœ์ข…ํ”„๋กœ์ ํŠธ/โœ๏ธ TIL

[ TIL ] ์ตœ์ข… ํ”„๋กœ์ ํŠธ_Day 29

by carrot0911 2025. 3. 11.

์˜ค๋Š˜ ์ง„ํ–‰ํ•œ ๋‚ด์šฉ๋“ค ๐Ÿง 

์—ฐ๋ น๋Œ€ ๋ณ„ ์ธ๊ธฐ ํ‚ค์›Œ๋“œ ์ƒ์œ„ 10๊ฐœ ์กฐํšŒ ์„ฑ๋Šฅ ๊ฐœ์„ ํ•˜๊ธฐ

์„ฑ๋Šฅ ๊ฐœ์„  ๊ณ„ํš

  1. ์ธ๋ฑ์Šค๋กœ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•˜๊ธฐ
  2. ์„œ๋ธŒ ์ฟผ๋ฆฌ๋กœ ๋‹จ์ผ ์ฟผ๋ฆฌ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•˜๊ธฐ

๋‘ ๊ฐ€์ง€ ์„ฑ๋Šฅ ๊ฐœ์„  ๊ณ„ํš์ด ์žˆ์—ˆ๋‹ค.
๊ทธ๋ž˜์„œ ํ˜„์žฌ ์ฟผ๋ฆฌ์˜ ์ƒํ™ฉ์ด ์–ด๋–ค์ง€ ๋จผ์ € ํŒŒ์•…ํ•ด ๋ณด๊ธฐ๋กœ ํ–ˆ๋‹ค.

ํ˜„์žฌ ์ฟผ๋ฆฌ์˜ ์ƒํ™ฉ

์„œ๋ธŒ ์ฟผ๋ฆฌ๋ฅผ ํ™œ์šฉํ•œ ์ฝ”๋“œ

์—ฐ๋ น๋Œ€๋ฅผ 25์„ธ~40์„ธ๋กœ ๊ณ ์ •ํ–ˆ๋‹ค. (๊ฐ€์žฅ ๋งŽ์€ ์ทจ์—… ์—ฐ๋ น์ธต์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ณ  ๊ณ ์ •ํ–ˆ๋‹ค.)

๊ทธ๋‹ค์Œ Postman์œผ๋กœ ๋ฐฐํฌ ์ค‘์ธ ์„œ๋ฒ„์™€ ๋กœ์ปฌ์—์„œ ์ง์ ‘ ์กฐํšŒ๋ฅผ ์ง„ํ–‰ํ•ด ๋ณด์•˜๋‹ค.

[ Local ]

ํ•œ๋ฒˆ ์กฐํšŒํ•˜๋Š”๋ฐ 31.49s๊ฐ€ ๊ฑธ๋ ธ๋‹ค…
๋งค์šฐ ๋งค์šฐ.. ์—„์ฒญ๋‚˜๊ฒŒ ๋Š๋ฆฐ ์†๋„์˜€๋‹ค..

[ ๋ฐฐํฌ ์ค‘์ธ ์„œ๋ฒ„ ]

์ด๋Ÿด ์ˆ˜๊ฐ€… ์‹ฌ์ง€์–ด ์„œ๋ฒ„์—์„œ ์กฐํšŒํ–ˆ์„ ๋•Œ๋Š” ์‹œ๊ฐ„์ด ๋„ˆ๋ฌด ์˜ค๋ž˜ ๊ฑธ๋ ค์„œ 504 Gateway Time-out์ด ๋ฐœ์ƒํ–ˆ๋‹ค..

ํ˜น์‹œ๋‚˜ ํ•˜๋Š” ๋งˆ์Œ์— ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ๋„ ํ•จ๊ป˜ ์ง„ํ–‰ํ•ด ๋ณด์•˜๋‹ค.
100 ์Šค๋ ˆ๋“œ 10์ดˆ๋กœ ์„ค์ •ํ•˜๊ณ  JMeter์—์„œ ํ…Œ์ŠคํŠธํ–ˆ์„ ๋•Œ ์—๋Ÿฌ์œจ์ด 90%๊ฐ€ ๋‚˜์™”๋‹ค..
๋ถ€ํ•˜ ์ฒ˜๋ฆฌ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ƒํƒœ์˜€๋‹ค.

“ํ˜น์‹œ๋‚˜ ์‹œ๊ฐ„ ๋•Œ๋ฌธ์ธ๊ฐ€..?”๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค์–ด์„œ Ramp-up period๋ฅผ 10์ดˆ์—์„œ 120์ดˆ๋กœ ๋„‰๋„‰ํ•˜๊ฒŒ ์„ค์ •ํ–ˆ๋‹ค.
๊ทธ๋žฌ๋”๋‹ˆ ์—ฌ์ „ํžˆ ์—๋Ÿฌ์œจ์ด 83%๋กœ ๋†’๊ฒŒ ๋‚˜์™”๋‹ค.

์„œ๋ธŒ ์ฟผ๋ฆฌ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ธ๊ธฐ ํ‚ค์›Œ๋“œ๋ฅผ ์กฐํšŒ ์‹œ ์„ฑ๋Šฅ์ด ๋งค์šฐ ๋งค์šฐ ๋Š๋ฆฌ๊ณ  ์ข‹์ง€ ์•Š์•˜๋‹ค..
๋ถ€ํ•˜ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ์˜๋ฏธ๋„ ์—†์—ˆ๋‹ค..

์ธ๋ฑ์Šค๋กœ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•˜๋“ , ์ฟผ๋ฆฌ๋ฅผ ๋ณ€๊ฒฝํ•˜์—ฌ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•˜๋“  ๊ฐœ์„ ์ด ๋‹น์žฅ ํ•„์š”ํ•œ ์ƒํ™ฉ์ด์—ˆ๋‹ค.

๊ทธ๋ž˜์„œ ๊ณ ๋ฏผ์ด ์ƒ๊ฒผ๋‹ค..
๊ณผ์—ฐ ์ด๋ ‡๊ฒŒ ๋Š๋ฆฐ ์„ฑ๋Šฅ์„ ๊ฐ€์ง„ ์ฟผ๋ฆฌ๋ฅผ ์ธ๋ฑ์Šค ๋งŒ์œผ๋กœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์„๊นŒ..??
์ง‘๊ณ„๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณ  ์žˆ๋Š” ์„œ๋ธŒ ์ฟผ๋ฆฌ๋ฅผ ํ•ด๊ฒฐํ•ด์•ผ ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ์•„๋‹๊นŒ??

๊ทธ๋ž˜์„œ ์ฟผ๋ฆฌ๋ฅผ ์ตœ์ ํ™”ํ•˜์—ฌ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์ฐพ์•„๋ณด์•˜๋‹ค.

์„œ๋ธŒ ์ฟผ๋ฆฌ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•๋“ค์„ ์ฐพ๋‹ค ๋ณด๋‹ˆ QueryDSL ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ œ๊ณตํ•˜๋Š” numberTemplate ๋ฉ”์„œ๋“œ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ง‘๊ณ„๊ฐ€ ๊ฐ€๋Šฅํ–ˆ๋‹ค!!
์„œ๋ธŒ ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๋‹จ์ผ ์ฟผ๋ฆฌ์—์„œ ๋ฐ”๋กœ count ๊ณ„์‚ฐ์ด ๊ฐ€๋Šฅํ•ด์ง„ ๊ฒƒ์ด๋‹ค!

์„œ๋ธŒ ์ฟผ๋ฆฌ๋ฅผ ๋‹จ์ผ ์ฟผ๋ฆฌ๋กœ ๋ณ€๊ฒฝํ–ˆ๋‹ค.

๋‹จ์ผ ์ฟผ๋ฆฌ๋กœ ๋ณ€๊ฒฝํ•œ ์ฝ”๋“œ

์œ„์˜ ์ฝ”๋“œ์ฒ˜๋Ÿผ ์„œ๋ธŒ ์ฟผ๋ฆฌ๋ฅผ ์‚ญ์ œํ•˜๊ณ  ๋‹จ์ผ ์ฟผ๋ฆฌ ๋‚ด์—์„œ count๋ฅผ ๋ฐ”๋กœ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณ€๊ฒฝํ–ˆ๋‹ค.
์„œ๋ธŒ ์ฟผ๋ฆฌ๊ฐ€ ์‚ฌ๋ผ์ง€๋ฉด์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์‹คํ–‰ํ•ด์•ผ ํ•  ์ฟผ๋ฆฌ๊ฐ€ 1ํšŒ๋กœ ์ค„์–ด๋“ค์–ด, ํ•„์š”ํ•˜์ง€ ์•Š์€ ์ฟผ๋ฆฌ ์‹คํ–‰์ด ์‚ฌ๋ผ์ง„๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ์„ฑ๋Šฅ์ด ์–ผ๋งˆ๋‚˜ ๊ฐœ์„ ๋˜์—ˆ์„๊นŒ?!

๋กœ์ปฌ์—์„œ Postman์„ ํ™œ์šฉํ•˜์—ฌ ์„ฑ๋Šฅ์„ ํ™•์ธํ•ด ๋ดค๋‹ค.

์™€… 31.49s๊ฐ€ ๊ฑธ๋ฆฌ๋˜ ์ฟผ๋ฆฌ๊ฐ€ 146ms๋กœ ํ™•์—ฐํ•˜๊ฒŒ ์ค„์–ด๋“  ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค..

JMeter๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ถ€ํ•˜ํ…Œ์ŠคํŠธ๋ฅผ ๋‹ค์‹œ ํ•œ๋ฒˆ ์ง„ํ–‰ํ•ด ๋ดค๋‹ค.
100 ์Šค๋ ˆ๋“œ 10์ดˆ๋กœ ์„ค์ •ํ•˜๊ณ  ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•œ ๊ฒฐ๊ณผ..

์—๋Ÿฌ์œจ์ด 0%์— ์‹œ๊ฐ„๋„ 50ms๋กœ ์—„์ฒญ๋‚˜๊ฒŒ ๊ฐœ์„ ์ด ๋œ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค!!

100 ์Šค๋ ˆ๋“œ 120์ดˆ๋กœ ์„ค์ •ํ•˜๊ณ  ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•œ ๊ฒฐ๊ณผ๋Š”

์—ญ์‹œ๋‚˜ ์—๋Ÿฌ์œจ์ด 0%์˜€๋‹ค!!

์ด๋กœ์จ ์„œ๋ธŒ ์ฟผ๋ฆฌ๋ฅผ ํ•ด๊ฒฐํ•œ ๊ฒƒ๋งŒ์œผ๋กœ๋„ ์„ฑ๋Šฅ ๊ฐœ์„ ์ด ์—„์ฒญ๋‚˜๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ์—ฌ๊ธฐ์„œ ํ•  ์ˆ˜ ์žˆ๋Š” ์ƒ๊ฐ!

user ํ…Œ์ด๋ธ”์˜ age ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค๋ฅผ ์ ์šฉํ•œ๋‹ค๋ฉด ์„ฑ๋Šฅ์ด ๋” ๊ฐœ์„ ๋˜์ง€ ์•Š์„๊นŒ??

๊ทธ๋ž˜์„œ ๋ฐ”๋กœ ์ธ๋ฑ์Šค๋ฅผ ์ ์šฉ ์ „ํ›„ ์„ฑ๋Šฅ์„ ๋น„๊ตํ•ด ๋ดค๋‹ค.

์ธ๋ฑ์Šค ์ ์šฉ ์ „ ์„ฑ๋Šฅ

-> Limit: 10 row(s)  (actual time=62.5..62.5 rows=10 loops=1)
    -> Sort: `count(uk1_0.id)` DESC, limit input to 10 row(s) per chunk  (actual time=62.5..62.5 rows=10 loops=1)
        -> Table scan on <temporary>  (actual time=62.4..62.4 rows=45 loops=1)
            -> Aggregate using temporary table  (actual time=62.4..62.4 rows=45 loops=1)
                -> Nested loop inner join  (cost=2547 rows=5073) (actual time=0.372..40.2 rows=26004 loops=1)
                    -> Nested loop inner join  (cost=771 rows=5073) (actual time=0.311..18.2 rows=26004 loops=1)
                        -> Filter: (u1_0.age between 25 and 40)  (cost=205 rows=222) (actual time=0.237..1.2 rows=1118 loops=1)
                            -> Table scan on u1_0  (cost=205 rows=2000) (actual time=0.235..1.03 rows=2000 loops=1)
                        -> Filter: (uk1_0.keyword_id is not null)  (cost=0.278 rows=22.8) (actual time=0.0081..0.0139 rows=23.3 loops=1118)
                            -> Covering index lookup on uk1_0 using UKobffw3p1x4w47vq32puin9a0o (user_id=u1_0.id)  (cost=0.278 rows=22.8) (actual time=0.00799..0.0125 rows=23.3 loops=1118)
                    -> Single-row index lookup on k1_0 using PRIMARY (id=uk1_0.keyword_id)  (cost=0.25 rows=1) (actual time=686e-6..712e-6 rows=1 loops=26004)

 

CREATE INDEX idx_user_age ON user (age);

์œ„ ์ฝ”๋“œ๋ฅผ ํ™œ์šฉํ•ด์„œ ์ธ๋ฑ์Šค๋ฅผ ์ƒ์„ฑํ•ด ์ฃผ๊ณ  ๋‹ค์‹œ ์„ฑ๋Šฅ์„ ํ™•์ธํ•ด ๋ณด์•˜๋‹ค!

์ธ๋ฑ์Šค ์ ์šฉ ํ›„ ์„ฑ๋Šฅ

-> Limit: 10 row(s)  (actual time=52.4..52.4 rows=10 loops=1)
    -> Sort: `count(uk1_0.id)` DESC, limit input to 10 row(s) per chunk  (actual time=52.4..52.4 rows=10 loops=1)
        -> Table scan on <temporary>  (actual time=52.3..52.3 rows=45 loops=1)
            -> Aggregate using temporary table  (actual time=52.3..52.3 rows=45 loops=1)
                -> Nested loop inner join  (cost=12010 rows=25524) (actual time=0.742..36 rows=26004 loops=1)
                    -> Nested loop inner join  (cost=3077 rows=25524) (actual time=0.698..16.1 rows=26004 loops=1)
                        -> Filter: (u1_0.age between 25 and 40)  (cost=226 rows=1118) (actual time=0.602..1.18 rows=1118 loops=1)
                            -> Covering index range scan on u1_0 using idx_user_age over (25 <= age <= 40)  (cost=226 rows=1118) (actual time=0.599..1.04 rows=1118 loops=1)
                        -> Filter: (uk1_0.keyword_id is not null)  (cost=0.269 rows=22.8) (actual time=0.00678..0.012 rows=23.3 loops=1118)
                            -> Covering index lookup on uk1_0 using UKobffw3p1x4w47vq32puin9a0o (user_id=u1_0.id)  (cost=0.269 rows=22.8) (actual time=0.00667..0.0107 rows=23.3 loops=1118)
                    -> Single-row index lookup on k1_0 using PRIMARY (id=uk1_0.keyword_id)  (cost=0.25 rows=1) (actual time=607e-6..633e-6 rows=1 loops=26004)

 

ํ•ญ๋ชฉ ์ธ๋ฑ์Šค ์ ์šฉ ์ „ ์ธ๋ฑ์Šค ์ ์šฉ ํ›„ ๊ฐœ์„  ์‚ฌํ•ญ
user ํ…Œ์ด๋ธ” ์กฐํšŒ ๋ฐฉ์‹ ALL (Full Table Scan) range (Index Range Scan) ์ธ๋ฑ์Šค ์ ์šฉ์œผ๋กœ ์ตœ์ ํ™”
JOIN ๋ฐฉ์‹
(userKeyword ํ…Œ์ด๋ธ”)
ref ref ๋™์ผ
keyword ํ…Œ์ด๋ธ” ์กฐํšŒ ๋ฐฉ์‹ eq_ref eq_ref ์ตœ์ ํ™” ์œ ์ง€
Extra (์ถ”๊ฐ€ ์—ฐ์‚ฐ) Using temporary;
Using filesort
Using where;
Using index;
Using temporary
ํŒŒ์ผ ์ •๋ ฌ ์ œ๊ฑฐ
&
์ธ๋ฑ์Šค ํ™œ์šฉ ์ฆ๊ฐ€
์‹คํ–‰ ์‹œ๊ฐ„ 62.5ms 52.4ms ์•ฝ 16% ์†๋„ ๊ฐœ์„ 

์„ฑ๋Šฅ์ด ์ข‹์•„์ง€๊ธด ํ–ˆ์œผ๋‚˜ ๋ˆˆ์— ๋„๊ฒŒ ๊ฐœ์„ ๋œ ๊ฒƒ์€ ์•„๋‹Œ ๊ฒƒ ๊ฐ™๋‹ค.
์‹ค์ œ๋กœ Postman์„ ์‹คํ–‰ํ–ˆ์„ ๋•Œ์˜ ๊ฒฐ๊ณผ๊ฐ€ ์ธ๋ฑ์Šค ์ ์šฉ ์ „๋ณด๋‹ค ์‘๋‹ต ์†๋„๊ฐ€ ๋” ๋Š˜์–ด๋‚œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

JMeter๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋˜‘๊ฐ™์€ ์กฐ๊ฑด์—์„œ ๋ถ€ํ•˜ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ด ๋ดค๋‹ค.

์„ฑ๋Šฅ์ด ๋” ์ข‹์•„์ง„ ๋ชจ์Šต์„ ๋ณด๊ธฐ ํž˜๋“ค์—ˆ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ์„ฑ๋Šฅ ๊ฐœ์„ ์ด ํฌ์ง€ ์•Š์€ ์ธ๋ฑ์Šค๋ฅผ ์ ์šฉํ•ด์•ผ ํ•˜๋Š” ๊ฑด๊ฐ€??

๊ณ ๋ฏผํ•œ ๋์— ๋‚˜์˜จ ๊ฒฐ๋ก ์€ ์ธ๋ฑ์Šค๋ฅผ ์ ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด์—ˆ๋‹ค.
user ํ…Œ์ด๋ธ”์—๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ํšŒ์›๊ฐ€์ž…์„ ์ง„ํ–‰ํ•  ๋•Œ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๊ฐ€ INSERT ๋˜์–ด ์ถ”๊ฐ€๋œ๋‹ค. ๊ทธ๋Ÿด ๋•Œ๋งˆ๋‹ค ์ธ๋ฑ์Šค๊ฐ€ ์ ์šฉ๋˜์–ด ์žˆ์œผ๋ฉด ๋‹ค์‹œ ์ธ๋ฑ์Šค๋ฅผ ์žฌ์ •๋ ฌ์ด ํ•„์š”ํ•˜๊ณ  ์“ฐ๊ธฐ ์—ฐ์‚ฐ์˜ ์„ฑ๋Šฅ์ด ์ €ํ•˜๋  ๊ฐ€๋Šฅ์„ฑ์ด ์กด์žฌํ•œ๋‹ค.
๋˜ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก ์ธ๋ฑ์Šค ๊ด€๋ฆฌ ๋น„์šฉ์ด ์ฆ๊ฐ€ํ•˜๊ฒŒ ๋œ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ด์œ ๋กœ ์ฟผ๋ฆฌ ์ตœ์ ํ™”๊นŒ์ง€ ์ง„ํ–‰ํ•˜๊ณ  ์ธ๋ฑ์Šค๋Š” ์ ์šฉํ•˜์ง€ ์•Š๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค.
์ฟผ๋ฆฌ ์ตœ์ ํ™”๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ์ถฉ๋ถ„ํžˆ ์‘๋‹ต ์†๋„๊ฐ€ ๊ฐœ์„ ๋˜์—ˆ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค!

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑํ•˜๊ธฐ

๋”๋ณด๊ธฐ
@Test
@DisplayName("์˜ˆ์™ธ ๋ฐœ์ƒ: ์กด์žฌํ•˜์ง€ ์•Š๋Š” jobOpeningId๋ฅผ ์กฐํšŒํ•  ๊ฒฝ์šฐ NotFoundException ๋ฐœ์ƒ")
void ์ฑ„์šฉ๊ณต๊ณ _๋‹จ๊ฑด_์กฐํšŒ_์‹คํŒจ() {
    // given
    Long jobOpeningId = 999L;

    when(jobOpeningRepository.findById(jobOpeningId)).thenReturn(Optional.empty());

    // when & then
    NotFoundException exception = assertThrows(
        NotFoundException.class,
        () -> jobOpeningFindByService.findById(jobOpeningId)
    );

    assertThat(exception.getMessage()).contains(DataErrorCode.JOB_OPENING_NOT_FOUND.getMessage());

    verify(jobOpeningRepository, times(1)).findById(jobOpeningId);
}
@ExtendWith(MockitoExtension.class)
public class SearchHistoryServiceTest {

    @InjectMocks
    private SearchHistoryService searchHistoryService;

    @Mock
    private RedisTemplate<String, String> redisTemplate;

    @Mock
    private ZSetOperations<String, String> zSetOperations;

    @Mock
    private SearchHistoryRepository searchHistoryRepository;

    private static final long EXPIRATION_TIME = 3600;

    @BeforeEach
    void setUp() {
        when(redisTemplate.opsForZSet()).thenReturn(zSetOperations);
    }

    @Test
    @DisplayName("๊ฒ€์ƒ‰์–ด๊ฐ€ Redis์— ์ •์ƒ์ ์œผ๋กœ ์ €์žฅ")
    void Redis์—_๊ฒ€์ƒ‰์–ด_์ •์ƒ์ ์œผ๋กœ_์ €์žฅ() {
        // given
        Long userId = 1L;
        String searchTerm = "Spring";
        String key = "user:" + userId + ":search_history";

        // when
        searchHistoryService.saveSearchTerm(userId, searchTerm);

        // then
        verify(zSetOperations, times(1)).add(eq(key), eq(searchTerm), anyDouble());
        verify(redisTemplate, times(1)).expire(eq(key), eq(EXPIRATION_TIME), eq(TimeUnit.SECONDS));
    }

    @Test
    @DisplayName("๊ฒ€์ƒ‰์–ด ๊ฐœ์ˆ˜๊ฐ€ MAX_SEARCH_HISOTRY_SIZE๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ๊ฐ€์žฅ ์˜ค๋ž˜๋œ ๊ฒ€์ƒ‰์–ด ์‚ญ์ œ")
    void Redis์—์„œ_๊ฐ€์žฅ_์˜ค๋ž˜๋œ_๊ฒ€์ƒ‰์–ด_์‚ญ์ œ() {
        //given
        Long userId = 2L;
        String searchTerm = "Redis";
        String key = "user:" + userId + ":search_history";

        when(zSetOperations.zCard(key)).thenReturn(11L);

        // when
        searchHistoryService.saveSearchTerm(userId, searchTerm);

        // then
        verify(zSetOperations, times(1)).add(eq(key), eq(searchTerm), anyDouble());
        verify(zSetOperations, times(1)).removeRange(eq(key),eq(0L), eq(0L));
        verify(redisTemplate, times(1)).expire(eq(key), eq(EXPIRATION_TIME), eq(TimeUnit.SECONDS));
    }

    @Test
    @DisplayName("Redis์— ๊ฒ€์ƒ‰ ๊ธฐ๋ก์ด ์กด์žฌํ•˜๋ฉด ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜")
    void Redis์—_๊ฒ€์ƒ‰_๊ธฐ๋ก_์กด์žฌํ•˜๋ฉด_๋ฐ์ดํ„ฐ_๋ฐ˜ํ™˜() {
        // given
        Long userId = 1L;
        String key = "user:" + userId + ":search_history";
        Set<String> searchTermSet = new LinkedHashSet<>(List.of("Java", "Spring"));

        when(zSetOperations.reverseRange(key, 0, 9)).thenReturn(searchTermSet);

        // when
        List<String> searchTermList = searchHistoryService.getRecentSearchTerms(userId);

        // then
        assertThat(searchTermList).containsExactly("Java", "Spring");

        verify(searchHistoryRepository, never()).findTop10ByUserIdOrderByCreatedAtDesc(anyLong());
    }

    @Test
    @DisplayName("Redis์— ๊ฒ€์ƒ‰ ๊ธฐ๋ก์ด ์—†์œผ๋ฉด DB์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•œ ํ›„ Redis์— ์ €์žฅํ•˜๊ณ  ๋ฐ˜ํ™˜")
    void Redis์—_๊ฒ€์ƒ‰_๊ธฐ๋ก์ด_์—†์œผ๋ฉด_DB์—์„œ_๋ฐ์ดํ„ฐ_์กฐํšŒํ•˜๊ณ _Redis์—_์ €์žฅ_ํ›„_๋ฐ˜ํ™˜() {
        // given
        Long userId = 2L;
        String key = "user:" + userId + ":search_history";
        User mockUser = User.toEntity("test@gmail.com", "์‚ฌ์šฉ์ž1", 27, 1, "password123");

        when(zSetOperations.reverseRange(key, 0, 9)).thenReturn(Collections.emptySet());

        SearchHistory history1 = SearchHistory.toEntity(mockUser, "Elasticsearch");
        SearchHistory history2 = SearchHistory.toEntity(mockUser, "Kafka");

        ReflectionTestUtils.setField(history1, "createdAt", LocalDateTime.now().minusMinutes(10));
        ReflectionTestUtils.setField(history2, "createdAt", LocalDateTime.now().minusMinutes(5));

        List<SearchHistory> searchTermListInDatabase = List.of(history1, history2);

        when(searchHistoryRepository.findTop10ByUserIdOrderByCreatedAtDesc(userId)).thenReturn(searchTermListInDatabase);

        // when
        List<String> resultList = searchHistoryService.getRecentSearchTerms(userId);

        //then
        assertThat(resultList).containsExactly("Kafka", "Elasticsearch");

        verify(searchHistoryRepository, times(1)).findTop10ByUserIdOrderByCreatedAtDesc(userId);

        verify(zSetOperations, times(1)).add(eq(key), eq("Elasticsearch"), anyDouble());
        verify(zSetOperations, times(1)).add(eq(key), eq("Kafka"), anyDouble());
    }
}

 

๋‚ด์ผ ๊ณ„ํš โฐ

  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ
  • ์ปค๋ฆฌ์–ด ์ฝ”์นญ ์ƒ๋‹ด
  • ํ”ผํ”ผํ‹ฐ ์†Œ๊ฐ ์ž‘์„ฑ
  • ๋ธŒ๋กœ์…” ์ž‘์„ฑ
  • ๋ฌธ์„œํ™” ์ž‘์—…ํ•˜๊ธฐ

+ ์ถ”๊ฐ€ ๊ณ„ํš์ด ๋” ์ƒ๊ธธ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค~_~