Skip to content

Latest commit

ย 

History

39 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

common

ํ•จ๊ป˜ํ•˜๊ฐœ์˜ ๊ณตํ†ต ๋ชจ๋“ˆ์ž…๋‹ˆ๋‹ค. ๋„๋ฉ”์ธ ์„œ๋น„์Šค 14๊ฐœ๊ฐ€ ์ „๋ถ€ ์“ฐ๋Š” ๊ฒƒ๋งŒ ๋‹ด์Šต๋‹ˆ๋‹ค.


๋จผ์ € ์ „์ฒด ๊ทธ๋ฆผ์„ ๋ณด๊ณ , ์ด ๋ ˆํฌ๊ฐ€ ๊ทธ ์•ˆ ์–ด๋””์— ์žˆ๋Š”์ง€ ๋ณธ ๋’ค ์ฝ์Šต๋‹ˆ๋‹ค.

โ‘  ์ „์ฒด ๊ตฌ์กฐ โ€” ์ธต์œผ๋กœ ๋ณธ ๊ฒƒ. ์œ„์—์„œ ์•„๋ž˜๋กœ ์š”์ฒญ์ด ๋‚ด๋ ค๊ฐ€๊ณ , ์–ด๋А ์ธต์— ๋ฌด์—‡์ด ์žˆ๋Š”์ง€.

์ „์ฒด ๊ตฌ์กฐ (์ธต)

โ‘ก ์ „์ฒด ๊ตฌ์กฐ โ€” ์„œ๋น„์Šค๋ผ๋ฆฌ ๋ฌด์—‡์„ ์ฃผ๊ณ ๋ฐ›๋Š”์ง€. ์ดˆ๋ก ์‹ค์„ ์ด /internal ํ˜ธ์ถœ, Kafka ํ‘œ๊ฐ€ ์ด๋ฒคํŠธ, ํ•˜๋Š˜์ƒ‰ ์ ์„ ์ด VPC ๊ฒฝ๊ณ„.

์ „์ฒด ๊ตฌ์กฐ (ํ˜ธ์ถœ ๊ด€๊ณ„)

โ‘ข ์ด ๋ ˆํฌ๋ฅผ ์ค‘์‹ฌ์œผ๋กœ. ์ง์ ‘ ์—ฐ๊ฒฐ๋œ ๊ฒƒ๋งŒ ๋‚จ๊ธด ๊ทธ๋ฆผ.

common ์„ ์ค‘์‹ฌ์œผ๋กœ

โ‘ โ‘ก ๋Š” service-template/docs ์— ์žˆ๋Š” ๊ฒƒ์„ ๊ฐ€๋ฆฌํ‚ต๋‹ˆ๋‹ค. ์„œ๋น„์Šค๊ฐ€ ๋Š˜์–ด๋„ ๊ทธ์ชฝ ํ•œ ๊ณณ๋งŒ ๊ณ ์นฉ๋‹ˆ๋‹ค.




๋ณธ๋ฌธ ์‹œ์ž‘

                    [ GitHub Packages ]
                             โ”‚
                             โ”‚  com.pawtrail:common:0.0.13  (jar)
                             โ–ผ
๋„๋ฉ”์ธ ์„œ๋น„์Šค 14๊ฐœ  โ”€โ”€โ–ถ  ๊ณตํ†ต ๋ชจ๋“ˆ   ์ž๋™ ์„ค์ • 7๊ฐœ๊ฐ€ ์กฐ๊ฑด์— ๋งž์œผ๋ฉด ์ผœ์ง
        โ”‚                    โ”‚
        โ”‚                    โ”œโ”€โ”€โ–ถ  ์‘๋‹ต ํ˜•์‹ ยท ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
        โ”‚                    โ”œโ”€โ”€โ–ถ  ์ธ์ฆ ํ—ค๋” โ†’ SecurityContext
        โ”‚                    โ”œโ”€โ”€โ–ถ  BaseEntity ยท ๊ฐ์‚ฌ ์ปฌ๋Ÿผ
        โ”‚                    โ”œโ”€โ”€โ–ถ  Outbox ยท Inbox
        โ”‚                    โ”œโ”€โ”€โ–ถ  ๋‹ค๋ฅธ ์„œ๋น„์Šค๋ฅผ ๋ถ€๋ฅด๋Š” RestClient ๋นŒ๋”
        โ”‚                    โ””โ”€โ”€โ–ถ  Flyway V1 ยท V2
        โ”‚
        โ””โ”€โ”€โ–ถ  ๊ฐ์ž ๋„๋ฉ”์ธ ์ฝ”๋“œ๋งŒ ์”€

์„œ๋น„์Šค๋Š” ์˜์กด์„ฑ ํ•œ ์ค„๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๋‚˜๋จธ์ง€๋Š” ์ž๋™ ์„ค์ •์ด ์•Œ์•„์„œ ์ผญ๋‹ˆ๋‹ค.




0. ์ด ๋ชจ๋“ˆ์ด ํ•˜๋Š” ์ผ

๊ณตํ†ต ๋ชจ๋“ˆ์ด ์—†์œผ๋ฉด ์ด๋ ‡๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์žˆ์œผ๋ฉด ์—†์œผ๋ฉด
์‘๋‹ต ํ˜•์‹ ํ•œ ๊ณณ์—์„œ ์ •์˜ ์„œ๋น„์Šค 14๊ฐœ๊ฐ€ ๊ฐ์ž ๋งŒ๋“ฆ โ†’ ํ˜•ํƒœ๊ฐ€ ๊ฐˆ๋ฆผ
์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ž๋™์œผ๋กœ ๋ถ™์Œ ์„œ๋น„์Šค๋งˆ๋‹ค @RestControllerAdvice ๋ฅผ ๋ณต์‚ฌ
๊ฐ์‚ฌ ์ปฌ๋Ÿผ BaseEntity ์ƒ์† ํ…Œ์ด๋ธ”๋งˆ๋‹ค 6์ปฌ๋Ÿผ์„ ์ง์ ‘
Outbox record() ํ•œ ์ค„ 5๋‹จ๊ณ„๋ฅผ ์†์œผ๋กœ โ€” ํ•˜๋‚˜๋งŒ ๋น ๋œจ๋ ค๋„ ์ด๋ฒคํŠธ๊ฐ€ ์•ˆ ๋‚˜๊ฐ
์ธ์ฆ ํ—ค๋”๋ฅผ ์ฝ์–ด ์ž๋™์œผ๋กœ ์ฑ„์›€ ํ•„ํ„ฐ๋ฅผ 14๋ฒˆ ๋ณต์‚ฌ

์ˆซ์ž๋กœ ๋ณด๋ฉด ์ด๋ ‡์Šต๋‹ˆ๋‹ค.

๊ฐ’ ์–ด๋””์—
์ž๋ฐ” ํŒŒ์ผ 36๊ฐœ 3์žฅ
์ž๋™ ์„ค์ • 7๊ฐœ 2์žฅ
Flyway ์Šคํฌ๋ฆฝํŠธ 2๊ฐœ (V1ยทV2) 3-5
๊ณตํ†ต ์—๋Ÿฌ ์ฝ”๋“œ 6๊ฐœ 3-2
ํ˜„์žฌ ๋ฒ„์ „ 0.0.13 5์žฅ
์†Œ๋น„ํ•˜๋Š” ์„œ๋น„์Šค ๋„๋ฉ”์ธ 14๊ฐœ ํ”Œ๋žซํผ 3๊ฐœ๋Š” ์•ˆ ์”€

๋„ฃ๋Š” ๊ฒƒ๊ณผ ์•ˆ ๋„ฃ๋Š” ๊ฒƒ์˜ ๊ธฐ์ค€์ž…๋‹ˆ๋‹ค.

๋„ฃ์Œ      ๋„๋ฉ”์ธ ์„œ๋น„์Šค๊ฐ€ ์ „๋ถ€ ์“ฐ๋Š” ๊ฒƒ
          ์‘๋‹ต ํ˜•์‹ ยท ์˜ˆ์™ธ ยท ์ธ์ฆ ยท ๊ฐ์‚ฌ ยท Outbox ยท Inbox

์•ˆ ๋„ฃ์Œ   ํŠน์ • ์„œ๋น„์Šค๋งŒ ์“ฐ๋Š” ๊ฒƒ
          PLACE_NOT_FOUND ๊ฐ™์€ ๋„๋ฉ”์ธ ์—๋Ÿฌ ์ฝ”๋“œ
          ํ† ํ”ฝ ์ด๋ฆ„ ์ƒ์ˆ˜
          ROUTE_NOT_FOUND (๊ฒŒ์ดํŠธ์›จ์ด๋งŒ ์”€)

ํ† ํ”ฝ ์ด๋ฆ„์„ ์—ฌ๊ธฐ ๋‘์ง€ ์•Š๋Š” ์ด์œ  โ€” ํ† ํ”ฝ์€ ๊ฐœ๋ฐœ ๋„์ค‘ ์ถ”๊ฐ€ยท๋ณ€๊ฒฝ๋˜๋Š”๋ฐ ๊ณตํ†ต ๋ชจ๋“ˆ์— ์žˆ์œผ๋ฉด ๊ทธ๋•Œ๋งˆ๋‹ค ์žฌ๋ฐฐํฌ์™€ ์ „ ์„œ๋น„์Šค ๋ฒ„์ „์—…์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ชจ๋“ˆ์˜ ๊ธฐ์ค€์€ "๊ฑฐ์˜ ๋ฐ”๋€Œ์ง€ ์•Š๋Š” ๊ฒƒ" ์ž…๋‹ˆ๋‹ค.




๋จผ์ € ์•Œ์•„ ๋‘๋ฉด ์ข‹์€ ๊ฒƒ 4๊ฐ€์ง€

์•„๋ž˜๋ฅผ ๋ชจ๋ฅด๋ฉด ์ด ๋ฌธ์„œ๊ฐ€ ์•ˆ ์ฝํž™๋‹ˆ๋‹ค. ํ•œ ๋ฌธ๋‹จ์”ฉ๋งŒ ๋ณด๊ณ  ๊ฐ‘๋‹ˆ๋‹ค.


โ‘  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ž€ โ€” ์ด ์ €์žฅ์†Œ๋Š” ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค

์„œ๋น„์Šค ์ €์žฅ์†Œ (auth ยท place ...)        ์ด ์ €์žฅ์†Œ (common)

  ๋นŒ๋“œ  โ”€โ”€โ–ถ  ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ jar              ๋นŒ๋“œ  โ”€โ”€โ–ถ  ๋ผ์›Œ ๋„ฃ๋Š” jar
              java -jar ๋กœ ๋œธ                        ํ˜ผ์ž์„œ๋Š” ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ํ•จ
              ๋„์ปค ์ด๋ฏธ์ง€๊ฐ€ ๋จ                        *๋‹ค๋ฅธ ์„œ๋น„์Šค์˜ build.gradle ์— ํ•œ ์ค„๋กœ ๋“ค์–ด๊ฐ

GitHub Packages ์— ์˜ฌ๋ ค ๋‘๊ณ  ๊ฐ ์„œ๋น„์Šค๊ฐ€ ๋‚ด๋ ค๋ฐ›์Šต๋‹ˆ๋‹ค. commonVersion=0.0.13 ๊ฐ€ ๊ทธ๊ฒƒ์ž…๋‹ˆ๋‹ค.


โ‘ก ์ž๋™ ์„ค์ •์ด๋ž€ โ€” ์˜์กด์„ฑ๋งŒ ๋„ฃ์œผ๋ฉด ์•Œ์•„์„œ ์ผœ์ง‘๋‹ˆ๋‹ค

์„œ๋น„์Šค๊ฐ€ build.gradle ์— common ์„ ์ถ”๊ฐ€
        โ”‚
        โ–ผ
์Šคํ”„๋ง ๋ถ€ํŠธ๊ฐ€ ๊ธฐ๋™ํ•˜๋ฉฐ jar ์•ˆ์˜ ๋ชฉ๋ก์„ ์ฝ์Œ        META-INF/spring/...AutoConfiguration.imports
        โ”‚
        โ–ผ
๋ชฉ๋ก์˜ ์„ค์ • ํด๋ž˜์Šค 6๊ฐœ๋ฅผ ํ•˜๋‚˜์”ฉ ๋ด„
        "์ด ์„œ๋น„์Šค์— JPA ๊ฐ€ ์žˆ๋‚˜?"  โ†’  ์žˆ์œผ๋ฉด JPA ๊ด€๋ จ Bean ์„ ์˜ฌ๋ฆผ
        "Kafka ๊ฐ€ ์žˆ๋‚˜?"           โ†’  ์žˆ์œผ๋ฉด Kafka ๊ด€๋ จ Bean ์„ ์˜ฌ๋ฆผ
        โ”‚
        โ–ผ
์„œ๋น„์Šค๋Š” ์•„๋ฌด๊ฒƒ๋„ ๋“ฑ๋กํ•˜์ง€ ์•Š์•˜๋Š”๋ฐ ์‘๋‹ต ํ˜•์‹ ยท ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ยท ์ธ์ฆ์ด ๋ถ™์–ด ์žˆ์Œ

"ํด๋ž˜์ŠคํŒจ์Šค์— ์žˆ๋‚˜" ๊ฐ€ ํŒ๋‹จ ๊ธฐ์ค€์ž…๋‹ˆ๋‹ค. ํด๋ž˜์ŠคํŒจ์Šค๋ž€ ๊ทธ ์„œ๋น„์Šค๊ฐ€ ๋นŒ๋“œ์— ๋„ฃ์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ „๋ถ€๋ฅผ ๋œปํ•ฉ๋‹ˆ๋‹ค. spring-data-jpa ๋ฅผ ๋„ฃ์—ˆ์œผ๋ฉด ํด๋ž˜์ŠคํŒจ์Šค์— ์žˆ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.


โ‘ข ์ด๋ฒคํŠธ๋ž€ โ€” ์„œ๋น„์Šค๋ผ๋ฆฌ ์ง์ ‘ ๋ถ€๋ฅด์ง€ ์•Š๋Š” ๋ฐฉ๋ฒ•

์ง์ ‘ ํ˜ธ์ถœ                              ์ด๋ฒคํŠธ

auth  โ”€โ”€โ–ถ  user  "ํ”„๋กœํ•„ ๋งŒ๋“ค์–ด"         auth  โ”€โ”€โ–ถ  Kafka  "๊ณ„์ •์ด ์ƒ๊ฒผ์Œ"
              โ”‚                                         โ”‚
              โ””โ”€โ”€ user ๊ฐ€ ์ฃฝ์–ด ์žˆ์œผ๋ฉด                    โ””โ”€โ”€โ–ถ  user ๊ฐ€ ๋‚˜์ค‘์— ์ฝ์–ด ๊ฐ
                  ๊ฐ€์ž…์ด ์‹คํŒจํ•จ                                auth ๋Š” user ๋ฅผ ๋ชฐ๋ผ๋„ ๋จ

auth ๊ฐ€ account.created ๋ฅผ ๋ฐœํ–‰ํ•˜๋ฉด user ๊ฐ€ ๊ทธ๊ฒƒ์„ ๋ฐ›์•„ ํ”„๋กœํ•„์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. auth ๋Š” user ๊ฐ€ ๋–  ์žˆ๋Š”์ง€, ๋ช‡ ๊ฐœ์ธ์ง€, ์–ด๋”” ์žˆ๋Š”์ง€ ๋ชฐ๋ผ๋„ ๋ฉ๋‹ˆ๋‹ค.


โ‘ฃ Outbox ๊ฐ€ ์™œ ํ•„์š”ํ•œ๊ฐ€ โ€” "์ €์žฅ์€ ๋๋Š”๋ฐ ์ด๋ฒคํŠธ๊ฐ€ ์•ˆ ๋‚˜๊ฐ”๋‹ค"

โ›” ๊ทธ๋ƒฅ ํ•˜๋ฉด

  โ‘  DB ์— ๊ณ„์ • ์ €์žฅ       ์„ฑ๊ณต
  โ‘ก Kafka ์— ์ด๋ฒคํŠธ ๋ฐœํ–‰   ์‹คํŒจ (Kafka ๊ฐ€ ์ž ๊น ์ฃฝ์Œ)
        โ”‚
        โ””โ”€โ”€ ๊ณ„์ •์€ ์žˆ๋Š”๋ฐ ํ”„๋กœํ•„์ด ์˜์˜ ์•ˆ ์ƒ๊น€. ์•„๋ฌด๋„ ๋ชจ๋ฆ„


โœ… Outbox ๋ฅผ ์“ฐ๋ฉด

  โ‘  DB ์— ๊ณ„์ • ์ €์žฅ       โ”€โ”
  โ‘ก DB ์— ์ด๋ฒคํŠธ๋„ ์ €์žฅ    โ”€โ”ดโ”€โ”€ ๊ฐ™์€ ํŠธ๋žœ์žญ์…˜ โ†’ ๋‘˜ ๋‹ค ๋˜๊ฑฐ๋‚˜ ๋‘˜ ๋‹ค ์•ˆ ๋จ
        โ”‚
        โ””โ”€โ”€โ–ถ  โ‘ข ๋ณ„๋„ ์ž‘์—…์ด outbox ํ‘œ๋ฅผ ์ฝ์–ด Kafka ๋กœ ๋ณด๋ƒ„
                    ์‹คํŒจํ•˜๋ฉด 5์ดˆ๋งˆ๋‹ค ๋‹ค์‹œ ์‹œ๋„

Inbox ๋Š” ๋ฐ˜๋Œ€์ชฝ์ž…๋‹ˆ๋‹ค. ๊ฐ™์€ ์ด๋ฒคํŠธ๊ฐ€ ๋‘ ๋ฒˆ ์˜ค๋ฉด(์žฌ์‹œ๋„ยท๋ฆฌ๋ฐธ๋Ÿฐ์‹ฑ) ํ•œ ๋ฒˆ๋งŒ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋ง‰์Šต๋‹ˆ๋‹ค.




์ด ๋ฌธ์„œ๋ฅผ ์ฝ๋Š” ์ˆœ์„œ

์ง€๊ธˆ ํ•˜๋ ค๋Š” ์ผ ๋ณผ ๊ณณ
์ƒˆ ์„œ๋น„์Šค์— ๋ถ™์ด๋ ค ํ•œ๋‹ค 1์žฅ
์ž๋™ ์„ค์ •์ด ์™œ ์•ˆ ์ผœ์ง€๋Š”์ง€ ๋ชจ๋ฅด๊ฒ ๋‹ค 2์žฅ โ†’ 6-1
๋ฌด์—‡์ด ๋“ค์–ด ์žˆ๋Š”์ง€ 3์žฅ
์ฝ”๋“œ์—์„œ ์–ด๋–ป๊ฒŒ ์“ฐ๋Š”์ง€ 4์žฅ
๊ณตํ†ต ๋ชจ๋“ˆ์„ ๊ณ ์ณ ๋ฐฐํฌํ•ด์•ผ ํ•œ๋‹ค 5์žฅ
๋ญ”๊ฐ€ ์•ˆ ๋œ๋‹ค 6์žฅ
"์™œ ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์—ˆ์ง€" 7์žฅ
์ง€๊ธˆ ์ƒํƒœ๊ฐ€ ๊ถ๊ธˆํ•˜๋‹ค 8์žฅ
๋ชจ๋ฅด๋Š” ๋ง์ด ๋‚˜์˜จ๋‹ค 9์žฅ

์ด ๋ชจ๋“ˆ์„ ๊ณ ์น˜๋Š” ์ผ์€ ๋“œ๋ญ…๋‹ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„์€ 1์žฅยท4์žฅ ๋งŒ ๋ณด๋ฉด ๋ฉ๋‹ˆ๋‹ค. 5์žฅ ์€ ์‹ค์ œ๋กœ ๊ณ ์น  ๋•Œ๋งŒ ๋ด…๋‹ˆ๋‹ค.


๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ผ ๋‹ค๋ฅธ ๋ ˆํฌ์™€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

1  ์‹คํ–‰๋˜์ง€ ์•Š์Œ           bootJar ๋ฅผ ๋„๊ณ  jar ๋ฅผ ์ผฌ (๋‹ค๋ฅธ ๋ ˆํฌ๋Š” ๋ฐ˜๋Œ€)
                          @SpringBootApplication ์ด ์—†์Œ

2  GitHub Packages ๋กœ ๋ฐฐํฌ  ghcr ์ด๋ฏธ์ง€๊ฐ€ ์•„๋‹ˆ๋ผ Maven ํŒจํ‚ค์ง€

3  ๋ฒ„์ „์ด ์˜๊ตฌ์             ๊ฐ™์€ ๋ฒ„์ „์„ ๋ฎ์–ด์“ธ ์ˆ˜ ์—†์Œ
                          publish ํ•œ ๋ฒˆ์ด ๋ฒˆํ˜ธ ํ•˜๋‚˜๋ฅผ ์†Œ๋ชจํ•จ

4  IntelliJ ๊ฐ€ ์˜คํƒ์„ ๋ƒ„     Could not autowire โ€” ์ปจํ…์ŠคํŠธ ๋ชจ๋ธ์„ ๋ชป ๋งŒ๋“ค์–ด์„œ
                          ๋นŒ๋“œ๋Š” ํ†ต๊ณผํ•จ




1. ์„œ๋น„์Šค์— ๋ถ™์ด๊ธฐ

service-template ์„ ๋ณต์ œํ–ˆ๋‹ค๋ฉด โ‘ โ‘กโ‘ฃ๋Š” ์ด๋ฏธ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. โ‘ข๋งŒ ํ™•์ธํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

โ‘  build.gradle ์— ์ €์žฅ์†Œ์™€ ์˜์กด์„ฑ
        โ”‚
        โ”‚   repositories { maven { url = "https://maven.pkg.github.com/paw-trail/common" } }
        โ”‚   implementation "com.pawtrail:common:${commonVersion}"
        โ–ผ
โ‘ก ํ™˜๊ฒฝ๋ณ€์ˆ˜ GPR_USER ยท GPR_TOKEN            ์—†์œผ๋ฉด 401 ๋กœ ๋นŒ๋“œ ์‹คํŒจ
        โ”‚
        โ–ผ
โ‘ข ์•ฑ ํด๋ž˜์Šค์— @EntityScan ยท @EnableJpaRepositories
        โ”‚      ๋‘ ๊ณณ ๋‹ค com.pawtrail.common ์„ ํฌํ•จ
        โ”‚      โ›” scanBasePackages ์—๋Š” ๋„ฃ์ง€ ๋ง ๊ฒƒ
        โ–ผ
โ‘ฃ config ์— spring.flyway.locations
        โ”‚      classpath:db/migration/common,classpath:db/migration/service
        โ–ผ
โ‘ค ๊ธฐ๋™                                     ์ž๋™ ์„ค์ • 7๊ฐœ๊ฐ€ ์กฐ๊ฑด์„ ๋ณด๊ณ  ์ผœ์ง




1-1. build.gradle

repositories {
    mavenCentral()

    // ๊ณตํ†ต ๋ชจ๋“ˆ์€ GitHub Packages ์— ์žˆ์Œ
    // ๊ณต๊ฐœ ์ €์žฅ์†Œ๋ผ๋„ ๋‚ด๋ ค๋ฐ›์„ ๋•Œ ์ธ์ฆ์„ ์š”๊ตฌํ•จ
    maven {
        name = "GitHubPackages"
        url = uri("https://maven.pkg.github.com/paw-trail/common")
        credentials {
            username = System.getenv("GPR_USER")
            password = System.getenv("GPR_TOKEN")
        }
    }
}

dependencies {
    implementation "com.pawtrail:common:${commonVersion}"

    // ๊ณตํ†ต ๋ชจ๋“ˆ์˜ PageResponse ๊ฐ€ Page ๋ฅผ ์”€
    // starter-data-redis ๊ฐ€ ์ „์ด๋กœ ๋Œ์–ด์˜ค์ง€๋งŒ ํŽ˜์ด์ง•์ด ์บ์‹œ ์˜์กด์„ฑ์— ๋งค๋‹ฌ๋ฆฌ๋ฏ€๋กœ ์ง์ ‘ ์„ ์–ธ
    // DB ๋ฅผ ์“ฐ์ง€ ์•Š๋Š” ์„œ๋น„์Šค๋„ ๋ชฉ๋ก ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ ์ด ์ค„์€ ๋‚จ๊ธธ ๊ฒƒ
    implementation 'org.springframework.data:spring-data-commons'
}

๊ณตํ†ต ๋ชจ๋“ˆ์ด ํ•จ๊ป˜ ๋ฌผ๋ ค์ฃผ๋Š” ๊ฒƒ์ด ๋‘˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„œ๋น„์Šค๊ฐ€ ๋”ฐ๋กœ ์ ์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค.

๋”ธ๋ ค์˜ค๋Š” ๊ฒƒ ๋ฌด์—‡์— ์“ฐ๋‚˜
loki-logback-appender ๋กœ๊ทธ๋ฅผ Loki ๋กœ ๋ณด๋ƒ„. ์ „ ์„œ๋น„์Šค๊ฐ€ ์˜ˆ์™ธ ์—†์ด ์”€
spring-boot-restclient ๋‹ค๋ฅธ ์„œ๋น„์Šค๋ฅผ ๋ถ€๋ฅผ ๋•Œ ์“ฐ๋Š” ์š”์ฒญ ํŒฉํ„ฐ๋ฆฌ

๋‚˜๋จธ์ง€ ์˜์กด์„ฑ์€ compileOnly ๋ผ ๋”ธ๋ ค์˜ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค. JPA ๋ฅผ ์•ˆ ์“ฐ๋Š” ์„œ๋น„์Šค์— JPA ๊ฐ€ ๋“ค์–ด๊ฐ€๋ฉด ์กฐ๊ฑด๋ถ€ ๋กœ๋”ฉ์„ ํ•ด๋‘” ์˜๋ฏธ๊ฐ€ ์—†์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด ๋‘˜๋งŒ api ์ธ ์ด์œ ๋Š” ๋น ๋œจ๋ ธ์„ ๋•Œ ์„œ๋น„์Šค๊ฐ€ ์กฐ์šฉํžˆ ๋˜๋Š” ์ฆ‰์‹œ ๊นจ์ง€๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๋ฒ„์ „์€ gradle.properties ์— ํ•œ ์ค„๋กœ ๋‘ก๋‹ˆ๋‹ค.

commonVersion=0.0.13

์ตœ์‹  ๋ฒ„์ „์€ ์กฐ์ง์˜ Packages ํŽ˜์ด์ง€์—์„œ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฌธ์„œ์˜ ์ˆซ์ž๊ฐ€ ๋‚ก์•˜์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋‘˜์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฆ„ ๊ฐ’ ๊ถŒํ•œ
GPR_USER GitHub ์‚ฌ์šฉ์ž๋ช… โ€”
GPR_TOKEN Personal Access Token (classic) ๋ฐ›๊ธฐ๋งŒ ํ•˜๋ฉด read:packages

OS ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ๋‘ก๋‹ˆ๋‹ค. Gradle ๋นŒ๋“œ๋Š” IntelliJ ์‹คํ–‰ ๊ตฌ์„ฑ์„ ์ฝ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Could not GET 'https://maven.pkg.github.com/paw-trail/common/...'
Received status code 401 from server: Unauthorized

์ด ์˜ค๋ฅ˜๊ฐ€ ๋‚˜๋ฉด ํ† ํฐ ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค. ๋ฉ”์‹œ์ง€์— ๊ทธ๋ ‡๊ฒŒ ์•ˆ ์ ํ˜€ ์žˆ์Šต๋‹ˆ๋‹ค.




1-2. ์•ฑ ํด๋ž˜์Šค โ€” ์—ฌ๊ธฐ๋งŒ ์†์œผ๋กœ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค

@SpringBootApplication
@EntityScan(basePackages = {"com.pawtrail.place", "com.pawtrail.common"})
@EnableJpaRepositories(basePackages = {"com.pawtrail.place", "com.pawtrail.common"})
public class PlaceApplication {

    public static void main(String[] args) {
        SpringApplication.run(PlaceApplication.class, args);
    }
}
์• ๋…ธํ…Œ์ด์…˜ ์™œ ํ•„์š”
@EntityScan OutboxMessage ยท ProcessedEvent ๊ฐ€ ์ด ๋ชจ๋“ˆ์— ์žˆ์Œ
@EnableJpaRepositories OutboxRepository ยท ProcessedEventRepository

๋‘˜ ๋‹ค ์ž๋™ ์„ค์ •์œผ๋กœ๋Š” ์•ˆ ์žกํž™๋‹ˆ๋‹ค. ์—”ํ‹ฐํ‹ฐ์™€ ๋ฆฌํฌ์ง€ํ„ฐ๋ฆฌ๋Š” ์Šค์บ” ๋Œ€์ƒ์ด๋ผ ์•ฑ ํด๋ž˜์Šค๊ฐ€ ์•Œ๋ ค ์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.


โ›” scanBasePackages ์—๋Š” ๋„ฃ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

// ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค
@SpringBootApplication(scanBasePackages = {"com.pawtrail.place", "com.pawtrail.common"})
์ž๋™ ์„ค์ • ํด๋ž˜์Šค๊ฐ€ ์ปดํฌ๋„ŒํŠธ ์Šค์บ”์— ๊ฑธ๋ฆผ
        โ”‚
        โ”œโ”€โ”€โ–ถ  ์ฒ˜๋ฆฌ ์ˆœ์„œ๊ฐ€ ๊นจ์ ธ ์กฐ๊ฑด ํ‰๊ฐ€๊ฐ€ ๋’ค์ง‘ํž˜
        โ””โ”€โ”€โ–ถ  ๊ฐ™์€ @Configuration ์ด ๋‘ ๋ฒˆ ๋“ฑ๋ก๋  ์ˆ˜ ์žˆ์Œ
                ์Šคํ”„๋ง ๋ถ€ํŠธ๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ๊ธˆํ•˜๋Š” ํ˜•ํƒœ

๋นˆ์€ ์ „๋ถ€ ์ž๋™ ์„ค์ •์˜ @Bean ์œผ๋กœ ๋“ฑ๋ก๋ฉ๋‹ˆ๋‹ค. ์Šค์บ”์ด ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค.


DB ๋ฅผ ์“ฐ์ง€ ์•Š๋Š” ์„œ๋น„์Šค๋Š” ๋‘˜ ๋‹ค ์ง€์›๋‹ˆ๋‹ค.

verdict ยท congestion ยท route ๊ฐ€ ์—ฌ๊ธฐ ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค. import ๋„ ํ•จ๊ป˜ ์ง€์›Œ์•ผ ์ปดํŒŒ์ผ์ด ํ†ต๊ณผํ•ฉ๋‹ˆ๋‹ค. service-template README 1-5 ์ฐธ๊ณ .




1-3. Flyway ์œ„์น˜

# config ์ €์žฅ์†Œ 1๊ณ„์ธต โ€” application.yml
spring:
  flyway:
    locations: classpath:db/migration/common,classpath:db/migration/service

๋‘ ๊ณณ์„ ๋‹ค ์ ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜๋‚˜๋งŒ ์ ์œผ๋ฉด ๊ทธ์ชฝ๋งŒ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

db/migration/common/     V1 ~ V19   ๊ณตํ†ต ๋ชจ๋“ˆ ๋Œ€์—ญ (jar ์•ˆ์— ์žˆ์Œ)
โ”œโ”€โ”€ V1__outbox.sql
โ””โ”€โ”€ V2__inbox.sql

db/migration/service/    V20 ~      ๊ฐ ์„œ๋น„์Šค
โ””โ”€โ”€ V20__place.sql

๋Œ€์—ญ์„ ๋‚˜๋ˆˆ ์ด์œ  โ€” ๊ณตํ†ต ๋ชจ๋“ˆ์ด ๋‚˜์ค‘์— V3 ๋ฅผ ์ถ”๊ฐ€ํ•ด๋„ ์ด๋ฏธ V20 ๊นŒ์ง€ ์‹คํ–‰ํ•œ ์„œ๋น„์Šค์—์„œ ์ˆœ์„œ๊ฐ€ ๊ผฌ์ด์ง€ ์•Š๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ config 1๊ณ„์ธต์— spring.flyway.out-of-order: true ๊ฐ€ ์ผœ์ ธ ์žˆ์Šต๋‹ˆ๋‹ค.




1-4. ์ž˜ ๋ถ™์—ˆ๋Š”์ง€ ํ™•์ธ

๊ธฐ๋™ ๋กœ๊ทธ์—์„œ ๋ด…๋‹ˆ๋‹ค.

Flyway: Successfully applied N migrations       V1 ยท V2 ๊ฐ€ ์‹คํ–‰๋จ
Tomcat started on port 8084

API ๋ฅผ ํ•˜๋‚˜ ๋ถˆ๋Ÿฌ ๋ด…๋‹ˆ๋‹ค.

curl http://localhost:8084/api/v1/nonexistent
{ "code": "RESOURCE_NOT_FOUND", "message": "์š”์ฒญํ•˜์‹  ๊ฒฝ๋กœ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.",
  "data": null, "traceId": "6a97..." }

์ด ์‘๋‹ต์ด ๋‚˜์˜ค๋ฉด ์„ธ ๊ฐ€์ง€๊ฐ€ ํ•œ ๋ฒˆ์— ํ™•์ธ๋ฉ๋‹ˆ๋‹ค.

CommonApiResponse       ํ˜•ํƒœ๊ฐ€ ๋งž์Œ
GlobalExceptionHandler  ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ๋ถ™์Œ
TraceIdResponseAdvice   traceId ๊ฐ€ ์ฑ„์›Œ์ง




2. ์ž๋™ ์„ค์ • 7๊ฐœ

์„œ๋น„์Šค๊ฐ€ ์•„๋ฌด๊ฒƒ๋„ ๋“ฑ๋กํ•˜์ง€ ์•Š์•„๋„ ์ผœ์ง‘๋‹ˆ๋‹ค. ํด๋ž˜์ŠคํŒจ์Šค์— ๋ฌด์—‡์ด ์žˆ๋Š”์ง€๋ฅผ ๋ณด๊ณ  ์Šคํ”„๋ง์ด ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค.

ํด๋ž˜์ŠคํŒจ์Šค์— ๋ฌด์—‡์ด ์žˆ๋‚˜          โ†’  ์–ด๋А ์ž๋™ ์„ค์ •์ด ์ผœ์ง€๋‚˜

์„œ๋ธ”๋ฆฟ ์›น + spring-webmvc         โ”€โ”€โ–ถ  CommonWebAutoConfiguration
์„œ๋ธ”๋ฆฟ ์›น + spring-security       โ”€โ”€โ–ถ  CommonSecurityAutoConfiguration
spring-data-jpa                   โ”€โ”€โ–ถ  CommonJpaAutoConfiguration
spring-data-jpa + spring-kafka    โ”€โ”€โ–ถ  CommonMessagingAutoConfiguration
spring-kafka                      โ”€โ”€โ–ถ  CommonKafkaAutoConfiguration
spring-web                        โ”€โ”€โ–ถ  CommonRestClientAutoConfiguration
(์กฐ๊ฑด ์—†์Œ)                        โ”€โ”€โ–ถ  CommonAsyncAutoConfiguration


DB ๋ฅผ ์“ฐ๋Š” ์„œ๋น„์Šค        7๊ฐœ ์ „๋ถ€ ์ผœ์ง        auth ยท user ยท pet ยท place ยท policy ...
๋ฌด์ƒํƒœ ์„œ๋น„์Šค            Web ยท Security ยท RestClient ยท Async    verdict ยท congestion ยท route
๊ฒŒ์ดํŠธ์›จ์ด               โ›” ์• ์ดˆ์— ์•ˆ ์”€ (ํ”Œ๋žซํผ 3๊ฐœ)




2-1. ์ผ๊ณฑ ๊ฐœ๊ฐ€ ํ•˜๋Š” ์ผ

์ž๋™ ์„ค์ • ์กฐ๊ฑด ๋“ฑ๋กํ•˜๋Š” Bean
CommonWebAutoConfiguration ์„œ๋ธ”๋ฆฟ ์›น + ResponseBodyAdvice GlobalExceptionHandler ยท TraceIdResponseAdvice
CommonSecurityAutoConfiguration ์„œ๋ธ”๋ฆฟ ์›น + SecurityFilterChain SecurityFilterChain ยท CustomSecurityExceptionHandler ยท AuthenticationManager
CommonJpaAutoConfiguration JpaRepository AuditorProvider + @EnableJpaAuditing
CommonMessagingAutoConfiguration JpaRepository + KafkaTemplate OutboxEventRecorder ยท OutboxPublisher ยท OutboxCommitListener ยท OutboxRelay ยท InboxProcessor + @EnableScheduling
CommonKafkaAutoConfiguration KafkaTemplate RecordMessageConverter ยท KafkaSecurityInterceptor ยท DefaultErrorHandler
CommonRestClientAutoConfiguration RestClient RestClientAuthInterceptor ยท defaultRestClientBuilder ยท internalRestClientBuilder ยท externalRestClientBuilder
CommonAsyncAutoConfiguration ์—†์Œ @EnableAsync

TraceIdResponseAdvice ๋งŒ ์กฐ๊ฑด์ด ํ•˜๋‚˜ ๋” ์žˆ์Šต๋‹ˆ๋‹ค.

@ConditionalOnClass(name = "io.micrometer.tracing.Tracer")

์ถ”์  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์—†์œผ๋ฉด ๊ทธ ๋นˆ๋งŒ ์•ˆ ๋งŒ๋“ค์–ด์ง‘๋‹ˆ๋‹ค. service-template ์—๋Š” spring-boot-starter-zipkin ์ด ์žˆ์–ด ํ•ญ์ƒ ์ผœ์ง‘๋‹ˆ๋‹ค.




2-2. ์กฐ๊ฑด ํŒ๋‹จ์— JpaRepository ๋ฅผ ์“ฐ๋Š” ์ด์œ 

EntityManager ๋กœ ํŒ๋‹จํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.

๋ฌด์ƒํƒœ ์„œ๋น„์Šค๊ฐ€ JPA ์Šคํƒ€ํ„ฐ๋ฅผ ์ง€์›€
        โ”‚
        โ”œโ”€โ”€ spring-data-jpa          โœ… ํ•จ๊ป˜ ์‚ฌ๋ผ์ง
        โ””โ”€โ”€ jakarta.persistence-api  โ›” ๋‚จ์•„ ์žˆ์Œ
                    โ–ฒ
                    โ””โ”€โ”€ hibernate-spatial ์ด hibernate-core ๋ฅผ ๊ฑฐ์ณ ์ „์ด๋กœ ๋Œ๊ณ  ์˜ด
                        (์ขŒํ‘œ ํƒ€์ž…์„ ์“ฐ๋Š” ์„œ๋น„์Šค๊ฐ€ ์žˆ์–ด ์ง€์šธ ์ˆ˜ ์—†์Œ)

EntityManager ๋กœ ํŒ๋‹จํ•˜๋ฉด ์กฐ๊ฑด์ด ์ฐธ์ด ๋˜์–ด ๋ฌด์ƒํƒœ ์„œ๋น„์Šค์—์„œ JPA ์„ค์ •์ด ์ผœ์ง€๊ณ , entityManagerFactory ๋นˆ์„ ๋ชป ์ฐพ์•„ ๊ธฐ๋™์— ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.


๋ฌธ์ž์—ด๋กœ ์“ฐ๋Š” ์ด์œ ์ž…๋‹ˆ๋‹ค.

@ConditionalOnClass(name = "org.springframework.data.jpa.repository.JpaRepository")
                    โ–ฒ
                    โ””โ”€โ”€ ํด๋ž˜์Šค ๋ฆฌํ„ฐ๋Ÿด๋กœ ์“ฐ๋ฉด ์กฐ๊ฑด ํ‰๊ฐ€ ์‹œ์ ์— ๋กœ๋”ฉ์„ ์‹œ๋„ํ•ด
                        NoClassDefFoundError ๊ฐ€ ๋‚  ์ˆ˜ ์žˆ์Œ

์Šคํ”„๋ง ์ž์ฒด ์ž๋™ ์„ค์ •๋„ ์ด ํŒจํ„ด์„ ์”๋‹ˆ๋‹ค.




2-3. ์„œ๋น„์Šค๊ฐ€ ๊ฐ™์€ ํƒ€์ž…์„ ์ •์˜ํ•˜๋ฉด ๋ฌผ๋Ÿฌ๋‚ฉ๋‹ˆ๋‹ค

๋Œ€๋ถ€๋ถ„์˜ @Bean ์— @ConditionalOnMissingBean ์ด ๋ถ™์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์„œ๋น„์Šค๊ฐ€ ์ž๊ธฐ SecurityFilterChain ์„ ์ •์˜
        โ”‚
        โ””โ”€โ”€โ–ถ  ๊ณตํ†ต ๋ชจ๋“ˆ์˜ ๊ฒƒ์ด ํ†ต์งธ๋กœ ๋ฌผ๋Ÿฌ๋‚จ
                  โš  ํ•จ๊ป˜ ์‚ฌ๋ผ์ง€๋Š” ๊ฒƒ๋“ค์„ ์„œ๋น„์Šค๊ฐ€ ๋‹ค์‹œ ๋„ฃ์–ด์•ผ ํ•จ

auth ๊ฐ€ ๊ทธ ์‚ฌ๋ก€์ž…๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธยท๊ฐ€์ž…์„ ์—ด์–ด์•ผ ํ•ด์„œ ์ž๊ธฐ ์ฒด์ธ์„ ์ •์˜ํ–ˆ๊ณ , ๊ณตํ†ต ์ฒด์ธ์ด ํ•˜๋˜ ์ผ์„ ์ „๋ถ€ ๋‹ค์‹œ ๋„ฃ์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•จ๊ป˜ ์‚ฌ๋ผ์ง€๋Š” ๊ฒƒ ๋น ๋œจ๋ฆฌ๋ฉด
HeaderAuthenticationFilter ๋“ฑ๋ก ์ธ์ฆ์ด ํ•„์š”ํ•œ ๊ฒฝ๋กœ๊ฐ€ ์ „๋ถ€ 401
/api/v1/admin/** โ†’ hasRole("ADMIN") ๊ด€๋ฆฌ์ž API ๊ฐ€ ์—ด๋ฆผ
/internal/** ยท /actuator/** permitAll ํ—ฌ์Šค์ฒดํฌ๊ฐ€ 401
CustomSecurityExceptionHandler ์—ฐ๊ฒฐ 401ยท403 ์ด ๊ณตํ†ต ํ˜•์‹์ด ์•„๋‹ˆ๊ฒŒ ๋จ

@ConditionalOnMissingBean ์€ ์ž๋™ ์„ค์ • ํด๋ž˜์Šค์—์„œ๋งŒ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๋ฐ˜ @Configuration ์€ ์‚ฌ์šฉ์ž ๋นˆ๊ณผ์˜ ์ฒ˜๋ฆฌ ์ˆœ์„œ๊ฐ€ ๋ณด์žฅ๋˜์ง€ ์•Š์•„ ์กฐ๊ฑด์ด ๋’ค์ง‘ํž ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ผ๊ณฑ ๊ฐœ๊ฐ€ ์ „๋ถ€ @AutoConfiguration ์ž…๋‹ˆ๋‹ค.


RestClient.Builder ๋นŒ๋” 3๊ฐœ๋งŒ ์˜ˆ์™ธ์ž…๋‹ˆ๋‹ค. ์ผ๋ถ€๋Ÿฌ ์•ˆ ๋ถ™์˜€์Šต๋‹ˆ๋‹ค.

๊ฐ™์€ ํƒ€์ž…์˜ ๋นˆ์ด ์ด๋ฏธ ์—ฌ๋Ÿฟ์ด๋ผ ์กฐ๊ฑด์ด ์–ธ์ œ๋‚˜ ๊ฑฐ์ง“์ด ๋˜์–ด ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ๋งŒ๋“ค์–ด์ง‘๋‹ˆ๋‹ค.

RecordMessageConverter ์™€ ๊ฒฐ๋ก ์ด ์ •๋ฐ˜๋Œ€๋ผ ํ—ท๊ฐˆ๋ฆฌ๊ธฐ ์‰ฌ์šด ์ž๋ฆฌ์ž…๋‹ˆ๋‹ค.

๊ทธ์ชฝ์€ ์„œ๋น„์Šค๊ฐ€ ์ž๊ธฐ ๊ฒƒ์„ ๋งŒ๋“ค๋ฉด ๋นˆ์ด ๋‘˜์ด ๋˜์–ด ์–ด๋А ์ชฝ๋„ ์ ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค ๋ผ ๋งŒ๋“ค์ง€ ์•Š๋Š” ๊ฒƒ์ด ๊ทœ์น™์ด๊ณ , ์ด์ชฝ์€ ์กฐ๊ฑด์„ ๋ถ™์ด์ง€ ์•Š๋Š” ๊ฒƒ์ด ๋‹ต์ž…๋‹ˆ๋‹ค.

๊ธฐ์ค€์€ ํ•˜๋‚˜์ž…๋‹ˆ๋‹ค. ๊ทธ ํƒ€์ž…์˜ ๋นˆ์„ ์ด๋ฏธ ๋ˆ„๊ฐ€ ๋งŒ๋“ค์–ด ๋‘๋А๋ƒ์ž…๋‹ˆ๋‹ค.




2-4. RestClient.Builder ๊ฐ€ ์…‹์ธ ์ด์œ 

์“ฐ์ž„์ด ์…‹์œผ๋กœ ๊ฐˆ๋ฆฝ๋‹ˆ๋‹ค. ์šฐ๋ฆฌ ์ฝ”๋“œ๊ฐ€ ์“ฐ๋Š” ๊ฒƒ์€ ์•„๋ž˜ ๋‘˜๋ฟ์ž…๋‹ˆ๋‹ค.

๋นˆ ๋ฌด์—‡์ด ๋ถ™์–ด ์žˆ๋‚˜ ๋ˆ„๊ฐ€ ์“ฐ๋‚˜
defaultRestClientBuilder ์•„๋ฌด๊ฒƒ๋„ ์—†์Œ. @Primary ์œ ๋ ˆ์นด์ฒ˜๋Ÿผ ํƒ€์ž…์œผ๋กœ ์ฐพ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
internalRestClientBuilder @LoadBalanced ยท ์ธ์ฆ ์ธํ„ฐ์…‰ํ„ฐ ยท ์‹œ๊ฐ„ ์ œํ•œ ์šฐ๋ฆฌ ์„œ๋น„์Šค๋ฅผ ๋ถ€๋ฅด๋Š” provider
externalRestClientBuilder ์‹œ๊ฐ„ ์ œํ•œ ๋ฐ”๊นฅ API ๋ฅผ ๋ถ€๋ฅด๋Š” provider

defaultRestClientBuilder ๊ฐ€ ์—†์œผ๋ฉด ์œ ๋ ˆ์นด๊ฐ€ ๊นจ์ง‘๋‹ˆ๋‹ค.

์œ ๋ ˆ์นด ํด๋ผ์ด์–ธํŠธ๋Š” ์ž๊ธฐ HTTP ํ˜ธ์ถœ์— ์ด ํƒ€์ž…์„ ์”๋‹ˆ๋‹ค.

ObjectProvider<RestClient.Builder>.getIfAvailable(RestClient::builder)
์ปจํ…Œ์ด๋„ˆ์˜ ํ›„๋ณด ์ˆ˜ ๊ฒฐ๊ณผ
0๊ฐœ ๊ธฐ๋ณธ๊ฐ’์„ ๋งŒ๋“ค์–ด ์”€
1๊ฐœ ๊ทธ๊ฒƒ์„ ์”€
2๊ฐœ ์ด์ƒ ยท @Primary ์—†์Œ NoUniqueBeanDefinitionException

0.0.10 ์—์„œ ๋นŒ๋”๋ฅผ ๋‘˜ ๋„ฃ์œผ๋ฉด์„œ 0๊ฐœ์˜€๋˜ ์ž๋ฆฌ๊ฐ€ 2๊ฐœ๊ฐ€ ๋˜์—ˆ๊ณ , ๊ทธ๋•Œ๋ถ€ํ„ฐ ์œ ๋ ˆ์นด ๋“ฑ๋ก๊ณผ ํ•˜ํŠธ๋น„ํŠธ๊ฐ€ ๋งค๋ฒˆ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.

๋“ฑ๋ก ์‹คํŒจ  โ”€โ”€โ–ถ  ์œ ๋ ˆ์นด์— ์„œ๋น„์Šค๊ฐ€ ์—†์Œ  โ”€โ”€โ–ถ  ๊ฒŒ์ดํŠธ์›จ์ด๊ฐ€ lb:// ๋ฅผ ๋ชป ํ’‚  โ”€โ”€โ–ถ  503

/actuator/health ๋Š” ๊ทธ๋™์•ˆ์—๋„ UP ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ์œ ๋ ˆ์นด ํ—ฌ์Šค ์ปดํฌ๋„ŒํŠธ๊ฐ€ UNKNOWN ์ด๋ฉด ์Šคํ”„๋ง์ด ์ „์ฒด ํŒ์ •์—์„œ ๋ฌด์‹œํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. 0.0.12 ์—์„œ @Primary ์ธ ๋งจ ๋นŒ๋”๋ฅผ ๋”ํ•ด ํ›„๋ณด๋ฅผ ํ•˜๋‚˜๋กœ ์ •ํ•ด ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.


๋งจ ๋นŒ๋”์— ์•„๋ฌด๊ฒƒ๋„ ์–น์ง€ ์•Š์€ ๊ฒƒ์€ ์˜๋„์ž…๋‹ˆ๋‹ค.

์œ ๋ ˆ์นด๋Š” ์š”์ฒญ ํŒฉํ„ฐ๋ฆฌ๋ฅผ ์ž๊ธฐ EurekaClientHttpRequestFactorySupplier ๋กœ ๋”ฐ๋กœ ๋„ฃ์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์— ์‹œ๊ฐ„ ์ œํ•œ์„ ์–น์–ด๋„ ์œ ๋ ˆ์นด์—๋Š” ๋ฐ˜์˜๋˜์ง€ ์•Š๊ณ , ๋Œ€์‹  ์ด ๋นŒ๋”๋ฅผ ์“ฐ๊ฒŒ ๋  ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๋™์ž‘๋งŒ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค. ๋น„์›Œ ๋‘๋ฉด ๊ทธ์ชฝ์ด ์Šค์Šค๋กœ ๋งŒ๋“ค์—ˆ์„ ๊ฒƒ๊ณผ ๊ฐ™์•„์ง‘๋‹ˆ๋‹ค.

์ด๋ฆ„์ด restClientBuilder ๊ฐ€ ์•„๋‹Œ ์ด์œ  โ€” ์Šคํ”„๋ง ๋ถ€ํŠธ์˜ RestClientAutoConfiguration ์ด ๊ทธ ์ด๋ฆ„์„ ์”๋‹ˆ๋‹ค. ๊ฒน์น˜๋ฉด BeanDefinitionOverrideException ์œผ๋กœ ๊ธฐ๋™์ด ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.


๋Œ€์‹  ์น˜๋ฅด๋Š” ๊ฒƒ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

@Primary ๊ฐ€ ์—†์„ ๋•Œ ์ง€๊ธˆ
@Qualifier ๋ฅผ ๋น ๋œจ๋ฆฌ๋ฉด ๊ธฐ๋™์—์„œ ๊ฑธ๋ฆผ ๋งจ ๋นŒ๋”๊ฐ€ ์กฐ์šฉํžˆ ์ฃผ์ž…๋จ
๊ทธ๊ฒƒ์ด ๋“œ๋Ÿฌ๋‚˜๋Š” ๋•Œ ์•ฑ ์‹œ์ž‘ ์‹œ lb:// ๋ฅผ ๋ชป ํ’€์–ด ํ˜ธ์ถœํ•  ๋•Œ
provider ์—์„œ @Qualifier ๋ˆ„๋ฝ  โ”€โ”€โ–ถ  ๋งจ ๋นŒ๋” ์ฃผ์ž…  โ”€โ”€โ–ถ  baseUrl("lb://place-service")
                                                  โ”€โ”€โ–ถ  ํ˜ธ์ถœ ์‹œ ์Šคํ‚ด lb ๋ฅผ ๋ชจ๋ฆ„

์•Œ๊ณ  ๋ฐ›์•„๋“ค์ธ ๊ฒƒ์ž…๋‹ˆ๋‹ค. provider ๋ฅผ ๋งŒ๋“ค ๋•Œ @Qualifier ๊ฐ€ ์žˆ๋Š”์ง€ ๋ฐ˜๋“œ์‹œ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ํ˜•ํƒœ๋Š” 4-7 ์— ์žˆ์Šต๋‹ˆ๋‹ค.




2-5. ๋ฌด์ƒํƒœ ์„œ๋น„์Šค์—์„œ ์•ˆ ์ผœ์ง€๋Š” ๊ฒƒ

verdict ยท congestion ยท route ๊ฐ€ JPA ์Šคํƒ€ํ„ฐ๋ฅผ ์ง€์šฐ๋ฉด ์…‹์ด ํ•จ๊ป˜ ๊บผ์ง‘๋‹ˆ๋‹ค.

๋ชป ์“ฐ๊ฒŒ ๋˜๋Š” ๊ฒƒ ๋Œ€์‹ 
BaseEntity ยท JPA Auditing ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์—†์œผ๋ฏ€๋กœ ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค
OutboxEventRecorder ๋ฌด์ƒํƒœ ์„œ๋น„์Šค๋Š” ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค
InboxProcessor ์ด๋ฒคํŠธ๋กœ ํ•˜๋Š” ์ผ์ด ์บ์‹œ ์‚ญ์ œ๋ฟ์ด๋ผ ์ค‘๋ณต์ด ๋ฌธ์ œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค

๊ทธ๋Œ€๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

CommonApiResponse ยท PageResponse      ์‘๋‹ต ํ˜•์‹
ErrorCode ยท CustomException           ์—๋Ÿฌ ์ฝ”๋“œ
GlobalExceptionHandler                ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
@CurrentUser ยท ๋ณด์•ˆ ํ•„ํ„ฐ               ์ธ์ฆ
EventEnvelope ยท DomainEvent           ์ด๋ฒคํŠธ ๋ด‰ํˆฌ (์†Œ๋น„ํ•  ๋•Œ ํ•„์š”)
internal ยท external ๋นŒ๋”              ๋‹ค๋ฅธ ์„œ๋น„์Šค์™€ ๋ฐ”๊นฅ API ํ˜ธ์ถœ

EventEnvelope ์— ์กฐ๊ฑด์„ ์•ˆ ๊ฑด ์ด์œ  โ€” verdict ๋„ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์•„ ์บ์‹œ๋ฅผ ์ง€์šฐ๋ฏ€๋กœ ๋ด‰ํˆฌ๋ฅผ ์—ญ์ง๋ ฌํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ˆœ์ˆ˜ ์ž๋ฐ”๋ผ JPA ์™€ ๋ฌด๊ด€ํ•ฉ๋‹ˆ๋‹ค.

CommonRestClientAutoConfiguration ์€ ๋ฌด์ƒํƒœ ์„œ๋น„์Šค์—์„œ๋„ ์ผœ์ง‘๋‹ˆ๋‹ค. ์กฐ๊ฑด์ด RestClient ์ด๊ณ  ๊ทธ ํด๋ž˜์Šค๋Š” spring-web ์— ์žˆ์–ด ์›น ์„œ๋น„์Šค๋ฉด ํ•ญ์ƒ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜คํžˆ๋ ค ๋ฌด์ƒํƒœ ์„œ๋น„์Šค๊ฐ€ ๋” ๋งŽ์ด ์”๋‹ˆ๋‹ค. verdict ๋Š” policy ๋ฅผ, congestion ๊ณผ route ๋Š” ๋ฐ”๊นฅ API ๋ฅผ ๋ถ€๋ฆ…๋‹ˆ๋‹ค.




3. ๋ฌด์—‡์ด ๋“ค์–ด ์žˆ๋‚˜

com.pawtrail.common
โ”‚
โ”œโ”€โ”€ config/                              ์ž๋™ ์„ค์ • 7๊ฐœ โ€” 2์žฅ
โ”‚   โ”œโ”€โ”€ Common*AutoConfiguration          ์กฐ๊ฑด์— ๋งž์œผ๋ฉด Bean ์„ ์˜ฌ๋ฆผ
โ”‚   โ””โ”€โ”€ RestClientProperties              app.rest-client ์˜ ์‹œ๊ฐ„ ์ œํ•œ
โ”‚
โ”œโ”€โ”€ response/                            ์‘๋‹ต ํ˜•์‹
โ”‚   โ”œโ”€โ”€ CommonApiResponse                 {code, message, data, traceId}
โ”‚   โ”œโ”€โ”€ PageResponse                      ๋ชฉ๋ก ์‘๋‹ต์˜ data ์•ˆ์ชฝ
โ”‚   โ””โ”€โ”€ TraceIdResponseAdvice             ์‘๋‹ต ์ง์ „์— traceId ๋ฅผ ์ฑ„์›€
โ”‚
โ”œโ”€โ”€ exception/                           ์—๋Ÿฌ ์ฝ”๋“œ ๊ทœ์•ฝ
โ”‚   โ”œโ”€โ”€ ErrorCode                         ์ธํ„ฐํŽ˜์ด์Šค โ€” ๊ฐ ์„œ๋น„์Šค๊ฐ€ ์ž๊ธฐ enum ์œผ๋กœ ๊ตฌํ˜„
โ”‚   โ”œโ”€โ”€ CommonErrorCode                   ์ „ ์„œ๋น„์Šค ๊ณตํ†ต 6๊ฐœ
โ”‚   โ”œโ”€โ”€ CustomException                   ์˜๋„์ ์œผ๋กœ ๋˜์ง€๋Š” ์œ ์ผํ•œ ์˜ˆ์™ธ
โ”‚   โ””โ”€โ”€ handler/GlobalExceptionHandler    ํ•ธ๋“ค๋Ÿฌ 5๊ฐœ
โ”‚
โ”œโ”€โ”€ entity/BaseEntity                    ๋ชจ๋“  ํ…Œ์ด๋ธ”์ด ์ƒ์†ํ•˜๋Š” 6์ปฌ๋Ÿผ
โ”œโ”€โ”€ audit/AuditorProvider                ์ง€๊ธˆ ์ž‘์—…ํ•˜๋Š” ์ฃผ์ฒด๊ฐ€ ๋ˆ„๊ตฌ์ธ์ง€
โ”œโ”€โ”€ enums/Role                           USER ยท ADMIN
โ”‚
โ”œโ”€โ”€ security/                            ์ธ์ฆ ์ •๋ณด ๋‹ค๋ฃจ๊ธฐ
โ”‚   โ”œโ”€โ”€ filter/HeaderAuthenticationFilter        ํ—ค๋” โ†’ SecurityContext
โ”‚   โ”œโ”€โ”€ handler/CustomSecurityExceptionHandler   401 ยท 403 ์„ ๊ณตํ†ต ํ˜•์‹์œผ๋กœ
โ”‚   โ”œโ”€โ”€ interceptor/RestClientAuthInterceptor    ํ˜ธ์ถœํ•  ๋•Œ ํ—ค๋”๋ฅผ ์‹ค์–ด ์คŒ
โ”‚   โ”œโ”€โ”€ principal/CustomUserPrincipal            accountId ยท role
โ”‚   โ””โ”€โ”€ annotation/CurrentUser                   ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ฃผ์ž…๋ฐ›์Œ
โ”‚
โ””โ”€โ”€ message/                             ์ด๋ฒคํŠธ
    โ”œโ”€โ”€ DomainEvent                       ๋ฐœํ–‰ํ•  ์ด๋ฒคํŠธ๊ฐ€ ๊ตฌํ˜„ํ•˜๋Š” ๊ณ„์•ฝ
    โ”œโ”€โ”€ EventEnvelope                     ๋ชจ๋“  ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์‹ธ๋Š” ๋ด‰ํˆฌ
    โ”œโ”€โ”€ AuthContextHeaders                X-User-Id ยท X-User-Role ์˜ ๋‹จ์ผ ์ถœ์ฒ˜
    โ”œโ”€โ”€ KafkaSecurityInterceptor          ์†Œ๋น„ํ•  ๋•Œ ์ธ์ฆ ์ •๋ณด๋ฅผ ๋ณต์›
    โ”‚
    โ”œโ”€โ”€ outbox/                           "์ €์žฅ์€ ๋๋Š”๋ฐ ์ด๋ฒคํŠธ๊ฐ€ ์•ˆ ๋‚˜๊ฐ”๋‹ค" ๋ฅผ ๋ง‰์Œ
    โ”‚   โ”œโ”€โ”€ OutboxMessage ยท OutboxRepository
    โ”‚   โ”œโ”€โ”€ OutboxEventRecorder            *์„œ๋น„์Šค๊ฐ€ ๋ถ€๋ฅด๋Š” ๋ฐœํ–‰ ์ž…๊ตฌ
    โ”‚   โ”œโ”€โ”€ OutboxPublisher                ์‹ค์ œ ์นดํ”„์นด ์ „์†ก
    โ”‚   โ”œโ”€โ”€ OutboxCommitListener           ์ปค๋ฐ‹ ์งํ›„ ๋ฐœํ–‰
    โ”‚   โ””โ”€โ”€ OutboxRelay                    ๋†“์นœ ๊ฑด ํšŒ์ˆ˜ (5์ดˆ)
    โ”‚
    โ””โ”€โ”€ inbox/                            "๊ฐ™์€ ์ด๋ฒคํŠธ๋ฅผ ๋‘ ๋ฒˆ ์ฒ˜๋ฆฌํ–ˆ๋‹ค" ๋ฅผ ๋ง‰์Œ
        โ”œโ”€โ”€ ProcessedEvent ยท ProcessedEventRepository
        โ””โ”€โ”€ InboxProcessor

src/main/resources/
โ”œโ”€โ”€ META-INF/spring/AutoConfiguration.imports    *์—†์œผ๋ฉด ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ์ผœ์ง€๋Š”๋ฐ ์˜ค๋ฅ˜๋„ ์•ˆ ๋‚จ
โ”œโ”€โ”€ db/migration/common/V1__outbox.sql ยท V2__inbox.sql
โ””โ”€โ”€ logback-loki-appender.xml                    ์„œ๋น„์Šค์˜ logback ์ด include ํ•ด์„œ ์”€




3-1. response โ€” ์‘๋‹ต ํ˜•์‹

์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ CommonApiResponse.success(data) ๋ฅผ ๋ฐ˜ํ™˜
        โ”‚
        โ–ผ
TraceIdResponseAdvice        ์‘๋‹ต ์ง์ „์— traceId ๋ฅผ ์ฑ„์›€
        โ”‚                     supports() ๊ฐ€ true ๊ณ ์ •
        โ–ผ                     ์‹ค์ œ ํŒ๋ณ„์€ beforeBodyWrite ์˜ instanceof
{ "code": "SUCCESS", "message": "...", "data": {}, "traceId": "6a97..." }


์˜ˆ์™ธ๊ฐ€ ๋˜์ ธ์ง€๋ฉด
        โ”‚
        โ–ผ
GlobalExceptionHandler       ErrorCode ๋ฅผ ๋ณด๊ณ  ์ƒํƒœ์™€ ๋ฉ”์‹œ์ง€๋ฅผ ์ •ํ•จ
        โ”‚
        โ–ผ
TraceIdResponseAdvice        *์‹คํŒจ ์‘๋‹ต๋„ ์—ฌ๊ธฐ๋ฅผ ์ง€๋‚จ
        โ”‚
        โ–ผ
{ "code": "PLACE_NOT_FOUND", "message": "...", "data": null, "traceId": "6a97..." }

CommonApiResponse ๋Š” ํ•„๋“œ 4๊ฐœ์ž…๋‹ˆ๋‹ค.

ํ•„๋“œ ์„ฑ๊ณต ์‹คํŒจ
code "SUCCESS" ์—๋Ÿฌ ์ฝ”๋“œ ์ด๋ฆ„
message ์•ˆ๋‚ด ๋ฌธ๊ตฌ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
data ์‹ค์ œ ๊ฐ’ null
traceId ์ž๋™์œผ๋กœ ์ฑ„์›Œ์ง ์ž๋™์œผ๋กœ ์ฑ„์›Œ์ง

ํ”„๋ก ํŠธ๋Š” code !== 'SUCCESS' ํ•œ ์ค„๋กœ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค.


@JsonInclude(NON_NULL) ์„ ๋ถ™์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋ถ™์ด๋ฉด ์‹คํŒจ ์‘๋‹ต์˜ "data": null ํ‚ค๊ฐ€ ์‚ฌ๋ผ์ง€๋Š”๋ฐ, API ๋ช…์„ธ์— ๊ทธ ํ‚ค๊ฐ€ ๋ชป๋ฐ•ํ˜€ ์žˆ์Šต๋‹ˆ๋‹ค.


withTraceId ๋Š” ์ธ์Šคํ„ด์Šค ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค.

// static ์œผ๋กœ ํ•ฉ์น˜๋ฉด ์•ˆ ๋จ
public CommonApiResponse<T> withTraceId(String traceId) { ... }

Advice ๋Š” ์„ฑ๊ณต๋ฟ ์•„๋‹ˆ๋ผ GlobalExceptionHandler ๊ฐ€ ๋งŒ๋“  ์‹คํŒจ ์‘๋‹ต๋„ ์ง€๋‚ฉ๋‹ˆ๋‹ค. static ์ด๋ฉด code ๊ฐ€ "SUCCESS" ๋กœ ๋ฎ์—ฌ 404 ๊ฐ€ ์„ฑ๊ณต์œผ๋กœ ๋‚˜๊ฐ‘๋‹ˆ๋‹ค.


supports() ๊ฐ€ true ๊ณ ์ •์ธ ์ด์œ ์ž…๋‹ˆ๋‹ค.

public boolean supports(MethodParameter returnType, Class<?> converterType) {
    return true;      // ์‹ค์ œ ํŒ๋ณ„์€ beforeBodyWrite ์˜ instanceof
}

returnType.getParameterType() ์€ ์„ ์–ธ๋œ ๋ฐ˜ํ™˜ ํƒ€์ž…์ด๋ผ ResponseEntity<CommonApiResponse<T>> ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” GlobalExceptionHandler ๊ฐ€ ๊ฑธ๋Ÿฌ์ง‘๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์‹คํŒจ ์‘๋‹ต์—๋งŒ traceId ๊ฐ€ ์กฐ์šฉํžˆ ๋ˆ„๋ฝ๋ฉ๋‹ˆ๋‹ค.


PageResponse ๋Š” ๋ณ€ํ™˜ ํ•จ์ˆ˜๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค.

PageResponse.from(page, PlaceOutput::from)
{ "content": [ ], "page": { "number": 0, "size": 20, "totalElements": 0, "totalPages": 0 } }

์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ๋…ธ์ถœํ•˜์ง€ ์•Š๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. ์•ˆ ๋„ฃ์œผ๋ฉด getContent() + ํŽ˜์ด์ง€ ์ •๋ณด ์กฐ๋ฆฝ์ด ํŽ˜์ด์ง• ์—”๋“œํฌ์ธํŠธ๋งˆ๋‹ค ๋ฐ˜๋ณต๋˜๊ณ , ํ•„๋“œ ์ˆœ์„œ๋ฅผ ๋ฐ”๊ฟ” ๋„ฃ์–ด๋„ ์ปดํŒŒ์ผ์ด ํ†ต๊ณผํ•ฉ๋‹ˆ๋‹ค.




3-2. exception โ€” ์—๋Ÿฌ ์ฝ”๋“œ ๊ทœ์•ฝ

ErrorCode ๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค.

public interface ErrorCode {
    HttpStatus getHttpStatus();
    String getCode();
    String getMessage();
}

enum ์€ ์ƒ์†์ด ์•ˆ ๋˜๋Š”๋ฐ ํ•ธ๋“ค๋Ÿฌ๋Š” ํ•˜๋‚˜๋กœ ๋ฐ›์•„์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๊ฐ ์„œ๋น„์Šค๊ฐ€ ์ž๊ธฐ enum ์œผ๋กœ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.


CommonErrorCode 6๊ฐœ โ€” ์ „ ์„œ๋น„์Šค ๊ณตํ†ต๋งŒ ๋‹ด์Šต๋‹ˆ๋‹ค.

์ฝ”๋“œ HTTP ์–ธ์ œ
VALIDATION_FAILED 400 @Valid ์‹คํŒจ ยท ํƒ€์ž… ๋ถˆ์ผ์น˜
AUTHENTICATION_FAILED 401 ์ธ์ฆ ์‹คํŒจ
ACCESS_DENIED 403 ๊ถŒํ•œ ์—†์Œ
RESOURCE_NOT_FOUND 404 ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์—†๋Š” ์ฃผ์†Œ
INTERNAL_ERROR 500 ๊ทธ ๋ฐ–์˜ ์˜ˆ์™ธ
EXTERNAL_API_ERROR 502 ์™ธ๋ถ€ API ํ˜ธ์ถœ ์‹คํŒจ

๋„๋ฉ”์ธ ๊ฐœ๋…์„ ๋„ฃ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. PLACE_NOT_FOUND ๊ฐ€ ์—ฌ๊ธฐ ์žˆ์œผ๋ฉด verdict ๋„ ๊ทธ๊ฒƒ์„ ๋ณด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.


CustomException ํ•˜๋‚˜๋ฟ์ด๊ณ  ํ•˜์œ„ ํด๋ž˜์Šค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

throw new CustomException(PlaceErrorCode.PLACE_NOT_FOUND);

์ƒํƒœ๊ฐ’์ด ์ด๋ฏธ ErrorCode ์— ์žˆ์–ด ์˜ˆ์™ธ ํด๋ž˜์Šค๋ฅผ ์ƒํƒœ๋ณ„๋กœ ๋‚˜๋ˆ„๋ฉด ๋‘ ๋ฒˆ์งธ ์ง„์‹ค์˜ ์›์ฒœ์ด ๋˜๊ณ  ๋‘˜์ด ์–ด๊ธ‹๋‚˜๋„ ์•„๋ฌด๋„ ๋ชจ๋ฆ…๋‹ˆ๋‹ค.

super() ์— code: message ๋ฅผ ์กฐ๋ฆฝํ•ด ๋กœ๊ทธ์˜ ์ŠคํƒํŠธ๋ ˆ์ด์Šค ์ฒซ ์ค„์— ์ฝ”๋“œ๊ฐ€ ๋‚จ์Šต๋‹ˆ๋‹ค. ํ•ธ๋“ค๋Ÿฌ ๋ฐ–์œผ๋กœ ์ƒˆ๋Š” ๊ฒฝ์šฐ(์นดํ”„์นด ์ปจ์Šˆ๋จธ ยท @Async)์˜ ์•ˆ์ „๋ง์ž…๋‹ˆ๋‹ค.


getCode() ๋Š” ๋ฐ˜๋“œ์‹œ name() ์„ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

@Override
public String getCode() {
    return name();
}

์ƒ์ˆ˜ ์ด๋ฆ„์ด ๊ณง ์‘๋‹ต code ์ด์ž API ๊ณ„์•ฝ์ธ๋ฐ ๊ทœ์น™์„ ์–ด๊ฒจ๋„ ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ์žก์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.


ํ•ธ๋“ค๋Ÿฌ 5๊ฐœ์ž…๋‹ˆ๋‹ค.

์žก๋Š” ๊ฒƒ ์‘๋‹ต ๋กœ๊ทธ
1 CustomException ErrorCode ์˜ ์ƒํƒœ warn
2 MethodArgumentNotValidException 400 + ํ•„๋“œ๋ณ„ ์˜ค๋ฅ˜ ๋ฐฐ์—ด warn
3 MethodArgumentTypeMismatchException 400 warn
4 NoResourceFoundException 404 warn
5 Exception 500 error + ์ŠคํƒํŠธ๋ ˆ์ด์Šค

4๋ฒˆ์ด ์—†์œผ๋ฉด ์˜คํƒ€ ๋‚œ URL ํ•˜๋‚˜๊ฐ€ 500 ์œผ๋กœ ๋‚˜๊ฐ‘๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด 5๋ฒˆ์ด ์ŠคํƒํŠธ๋ ˆ์ด์Šค๋ฅผ ์ฐ์–ด Loki ์—์„œ ์ง„์งœ ์˜ค๋ฅ˜๋ฅผ ๋ชป ์ฐพ์Šต๋‹ˆ๋‹ค.


401ยท403 ์€ ์—ฌ๊ธฐ๋กœ ์˜ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์‹œํ๋ฆฌํ‹ฐ ํ•„ํ„ฐ  โ”€โ”€โ–ถ  DispatcherServlet  โ”€โ”€โ–ถ  ์ปจํŠธ๋กค๋Ÿฌ
      โ–ฒ                                        โ–ฒ
      โ”‚                                        โ”‚
   401ยท403 ์ด ์—ฌ๊ธฐ์„œ ๊ฒฐ์ •๋จ              @RestControllerAdvice ๊ฐ€ ์žก๋Š” ๋ฒ”์œ„
   โ†’ CustomSecurityExceptionHandler ๊ฐ€ ์ฒ˜๋ฆฌ

๊ฒ€์ฆ ๋•Œ 401 ์ด Content-Length: 0 ์œผ๋กœ ๋ฐ”๋”” ์—†์ด ๋‚˜๊ฐ„ ์ ์ด ์žˆ์–ด ๊ทธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.




3-3. entity ยท audit โ€” ๊ฐ์‚ฌ ์ปฌ๋Ÿผ

BaseEntity 6์ปฌ๋Ÿผ์ž…๋‹ˆ๋‹ค.

์ปฌ๋Ÿผ ์ฑ„์šฐ๋Š” ๊ฒƒ updatable
createdAt ยท createdBy JPA Auditing false
updatedAt ยท updatedBy JPA Auditing ๊ธฐ๋ณธ๊ฐ’
deletedAt ยท deletedBy ์ˆ˜๋™ โ€” delete(auditorProvider.current()) ๊ธฐ๋ณธ๊ฐ’

โš  updatedBy ์— updatable = false ๋ฅผ ๋ถ™์ด๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค. UPDATE ๋ฌธ์—์„œ ๋น ์ ธ @LastModifiedBy ๊ฐ€ ๊ฐ’์„ ์ฑ„์›Œ๋„ DB ์— ์•ˆ ๋‚˜๊ฐ€๊ณ  ์˜ค๋ฅ˜๋„ ๋กœ๊ทธ๋„ ์—†์Šต๋‹ˆ๋‹ค.


AuditorProvider ๋Š” ์„ธ ๊ฐˆ๋ž˜์ž…๋‹ˆ๋‹ค.

์ธ์ฆ ์—†์Œ (๋ฐฐ์น˜)                    app.auditor.system-name     ingest-batch
์ต๋ช… (AnonymousAuthenticationToken)  app.auditor.system-name     SYSTEM
์ธ์ฆ๋จ                              principal.accountId

current() ๊ฐ€ ์ธ์Šคํ„ด์Šค ๋ฉ”์„œ๋“œ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. static ์ด๋ฉด ์„ค์ •์œผ๋กœ ์ฃผ์ž…๋ฐ›์€ systemName ์„ ๋ชป ์จ์„œ ๋ฐฐ์น˜์˜ ingest-batch ์™€ SYSTEM ์ด ๊ฐˆ๋ฆฝ๋‹ˆ๋‹ค.

Optional.empty() ๋ฅผ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฐ˜ํ™˜ํ•˜๋ฉด ์ปฌ๋Ÿผ์ด null ๋กœ ๋‚จ๊ณ  nullable = false ์—์„œ ํ„ฐ์ง‘๋‹ˆ๋‹ค.


์†Œํ”„ํŠธ ๋”œ๋ฆฌํŠธ์˜ ์ง„์งœ ๋…ผ์ ์€ ์กฐํšŒ์ž…๋‹ˆ๋‹ค.

๋ชจ๋“  ์กฐํšŒ์— WHERE deleted_at IS NULL ์ด ๋ถ™์–ด์•ผ ํ•จ
        โ”‚
        โ””โ”€โ”€ ํ•˜๋‚˜๋งŒ ๋น ๋œจ๋ฆฌ๋ฉด ์‚ญ์ œ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋…ธ์ถœ๋จ

์•„์ง ๋ฐฉ์‹์„ ์ •ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ํ›„๋ณด๋Š” @SQLRestriction ยท ๋ฆฌํฌ์ง€ํ„ฐ๋ฆฌ๋งˆ๋‹ค ๋ช…์‹œ ยท QueryDSL ๊ณตํ†ต ์กฐ๊ฑด์ด๊ณ , ์‹ค์ œ ์กฐํšŒ๋ฅผ ์งœ๋Š” ์„œ๋น„์Šค์—์„œ ์ •ํ•ฉ๋‹ˆ๋‹ค.




3-4. security โ€” ์ธ์ฆ ์ •๋ณด

๊ฒŒ์ดํŠธ์›จ์ด  โ”€โ”€โ–ถ  X-User-Id ยท X-User-Role ํ—ค๋”
                        โ”‚
                        โ–ผ
        HeaderAuthenticationFilter        ํ—ค๋”๋ฅผ ์ฝ์–ด SecurityContext ๋ฅผ ์ฑ„์›€
                        โ”‚                 ํ—ค๋”๊ฐ€ ์—†์œผ๋ฉด ์‹ฌ์ง€ ์•Š๊ณ  ํ†ต๊ณผ
                        โ–ผ
        CustomUserPrincipal(accountId, role)
                        โ”‚
                        โ–ผ
        @CurrentUser ๋กœ ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ๊บผ๋ƒ„

SecurityContext ๋Š” "์ด ์š”์ฒญ์„ ๋ณด๋‚ธ ์‚ฌ๋žŒ์ด ๋ˆ„๊ตฌ์ธ๊ฐ€" ๋ฅผ ์š”์ฒญ์ด ๋๋‚  ๋•Œ๊นŒ์ง€ ๋“ค๊ณ  ์žˆ๋Š” ์ž๋ฆฌ์ž…๋‹ˆ๋‹ค. ํ•„ํ„ฐ๊ฐ€ ํ—ค๋”๋ฅผ ์ฝ์–ด ๊ฑฐ๊ธฐ ์ฑ„์›Œ ๋‘๋ฉด ์ปจํŠธ๋กค๋Ÿฌ๋Š” @CurrentUser ๋กœ ๊บผ๋‚ด๊ธฐ๋งŒ ํ•ฉ๋‹ˆ๋‹ค.

@GetMapping
public CommonApiResponse<List<PetOutput>> getMyPets(@CurrentUser CustomUserPrincipal principal) {
    return CommonApiResponse.success(petService.findByAccount(principal.accountId()));
}

๊ธฐ๋ณธ ๋ณด์•ˆ ์ฒด์ธ์˜ ๊ทœ์น™ ์…‹์ž…๋‹ˆ๋‹ค.

๊ฒฝ๋กœ ๊ทœ์น™
/internal/** ยท /actuator/** permitAll
/api/v1/admin/** hasRole("ADMIN")
๊ทธ ์™ธ ์ธ์ฆ ํ•„์ˆ˜

๊ด€๋ฆฌ์ž ๋ณดํ˜ธ๋ฅผ ๊ณตํ†ต ๋ชจ๋“ˆ์— ๋‘” ์ด์œ ๋Š” ๊ด€๋ฆฌ์ž ๊ธฐ๋Šฅ์ด ์—ฌ๋Ÿฌ ์„œ๋น„์Šค์— ํฉ์–ด์ ธ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๊ฐ์ž ๋ง‰๊ฒŒ ํ•˜๋ฉด ํ•œ ๊ณณ๋งŒ ๋น ๋œจ๋ ค๋„ ๊ทธ ์„œ๋น„์Šค๊ฐ€ ์—ด๋ฆฝ๋‹ˆ๋‹ค.


hasRole("ADMIN") ์ด ๋™์ž‘ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

๊ฒŒ์ดํŠธ์›จ์ด๊ฐ€ ๋„ฃ์Œ     X-User-Role: ADMIN        ์ ‘๋‘์‚ฌ ์—†์Œ
ํ•„ํ„ฐ๊ฐ€ ๋งŒ๋“ฆ          "ROLE_" + role   โ†’  ROLE_ADMIN
hasRole("ADMIN")     ๋‚ด๋ถ€์ ์œผ๋กœ ROLE_ADMIN ์„ ์ฐพ์Œ

์ ‘๋‘์‚ฌ ๊ทœ์น™์„ ๋ฐ”๊พธ๋ฉด ๊ด€๋ฆฌ์ž ๊ฒฝ๋กœ๊ฐ€ 403 ๋งŒ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์›์ธ์ด ์•ˆ ๋“œ๋Ÿฌ๋‚ฉ๋‹ˆ๋‹ค.


ํ•„ํ„ฐ๋ฅผ @Component ๋กœ ๋‘์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

@Component ๋ฉด Boot ๊ฐ€ ์‹œํ๋ฆฌํ‹ฐ ์ฒด์ธ๊ณผ ์„œ๋ธ”๋ฆฟ ์ „์—ญ ํ•„ํ„ฐ ์–‘์ชฝ์— ๋“ฑ๋กํ•ด ์š”์ฒญ๋งˆ๋‹ค ๋‘ ๋ฒˆ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์ž๋™ ์„ค์ •์—์„œ new ๋กœ ๋งŒ๋“ค์–ด ์ฒด์ธ์—๋งŒ ๋ถ™์ž…๋‹ˆ๋‹ค.


CustomUserPrincipal ์ด AuthenticatedPrincipal ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

public record CustomUserPrincipal(UUID accountId, Role role) implements AuthenticatedPrincipal {
    @Override public String getName() { return accountId.toString(); }
}

์•„๋ฌด๊ฒƒ๋„ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์œผ๋ฉด getName() ์ด toString() ์„ ๋ฐ˜ํ™˜ํ•ด created_by ์— ๊ฐ์ฒด ํ•ด์‹œ๊ฐ€ ์กฐ์šฉํžˆ ์Œ“์ž…๋‹ˆ๋‹ค.

UserDetails ๋ฅผ ์•ˆ ์“ด ์ด์œ  โ€” ๊ฒŒ์ดํŠธ์›จ์ด๊ฐ€ ์ด๋ฏธ ๊ฒ€์ฆํ•˜๊ณ  ํ—ค๋”๋งŒ ๋„˜๊ธฐ๋Š” ๋ชจ๋ธ์ด๋ผ getPassword() ์™€ ๊ณ„์ • ์ƒํƒœ ํ”Œ๋ž˜๊ทธ 4๊ฐœ๊ฐ€ ์ „๋ถ€ ๋ฌด์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.




3-5. message/outbox โ€” ์ด๋ฒคํŠธ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณด๋‚ด๊ธฐ

ํŠธ๋žœ์žญ์…˜์ด๋ž€ "์—ฌ๋Ÿฌ ์“ฐ๊ธฐ๋ฅผ ํ•œ ๋ฉ์–ด๋ฆฌ๋กœ โ€” ์ „๋ถ€ ๋˜๊ฑฐ๋‚˜ ์ „๋ถ€ ์•ˆ ๋˜๊ฑฐ๋‚˜" ์ž…๋‹ˆ๋‹ค. ๊ณ„์ • INSERT ์™€ outbox INSERT ๋ฅผ ํ•œ ํŠธ๋žœ์žญ์…˜์— ๋„ฃ์œผ๋ฉด ๋‘˜ ์ค‘ ํ•˜๋‚˜๋งŒ ๋‚จ๋Š” ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.

์„œ๋น„์Šค ํŠธ๋žœ์žญ์…˜
  โ”‚
  โ”œโ”€โ”€โ–ถ  ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐ์ดํ„ฐ INSERT
  โ”‚
  โ””โ”€โ”€โ–ถ  outboxEventRecorder.record(์ด๋ฒคํŠธ)      *ํ•œ ์ค„
              โ”‚
              โ”œโ”€โ”€โ–ถ  EventEnvelope.of(์ด๋ฒคํŠธ)          ๋ด‰ํˆฌ ์ƒ์„ฑ
              โ”œโ”€โ”€โ–ถ  JsonMapper ๋กœ ์ง๋ ฌํ™”
              โ”œโ”€โ”€โ–ถ  outbox ํ–‰ INSERT                 โ† ๊ฐ™์€ ํŠธ๋žœ์žญ์…˜
              โ””โ”€โ”€โ–ถ  ์Šคํ”„๋ง ์ด๋ฒคํŠธ ๋ฐœํ–‰
                        โ”‚
  ์ปค๋ฐ‹ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    โ”‚
    โ–ผ
  OutboxCommitListener      @Async ยท @TransactionalEventListener(AFTER_COMMIT)
    โ”‚
    โ””โ”€โ”€โ–ถ  OutboxPublisher.publish(id)           @Transactional(REQUIRES_NEW)
              โ”‚
              โ”œโ”€โ”€โ–ถ  FOR UPDATE SKIP LOCKED ๋กœ ํ–‰์„ ์ž ๊ธˆ
              โ”œโ”€โ”€โ–ถ  kafkaTemplate.send()
              โ”‚        ์„ฑ๊ณต โ”€โ”€โ–ถ  published_at ๊ธฐ๋ก
              โ”‚        ์‹คํŒจ โ”€โ”€โ–ถ  retry_count +1 ยท last_error ๊ธฐ๋ก
              โ–ผ
  OutboxRelay               5์ดˆ๋งˆ๋‹ค ยท ๋ฏธ๋ฐœํ–‰ ๊ฑด์„ ์ฃผ์›€ (์•ˆ์ „๋ง)
    โ”‚
    โ”œโ”€โ”€ retry_count < 10   ๋‹ค์‹œ publish
    โ””โ”€โ”€ retry_count >= 10  ํฌ๊ธฐ. ๊ด€๋ฆฌ์ž ์žฌ๋ฐœํ–‰ API ๋กœ๋งŒ ๋˜์‚ด๋ฆด ์ˆ˜ ์žˆ์Œ

OutboxEventRecorder ๊ฐ€ ์—†์œผ๋ฉด ์„œ๋น„์Šค๊ฐ€ 5๋‹จ๊ณ„๋ฅผ ์†์œผ๋กœ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

โ‘  EventEnvelope.of(event)
โ‘ก JsonMapper ๋กœ ์ง๋ ฌํ™”
โ‘ข OutboxMessage.create(...)
โ‘ฃ outboxRepository.save()
โ‘ค applicationEventPublisher.publishEvent(saved)     โ† ๋น ๋œจ๋ฆฌ๋ฉด ์˜ค๋ฅ˜ ์—†์ด 5์ดˆ ์ง€์—ฐ

โ‘ค๋ฅผ ๋น ๋œจ๋ฆฌ๋ฉด ์—๋Ÿฌ๊ฐ€ ์•ˆ ๋‚˜๊ณ  ์ง€์—ฐ๋งŒ ์ƒ๊ฒจ ์•Œ์•„์ฑ„๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.


@Transactional(propagation = MANDATORY) ์ž…๋‹ˆ๋‹ค.

outboxEventRecorder.record(new PlaceUpdatedEvent(placeId));

ํŠธ๋žœ์žญ์…˜ ์—†์ด ๋ถ€๋ฅด๋ฉด ํ˜ธ์ถœ ์‹œ์ ์— ์ฆ‰์‹œ ์˜ˆ์™ธ๊ฐ€ ๋‚ฉ๋‹ˆ๋‹ค.

IllegalTransactionStateException: No existing transaction found

Outbox ์˜ ์ „์ œ(๋น„์ฆˆ๋‹ˆ์Šค ๋ฐ์ดํ„ฐ์™€ ์ด๋ฒคํŠธ๊ฐ€ ์›์ž์ )๋ฅผ ๊ทœ์•ฝ์ด ์•„๋‹ˆ๋ผ ๊ตฌ์กฐ๋กœ ๊ฐ•์ œํ•˜๋Š” ์ž๋ฆฌ์ž…๋‹ˆ๋‹ค.


OutboxRelay ๋Š” ํ•œ ์ธ์Šคํ„ด์Šค์—์„œ๋งŒ ์ผญ๋‹ˆ๋‹ค.

app:
  outbox:
    relay:
      enabled: true      # ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๋Š” ์„œ๋น„์Šค๋งŒ. ๋‹ค์ธ์Šคํ„ด์Šค๋ฉด ํ•˜๋‚˜๋งŒ

์—ฌ๋Ÿฌ Relay ๊ฐ€ ๋Œ๋ฉด ์„œ๋กœ "์•ž์— ๋ฏธ๋ฐœํ–‰ ๊ฑด์ด ์—†๋‹ค" ๊ณ  ํŒ๋‹จํ•ด ์ˆœ์„œ ๋ณด์žฅ์ด ๊นจ์ง‘๋‹ˆ๋‹ค.


FOR UPDATE SKIP LOCKED ๋กœ ์ค‘๋ณต ๋ฐœํ–‰์„ ๋ง‰์Šต๋‹ˆ๋‹ค.

๋ฆฌ์Šค๋„ˆ์™€ Relay ๊ฐ€ ๊ฐ™์€ ํ–‰์„ ๋™์‹œ์— ์ง‘์„ ์ˆ˜ ์žˆ์Œ
        โ”‚
        โ””โ”€โ”€ ์‹ค์ œ๋กœ ๊ฒช์—ˆ์Œ โ€” ์ฒซ ๋ฐœํ–‰์—์„œ ์นดํ”„์นด ํ”„๋กœ๋“€์„œ ์ƒ์„ฑ์— 0.6์ดˆ๊ฐ€ ๊ฑธ๋ฆฌ๋Š” ์‚ฌ์ด
            Relay ํด๋ง(5์ดˆ)์ด ๋ผ์–ด๋“ค์–ด ๊ฐ™์€ ์ด๋ฒคํŠธ๊ฐ€ 2๊ฑด ๋‚˜๊ฐ

OutboxPublisher ๊ฐ€ ํ–‰์„ ์ž ๊ทธ๊ณ  ์‹œ์ž‘ํ•˜๋ฏ€๋กœ ๋’ค์—ฃ๊ฒƒ์€ ์กฐํšŒ ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.


์žฌ์‹œ๋„ 10ํšŒ๋ฅผ ๋„˜๊ธฐ๋ฉด Relay ๊ฐ€ ํฌ๊ธฐํ•ฉ๋‹ˆ๋‹ค.

retry_count >= 10
        โ”‚
        โ”œโ”€โ”€ Relay ์กฐํšŒ์—์„œ ๋น ์ง            published_at ์ด NULL ์ธ ์ฑ„๋กœ ์˜์›ํžˆ ๋‚จ์Œ
        โ””โ”€โ”€ ๋˜์‚ด๋ฆด ์ˆ˜๋‹จ์€ ๊ด€๋ฆฌ์ž ์žฌ๋ฐœํ–‰ API ํ•˜๋‚˜๋ฟ
                findGivenUpMessages(Pageable) ๋กœ ๋ชฉ๋ก์„ ๋ฝ‘๊ณ 
                OutboxPublisher.publish(id) ๋ฅผ ์ง์ ‘ ํ˜ธ์ถœ

๊ฐ ์„œ๋น„์Šค๊ฐ€ ๊ทธ API ๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ณตํ†ต ๋ชจ๋“ˆ์€ ์กฐํšŒ ๋ฉ”์„œ๋“œ๊นŒ์ง€๋งŒ ์ค๋‹ˆ๋‹ค. service-template README 7-7 ์— ๋งŒ๋“œ๋Š” ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค.




3-6. message/inbox โ€” ๊ฐ™์€ ์ด๋ฒคํŠธ๋ฅผ ๋‘ ๋ฒˆ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๊ธฐ

์นดํ”„์นด์—์„œ ์ˆ˜์‹ 
  โ”‚
  โ””โ”€โ”€โ–ถ  inboxProcessor.processOnce(eventId, topic, ๋กœ์ง)
              โ”‚
              โ”œโ”€โ”€โ–ถ  existsById(eventId)          ํ”ํ•œ ๊ฒฝ์šฐ๋ฅผ ์‹ธ๊ฒŒ ๋ง‰์Œ
              โ”‚        ์žˆ์Œ โ”€โ”€โ–ถ  ๊ฑด๋„ˆ๋œ€ (์ด๋ฏธ ์ฒ˜๋ฆฌ)
              โ”‚
              โ”œโ”€โ”€โ–ถ  ProcessedEvent ์ €์žฅ          PK ์ถฉ๋Œ์ด ๋“œ๋ฌธ ๊ฒฝ์Ÿ์„ ํ™•์‹คํžˆ ๋ง‰์Œ
              โ”‚        ์ถฉ๋Œ โ”€โ”€โ–ถ  ์˜ˆ์™ธ โ†’ ์žฌ์‹œ๋„์—์„œ ๊ฑด๋„ˆ๋œ€
              โ”‚
              โ””โ”€โ”€โ–ถ  ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์‹คํ–‰            *๊ฐ™์€ ํŠธ๋žœ์žญ์…˜

  ๋‘˜์„ ํ•œ ํŠธ๋žœ์žญ์…˜์— ๋ฌถ๋Š” ์ด์œ 
    ๋”ฐ๋กœ ์ปค๋ฐ‹ํ•˜๋ฉด   "๋กœ์ง ์„ฑ๊ณต + ๊ธฐ๋ก ์‹คํŒจ"  โ†’  ์ค‘๋ณต ์ฒ˜๋ฆฌ
                   "๊ธฐ๋ก ์„ฑ๊ณต + ๋กœ์ง ์‹คํŒจ"  โ†’  ์˜์˜ ๊ฑด๋„ˆ๋œ€

ProcessedEvent ๊ฐ€ Persistable ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

@Override public boolean isNew() { return true; }

ID ๊ฐ€ ๋ฐœํ–‰์ž์—๊ฒŒ์„œ ์˜จ ๊ฐ’์ด๋ผ save() ์‹œ์ ์— null ์ด ์•„๋‹ˆ๊ณ , ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ๊ฐ€ isNew() = false ๋กœ ํŒ๋‹จํ•ด persist() ๊ฐ€ ์•„๋‹ˆ๋ผ merge() ๋ฅผ ๋ถ€๋ฆ…๋‹ˆ๋‹ค.

merge ๋Š” SELECT ํ›„ ํ–‰์ด ์žˆ์œผ๋ฉด UPDATE
        โ”‚
        โ””โ”€โ”€ PK ์ถฉ๋Œ์ด ์•ˆ ๋‚˜๊ณ  ์กฐ์šฉํžˆ UPDATE โ†’ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ์ค‘๋ณต ์‹คํ–‰
              "PK ๊ฐ€ ๋“œ๋ฌธ ๊ฒฝ์Ÿ์„ ํ™•์‹คํžˆ ๋ง‰๋Š”๋‹ค" ๋Š” ์„ค๊ณ„ ๊ทผ๊ฑฐ๊ฐ€ ๋ฌด๋„ˆ์ง

์˜ˆ์™ธ๋ฅผ ์žก์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ ๋˜์ง‘๋‹ˆ๋‹ค.

์ปจ์Šˆ๋จธ ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€์•ผ DefaultErrorHandler ๊ฐ€ ๋™์ž‘
        1์ดˆ โ†’ 2์ดˆ โ†’ 4์ดˆ ๋กœ 3ํšŒ ์žฌ์‹œ๋„
        ๊ทธ๋ž˜๋„ ์‹คํŒจํ•˜๋ฉด {์›๋ณธํ† ํ”ฝ}.dlq ๋กœ ๋ณด๋‚ด๊ณ  ๋„˜์–ด๊ฐ

try-catch ๋กœ ์‚ผํ‚ค๋ฉด ์‹คํŒจํ•œ ์ด๋ฒคํŠธ๊ฐ€ ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์–ด ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.


processed_event ๋Š” ๋ฌดํ•œํžˆ ์Œ“์ž…๋‹ˆ๋‹ค.

์ง€์šฐ๋Š” ๋กœ์ง์ด ์—†์Šต๋‹ˆ๋‹ค. ์ง€๊ธˆ ๊ทœ๋ชจ์—์„œ๋Š” ๋ฌด๋ฐฉํ•˜๊ณ  (processed_at) ์ธ๋ฑ์Šค๋ฅผ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด ๋‘์—ˆ์Šต๋‹ˆ๋‹ค.




3-7. message โ€” ๋ด‰ํˆฌ์™€ ๊ณ„์•ฝ

DomainEvent ๋Š” ๋ฉ”์„œ๋“œ ์…‹์ž…๋‹ˆ๋‹ค.

public interface DomainEvent {
    @JsonIgnore String getTopic();
    @JsonIgnore String getAggregateType();
    @JsonIgnore String getAggregateId();
}

@JsonIgnore ๋ฅผ ์ธํ„ฐํŽ˜์ด์Šค ์„ ์–ธ์— ๋ถ™์˜€์Šต๋‹ˆ๋‹ค.

์•ˆ ๋ถ™์ด๋ฉด
    ์ž๋ฐ”๋นˆ ๊ฒŒํ„ฐ๋ผ Jackson ์ด data ์•ˆ์—๋„ ์ง๋ ฌํ™”ํ•จ
        โ”‚
        โ””โ”€โ”€ ํ™•์ • payload ๋ช…์„ธ์— ์—†๋Š” ํ•„๋“œ 3๊ฐœ๊ฐ€ ๋“ค์–ด๊ฐ€๊ณ 
            ์†Œ๋น„์ž ์—ญ์ง๋ ฌํ™”๊ฐ€ ํ„ฐ์งˆ ์ˆ˜ ์žˆ์Œ

์• ๋…ธํ…Œ์ด์…˜์ด ๊ตฌํ˜„์ฒด์— ์ƒ์†๋˜๋Š” ๊ฒƒ์€ auth ์ฒซ ํšŒ์›๊ฐ€์ž…์—์„œ ์‹ค๋ฌผ๋กœ ํ™•์ธ๋์Šต๋‹ˆ๋‹ค.


EventEnvelope ์˜ ๊ตฌ์„ฑ์ž…๋‹ˆ๋‹ค.

{
  "eventId": "01a0...",          ์ค‘๋ณต ํŒ๋‹จ ํ‚ค (UUID v7)
  "eventType": "place.updated",
  "occurredAt": "2026-09-01T14:22:10",
  "aggregateType": "Place",
  "aggregateId": "01a0...",      ํŒŒํ‹ฐ์…˜ ํ‚ค
  "data": { }                    *๊ฐ ์„œ๋น„์Šค๊ฐ€ ๋”ฐ๋กœ ์ •์˜
}

data ์•ˆ์ชฝ DTO ๋Š” ๊ณตํ†ต ๋ชจ๋“ˆ์— ๋‘์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‘๋ฉด ๋ฐœํ–‰ํ•˜๋Š” ์ชฝ์ด ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ ๋ฐ›๋Š” ์ชฝ๊นŒ์ง€ ๋‹ค์‹œ ๋ฐฐํฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


๋ฐ›๋Š” ์ชฝ์€ ์ž๊ธฐ DTO ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

@JsonIgnoreProperties(ignoreUnknown = true)       // ํ•„์ˆ˜
public record PlaceUpdatedMessage(UUID placeId) { }

์ œ๋„ค๋ฆญ์— ํƒ€์ž… ์ œ์•ฝ์ด ์—†์–ด ๋ฐ›๋Š” ์ชฝ DTO ๋Š” DomainEvent ๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค.


RecordMessageConverter ๊ฐ€ ๋ด‰ํˆฌ๋ฅผ ํƒ€์ž… ๊ทธ๋Œ€๋กœ ๋ฐ›๊ฒŒ ํ•ด ์ค๋‹ˆ๋‹ค.

@KafkaListener(topics = "place.updated", groupId = "${spring.application.name}")
public void onPlaceUpdated(EventEnvelope<PlaceUpdatedMessage> envelope) { ... }

์—†์œผ๋ฉด value-deserializer ๊ฐ€ StringDeserializer ๋ผ ๋ฆฌ์Šค๋„ˆ์— String ์ด ๋“ค์–ด์™€ MessageConversionException ์ด ๋‚ฉ๋‹ˆ๋‹ค.

๋ฐœํ–‰ ์ชฝ๊ณผ ๊ฐ™์€ JsonMapper ๋ฅผ ๋„˜๊น๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๋งคํผ๋ฉด ๋ด‰ํˆฌ์˜ LocalDateTime occurredAt ํ‘œํ˜„์ด ๋‚˜๊ฐˆ ๋•Œ์™€ ๋“ค์–ด์˜ฌ ๋•Œ ์–ด๊ธ‹๋‚  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


AuthContextHeaders ๊ฐ€ ํ—ค๋” ์ด๋ฆ„์˜ ๋‹จ์ผ ์ถœ์ฒ˜์ž…๋‹ˆ๋‹ค.

X-User-Id ยท X-User-Role
        โ”‚
        โ”œโ”€โ”€ HeaderAuthenticationFilter        HTTP ๋กœ ๋ฐ›์„ ๋•Œ
        โ”œโ”€โ”€ RestClientAuthInterceptor         ์„œ๋น„์Šค๋ฅผ ๋ถ€๋ฅผ ๋•Œ (๋ฏธ๋ฐฐ์„ )
        โ”œโ”€โ”€ KafkaSecurityInterceptor          ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์„ ๋•Œ
        โ””โ”€โ”€ โš  ๊ฒŒ์ดํŠธ์›จ์ด๊ฐ€ ์ž๊ธฐ ์ €์žฅ์†Œ์— ๋”ฐ๋กœ ์ ์Œ   โ† 4๋ฒˆ์งธ ์‚ฌ๋ณธ. common ์„ ์•ˆ ์”€

์–ด๊ธ‹๋‚˜๋ฉด ํ•„ํ„ฐ๊ฐ€ ํ—ค๋”๋ฅผ ๋ชป ์ฐพ์•„ ์ธ์ฆ ์—†์ด ํ†ต๊ณผ์‹œํ‚ค๊ณ , ๊ทธ ๋’ค ๊ฒฝ๋กœ ๊ทœ์น™์—์„œ ๋ง‰ํ˜€ 401 ๋กœ๋งŒ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.




4. ์“ฐ๋Š” ๋ฒ•

์ž์ฃผ ์“ฐ๋Š” ์—ฌ์„ฏ ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค. ์„œ๋น„์Šค ์ฝ”๋“œ์—์„œ ์‹ค์ œ๋กœ ์ด๋ ‡๊ฒŒ ์”๋‹ˆ๋‹ค.




4-1. ์‘๋‹ต ๊ฐ์‹ธ๊ธฐ

@GetMapping("/{placeId}")
public CommonApiResponse<PlaceOutput> getPlace(@PathVariable UUID placeId) {
    return CommonApiResponse.success(placeService.getPlace(placeId));
}

๋ชฉ๋ก์€ PageResponse ๋ฅผ ํ•จ๊ป˜ ์”๋‹ˆ๋‹ค.

@GetMapping
public CommonApiResponse<PageResponse<PlaceOutput>> getPlaces(Pageable pageable) {
    Page<Place> page = placeService.search(pageable);
    return CommonApiResponse.success(PageResponse.from(page, PlaceOutput::from));
}

์ƒํƒœ ์ฝ”๋“œ๋ฅผ ๋ฐ”๊พธ๋ ค๋ฉด ResponseEntity ๋กœ ๊ฐ์Œ‰๋‹ˆ๋‹ค.

return ResponseEntity.status(HttpStatus.CREATED)
        .body(CommonApiResponse.success(output));

traceId ๋Š” ์‹ ๊ฒฝ ์“ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‘๋‹ต ์ง์ „์— ์ž๋™์œผ๋กœ ์ฑ„์›Œ์ง‘๋‹ˆ๋‹ค.

๋ฐ˜ํ™˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด CommonApiResponse.<Void>success(null) ์ž…๋‹ˆ๋‹ค. ๋ฌด์ธ์ž ๋ฒ„์ „์€ ๋งŒ๋“ค์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.




4-2. ์—๋Ÿฌ ์ฝ”๋“œ ๋งŒ๋“ค๊ณ  ๋˜์ง€๊ธฐ

์„œ๋น„์Šค๋งˆ๋‹ค ์ž๊ธฐ enum ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

package com.pawtrail.place.domain.exception;

import com.pawtrail.common.exception.ErrorCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@Getter
@RequiredArgsConstructor
public enum PlaceErrorCode implements ErrorCode {

    PLACE_NOT_FOUND(HttpStatus.NOT_FOUND, "์žฅ์†Œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."),
    PLACE_ALREADY_CLOSED(HttpStatus.CONFLICT, "์ด๋ฏธ ํ์—… ์ฒ˜๋ฆฌ๋œ ์žฅ์†Œ์ž…๋‹ˆ๋‹ค.");

    private final HttpStatus httpStatus;
    private final String message;

    // ์ƒ์ˆ˜ ์ด๋ฆ„์ด ๊ทธ๋Œ€๋กœ ์‘๋‹ต code ๊ฐ€ ๋จ
    // ๋‹ค๋ฅธ ๋ฌธ์ž์—ด์„ ๋Œ๋ ค์ฃผ๋ฉด ํ”„๋ก ํŠธ๊ฐ€ ๋ณด๋Š” ๊ฐ’๊ณผ ์ฝ”๋“œ์— ์ ํžŒ ์ด๋ฆ„์ด ๊ฐˆ๋ฆผ
    @Override
    public String getCode() {
        return name();
    }
}

๋˜์งˆ ๋•Œ๋Š” CustomException ํ•˜๋‚˜๋งŒ ์”๋‹ˆ๋‹ค.

Place place = placeRepository.findById(placeId)
        .orElseThrow(() -> new CustomException(PlaceErrorCode.PLACE_NOT_FOUND));

์ปจํŠธ๋กค๋Ÿฌ์— try-catch ๋ฅผ ์“ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. GlobalExceptionHandler ๊ฐ€ ์žก์Šต๋‹ˆ๋‹ค.

์˜ˆ์™ธ ํด๋ž˜์Šค๋ฅผ ์ƒˆ๋กœ ๋งŒ๋“ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ƒํƒœ์™€ ๋ฉ”์‹œ์ง€๊ฐ€ ๋‘ ๊ณณ์— ์ƒ๊น๋‹ˆ๋‹ค.


์–ธ์ œ ๊ณตํ†ต ์ฝ”๋“œ๋ฅผ ์“ฐ๋‚˜

์“ฐ๋Š” ๊ฒƒ
์–ด๋А ์„œ๋น„์Šค์—์„œ๋‚˜ ๊ฐ™์€ ๋œป CommonErrorCode
์ด ๋„๋ฉ”์ธ๋งŒ์˜ ์‹คํŒจ ์ž๊ธฐ ErrorCode

RESOURCE_NOT_FOUND ๋กœ ๋ญ‰๋šฑ๊ทธ๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ”„๋ก ํŠธ๊ฐ€ "๋ฌด์—‡์„" ๋ชป ์ฐพ์•˜๋Š”์ง€ ์•Œ์•„์•ผ ํ™”๋ฉด์„ ๋‹ค๋ฅด๊ฒŒ ๋ณด์—ฌ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๊ณ„์•ฝ ์œ„๋ฐ˜์€ CustomException ์ด ์•„๋‹™๋‹ˆ๋‹ค.

// ์šฐ๋ฆฌ ์ฝ”๋“œ๊ฐ€ ์ž˜๋ชป ๋ถ€๋ฅธ ๊ฒƒ โ€” 400 ์ด ์•„๋‹ˆ๋ผ 500 ์ด ๋งž์Œ
if (passwordHash == null) {
    throw new IllegalArgumentException("LOCAL ๊ณ„์ •์€ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค");
}

"์„œ๋น„์Šค์—์„œ ๋˜์งˆ ์œ ์ผํ•œ ์˜ˆ์™ธ๋Š” CustomException" ๊ทœ์น™์€ ๋น„์ฆˆ๋‹ˆ์Šค ์˜ˆ์™ธ๋ฅผ ๋งํ•˜๋Š” ๊ฒƒ์ด๊ณ , ๊ณ„์•ฝ ์œ„๋ฐ˜์€ ์„ฑ๊ฒฉ์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค.




4-3. ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ๊บผ๋‚ด๊ธฐ

@GetMapping
public CommonApiResponse<List<PetOutput>> getMyPets(
        @CurrentUser CustomUserPrincipal principal) {

    return CommonApiResponse.success(petService.findByAccount(principal.accountId()));
}

CustomUserPrincipal ์€ ๋‘˜๋งŒ ๊ฐ€์ง‘๋‹ˆ๋‹ค.

record CustomUserPrincipal(UUID accountId, Role role)

๊ฒŒ์ดํŠธ์›จ์ด๊ฐ€ ํ—ค๋”๋กœ ๋„ฃ์–ด ์ค€ ๊ฐ’์ด๋ผ ํ† ํฐ์„ ํŒŒ์‹ฑํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ์„œ๋น„์Šค์— ์—†์Šต๋‹ˆ๋‹ค.




4-4. ์—”ํ‹ฐํ‹ฐ ๋งŒ๋“ค๊ธฐ

@Entity
@Table(name = "place")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Place extends BaseEntity {

    @Id
    @UuidGenerator(style = UuidGenerator.Style.VERSION_7)
    @Column(columnDefinition = "uuid")
    private UUID id;

    @Column(nullable = false, length = 200)
    private String name;
}

์‚ญ์ œ๋งŒ ์ˆ˜๋™์ž…๋‹ˆ๋‹ค.

place.delete(auditorProvider.current());

auditorProvider.current() ๋ฅผ ์“ฐ๋Š” ์ด์œ ๋Š” ์ƒ์„ฑยท์ˆ˜์ •๊ณผ ๊ฐ™์€ ์ถœ์ฒ˜์˜ ๊ฐ’์„ ๋„ฃ๊ธฐ ์œ„ํ•ด์„œ์ž…๋‹ˆ๋‹ค.

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ์—๋„ 6์ปฌ๋Ÿผ์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ddl-auto: validate ๋ผ ์–ด๊ธ‹๋‚˜๋ฉด ๊ธฐ๋™์ด ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.




4-5. ์ด๋ฒคํŠธ ๋ฐœํ–‰ํ•˜๊ธฐ

์ด๋ฒคํŠธ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

package com.pawtrail.place.domain.event.payload;

import com.pawtrail.common.message.DomainEvent;
import java.util.UUID;

public record PlaceUpdatedEvent(UUID placeId) implements DomainEvent {

    @Override
    public String getTopic() {
        // infra ์˜ create-topics.sh ์— ๊ฐ™์€ ์ด๋ฆ„์ด ์žˆ์–ด์•ผ ํ•จ
        return "place.updated";
    }

    @Override
    public String getAggregateType() {
        return "Place";
    }

    @Override
    public String getAggregateId() {
        return placeId.toString();
    }
}

๋ฐœํ–‰์€ ํ•œ ์ค„์ž…๋‹ˆ๋‹ค.

@Transactional                                    // ๋ฐ˜๋“œ์‹œ ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ
public void updatePlace(UUID placeId, PlaceUpdateInput input) {

    Place place = placeRepository.findById(placeId)
            .orElseThrow(() -> new CustomException(PlaceErrorCode.PLACE_NOT_FOUND));

    place.update(input.name());

    outboxEventRecorder.record(new PlaceUpdatedEvent(placeId));
}

๋‹ด๋Š” ๊ฐ’์„ ์ตœ์†Œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ›๋Š” ์ชฝ์ด /internal ๋กœ ๋‹ค์‹œ ์ฝ์„ ์ˆ˜ ์žˆ๋Š” ๊ฐ’์€ ์‹ฃ์ง€ ์•Š๊ณ , ๊ฐœ์ธ์ •๋ณด๋Š” ๋”์šฑ ์‹ฃ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.




4-6. ์ด๋ฒคํŠธ ๋ฐ›๊ธฐ

์†Œ๋น„ ์ „์šฉ DTO ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

package com.pawtrail.search.infrastructure.message.kafka.consumer.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.UUID;

@JsonIgnoreProperties(ignoreUnknown = true)       // ํ•„์ˆ˜
public record PlaceUpdatedMessage(UUID placeId) { }
๊ทœ์น™ ์™œ
@JsonIgnoreProperties ๋ฅผ ๋ถ™์ž„ ์—†์œผ๋ฉด ๋ฐœํ–‰ ์ชฝ์ด ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ์ˆœ๊ฐ„ ๊นจ์ง€๊ณ  ๋ฐฐํฌ ์ˆœ์„œ๊ฐ€ ๋ฌถ์ž„
DomainEvent ๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์Œ ๋ฐ›๋Š” ์ชฝ์—๋Š” ํ† ํ”ฝยท์ง‘ํ•ฉ์ฒด ๊ฐ’์ด ์˜๋ฏธ๊ฐ€ ์—†์Œ
์‹ค์ œ๋กœ ์“ฐ๋Š” ํ•„๋“œ๋งŒ ๋‚˜๋จธ์ง€๋Š” ๋ฌด์‹œ๋จ

๋ฆฌ์Šค๋„ˆ๋Š” ๋ด‰ํˆฌ๋ฅผ ํƒ€์ž… ๊ทธ๋Œ€๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.

@KafkaListener(topics = "place.updated", groupId = "${spring.application.name}")
public void onPlaceUpdated(EventEnvelope<PlaceUpdatedMessage> envelope) {
    inboxProcessor.processOnce(
            envelope.eventId(),
            envelope.eventType(),
            () -> searchIndexService.reindex(envelope.data().placeId())
    );
}

์ง€์ผœ์•ผ ํ•  ๊ฒƒ ์…‹์ž…๋‹ˆ๋‹ค.

1  ํ† ํ”ฝ ๋ฌธ์ž์—ด์ด ๋ฐœํ–‰ ์ชฝ getTopic() ๊ณผ ์ •ํ™•ํžˆ ๊ฐ™์•„์•ผ ํ•จ
     ์–ด๊ธ‹๋‚˜๋„ ์˜ค๋ฅ˜๊ฐ€ ์•ˆ ๋‚˜๊ณ  ์ด๋ฒคํŠธ๋งŒ ์˜ค์ง€ ์•Š์Œ

2  processOnce ๋กœ ๊ฐ์Œˆ
     ๊ฐ™์€ ์ด๋ฒคํŠธ๋ฅผ ๋‘ ๋ฒˆ ๋ฐ›์•„๋„ ํ•œ ๋ฒˆ๋งŒ ์ฒ˜๋ฆฌ๋จ
     ์žฌ์‹œ๋„ยท๋ฆฌ๋ฐธ๋Ÿฐ์‹ฑ์œผ๋กœ ๋‘ ๋ฒˆ ์˜ค๋Š” ๊ฒƒ์€ ์ •์ƒ์ ์ธ ์ผ

3  ์˜ˆ์™ธ๋ฅผ ์žก์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ ๋˜์ง
     ์ปจ์Šˆ๋จธ ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€์•ผ ์žฌ์‹œ๋„์™€ DLQ ๊ฐ€ ๋™์ž‘
     try-catch ๋กœ ์‚ผํ‚ค๋ฉด ์‹คํŒจํ•œ ์ด๋ฒคํŠธ๊ฐ€ ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์–ด ์‚ฌ๋ผ์ง




4-7. ๋‹ค๋ฅธ ์„œ๋น„์Šค ๋ถ€๋ฅด๊ธฐ

๋นŒ๋”๋ฅผ ์ฃผ์ž…๋ฐ›์•„ RestClient ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. RestClient.builder() ๋ฅผ ์ง์ ‘ ๋ถ€๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์ธ์ฆ ํ—ค๋”๋„ lb:// ํ•ด์„๋„ ์‹œ๊ฐ„ ์ œํ•œ๋„ ๋ถ™์ง€ ์•Š์Šต๋‹ˆ๋‹ค.


๋นŒ๋”๊ฐ€ ๋‘˜์ž…๋‹ˆ๋‹ค. ๋ถ€๋ฅด๋Š” ๋Œ€์ƒ์œผ๋กœ ๊ณ ๋ฆ…๋‹ˆ๋‹ค.

internalRestClientBuilder externalRestClientBuilder
๋ถ€๋ฅด๋Š” ๊ณณ ์šฐ๋ฆฌ ์„œ๋น„์Šค ์นด์นด์˜ค๋งต ยท ๊ธฐ์ƒ์ฒญ ยท ๊ด€๊ด‘๊ณต์‚ฌ
์ฃผ์†Œ lb://place-service https://apis.data.go.kr
์ฃผ์†Œ๋ฅผ ํ‘ธ๋Š” ๊ฒƒ ์œ ๋ ˆ์นด ์„ค์ •์— ๋ฐ•ํžŒ ๊ณ ์ • ์ฃผ์†Œ
์ธ์ฆ ํ—ค๋” X-User-Id ยท X-User-Role ์„ ์‹ค์Œ ์‹ฃ์ง€ ์•Š์Œ

์…‹์งธ ๋นˆ defaultRestClientBuilder ๋Š” ์šฐ๋ฆฌ ์ฝ”๋“œ๊ฐ€ ์“ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์œ ๋ ˆ์นด์ฒ˜๋Ÿผ ํƒ€์ž…์œผ๋กœ ์ฐพ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์œ„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด์œ ๋Š” 2-4 ์— ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐ”๊นฅ ์‹œ์Šคํ…œ์— ์šฐ๋ฆฌ ์‚ฌ์šฉ์ž ์‹๋ณ„์ž๋ฅผ ๋ณด๋‚ผ ์ด์œ ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๋นŒ๋”๊ฐ€ ๋‘˜์ž…๋‹ˆ๋‹ค. @LoadBalanced ๊ฐ€ ๋ถ™์€ ๋นŒ๋”๋Š” https://... ๋ฅผ ์„œ๋น„์Šค ์ด๋ฆ„์œผ๋กœ ๋ณด๊ณ  ์œ ๋ ˆ์นด์—์„œ ์ฐพ์œผ๋ ค๋‹ค ์‹คํŒจํ•˜๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค.


@Qualifier ๋ฅผ ๋ฐ˜๋“œ์‹œ ๋ถ™์ž…๋‹ˆ๋‹ค.

@Component
public class PolicyProviderImpl implements PolicyProvider {

    private final RestClient restClient;

    public PolicyProviderImpl(
            @Qualifier("internalRestClientBuilder") RestClient.Builder builder) {

        // ๋ถ€๋ฅผ ์„œ๋น„์Šค์˜ ์ด๋ฆ„์„ ์—ฌ๊ธฐ์„œ ํ•œ ๋ฒˆ๋งŒ ๋ฐ•์•„ ๋‘ 
        this.restClient = builder.baseUrl("lb://policy-service").build();
    }
}

@RequiredArgsConstructor ๋ฅผ ์“ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋กฌ๋ณต์ด ๋งŒ๋“œ๋Š” ์ƒ์„ฑ์ž์—๋Š” @Qualifier ๊ฐ€ ๋ถ™์ง€ ์•Š์•„ ์–ด๋А ๋นŒ๋”๊ฐ€ ๋“ค์–ด์˜ฌ์ง€ ์ •ํ•ด์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ƒ์„ฑ์ž๋ฅผ ์†์œผ๋กœ ์”๋‹ˆ๋‹ค.

@Qualifier ๋ฅผ ๋น ๋œจ๋ฆฌ๋ฉด @Primary ์ธ ๋งจ ๋นŒ๋”๊ฐ€ ์กฐ์šฉํžˆ ์ฃผ์ž…๋ฉ๋‹ˆ๋‹ค. ๊ทธ ๋นŒ๋”์—๋Š” ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ ์ธํ„ฐ์…‰ํ„ฐ๊ฐ€ ์—†์–ด lb:// ๋ฅผ ํ’€์ง€ ๋ชปํ•˜๊ณ , ๊ธฐ๋™์ด ์•„๋‹ˆ๋ผ ์‹ค์ œ๋กœ ํ˜ธ์ถœํ•˜๋Š” ์ˆœ๊ฐ„์— ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. ๋Šฆ๊ฒŒ ๋“œ๋Ÿฌ๋‚˜๋Š” ๊ฒƒ์„ ์•Œ๊ณ  ๋ฐ›์•„๋“ค์ธ ๊ฒƒ์ด๋ผ ์—ฌ๊ธฐ๋ฅผ ์“ธ ๋•Œ ๋ฐ˜๋“œ์‹œ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.


๋นŒ๋”๋Š” ์ฃผ์ž…๋ฐ›์„ ๋•Œ๋งˆ๋‹ค ์ƒˆ๋กœ ๋งŒ๋“ค์–ด์ง‘๋‹ˆ๋‹ค.

ํ”„๋กœํ† ํƒ€์ž… ๋นˆ์ด๋ผ ์ฃผ์ž… ์ง€์ ๋งˆ๋‹ค ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค๊ฐ€ ์˜ด
        โ”‚
        โ””โ”€โ”€ PlaceProvider ๊ฐ€ baseUrl ์„ ๊ฑธ์–ด๋„
              VerdictProvider ์˜ ๋นŒ๋”๋Š” ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š์Œ

RestClient.Builder ๋Š” ์ž๊ธฐ๋ฅผ ๊ณ ์น˜๊ณ  ์ž๊ธฐ๋ฅผ ๋Œ๋ ค์ฃผ๋Š” ๋ฌผ๊ฑด์ด๋ผ ์‹ฑ๊ธ€ํ„ด์ด์—ˆ๋‹ค๋ฉด ํ•œ์ชฝ์ด ๊ฑด baseUrl ์ด ๋‹ค๋ฅธ ์ชฝ๊นŒ์ง€ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค. ์Šคํ”„๋ง ๋ถ€ํŠธ๊ฐ€ ์ž๊ธฐ RestClient.Builder ๋นˆ์„ ํ”„๋กœํ† ํƒ€์ž…์œผ๋กœ ๋‘๋Š” ์ด์œ ๋„ ๊ฐ™์Šต๋‹ˆ๋‹ค.


์‹คํŒจํ–ˆ์„ ๋•Œ์˜ ์ฒ˜๋ฆฌ๋Š” ๊ณตํ†ต ๋ชจ๋“ˆ์ด ์ •ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๊ฐ™์€ ์„œ๋น„์Šค ์•ˆ์—์„œ๋„ API ๋งˆ๋‹ค ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

GET /users/me ์˜ ํ›„๊ธฐ ์ˆ˜      ์‹คํŒจํ•˜๋ฉด ๊ทธ ๊ฐ’๋งŒ null ๋กœ ๋‘๊ณ  ๋‚˜๋จธ์ง€๋ฅผ ๋‚ด๋ ค๋ณด๋ƒ„
POST /visits ์˜ ํŒ์ •          ์‹คํŒจํ•˜๋ฉด ์š”์ฒญ ์ž์ฒด๋ฅผ ์‹คํŒจ์‹œํ‚ด
                              ํŒ์ • ์Šค๋ƒ…์ƒท์€ ๋‚˜์ค‘์— ๊ณ ์น  ์ˆ˜๋‹จ์ด ์—†์–ด
                              ํ‹€๋ฆฐ ๊ฐ’์„ ๋‚จ๊ธฐ๋ฉด ์•ˆ ๋จ

์‹œ๊ฐ„ ์ œํ•œ์€ config ์ €์žฅ์†Œ 1๊ณ„์ธต์— ์žˆ์Šต๋‹ˆ๋‹ค.

app:
  rest-client:
    connect-timeout: 2s
    read-timeout: 5s

์—†์–ด๋„ ์„œ๋น„์Šค๋Š” ๋œน๋‹ˆ๋‹ค. ๊ณตํ†ต ๋ชจ๋“ˆ์— ๊ฐ™์€ ๊ฐ’์ด ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋“ค์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์„œ๋น„์Šค๋งˆ๋‹ค ๋‹ฌ๋ผ์•ผ ํ•˜๋ฉด 2๊ณ„์ธต์—์„œ ๋ฎ์–ด์”๋‹ˆ๋‹ค. place ๋Š” ๋‹จ์ˆœ ์กฐํšŒ๋ผ ์งง์•„๋„ ๋˜์ง€๋งŒ LLM ์„ ๋ถ€๋ฅด๋Š” ์ž๋ฆฌ๋Š” ์ˆ˜์‹ญ ์ดˆ๊ฐ€ ๊ฑธ๋ฆฝ๋‹ˆ๋‹ค.




5. ๋ฒ„์ „์„ ์˜ฌ๋ฆฌ๊ณ  ๋ฐฐํฌํ•˜๊ธฐ

์ด ๋ชจ๋“ˆ์„ ๊ณ ์น  ๋•Œ๋งŒ ๋ด…๋‹ˆ๋‹ค. ์ž์ฃผ ์žˆ๋Š” ์ผ์ด ์•„๋‹™๋‹ˆ๋‹ค.

โ‘  ์ฝ”๋“œ๋ฅผ ๊ณ ์น˜๊ณ  build.gradle ์˜ version ์„ ์˜ฌ๋ฆผ
        โ”‚
        โ–ผ
โ‘ก ./gradlew publishToMavenLocal              *๋กœ์ปฌ์— ๋จผ์ € ๋„ฃ์Œ
        โ”‚
        โ–ผ
โ‘ข ์†Œ๋น„ ์„œ๋น„์Šค์—์„œ ๊ฒ€์ฆ
        build.gradle ์˜ repositories ๋งจ ์•ž์— mavenLocal() ์ž„์‹œ ์ถ”๊ฐ€
        gradle.properties ์˜ commonVersion ์„ ์ƒˆ ๋ฒ„์ „์œผ๋กœ
        ./gradlew build ๊ฐ€ ํ†ต๊ณผํ•˜๋Š”์ง€
        โ”‚
        โ–ผ
โ‘ฃ ./gradlew publish                          GitHub Packages ๋กœ
        โ”‚
        โ–ผ
โ‘ค mavenLocal() ์„ ์ง€์šฐ๊ณ  ๋‹ค์‹œ ๋นŒ๋“œ            *์ˆœ์„œ ์ฃผ์˜
        โ”‚
        โ–ผ
โ‘ฅ ๋ฒ„์ „ ํ‘œ๊ธฐ๋ฅผ ์—ฌ๋Ÿฌ ๊ณณ์— ๋ฐ˜์˜




5-1. โ‘กโ‘ข ์„ ๊ฑด๋„ˆ๋›ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค

GitHub Packages ๋Š” ๊ฐ™์€ ๋ฒ„์ „์„ ๋ฎ์–ด์“ธ ์ˆ˜ ์—†์Œ
        โ”‚
        โ””โ”€โ”€ publish ํ•œ ๋ฒˆ์ด ๋ฒ„์ „ ํ•˜๋‚˜๋ฅผ ์˜๊ตฌํžˆ ์†Œ๋ชจํ•จ

๋กœ์ปฌ ์ €์žฅ์†Œ๋Š” ๋ช‡ ๋ฒˆ์ด๋“  ๋‹ค์‹œ ๋„ฃ์„ ์ˆ˜ ์žˆ์Œ
        โ”‚
        โ””โ”€โ”€ ์ž˜๋ชป๋œ ๊ฒƒ์„ ์˜ฌ๋ ค ๋ฒ„์ „์„ ๋ฒ„๋ฆฌ๋Š” ๋Œ€์‹  ์—ฌ๊ธฐ์„œ ๋จผ์ € ํ™•์ธ

mavenLocal() ์€ ๋ฐ›์•„ ์“ฐ๋Š” ์ชฝ์— ๋„ฃ์Šต๋‹ˆ๋‹ค.

// auth-service/build.gradle
repositories {
    mavenLocal()        // โ† ์ž„์‹œ. publish ๋’ค์— ์ง€์›€
    mavenCentral()
    maven { ... }
}

common ์— ๋„ฃ์œผ๋ฉด ์•„๋ฌด ํšจ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ž๊ธฐ๊ฐ€ ์ž๊ธฐ๋ฅผ ์•ˆ ๋‚ด๋ ค๋ฐ›๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. 2026.9.1 ์— ์‹ค์ œ๋กœ ํ—ท๊ฐˆ๋ฆฐ ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.


โ‘ค ์˜ ์ˆœ์„œ๋ฅผ ์ง€ํ‚ต๋‹ˆ๋‹ค.

mavenLocal() ์„ ๋จผ์ € ์ง€์›€
        โ”‚
        โ””โ”€โ”€ ghcr ์— ์•„์ง ์—†๋Š” ๋ฒ„์ „์„ ๋ชป ์ฐพ์•„ ๋นŒ๋“œ๊ฐ€ ๊นจ์ง
              Could not find com.pawtrail:common:0.0.10

publish ๊ฐ€ ๋๋‚œ ๋’ค์— ์ง€์›๋‹ˆ๋‹ค.


์†Œ๋น„ ์ชฝ commonVersion ๋„ ํ•จ๊ป˜ ์˜ฌ๋ ค์•ผ ๊ฒ€์ฆ์ด ๋ฉ๋‹ˆ๋‹ค.

์˜› ๊ฐ’์ด๋ฉด ghcr ์—์„œ ์˜› ๋ฒ„์ „์„ ๋ฐ›์•„ ๊ทธ๋ƒฅ ํ†ต๊ณผํ•ฉ๋‹ˆ๋‹ค.




5-2. publish ํ•  ๋•Œ ๊ฑธ๋ฆฌ๋Š” ๊ฒƒ

ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ํ„ฐ๋ฏธ๋„ ์„ธ์…˜๋งˆ๋‹ค ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.

$env:GPR_USER  = "<GitHub ์•„์ด๋””>"
$env:GPR_TOKEN = (Get-Content C:\Tour_Prj\GPR_TOKEN.txt).Trim()
./gradlew publish
export GPR_USER=<GitHub ์•„์ด๋””>
export GPR_TOKEN=$(cat ~/GPR_TOKEN.txt | tr -d '\n')
./gradlew publish
๊ฑธ๋ฆฌ๋Š” ๊ฒƒ ์ฆ์ƒ
์ƒˆ ํ„ฐ๋ฏธ๋„์—์„œ ์‹คํ–‰ 401 Unauthorized
.Trim() ์„ ์•ˆ ํ•จ ํŒŒ์ผ ๋ ๊ฐœํ–‰์ด ๊ฐ’์— ์„ž์—ฌ ๋˜ 401
ํ† ํฐ์— write:packages ๊ฐ€ ์—†์Œ 401

๋งค๋ฒˆ ๋„ฃ๊ธฐ ์„ฑ๊ฐ€์‹œ๋ฉด OS ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๋‘ก๋‹ˆ๋‹ค. ํ„ฐ๋ฏธ๋„ ์žฌ์‹œ์ž‘์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.




5-3. โ‘ฅ ๋ฒ„์ „ ํ‘œ๊ธฐ๋ฅผ ๋ฐ˜์˜ํ•  ๊ณณ

service-template/gradle.properties        commonVersion
service-template/README.md                1-4-2 ยท 7-1 ยท 6-4 ํ‘œ      ์„ธ ๊ณณ
<์ด๋ฏธ ๋งŒ๋“  ์„œ๋น„์Šค>/gradle.properties        ํ…œํ”Œ๋ฆฟ์€ ๋ณต์‚ฌ ํ›„ ์—ฐ๊ฒฐ์ด ๋Š๊ธฐ๋ฏ€๋กœ ๊ฐ๊ฐ

โš  ์‹ค์ œ๋กœ ๋น ๋œจ๋ฆฐ ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. 0.0.7 ๋กœ ์˜ฌ๋ฆด ๋•Œ README ๋ฅผ ํ•œ ๊ณณ๋งŒ ๊ณ ์ณ ๋‚˜๋จธ์ง€ ๋‘˜์ด 0.0.4 ๋กœ ๋‚จ์•„ ์žˆ์—ˆ๊ณ  0.0.8 ์—์„œ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค.


์„œ๋น„์Šค๊ฐ€ ๋ฒ„์ „์„ ์•ˆ ์˜ฌ๋ฆฌ๋ฉด ์˜›๊ฒƒ์œผ๋กœ ๊ณ„์† ๋นŒ๋“œ๋ฉ๋‹ˆ๋‹ค.

์ปดํŒŒ์ผ์€ ์ •์ƒ์ ์œผ๋กœ ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์•Œ์•„์ฑ„๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ๊ณตํ†ต ๋ชจ๋“ˆ์ด ๋ฐ”๋€Œ์—ˆ๋‹ค๋Š” ๊ณต์ง€๋ฅผ ๋ฐ›์œผ๋ฉด ์ด ๊ฐ’๋ถ€ํ„ฐ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.




5-4. jar ์•ˆ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค

jar tf build/libs/common-0.0.13.jar | grep -E "AutoConfiguration.imports|db/migration|Common.*AutoConfiguration.class"

๋“ค์–ด๊ฐ€์•ผ ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com/pawtrail/common/config/Common*AutoConfiguration.class          6๊ฐœ
db/migration/common/V1__outbox.sql
db/migration/common/V2__inbox.sql
com/pawtrail/common/entity/QBaseEntity.class                       *QueryDSL ์ƒ์„ฑ๋ฌผ

imports ํŒŒ์ผ์ด ์—†์œผ๋ฉด ์ž๋™ ์„ค์ •์ด ํ•˜๋‚˜๋„ ์•ˆ ์ผœ์ง€๋Š”๋ฐ ์˜ค๋ฅ˜๋„ ์•ˆ ๋‚ฉ๋‹ˆ๋‹ค.

QBaseEntity.class ๋„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ์—†์œผ๋ฉด BaseEntity ๋ฅผ ์ƒ์†ํ•˜๋Š” ์„œ๋น„์Šค์—์„œ ์ปดํŒŒ์ผ์ด ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. 0.0.6 ์—์„œ ์‹ค์ œ๋กœ ๊ฒช์—ˆ์Šต๋‹ˆ๋‹ค.




5-5. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ผ build.gradle ์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค

// ์‹คํ–‰ ๊ฐ€๋Šฅ jar ๋ฅผ ๋งŒ๋“ค์ง€ ์•Š์Œ
tasks.named('bootJar') { enabled = false }
tasks.named('jar')     { enabled = true  }

โ›” ๋‹ค๋ฅธ ๋ ˆํฌ์™€ ์ •๋ฐ˜๋Œ€์ž…๋‹ˆ๋‹ค. ์„œ๋น„์Šค ๋ ˆํฌ๋Š” jar ๋ฅผ ๋„๊ณ  bootJar ๋งŒ ์ผญ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ๋ฐ”๊พธ๋ฉด publish ์‚ฐ์ถœ๋ฌผ์ด ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.


์˜์กด์„ฑ์ด compileOnly ์ž…๋‹ˆ๋‹ค.

compileOnly 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly api
์†Œ๋น„ ์„œ๋น„์Šค์— ์ „์ด ์•ˆ ๋จ ๋จ
๋ฌด์ƒํƒœ ์„œ๋น„์Šค JPA ๊ฐ€ ์•ˆ ๋”ธ๋ ค ์˜ด โœ… JPA ๊ฐ€ ๋”ธ๋ ค ์™€ ์กฐ๊ฑด์ด ํ•ญ์ƒ ์ฐธ โ›”

api ๋กœ ํ•˜๋ฉด verdict ์— JPA ๊ฐ€ ๋”ธ๋ ค ์™€ @ConditionalOnClass ๊ฐ€ ์ฐธ์ด ๋˜๊ณ  DataSource ๊ฐ€ ์—†์–ด ๊ธฐ๋™์— ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.




5-6. ํ…Œ์ŠคํŠธ

์ด ์ €์žฅ์†Œ์˜ ํ…Œ์ŠคํŠธ๋Š” ํ•˜๋‚˜๋ฟ์ž…๋‹ˆ๋‹ค. CommonApiResponseTest ์ž…๋‹ˆ๋‹ค.

src/test/java/com/pawtrail/common/response/CommonApiResponseTest.java

์‘๋‹ต ๋ด‰ํˆฌ๋ฅผ ๋‹ค์‹œ ์ฝ์„ ์ˆ˜ ์žˆ๋Š”์ง€๋งŒ ๋ด…๋‹ˆ๋‹ค. ๋งŒ๋“œ๋Š” ๊ธธ์€ ์„œ๋น„์Šค๊ฐ€ ๋งค์ผ ์“ฐ๋ฏ€๋กœ ๊นจ์ง€๋ฉด ๋ฐ”๋กœ ๋“œ๋Ÿฌ๋‚˜์ง€๋งŒ, ์ฝ๋Š” ๊ธธ์€ ์„œ๋น„์Šค ๊ฐ„ ํ˜ธ์ถœ์—์„œ๋งŒ ์“ฐ์—ฌ ๋ง‰ํ˜€ ์žˆ์–ด๋„ ํ•œ์ฐธ ๋ชจ๋ฆ…๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ 0.0.12 ๊นŒ์ง€ ๋ง‰ํ˜€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๋ฌด์—‡์„ ์™œ
์™•๋ณต success() ๋กœ ๋งŒ๋“  ๊ฒƒ์„ ๋‹ค์‹œ ์ฝ์Œ
์„œ๋น„์Šค๊ฐ€ ๋ณด๋‚ด๋Š” ํ˜•ํƒœ ์‹ค์ œ ์‘๋‹ต JSON ๊ทธ๋Œ€๋กœ. traceId ํฌํ•จ
data ๊ฐ€ null ์‹คํŒจ ์‘๋‹ต๋„ ์ฝํ˜€์•ผ ํ•จ
๋ชจ๋ฅด๋Š” ํ•„๋“œ ์•ž์œผ๋กœ ํ•„๋“œ๊ฐ€ ๋Š˜์–ด๋„ ์†Œ๋น„์ž๊ฐ€ ์•ˆ ๊นจ์ ธ์•ผ ํ•จ
๋ชฉ๋ก ์‘๋‹ต CommonApiResponse<PageResponse<T>> โ€” ์ œ๋„ค๋ฆญ 2์ค‘ ์ค‘์ฒฉ
UUID ์‹๋ณ„์ž๊ฐ€ ์ „๋ถ€ UUID ์ž„

JsonMapper ๋ฅผ ์”๋‹ˆ๋‹ค. ObjectMapper ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค.

์ด ํ”„๋กœ์ ํŠธ๋Š” Jackson ์ด ๋‘ ์„ธ๋Œ€ ์„ž์—ฌ ์žˆ์Šต๋‹ˆ๋‹ค.

ํŒจํ‚ค์ง€ ์ด ํ”„๋กœ์ ํŠธ๊ฐ€
์• ๋…ธํ…Œ์ด์…˜ com.fasterxml.jackson.annotation ์”€ (Jackson 3 ๋„ ์ด ํŒจํ‚ค์ง€ ๊ทธ๋Œ€๋กœ)
๋งคํผ (Jackson 3) tools.jackson.databind.json.JsonMapper ์”€
๋งคํผ (Jackson 2) com.fasterxml.jackson.databind.ObjectMapper ํด๋ž˜์ŠคํŒจ์Šค์—” ์žˆ์œผ๋‚˜ ์•ˆ ์”€

ObjectMapper ๋กœ ์จ๋„ ์ปดํŒŒ์ผ์€ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์‹ค์ œ๋กœ ์“ฐ์ง€ ์•Š๋Š” ๋งคํผ๋ฅผ ๊ฒ€์ฆํ•˜๊ฒŒ ๋˜์–ด ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•ด๋„ ์‹ค๋ฌผ์ด ๊นจ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


build.gradle ์— useJUnitPlatform() ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

tasks.named('test') {
    useJUnitPlatform()
}

์—†์œผ๋ฉด ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋‚˜๋„ ๋ชป ์ฐพ๊ณ  ๋นŒ๋“œ๊ฐ€ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๊ฐ€ 0๊ฐœ์˜€์„ ๋•Œ๋Š” ์ด ๋ธ”๋ก์ด ์—†์–ด๋„ ๋“œ๋Ÿฌ๋‚˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.




6. ๋ง‰ํžˆ๊ธฐ ์‰ฌ์šด ์ž๋ฆฌ




6-1. ์ž๋™ ์„ค์ •์ด ์•ˆ ์ผœ์งˆ ๋•Œ

์ฆ์ƒ ์›์ธ
entityManagerFactory ๋นˆ์„ ๋ชป ์ฐพ์Œ ์กฐ๊ฑด ์˜คํŒ. DB ์—†๋Š” ์„œ๋น„์Šค์ธ๋ฐ JPA ์ž๋™ ์„ค์ •์ด ์ผœ์ง
์•„๋ฌด Bean ๋„ ์•ˆ ์˜ฌ๋ผ์˜ค๋Š”๋ฐ ์˜ค๋ฅ˜๋„ ์—†์Œ jar ์— AutoConfiguration.imports ๊ฐ€ ์•ˆ ๋“ค์–ด๊ฐ
์‘๋‹ต์ด ๊ณตํ†ต ํ˜•์‹์ด ์•„๋‹˜ CommonWebAutoConfiguration ์ด ์•ˆ ์ผœ์ง. ์„œ๋ธ”๋ฆฟ ์›น์ธ์ง€ ํ™•์ธ
/api/v1/admin/** ์ด ์—ด๋ ค ์žˆ์Œ ์„œ๋น„์Šค๊ฐ€ ์ž๊ธฐ SecurityFilterChain ์„ ์ •์˜ํ•จ. ๊ทธ ์ค„์„ ์ง์ ‘ ๋„ฃ์–ด์•ผ ํ•จ
์ธ์ฆ์ด ํ•„์š”ํ•œ ๊ฒฝ๋กœ๊ฐ€ ์ „๋ถ€ 401 ๊ฐ™์Œ. HeaderAuthenticationFilter ๋ฅผ ์ง์ ‘ ๋“ฑ๋กํ•ด์•ผ ํ•จ
Using generated security password AuthenticationManager ๋นˆ์ด ์—†์Œ. 0.0.7 ์ดํ›„๋กœ๋Š” ์•ˆ ๋‚˜์™€์•ผ ์ •์ƒ

entityManagerFactory ์˜ค๋ฅ˜๊ฐ€ ๋‚˜๋ฉด

DB ๋ฅผ ์“ฐ์ง€ ์•Š๋Š” ์„œ๋น„์Šค์ธ๊ฐ€
        โ”‚
        โ”œโ”€โ”€ ์˜ˆ  โ†’  build.gradle ์—์„œ ๋ฐ์ดํ„ฐ ๋ธ”๋ก์„ ํ†ต์งธ๋กœ ์ง€์› ๋Š”์ง€
        โ”‚           ์•ฑ ํด๋ž˜์Šค์˜ @EntityScan ยท @EnableJpaRepositories ๋„ ์ง€์› ๋Š”์ง€
        โ”‚           (service-template README 1-5)
        โ”‚
        โ””โ”€โ”€ ์•„๋‹ˆ์˜ค โ†’  spring.datasource ์„ค์ •์ด ๋‚ด๋ ค์˜ค๋Š”์ง€




6-2. ์ด๋ฒคํŠธ๊ฐ€ ์•ˆ ๋‚˜๊ฐˆ ๋•Œ

์ฆ์ƒ ์›์ธ
IllegalTransactionStateException record() ๋ฅผ ํŠธ๋žœ์žญ์…˜ ์—†์ด ๋ถˆ๋ €์Œ
outbox ํ–‰์€ ์žˆ๋Š”๋ฐ ์นดํ”„์นด์— ์—†์Œ ํ† ํ”ฝ์ด ์—†์Œ (์ž๋™ ์ƒ์„ฑ์ด ๊บผ์ ธ ์žˆ์Œ) ยท retry_count ํ™•์ธ
๊ฐ™์€ ์ด๋ฒคํŠธ๊ฐ€ ๋‘ ๋ฒˆ ๋‚˜๊ฐ 0.0.8 ์ด์ „ ๋ฒ„์ „. ๋ฒ„์ „์„ ์˜ฌ๋ฆด ๊ฒƒ
๋ฐœํ–‰์ด 5์ดˆ์”ฉ ์ง€์—ฐ๋จ OutboxCommitListener ๊ฐ€ ์•ˆ ๋Ž. record() ๋ฅผ ์•ˆ ์“ฐ๊ณ  ์†์œผ๋กœ ์ €์žฅํ–ˆ๋Š”์ง€
retry_count ๊ฐ€ 10 ์ธ ํ–‰์ด ๋‚จ์Œ Relay ๊ฐ€ ํฌ๊ธฐํ•จ. ๊ด€๋ฆฌ์ž ์žฌ๋ฐœํ–‰ API ๋กœ๋งŒ ๋˜์‚ด๋ฆผ

์ง์ ‘ ํ™•์ธ

docker compose exec postgres psql -U auth_svc -d auth_db -P pager=off -c \
  "SELECT id, topic, retry_count, last_error, published_at FROM outbox ORDER BY created_at;"




6-3. ์ด๋ฒคํŠธ๋ฅผ ๋ชป ๋ฐ›์„ ๋•Œ

์ฆ์ƒ ์›์ธ
MessageConversionException RecordMessageConverter ๊ฐ€ ์—†์Œ. ์„œ๋น„์Šค๊ฐ€ ์ž๊ธฐ ๊ฒƒ์„ ์ •์˜ํ–ˆ๋Š”์ง€
๋ฆฌ์Šค๋„ˆ๊ฐ€ ์•„์˜ˆ ์•ˆ ๋ถˆ๋ฆผ ํ† ํ”ฝ ์ด๋ฆ„์ด ๋ฐœํ–‰ ์ชฝ๊ณผ ๋‹ค๋ฆ„. ์˜ค๋ฅ˜๊ฐ€ ์•ˆ ๋‚˜๊ณ  ์กฐ์šฉํ•จ
data ๊ฐ€ LinkedHashMap ์†Œ๋น„ DTO ํƒ€์ž…์„ ์•ˆ ์ ์—ˆ์Œ
๋ฐœํ–‰ ์ชฝ์ด ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์ž ๊นจ์ง ์†Œ๋น„ DTO ์— @JsonIgnoreProperties(ignoreUnknown = true) ๊ฐ€ ์—†์Œ
๊ฐ™์€ ์ด๋ฒคํŠธ๊ฐ€ ๋‘ ๋ฒˆ ์ฒ˜๋ฆฌ๋จ processOnce ๋กœ ์•ˆ ๊ฐ์ŒŒ์Œ
์‹คํŒจํ•œ ์ด๋ฒคํŠธ๊ฐ€ ์‚ฌ๋ผ์ง try-catch ๋กœ ์‚ผ์ผฐ์Œ. ์˜ˆ์™ธ๋ฅผ ๊ทธ๋Œ€๋กœ ๋˜์งˆ ๊ฒƒ

RecordMessageConverter ๋ฅผ ์„œ๋น„์Šค๊ฐ€ ์ •์˜ํ•˜๋ฉด ๋‘˜์ด ๋˜์–ด ๋‘˜ ๋‹ค ์ ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Boot ์ด getIfUnique() ๋กœ ์ง‘์–ด๊ฐ€๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.




6-4. ๋นŒ๋“œ๊ฐ€ ์•ˆ ๋  ๋•Œ

์ฆ์ƒ ์›์ธ
Received status code 401 GPR_USER ยท GPR_TOKEN ์ด ์—†๊ฑฐ๋‚˜ ํ‹€๋ฆผ
Could not find com.pawtrail:common:x.y.z ์•„์ง publish ์•ˆ ๋œ ๋ฒ„์ „. ๋˜๋Š” mavenLocal() ์„ ๋จผ์ € ์ง€์›€
QBaseEntity ๋ฅผ ๋ชป ์ฐพ์Œ 0.0.6 ์ด์ „ ๋ฒ„์ „. ๋ฒ„์ „์„ ์˜ฌ๋ฆด ๊ฒƒ
com.fasterxml.jackson.databind.ObjectMapper ๊ฐ€ ์ปดํŒŒ์ผ ์•ˆ ๋จ common ์—๋Š” Jackson 3๋งŒ ์žˆ์Œ. JsonMapper ๋ฅผ ์“ธ ๊ฒƒ

Jackson ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค.

common                Jackson 3 ๋งŒ       tools.jackson.core:jackson-databind
                      starter-webmvc โ†’ starter-jackson ์ด ์ „์ด๋กœ ๋ฐ๋ ค์˜ด

์„œ๋น„์Šค                 Jackson 2 ยท 3 ๋‘˜ ๋‹ค
                      eureka-client ยท config-client ยท resilience4j ยท springdoc ์ด 2๋ฅผ ๋ฐ๋ ค์˜ด

์ฃผ์ž…๋ฐ›์„ ๋นˆ์€ JsonMapper ์ž…๋‹ˆ๋‹ค. Boot 4 ๊ฐ€ ์ž๋™ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ๊ทธ๊ฒƒ์ด๊ณ  @Primary ์ž…๋‹ˆ๋‹ค. ๋Œ€์ฒดํ•˜๋ ค๋ฉด @Bean ObjectMapper ๊ฐ€ ์•„๋‹ˆ๋ผ @Bean JsonMapper ์—ฌ์•ผ ์ž๋™ ์„ค์ •์ด ๋ฌผ๋Ÿฌ๋‚ฉ๋‹ˆ๋‹ค.

Jackson 3 ๋Š” ์–ธ์ฒดํฌ ์˜ˆ์™ธ๋ผ writeValueAsString ์— try-catch ๊ฐ€ ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค. ๋‚ ์งœ๋Š” ๊ธฐ๋ณธ์ด ISO-8601 ๋ฌธ์ž์—ด์ด๋ผ ์šฐ๋ฆฌ ๋ช…์„ธ์™€ ์„ค์ • ์—†์ด ๋งž์Šต๋‹ˆ๋‹ค.




6-5. IntelliJ ๊ฐ€ ๋‚ด๋Š” ์˜คํƒ

Could not autowire. No beans of type '...' found.

common ์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ผ @SpringBootApplication ์ด ์—†์–ด IDE ๊ฐ€ ์ปจํ…์ŠคํŠธ ๋ชจ๋ธ์„ ๋ชป ๋งŒ๋“ญ๋‹ˆ๋‹ค. ๋นŒ๋“œ๋Š” ํ†ต๊ณผํ•ฉ๋‹ˆ๋‹ค.


.properties ํ•œ๊ธ€์ด ๊นจ์งˆ ๋•Œ

Settings โ†’ Editor โ†’ File Encodings
  Default encoding for properties files      UTF-8
  Transparent native-to-ascii conversion     ์ฒดํฌ ํ•ด์ œ

์„ค์ •์„ ๋ฐ”๊พธ๊ธฐ ์ „์— ๊ทธ ํŒŒ์ผ์„ ์ €์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ™”๋ฉด๋งŒ ๊นจ์ง„ ์ƒํƒœ์—์„œ ์ €์žฅํ•˜๋ฉด ์›๋ณธ์ด ๋ฌผ์Œํ‘œ๋กœ ๋ฐ”๋€Œ์–ด ๋ณต๊ตฌ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.




7. ์™œ ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์—ˆ๋‚˜

๊ฐ ํ•ญ๋ชฉ์€ ๋ฌธ์ œ โ†’ ๊ณ ๋ฅธ ๊ฒƒ โ†’ ๋ฒ„๋ฆฐ ๊ฒƒ ์ˆœ์„œ์ž…๋‹ˆ๋‹ค.




7-1. ๋“ฑ๋ก ๋ฐฉ์‹ โ€” ์Šค์บ”์ด ์•„๋‹ˆ๋ผ imports

๊ณ ๋ฆ„ ๋ฒ„๋ฆผ
AutoConfiguration.imports ๋กœ๋งŒ ๋“ฑ๋ก ์†Œ๋น„ ์„œ๋น„์Šค๊ฐ€ scanBasePackages ์— ๋„ฃ๊ธฐ
@ConditionalOnMissingBean ์ด ์‹ ๋ขฐ๋จ ์ผ๋ฐ˜ @Configuration ์€ ์ฒ˜๋ฆฌ ์ˆœ์„œ๊ฐ€ ์•ˆ ๋ณด์žฅ๋ผ ์กฐ๊ฑด์ด ๋’ค์ง‘ํž˜
์•ฑ ํด๋ž˜์Šค์— ์ ์„ ๊ฒƒ์ด ์ค„์–ด๋“ฆ 14๊ฐœ ๋ ˆํฌ๊ฐ€ ์ •ํ™•ํžˆ ์ ์–ด์•ผ ํ•˜๋Š” ๊ตฌ์กฐ

auth ๊ฐ€ ์ž๊ธฐ SecurityFilterChain ์„ ์ •์˜ํ•˜๋Š” ์ž๋ฆฌ๊ฐ€ ์„ค๊ณ„์— ์ด๋ฏธ ์žˆ์—ˆ๊ณ , ๊ทธ๊ฒƒ์ด ์„ฑ๋ฆฝํ•˜๋ ค๋ฉด ์กฐ๊ฑด์ด ์‹ ๋ขฐ๋ผ์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

์–‘์ชฝ์— ๊ฑธ๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค. ์ž๋™ ์„ค์ • ํด๋ž˜์Šค๊ฐ€ ์Šค์บ”์— ์žกํžˆ๋ฉด ์ฒ˜๋ฆฌ ์ˆœ์„œ๊ฐ€ ๊นจ์ ธ ์กฐ๊ฑด ํ‰๊ฐ€๊ฐ€ ๋’ค์ง‘ํžˆ๊ณ  ๊ฐ™์€ @Configuration ์ด ๋‘ ๋ฒˆ ๋“ฑ๋ก๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์Šคํ”„๋ง ๋ถ€ํŠธ๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ๊ธˆํ•˜๋Š” ํ˜•ํƒœ์ž…๋‹ˆ๋‹ค.


๊ทธ๋ž˜์„œ @Component ๋ฅผ ์ „๋ถ€ @Bean ์œผ๋กœ ์˜ฎ๊ฒผ์Šต๋‹ˆ๋‹ค.

imports ๋กœ๋งŒ ๋“ฑ๋กํ•˜๋ฉด
        โ”‚
        โ””โ”€โ”€ @Component ๋Š” ์Šค์บ”์ด ์žก๋Š” ๊ฒƒ์ด๋ผ ๋นˆ์ด ์•ˆ ๋จ
              AuditorProvider ยท GlobalExceptionHandler ยท InboxProcessor ...
                    โ”‚
                    โ””โ”€โ”€ ์ž๋™ ์„ค์ •์˜ @Bean ์œผ๋กœ ์„ ์–ธ

@RestControllerAdvice ๋Š” ๋–ผ๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค. GlobalExceptionHandler ยท TraceIdResponseAdvice ๋Š” ์• ๋…ธํ…Œ์ด์…˜์œผ๋กœ ์ฐพ์•„์ง€๋ฏ€๋กœ @Bean ์œผ๋กœ ๋“ฑ๋กํ•ด๋„ ํด๋ž˜์Šค์˜ ์• ๋…ธํ…Œ์ด์…˜์€ ์œ ์ง€ํ•ด์•ผ ์–ด๋“œ๋ฐ”์ด์Šค๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.




7-2. ์ž๋™ ์„ค์ •์„ ์ผ๊ณฑ์œผ๋กœ ๋‚˜๋ˆˆ ์ด์œ 

์กฐ๊ฑด ์ถ•์ด ์ •ํ™•ํžˆ ์ด ๊ฐœ์ˆ˜๋กœ ๊ฐˆ๋ฆผ

์„œ๋ธ”๋ฆฟ ์›น + webmvc          Web
์„œ๋ธ”๋ฆฟ ์›น + security        Security
JPA                        Jpa
JPA + Kafka                Messaging
Kafka                      Kafka
spring-web                 RestClient
์—†์Œ                        Async

config/ ํด๋”์— ๋ชจ์€ ์ด์œ ์ž…๋‹ˆ๋‹ค.

์ž๋™ ์„ค์ • ํด๋ž˜์Šค๋Š” ์Šค์บ”์— ๊ฑธ๋ฆฌ๋ฉด ์•ˆ ๋˜๋Š” ํŠน์ˆ˜ ๋ถ€๋ฅ˜ ํ•œ ํด๋”์— ๊ฒฉ๋ฆฌํ•˜๋Š” ํŽธ์ด ์•ˆ์ „
AutoConfiguration.imports ์™€ 1:1 ๋กœ ๋Œ€์กฐ๋จ ๋น ๋œจ๋ ธ๋Š”์ง€ ๋ˆˆ์œผ๋กœ ๋ณด์ž„

@EnableAsync ๋งŒ ์กฐ๊ฑด์ด ์—†์Šต๋‹ˆ๋‹ค.

outbox ์กฐ๊ฑด์— ๋ฌถ์—ฌ ์žˆ์œผ๋ฉด "outbox ๊ฐ€ ์ผœ์งˆ ๋•Œ๋งŒ ๋น„๋™๊ธฐ๊ฐ€ ๋˜๋Š”" ๊ฒฐํ•ฉ์ด ์ƒ๊น๋‹ˆ๋‹ค. verdict ์˜ policy + pet ๋ณ‘๋ ฌ ํ˜ธ์ถœ์ฒ˜๋Ÿผ outbox ์™€ ๋ฌด๊ด€ํ•œ @Async ์‚ฌ์šฉ์ฒ˜๊ฐ€ ์„ค๊ณ„์— ์žˆ์Šต๋‹ˆ๋‹ค.

์Šค๋ ˆ๋“œํ’€์€ ์ž์ฒด Executor ๋นˆ์„ ๋งŒ๋“ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Boot ์˜ applicationTaskExecutor + spring.task.execution.pool.* ํ”„๋กœํผํ‹ฐ์— ๋งก๊น๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๊ฐ€ ๋นˆ์„ ์ •์˜ํ•˜๋ฉด TaskExecutionAutoConfiguration ๊ณผ ์ถฉ๋Œํ•  ์—ฌ์ง€๊ฐ€ ์ƒ๊น๋‹ˆ๋‹ค.




7-3. @Async ์™€ @Transactional ์„ ๊ฐ™์€ ๋ฉ”์„œ๋“œ์— ์•ˆ ๋ถ™์ž„

๋‘˜ ๋‹ค ๊ธฐ๋ณธ order ๊ฐ€ LOWEST_PRECEDENCE
        โ”‚
        โ””โ”€โ”€ ํ”„๋ก์‹œ ์ ์šฉ ์ˆœ์„œ๊ฐ€ ๋ณด์žฅ๋˜์ง€ ์•Š์Œ
              @Transactional ์ด ๋ฐ”๊นฅ์ด๋ฉด ํ˜ธ์ถœ ์Šค๋ ˆ๋“œ์—์„œ ํŠธ๋žœ์žญ์…˜์„ ์—ด๊ณ 
              ๋น„๋™๊ธฐ๋กœ ๋„˜์–ด๊ฐ€๋ฉฐ ๋Š๊น€

๋นˆ์„ ๋‘˜๋กœ ์ชผ๊ฐฐ์Šต๋‹ˆ๋‹ค.

OutboxCommitListener    @Async + @TransactionalEventListener    ํŠธ๋žœ์žญ์…˜ ์—†์Œ
        โ”‚
        โ””โ”€โ”€โ–ถ  OutboxPublisher    @Transactional(REQUIRES_NEW)   ๋ฐœํ–‰ + ์ƒํƒœ ๊ฐฑ์‹ 

๋ถ€์ˆ˜ ํšจ๊ณผ โ€” Relay ์™€ Listener ์— ๋‘ ๋ฒŒ ๋ณต์‚ฌ๋ผ ์žˆ๋˜ ๋ฐœํ–‰ ๋กœ์ง์ด ํ•œ ๊ณณ์œผ๋กœ ๋ชจ์˜€์Šต๋‹ˆ๋‹ค.


publish(UUID) ๊ฐ€ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์•„๋‹ˆ๋ผ ID ๋ฅผ ๋ฐ›๋Š” ์ด์œ ์ž…๋‹ˆ๋‹ค.

๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์—์„œ ์˜จ ์—”ํ‹ฐํ‹ฐ๋Š” ์ค€์˜์†์ด๋ผ dirty checking ์ด ์•ˆ ๊ฑธ๋ฆฝ๋‹ˆ๋‹ค. ์ƒˆ ํŠธ๋žœ์žญ์…˜์ด ์ง์ ‘ ์กฐํšŒํ•ด์•ผ save() ์—†์ด UPDATE ๊ฐ€ ๋‚˜๊ฐ‘๋‹ˆ๋‹ค.




7-4. ๋ฌด์—‡์„ ์—ฌ๊ธฐ ๋„ฃ๊ณ  ๋ฌด์—‡์„ ์•ˆ ๋„ฃ๋‚˜

๊ณตํ†ต ๋ชจ๋“ˆ์— ๋„ฃ์„์ง€๋Š” ๋งค๋ฒˆ ํŒ๋‹จํ•ด์•ผ ํ•˜๋Š” ๋ฌผ์Œ์ž…๋‹ˆ๋‹ค. ๋„ฃ์œผ๋ฉด 17๊ฐœ ์„œ๋น„์Šค๊ฐ€ ํ•จ๊ป˜ ์ง€๊ณ  ๊ฐ€๊ณ , ์•ˆ ๋„ฃ์œผ๋ฉด ๊ฐ™์€ ์ฝ”๋“œ๊ฐ€ ์—ฌ๋Ÿฌ ์ €์žฅ์†Œ์— ์ƒ๊น๋‹ˆ๋‹ค.

๊ธฐ์ค€ 3๊ฐœ๋ฅผ ์ „๋ถ€ ํ†ต๊ณผํ•ด์•ผ ๋„ฃ์Šต๋‹ˆ๋‹ค.

๋ฌป๋Š” ๊ฒƒ ๋ชป ๋„˜์œผ๋ฉด ๋ฌด์—‡์ด ์ƒ๊ธฐ๋‚˜
โ‘  ๊ฑฐ์˜ ๋ฐ”๋€Œ์ง€ ์•Š๋Š”๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ์žฌ๋ฐฐํฌ + ์ „ ์„œ๋น„์Šค ๋ฒ„์ „์—…
โ‘ก ์ „ ์„œ๋น„์Šค๊ฐ€ ์“ฐ๋Š”๊ฐ€ ์•ˆ ์“ฐ๋Š” ์„œ๋น„์Šค๊ฐ€ ์ด์œ  ์—†์ด ์ง์„ ์ง‘๋‹ˆ๋‹ค
โ‘ข ์ƒˆ ์˜์กด์„ฑ์„ ๋ฌผ๊ณ  ์˜ค์ง€ ์•Š๋Š”๊ฐ€ ์•ˆ ์“ฐ๋Š” ์„œ๋น„์Šค์—๋„ ๊ทธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋”ธ๋ ค๊ฐ‘๋‹ˆ๋‹ค

์ง€๊ธˆ๊นŒ์ง€์˜ ํŒ์ •์ž…๋‹ˆ๋‹ค.

๋Œ€์ƒ ๊ฒฐ๊ณผ ๊ทผ๊ฑฐ
BaseEntity ยท ErrorCode ยท ์‘๋‹ต ๋ž˜ํผ ยท ๋ณด์•ˆ ์ฒด์ธ ๋„ฃ์Œ ๊ธฐ์ค€ 3๊ฐœ ์ „๋ถ€ ํ†ต๊ณผ
/api/v1/admin/** โ†’ hasRole("ADMIN") ํ•œ ์ค„ ๋„ฃ์Œ ํ•œ ์ค„๋กœ 17๊ฐœ๊ฐ€ ์ž๋™ ๋ณดํ˜ธ๋จ. ๊ฐ์ž ๋ถ™์ด๊ฒŒ ํ•˜๋ฉด ๋น ๋œจ๋ฆฐ ์„œ๋น„์Šค๋งŒ ๋šซ๋ฆผ
RestClientAuthInterceptor ยท ๋นŒ๋” 3๊ฐœ ๋„ฃ์Œ ์ง์ด ์—ฌ๊ธฐ ์žˆ์Œ. ์•„๋ž˜ ์ฐธ๊ณ 
ํ† ํ”ฝ ์ด๋ฆ„ ์ƒ์ˆ˜ ์•ˆ ๋„ฃ์Œ โ‘  โ€” ์ž‘์—… ๋„์ค‘ ์ถ”๊ฐ€ยท๋ณ€๊ฒฝยท์‚ญ์ œ๋จ
S3 presigned ๋ฐœ๊ธ‰ ์•ˆ ๋„ฃ์Œ โ‘กโ‘ข โ€” user ยท pet ยท review 3๊ฐœ๋งŒ ์“ฐ๊ณ  AWS SDK ๊ฐ€ ๋‚˜๋จธ์ง€ 14๊ฐœ์—๋„ ๋”ธ๋ ค๊ฐ
์ด๋ฒคํŠธ payload DTO ์•ˆ ๋„ฃ์Œ ๋ฐœํ–‰์ž๊ฐ€ ํ•„๋“œ ํ•˜๋‚˜๋ฅผ ๊ณ ์น  ๋•Œ ๋ฐ›๋Š” ์ชฝ์ด ์ „๋ถ€ ์žฌ๋ฐฐํฌ๋จ. ์ด๋ฒคํŠธ๋กœ ๋–ผ์–ด ๋†“์€ ๊ฒƒ์„ ๋‹ค์‹œ ๋ถ™์ด๋Š” ์…ˆ์ž…๋‹ˆ๋‹ค
AfterCommitExecutor ์•ˆ ๋„ฃ์Œ โ‘ก โ€” auth ยท user ยท pet ยท review 4๊ฐœ๋งŒ ์”๋‹ˆ๋‹ค. ๋‚˜๋จธ์ง€๋Š” DB ๊ฐ€ ์—†์–ด ํŠธ๋žœ์žญ์…˜ ์ž์ฒด๊ฐ€ ์—†๊ฑฐ๋‚˜ Redis ยท S3 ๋ฅผ ์•ˆ ๊ฑด๋“œ๋ฆฝ๋‹ˆ๋‹ค

๊ธฐ์ค€ โ‘ก์˜ ๋ฌธํ„ฑ์ด ์‹ค์ œ๋กœ ์–ด๋””์ฏค์ธ์ง€๊ฐ€ ์ด ํ‘œ์—์„œ ๋“œ๋Ÿฌ๋‚ฉ๋‹ˆ๋‹ค. S3 presigned ๋Š” 3๊ฐœ, AfterCommitExecutor ๋Š” 4๊ฐœ์˜€๊ณ  ๋‘˜ ๋‹ค ์•ˆ ๋„ฃ์—ˆ์Šต๋‹ˆ๋‹ค.


ใ€Œ์„œ๋น„์Šค ์ฝ”๋“œ๋ฅผ ์•ˆ ๋ณธ๋‹คใ€๋Š” ๊ฒƒ๋งŒ์œผ๋กœ๋Š” ๊ธฐ์ค€์ด ๋ชป ๋ฉ๋‹ˆ๋‹ค.

RestClientAuthInterceptor ๊ฐ€ ํ†ต๊ณผํ•œ ์ง„์งœ ๊ทผ๊ฑฐ๋Š” ์ง์ด ์—ฌ๊ธฐ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ํ•˜๋Š” ์ผ ์–ด๋”” ์žˆ๋‚˜
HeaderAuthenticationFilter ์š”์ฒญ ํ—ค๋”๋ฅผ ์ฝ์–ด SecurityContext ๋ฅผ ์ฑ„์›€ ์ด ๋ชจ๋“ˆ
RestClientAuthInterceptor SecurityContext ๋ฅผ ๊บผ๋‚ด ์š”์ฒญ ํ—ค๋”๋กœ ์‹ค์Œ ์ด ๋ชจ๋“ˆ์ด์–ด์•ผ ์ง์ด ๋งž์Œ

์ฑ„์šฐ๋Š” ์ชฝ์ด ์ด ๋ชจ๋“ˆ์ด๋ฏ€๋กœ ๊บผ๋‚ด ์“ฐ๋Š” ์ชฝ๋„ ์ด ๋ชจ๋“ˆ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. AfterCommitExecutor ๋„ ์„œ๋น„์Šค ์ฝ”๋“œ๋ฅผ ์ „ํ˜€ ๋ณด์ง€ ์•Š์ง€๋งŒ ์ง์ด ์—†์Šต๋‹ˆ๋‹ค. ์Šคํ”„๋ง์ด ์ฃผ๋Š” API ๋ฅผ 30์ค„๋กœ ๊ฐ์‹ผ ๋…๋ฆฝ ์œ ํ‹ธ๋ฆฌํ‹ฐ์ด๊ณ  ์ด ๋ชจ๋“ˆ์˜ ๋ฌด์—‡๊ณผ๋„ ๋งž๋ฌผ๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.


3๊ณณ์— ํ•„์š”ํ•ด์ง€๋ฉด ๋‹ค์‹œ ๋ด…๋‹ˆ๋‹ค.

๊ฐ™์€ ์ฝ”๋“œ๊ฐ€ ๋ช‡ ๊ณณ์— ํ•„์š”ํ•ด์กŒ๋Š”์ง€๊ฐ€ ๊ธฐ์ค€์ž…๋‹ˆ๋‹ค.

1๊ณณ  private ๋ฉ”์„œ๋“œ   โ”€โ”€โ–ถ   2๊ณณ  ๋ณ„๋„ ํด๋ž˜์Šค   โ”€โ”€โ–ถ   3๊ณณ  ์ด ๋ชจ๋“ˆ

auth ์˜ AfterCommitExecutor ๊ฐ€ ์ด ๊ฒฝ๋กœ๋ฅผ ๋ฐŸ์•˜์Šต๋‹ˆ๋‹ค. ์›๋ž˜ AuthService ์•ˆ์˜ private ๋ฉ”์„œ๋“œ์˜€๋Š”๋ฐ ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •๋„ ๊ฐ™์€ ๊ฒƒ์ด ํ•„์š”ํ•ด์ ธ 2๊ณณ์ด ๋˜์ž ํด๋ž˜์Šค๋กœ ๊บผ๋ƒˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์ธก์ด ์•„๋‹ˆ๋ผ ์‹ค๋ฌผ์ด ์Œ“์ธ ๋’ค์— ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค. ๋ฏธ๋ฆฌ ์˜ฌ๋ ค ๋‘์—ˆ๋‹ค๊ฐ€ ์•ˆ ์“ฐ๊ฒŒ ๋˜๋ฉด 17๊ฐœ๊ฐ€ ์ง€๊ณ  ๊ฐ€๋Š” ๋นˆ์ด ํ•˜๋‚˜ ๋Š˜์–ด๋‚ฉ๋‹ˆ๋‹ค.




7-5. ๋ชจ๋“ˆ์„ ์ชผ๊ฐœ์ง€ ์•Š์€ ์ด์œ 

common-web ยท common-jpa ยท common-messaging ์œผ๋กœ ๋‚˜๋ˆ„๋ฉด ์กฐ๊ฑด๋ถ€ ๋กœ๋”ฉ์ด ๊ฑฐ์˜ ๋ถˆํ•„์š”ํ•ด์ง‘๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์•ˆ ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ชผ๊ฐœ๋ฉด ์ง€๊ธˆ
๋ ˆํฌ ยท publish ยท ๋ฒ„์ „ 3๋ฐฐ 1๊ฐœ
์กฐ๊ฑด๋ถ€ ๋กœ๋”ฉ ๊ฑฐ์˜ ๋ถˆํ•„์š” ์• ๋…ธํ…Œ์ด์…˜ ํ•œ ์ค„
๊ฒฝ๊ณ„๋ฅผ ์ •ํ•˜๋Š” ๊ทผ๊ฑฐ ์—†์Œ โ€” PageResponse ๊ฐ€ Page ๋ฅผ ์ฐธ์กฐํ•˜๋Š”๋ฐ web ์ธ๊ฐ€ jpa ์ธ๊ฐ€ โ€”
๋˜๋Œ๋ฆฌ๊ธฐ ๋น„์Œˆ โ€”

์„œ๋น„์Šค๊ฐ€ 0๊ฐœ์˜€๋˜ ์‹œ์ ์— ๊ฒฝ๊ณ„๋ฅผ ์ •ํ•  ๊ทผ๊ฑฐ๊ฐ€ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.


verdict ๊ฐ€ common ์„ ์•„์˜ˆ ์•ˆ ์“ฐ๋Š” ์•ˆ๋„ ๊ฒ€ํ† ํ–ˆ์Šต๋‹ˆ๋‹ค.

verdict ๋Š” common 7๊ฐœ ํŒจํ‚ค์ง€ ์ค‘ 6๊ฐœ๋ฅผ ์”€
        โ”‚
        โ””โ”€โ”€ entity/ + audit/ ํ•˜๋‚˜๋งŒ ์•ˆ ์”€
              ์•ˆ ์“ฐ๋ฉด 6๊ฐœ๋ฅผ ๋ณต์‚ฌํ•ด์•ผ ํ•˜๊ณ 
              ํ•œ์ชฝ๋งŒ ๊ณ ์ณ์ง€๋ฉด ์‘๋‹ต ํ˜•ํƒœ๊ฐ€ ์กฐ์šฉํžˆ ๊ฐˆ๋ฆผ

์ตœํ›„ ์ˆ˜๋‹จ์œผ๋กœ ๋‚จ๊ฒจ ๋‘์—ˆ์Šต๋‹ˆ๋‹ค. ์กฐ๊ฑด๋ถ€ ๋กœ๋”ฉ์ด ๊ณ„์† ๋ฌธ์ œ๋ฅผ ์ผ์œผํ‚ค๋ฉด ๊ทธ๋•Œ ๋ด…๋‹ˆ๋‹ค.




7-6. ํ† ํ”ฝ ์ด๋ฆ„์„ ์—ฌ๊ธฐ ๋‘์ง€ ์•Š๋Š” ์ด์œ 

ํ† ํ”ฝ์€ ๊ฐœ๋ฐœ ๋„์ค‘ ์ถ”๊ฐ€ยท๋ณ€๊ฒฝยท์‚ญ์ œ๋จ
        โ”‚
        โ””โ”€โ”€ ๊ณตํ†ต ๋ชจ๋“ˆ์— ์žˆ์œผ๋ฉด ๊ทธ๋•Œ๋งˆ๋‹ค
              ์žฌ๋ฐฐํฌ + ์ „ ์„œ๋น„์Šค ๋ฒ„์ „์—…์ด ํ•„์š”ํ•จ

์ด ๋ชจ๋“ˆ์˜ ๊ธฐ์ค€์€ "๊ฑฐ์˜ ๋ฐ”๋€Œ์ง€ ์•Š๋Š” ๊ฒƒ" ์ž…๋‹ˆ๋‹ค. 7-4 ์˜ ๊ธฐ์ค€ โ‘  ์— ๊ฑธ๋ฆฐ ์‚ฌ๋ก€์ž…๋‹ˆ๋‹ค.

๋Œ€์‹  ๋ฌธ์ž์—ด์ด ๋‘ ์ €์žฅ์†Œ์— ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ๋ฐœํ–‰ ์ชฝ getTopic() ๊ณผ ๋ฐ›๋Š” ์ชฝ @KafkaListener(topics = ...) ์ž…๋‹ˆ๋‹ค.

์–ด๊ธ‹๋‚˜๋ฉด ์˜ค๋ฅ˜๊ฐ€ ๋‚˜์ง€ ์•Š๊ณ  ์ด๋ฒคํŠธ๋งŒ ์˜ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์™„ํ™”์ฑ…์€ service-template README 9-2 ๋ฅผ ๋‹จ์ผ ์ฐธ์กฐ๋กœ ์‚ผ๊ณ  Kafka UI ๋กœ ์‹ค๋ฌผ์„ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

AuthContextHeaders ๋„ ๊ฐ™์€ ๋ถ€๋ฅ˜์ž…๋‹ˆ๋‹ค โ€” ๊ฒŒ์ดํŠธ์›จ์ด๊ฐ€ common ์„ ์•ˆ ์จ์„œ 4๋ฒˆ์งธ ์‚ฌ๋ณธ์ด ๋ฉ๋‹ˆ๋‹ค.




7-7. ๋ฐœํ–‰ ์‹œ ์ธ์ฆ ํ—ค๋”๋ฅผ ์‹ฃ์ง€ ์•Š์Œ

์ปจ์Šˆ๋จธ๊ฐ€ ๋งŒ๋“œ๋Š” ํ–‰์˜ created_by
        โ”‚
        โ””โ”€โ”€ AuditorProvider.current() ๊ฐ€ ํŒ์ •ํ•œ ๊ฐ’
              ๊ฐ ์„œ๋น„์Šค์˜ app.auditor.system-name (ingest ๋ฉด ingest-batch)
๋ฒ„๋ฆผ ์™œ
outbox ์— created_by ์ปฌ๋Ÿผ ์ถ”๊ฐ€ OutboxMessage.create() ๋ฅผ ๋ถ€๋ฅด๋Š” ์„œ๋น„์Šค๊ฐ€ ๋งค๋ฒˆ ๊ฐ’์„ ๋„˜๊ฒจ์•ผ ํ•จ. ๋น ๋œจ๋ฆฌ๋ฉด ์กฐ์šฉํžˆ null
@PrePersist ๋กœ ์ž๋™ํ™” ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์Šคํ”„๋ง ๋นˆ์„ ์ฐธ์กฐํ•˜๊ฒŒ ๋˜์–ด ์›์น™๊ณผ ์–ด๊ธ‹๋‚จ

๋Œ€๊ฐ€๊ฐ€ ์ž‘์€ ๊ทผ๊ฑฐ โ€” ์‚ฌ์šฉ์ž๊ฐ€ ์ผ์œผํ‚ค๋Š” ์ด๋ฒคํŠธ ์…‹์˜ ์†Œ๋น„์ž๊ฐ€ ํ•˜๋Š” ์ผ์ด ์žฌ๊ฒ€ํ†  ํ ์ ์žฌ ยท ์บ์‹œ ๋ฌดํšจํ™” ยท ์ž๊ธฐ ๋ฐ์ดํ„ฐ ์‚ญ์ œ๋ผ ์ƒˆ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋งŒ๋“œ๋Š” ์ž๋ฆฌ๊ฐ€ ๊ฑฐ์˜ ์—†์Šต๋‹ˆ๋‹ค.

์ธํ„ฐ์…‰ํ„ฐ๋Š” "ํ—ค๋” ์žˆ์œผ๋ฉด ๋ณต์›, ์—†์œผ๋ฉด ํ†ต๊ณผ" ๋ผ ๋‚˜์ค‘์— ๋ฐœํ–‰ ์ชฝ์„ ๋ถ™์—ฌ๋„ ์ธํ„ฐ์…‰ํ„ฐ ์ฝ”๋“œ๋Š” ์•ˆ ๋ฐ”๋€๋‹ˆ๋‹ค. ๊ณ ์น  ์ž๋ฆฌ๋„ OutboxPublisher ํ•œ ๊ณณ์ž…๋‹ˆ๋‹ค.


๋ฐœํ–‰ ์ชฝ ์ธํ„ฐ์…‰ํ„ฐ๊ฐ€ ์• ์ดˆ์— ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

Outbox ๋ฅผ ์“ฐ๋ฏ€๋กœ kafkaTemplate.send() ๋ฅผ ๋ถ€๋ฅด๋Š” ๊ณณ์ด OutboxPublisher ํ•œ ๊ณณ๋ฟ
        โ”‚
        โ””โ”€โ”€ ๊ฑฐ๊ธฐ๋Š” @Async ยท ์Šค์ผ€์ค„๋Ÿฌ ์Šค๋ ˆ๋“œ๋ผ SecurityContext ๊ฐ€ ์ด๋ฏธ ๋น„์–ด ์žˆ์Œ
              ๊บผ๋‚ผ ๊ฐ’์ด ์—†์Œ




7-8. UUID v7 ์ƒ์„ฑ๊ธฐ๊ฐ€ ๋‘ ๋ฒŒ์ธ ์ด์œ 

PK          Hibernate @UuidGenerator(style = VERSION_7)
์ด๋ฒคํŠธ ID    EventEnvelope.generateUuidV7()   ์ž์ฒด ๊ตฌํ˜„

Hibernate ๊ฒƒ์„ ์žฌ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.

UuidVersion7Strategy ๋Š” hibernate-core ์— ์žˆ์Œ
        โ”‚
        โ””โ”€โ”€ ๋ฌด์ƒํƒœ ์„œ๋น„์Šค(verdict ยท congestion)์—๋Š” Hibernate ๊ฐ€ ์—†์Œ
              ๊ทธ๋Ÿฐ๋ฐ verdict ๋„ ์ด๋ฒคํŠธ๋ฅผ ์†Œ๋น„ํ•˜๋ฉฐ EventEnvelope ๋ฅผ ๋‹ค๋ฃธ
                    โ”‚
                    โ””โ”€โ”€ ์žฌ์‚ฌ์šฉํ•˜๋ฉด message/ ๊ฐ€ JPA ์— ๋ฌถ์—ฌ
                        ์กฐ๊ฑด๋ถ€ ๋กœ๋”ฉ์˜ ์˜๋ฏธ๊ฐ€ ์‚ฌ๋ผ์ง




7-9. X-Original-Trace-Id ๋ฅผ ์†์œผ๋กœ ์•ˆ ๋„ฃ์Œ

์Šคํ”„๋ง ์นดํ”„์นด Observation ์— ๋งก๊น๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ํ—ค๋”    ์šฐ๋ฆฌ๊ฐ€ ๋„ฃ์Œ      X-User-Id ยท X-User-Role
W3C ํ‘œ์ค€ ํ—ค๋”      ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€     traceparent

์†์œผ๋กœ ๋„ฃ์œผ๋ฉด ํ—ค๋”๊ฐ€ ์ค‘๋ณต๋˜์–ด ํ‘œ์ค€์ƒ ๋ฌดํšจ ์ฒ˜๋ฆฌ๋˜๊ณ  ๋ฐ›๋Š” ์ชฝ์ด ์ƒˆ trace ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

โš  ๋‹จ Kafka ๋Š” Observation ์„ ์ผœ์•ผ๋งŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

spring:
  kafka:
    template:  { observation-enabled: true }
    listener:  { observation-enabled: true }

ํ”„๋กœ๋“€์„œ๋งŒ ์ผœ๋ฉด ํ—ค๋”๋Š” ์‹ค๋ ค ๊ฐ€์ง€๋งŒ ์ปจ์Šˆ๋จธ๊ฐ€ ์ฝ์ง€ ์•Š์•„ ๋ฐ˜์ชฝ์ž…๋‹ˆ๋‹ค.




8. ํ˜„์žฌ ์ƒํƒœ




8-1. ๋ฒ„์ „ ์ด๋ ฅ

๋ฒ„์ „ ๋ฌด์—‡์ด ์™œ
0.0.1 ~ 0.0.4 ์ดˆ๊ธฐ ๊ตฌ์ถ• 1~7๋‹จ๊ณ„
0.0.5 findUnpublishedIgnoringRetryLimit ์ถ”๊ฐ€ Relay ๊ฐ€ ํฌ๊ธฐํ•œ ๊ฑด์„ ๊ด€๋ฆฌ์ž๊ฐ€ ๋ณผ ์ˆ˜๋‹จ์ด ์—†์—ˆ์Œ
0.0.6 QueryDSL ํ”„๋กœ์„ธ์„œ BaseEntity ๋ฅผ ์ƒ์†ํ•˜๋ฉด QBaseEntity ๊ฐ€ ์—†์–ด ์ปดํŒŒ์ผ ์‹คํŒจ
0.0.7 RESOURCE_NOT_FOUND ยท AuthenticationManager ์—†๋Š” ๊ฒฝ๋กœ๊ฐ€ 500 ยท ๋กœ๊ทธ์— ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ฐํž˜
0.0.8 FOR UPDATE SKIP LOCKED ๊ฐ™์€ ์ด๋ฒคํŠธ๊ฐ€ ๋‘ ๋ฒˆ ๋ฐœํ–‰๋จ
0.0.9 findGivenUpMessages ๋กœ ๊ต์ฒด ์ฃผ์„๊ณผ ์ฟผ๋ฆฌ๊ฐ€ ์–ด๊ธ‹๋‚˜ ์žˆ์—ˆ์Œ
0.0.10 ์„œ๋น„์Šค ๊ฐ„ ํ˜ธ์ถœ ๊ธฐ๋ฐ˜ ์ธํ„ฐ์…‰ํ„ฐ๊ฐ€ ํด๋ž˜์Šค๋งŒ ์žˆ๊ณ  ์–ด๋””์—๋„ ์•ˆ ๋ถ™์–ด ์žˆ์—ˆ์Œ
0.0.11 ๋นŒ๋”๋ฅผ ํ”„๋กœํ† ํƒ€์ž…์œผ๋กœ ยท ์š”์ฒญ ํŒฉํ„ฐ๋ฆฌ๋ฅผ detect() ๋กœ ๋นŒ๋”๊ฐ€ ์‹ฑ๊ธ€ํ„ด์ด๋ผ baseUrl ์ด ์„œ๋กœ ๋ฎ์—ˆ์Œ
0.0.12 @Primary ์ธ ๋งจ ๋นŒ๋” ์ถ”๊ฐ€ ๋นŒ๋”๊ฐ€ ๋‘˜์ด๋ผ ์œ ๋ ˆ์นด๊ฐ€ ํ•˜๋‚˜๋ฅผ ๋ชป ๊ณจ๋ž์Œ
0.0.13 CommonApiResponse ์— @JsonCreator ๋งŒ๋“ค ์ˆ˜๋งŒ ์žˆ๊ณ  ์ฝ์„ ์ˆ˜๊ฐ€ ์—†์—ˆ์Œ

0.0.6 ์ด ๊ฐ€์žฅ ๋„“๊ฒŒ ์˜ํ–ฅ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

BaseEntity ๋ฅผ @MappedSuperclass ๋กœ ์ œ๊ณตํ•˜๋ฉด์„œ ๊ทธ Q ํด๋ž˜์Šค๋ฅผ ์•ˆ ๋งŒ๋“ค์–ด ์ค€ ๊ฒƒ
        โ”‚
        โ””โ”€โ”€ ๋„๋ฉ”์ธ ์„œ๋น„์Šค 13๊ฐœ๊ฐ€ ์ „๋ถ€ ๊ฐ™์€ ์ž๋ฆฌ๋ฅผ ๋ฐŸ์Œ
              service-template ์€ BaseEntity ๋ฅผ ์ƒ์†ํ•˜๋Š” ์‹ค๋ฌผ์ด ์—†์–ด
              ์ง€๊ธˆ๊นŒ์ง€ ์•ˆ ๋“œ๋Ÿฌ๋‚ฌ๊ณ  auth ๊ฐ€ ์ฒซ ์‚ฌ๋ก€์˜€์Œ

0.0.9 ๊ฐ€ feat ์ด ์•„๋‹ˆ๋ผ fix ์ธ ์ด์œ ์ž…๋‹ˆ๋‹ค.

0.0.5 ์˜ ์ฃผ์„์€ ์ด๋ฏธ "๊ด€๋ฆฌ์ž๋Š” ์ž„๊ณ„๋ฅผ ๋„˜๊ธด ๊ฑด์„ ๋ด์•ผ ํ•จ" ์ด๋ผ๊ณ  ๋งํ•˜๋Š”๋ฐ ์ฟผ๋ฆฌ์—๋Š” ๊ทธ ์กฐ๊ฑด์ด ์—†์–ด ๋ฏธ๋ฐœํ–‰ ์ „๋ถ€๋ฅผ ๋Œ๋ ค์ฃผ๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.


0.0.11 ์ด 0.0.10 ๋ฐ”๋กœ ๋‹ค์Œ์— ๋‚˜์˜จ ์ด์œ ์ž…๋‹ˆ๋‹ค.

0.0.10 ์˜ ๋นŒ๋” ๋นˆ ๋‘˜์— ์Šค์ฝ”ํ”„๊ฐ€ ์—†์–ด ์‹ฑ๊ธ€ํ„ด์ด์—ˆ์Œ
        โ”‚
        โ””โ”€โ”€ ์ฃผ์ž…๋ฐ›์€ ๋ชจ๋‘๊ฐ€ ๊ฐ™์€ ์ธ์Šคํ„ด์Šค๋ฅผ ๋‚˜๋ˆ  ์”€
              PlaceProvider ๊ฐ€ baseUrl ์„ ๊ฑธ๋ฉด VerdictProvider ๊ฒƒ๊นŒ์ง€ ๋ฐ”๋€œ
              ์ƒ์„ฑ์ž์—์„œ ๊ฑธ์ž๋งˆ์ž build() ํ•˜๋ฉด ๊ฒฐ๊ณผ๋Š” ๋งž์œผ๋‚˜ ์ˆœ์„œ์— ๊ธฐ๋Œ„ ๊ฒƒ์ž„

provider ๋ฅผ ํ•˜๋‚˜๋„ ๋งŒ๋“ค์ง€ ์•Š์€ ์‹œ์ ์ด๋ผ ๋“œ๋Ÿฌ๋‚˜๊ธฐ ์ „์— ์žก์•˜์Šต๋‹ˆ๋‹ค.


0.0.12 ๋Š” 0.0.10 ์˜ ๊ฒฐํ•จ์ด ๋’ค๋Šฆ๊ฒŒ ๋“œ๋Ÿฌ๋‚œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋นŒ๋”๋ฅผ ๋‘˜ ๋„ฃ์€ ๊ฒƒ์ด 0.0.10 ์ด๊ณ  ์œ ๋ ˆ์นด๊ฐ€ ๊ทธ๋•Œ๋ถ€ํ„ฐ ๊นจ์ ธ ์žˆ์—ˆ์œผ๋‚˜, auth ๋Š” 0.0.9 ์— ๋จธ๋ฌผ๋Ÿฌ ์žˆ์—ˆ๊ณ  user ๋Š” ์•„์ง ๊ฒŒ์ดํŠธ์›จ์ด๋ฅผ ๊ฑฐ์ณ ๋ถ€๋ฅด์ง€ ์•Š์•„ ์•„๋ฌด๋„ ์•Œ์•„์ฑ„์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. 0.0.11 ๋กœ auth ๋ฅผ ์˜ฌ๋ฆฌ๋ฉด์„œ ์ฆ์ƒ์ด ๋“œ๋Ÿฌ๋‚ฌ์Šต๋‹ˆ๋‹ค.

0.0.10  ๋นŒ๋” 2๊ฐœ  โ”€โ”€โ–ถ  user ๋งŒ ์กฐ์šฉํžˆ ๋“ฑ๋ก ์‹คํŒจ        ์•„๋ฌด๋„ ์•ˆ ๋ด„
0.0.11  auth ๋„ ์˜ฌ๋ฆผ  โ”€โ”€โ–ถ  ๊ฒŒ์ดํŠธ์›จ์ด 503 ยท ์œ ๋ ˆ์นด์— AUTH-SERVICE ์—†์Œ
0.0.12  @Primary ๋งจ ๋นŒ๋”  โ”€โ”€โ–ถ  registration status: 204

0.0.13 ์€ ๋ด‰ํˆฌ๋ฅผ ์ฝ์„ ์ˆ˜ ์—†๋˜ ๊ฒƒ์„ ๊ณ ์ณค์Šต๋‹ˆ๋‹ค.

CommonApiResponse ๋Š” ๋งŒ๋“œ๋Š” ์ชฝ๊ณผ ์ฝ๋Š” ์ชฝ์ด ๋‘˜ ๋‹ค ์“ฐ๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. ์„œ๋น„์Šค๊ฐ€ ์‘๋‹ต์„ ๋งŒ๋“ค ๋•Œ ์“ฐ๊ณ , ๊ทธ ์‘๋‹ต์„ ๋ฐ›๋Š” ๋‹ค๋ฅธ ์„œ๋น„์Šค๊ฐ€ ๊ฐ™์€ ํƒ€์ž…์œผ๋กœ ์ฝ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๋งŒ๋“œ๋Š” ๊ธธ๋งŒ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

private CommonApiResponse(...)     ์ƒ์„ฑ์ž๊ฐ€ private
์—†์Œ                               ๊ธฐ๋ณธ ์ƒ์„ฑ์ž ยท @JsonCreator
๊ฒฐ๊ณผ                               ๋ฐ›๋Š” ์ชฝ์ด ์˜ˆ์™ธ๋กœ ์‹คํŒจ

๋ถ€๋ฅด๋Š” ์ชฝ์ด ๊ทธ ์˜ˆ์™ธ๋ฅผ ์‚ผํ‚ค๋ฉด ๊ฐ’๋งŒ ์กฐ์šฉํžˆ ๋น„๊ณ  ํ™”๋ฉด์€ ๋ฉ€์ฉกํ•ด ๋ณด์ž…๋‹ˆ๋‹ค. user ๊ฐ€ review ๋ฅผ ๋ถ€๋ฅด๋Š” ์ž๋ฆฌ์—์„œ ์ฒ˜์Œ ๋“œ๋Ÿฌ๋‚ฌ์Šต๋‹ˆ๋‹ค. ์ด ์ž๋ฆฌ๋ฅผ ์ง€ํ‚ค๋ ค๊ณ  5-6 ์˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ํ•จ๊ป˜ ๋„ฃ์—ˆ์Šต๋‹ˆ๋‹ค.

์š”์ฒญ ํŒฉํ„ฐ๋ฆฌ๋ฅผ ํ•จ๊ป˜ ๋ฐ”๊พผ ๊ฒƒ์€ SimpleClientHttpRequestFactory ๊ฐ€ PATCH ๋ฅผ ๋ณด๋‚ด์ง€ ๋ชปํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. HttpURLConnection ๊ธฐ๋ฐ˜์ด๋ผ ๊ทธ๋ ‡์Šต๋‹ˆ๋‹ค. ์—ฐ๊ฒฐ์„ ์žฌ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์€ ๊ทœ๋ชจ๊ฐ€ ์ปค์งˆ ๋•Œ์˜ ๋ฌธ์ œ์ง€๋งŒ PATCH ๋Š” ์“ฐ๋ ค๋Š” ์ˆœ๊ฐ„ ๋ง‰ํžˆ๋Š” ๊ฒƒ์ด๋ผ ์„ฑ๊ฒฉ์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค.




8-2. ์‹ค๋ฌผ๋กœ ๊ฒ€์ฆ๋œ ๊ฒƒ

โœ… GlobalExceptionHandler 5๊ฐœ          service-template ์ž„์‹œ ์ปจํŠธ๋กค๋Ÿฌ๋กœ 5์ข…
โœ… TraceIdResponseAdvice               ์‹คํŒจ ์‘๋‹ต์—๋„ traceId ๊ฐ€ ์‹ค๋ฆผ
โœ… jsonb + Jackson 3                   auth ์ฒซ ํšŒ์›๊ฐ€์ž…
โœ… DomainEvent ์˜ @JsonIgnore ์ƒ์†       payload ์— ๋ผ์šฐํŒ… ํ•„๋“œ 3๊ฐœ๊ฐ€ ์•ˆ ์ƒ˜
โœ… Outbox โ†’ Kafka                      auth account.created
โœ… FOR UPDATE SKIP LOCKED              Kafka ๋ฅผ ๋‚ด๋ ค ๊ฒน์น˜๋Š” ์ƒํ™ฉ์„ ๋งŒ๋“ค์–ด ํ™•์ธ
โœ… OutboxRelay ์•ˆ์ „๋ง                   ๋ฆฌ์Šค๋„ˆ๊ฐ€ ์‹คํŒจํ•œ ๊ฑด์„ ์ฃผ์›Œ ๋ฐœํ–‰
โœ… ๊ด€๋ฆฌ์ž ๊ฒฝ๋กœ๊ฐ€ ADMIN ์œผ๋กœ๋งŒ ํ†ต๊ณผ        auth #17 ๊ฒ€์ฆ
โœ… jar ๋‚ด์šฉ                            imports ยท ์ž๋™ ์„ค์ • 7๊ฐœ ยท V1 ยท V2 ยท QBaseEntity
โœ… 0.0.11 ๊ธฐ๋™                         user ยท auth ๋‘˜ ๋‹ค UP
                                      ๋‚จ์„ ์•ˆ ๋ถ€๋ฅด๋Š” auth ์—์„œ๋„ ์ƒˆ ์ž๋™ ์„ค์ •์ด ์ •์ƒ
โœ… 0.0.12 ์œ ๋ ˆ์นด ๋“ฑ๋ก                   registration status: 204
                                      /eureka/apps ์— USER-SERVICE ๊ฐ€ ๋œธ
โœ… CommonApiResponse ์—ญ์ง๋ ฌํ™”           ๋‹จ์œ„ ํ…Œ์ŠคํŠธ 6๊ฐœ (5์žฅ ์ฐธ๊ณ )
                                      ์ œ๋„ค๋ฆญ 2์ค‘ ์ค‘์ฒฉ ยท UUID ยท data ๊ฐ€ null ์ธ ์‹คํŒจ ์‘๋‹ต




8-3. ์•„์ง ํ™•์ธ ๋ชป ํ•œ ๊ฒƒ

์†Œ๋น„์ž๊ฐ€ ์žˆ๋Š” ์„œ๋น„์Šค๊ฐ€ ์ƒ๊ฒจ์•ผ ํ™•์ธ๋ฉ๋‹ˆ๋‹ค.

โฌœ Inbox ์ „์ฒด ํ๋ฆ„              InboxProcessor ์˜ ํŠธ๋žœ์žญ์…˜์ด ์‹ค์ œ๋กœ ๋ฌถ์ด๋‚˜
โฌœ KafkaSecurityInterceptor    ๋ฆฌ์Šค๋„ˆ ์ปจํ…Œ์ด๋„ˆ์— ์‹ค์ œ๋กœ ๋ถ™๋‚˜
โฌœ ์žฌ์‹œ๋„ ยท DLQ                1s โ†’ 2s โ†’ 4s ๋’ค {ํ† ํ”ฝ}.dlq ๋กœ ๊ฐ€๋‚˜
โฌœ RecordMessageConverter      EventEnvelope<T> ๋กœ ๋ฐ›์•„์ง€๋‚˜

auth ๋กœ๋Š” ํ™•์ธํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. @KafkaListener ๊ฐ€ ํ•œ ๊ฐœ๋„ ์—†์–ด ์ธํ„ฐ์…‰ํ„ฐ๊ฐ€ ๋ถ™์„ ์ž๋ฆฌ ์ž์ฒด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

user ยท pet ยท search ยท review ยท notification ์ค‘ ํ•˜๋‚˜๊ฐ€ ์ƒ๊ธฐ๋ฉด ํ™•์ธ๋ฉ๋‹ˆ๋‹ค.




8-4. ์•Œ๋ ค์ง„ ๊ฒฐํ•จ

HttpMessageNotReadableException ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

๊นจ์ง„ JSON ์„ ๋ณด๋‚ด๋ฉด
        โ”‚
        โ””โ”€โ”€ Exception ํด๋ฐฑ์ด ์žก์•„ INTERNAL_ERROR 500
              ๋ณธ๋ฌธ ํ˜•์‹์ด ํ‹€๋ฆฐ ๊ฒƒ์€ ํด๋ผ์ด์–ธํŠธ ์ž˜๋ชป์ด๋ฏ€๋กœ 400 ์ด ๋งž์Œ
              ์ง€๊ธˆ์€ ์„œ๋ฒ„ ์˜ค๋ฅ˜๋กœ ๋ณด๊ณ ๋˜์–ด ๋กœ๊ทธยท๋ชจ๋‹ˆํ„ฐ๋ง์—์„œ ์ง„์งœ ์žฅ์• ์™€ ์„ž์ž„

์žฌํ˜„

curl -X POST http://localhost:8081/api/v1/auth/login \
  -H "Content-Type: application/json" -d '{"email"'

CommonErrorCode.VALIDATION_FAILED ๋ฅผ ๊ทธ๋Œ€๋กœ ์“ฐ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ์— ์ด ๋ชจ๋“ˆ์„ ์†๋ณผ ๋•Œ ํ•จ๊ป˜ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.


HttpRequestMethodNotSupportedException ๋„ ๊ฐ™์Šต๋‹ˆ๋‹ค. 405 ์ธ๋ฐ 500 ์œผ๋กœ ๋‚˜๊ฐ‘๋‹ˆ๋‹ค.




8-5. ์•„์ง ์•ˆ ์ •ํ•œ ๊ฒƒ

๋ฌด์—‡ ์–ธ์ œ
์†Œํ”„ํŠธ ๋”œ๋ฆฌํŠธ ์กฐํšŒ ๋ฐฉ์‹ ์‹ค์ œ ์กฐํšŒ๋ฅผ ์งœ๋Š” ์„œ๋น„์Šค์—์„œ
processed_event ์ •๋ฆฌ ๋ฐฐ์น˜ ์ง€๊ธˆ ๊ทœ๋ชจ์—์„œ๋Š” ๋ถˆํ•„์š”
max.block.ms ๋ฅผ ๋‚ฎ์ถœ์ง€ ์นดํ”„์นด๊ฐ€ ์ฃฝ์—ˆ์„ ๋•Œ 3์ดˆ ํƒ€์ž„์•„์›ƒ์ด 60์ดˆ์— ๊ฐ€๋ ค์ง
์กฐ์ง ์ „์šฉ BOM ์ง€๊ธˆ์€ Boot ๋ฒ„์ „์ด 21๊ฐœ ๋ ˆํฌ์— ์‚ฌ๋ณธ์œผ๋กœ ์žˆ์Œ

RestClientAuthInterceptor ๋Š” 0.0.10 ์—์„œ ๋ฐฐ์„ ๋์Šต๋‹ˆ๋‹ค.

internalRestClientBuilder ์— ๋ถ™์–ด ์žˆ์–ด ๊ทธ ๋นŒ๋”๋กœ ๋งŒ๋“  RestClient ๋กœ ํ˜ธ์ถœํ•˜๋ฉด X-User-Id ์™€ X-User-Role ์ด ์ž๋™์œผ๋กœ ์‹ค๋ฆฝ๋‹ˆ๋‹ค. ์“ฐ๋Š” ๋ฒ•์€ 4-7 ์— ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค๋งŒ ํ—ค๋”๊ฐ€ ์‹ค์ œ๋กœ ๊ฑด๋„ˆ๊ฐ€๋Š”์ง€๋Š” ์•„์ง ํ™•์ธ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

ํ™•์ธ ๋ฐฉ๋ฒ•   ์„œ๋น„์Šค A ๊ฐ€ ์ธ์ฆ๋œ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋‹ค B ๋ฅผ ํ˜ธ์ถœํ–ˆ์„ ๋•Œ
            B ๊ฐ€ ๋งŒ๋“  ํ–‰์˜ created_by ๊ฐ€ SYSTEM ์ด ์•„๋‹ˆ๋ผ ์‹ค์ œ accountId ์—ฌ์•ผ ํ•จ

์ง€๊ธˆ ๋ชป ํ•˜๋Š” ์ด์œ    ๋ถ€๋ฅด๋Š” ์ชฝ(user)์€ ์žˆ์œผ๋‚˜ ๋ฐ›๋Š” ์ชฝ์ด ์•„์ง ์—†์Œ
                   place ยท verdict ยท review ์ค‘ ํ•˜๋‚˜๊ฐ€ ์ƒ๊ธฐ๋ฉด ํ™•์ธ๋จ

์—๋Ÿฌ๊ฐ€ ์•ˆ ๋‚˜๋ฏ€๋กœ ์ด๊ฒƒ์„ ์•ˆ ๋ณด๋ฉด ๋ชจ๋ฆ…๋‹ˆ๋‹ค. ํ—ค๋”๊ฐ€ ์•ˆ ์‹ค๋ ค๋„ ํ˜ธ์ถœ์€ ์„ฑ๊ณตํ•˜๊ณ  created_by ๋งŒ ์กฐ์šฉํžˆ SYSTEM ์œผ๋กœ ๋‚จ์Šต๋‹ˆ๋‹ค.




9. ์šฉ์–ด

๊ณตํ†ต ์šฉ์–ด๋Š” service-template README 11์žฅ์— ์žˆ์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ๋Š” ์ด ๋ชจ๋“ˆ์—์„œ๋งŒ ์“ฐ๋Š” ๋ง์ž…๋‹ˆ๋‹ค.

์šฉ์–ด ๋œป
๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ˜ผ์ž ์‹คํ–‰๋˜์ง€ ์•Š๊ณ  ๋‹ค๋ฅธ ์•ฑ์— ๋ผ์›Œ์ง€๋Š” ์ฝ”๋“œ ๋ฌถ์Œ. ์ด ์ €์žฅ์†Œ
jar ์ž๋ฐ” ์ฝ”๋“œ๋ฅผ ์••์ถ•ํ•œ ํŒŒ์ผ. ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ๊ฒƒ๊ณผ ๋ผ์›Œ ๋„ฃ๋Š” ๊ฒƒ์ด ์žˆ์Œ
์ž๋™ ์„ค์ • ์˜์กด์„ฑ์ด ์žˆ์œผ๋ฉด ์Šคํ”„๋ง์ด ์•Œ์•„์„œ Bean ์„ ์˜ฌ๋ฆฌ๋Š” ๊ฒƒ. @AutoConfiguration
ํด๋ž˜์ŠคํŒจ์Šค ๊ทธ ์„œ๋น„์Šค๊ฐ€ ๋นŒ๋“œ์— ๋„ฃ์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ „๋ถ€. ์ž๋™ ์„ค์ •์˜ ํŒ๋‹จ ๊ธฐ์ค€
Bean ์Šคํ”„๋ง์ด ๋งŒ๋“ค์–ด ๊ด€๋ฆฌํ•˜๋Š” ๊ฐ์ฒด. @Bean ยท @Component
@ConditionalOnClass ์ด ํด๋ž˜์Šค๊ฐ€ ํด๋ž˜์ŠคํŒจ์Šค์— ์žˆ์„ ๋•Œ๋งŒ ์ผœ๋ผ
@ConditionalOnMissingBean ๊ฐ™์€ ํƒ€์ž…์˜ Bean ์ด ์—†์„ ๋•Œ๋งŒ ๋งŒ๋“ค๋ผ โ€” ์„œ๋น„์Šค๊ฐ€ ์ •์˜ํ•˜๋ฉด ๋ฌผ๋Ÿฌ๋‚จ
ํŠธ๋žœ์žญ์…˜ ์—ฌ๋Ÿฌ ์“ฐ๊ธฐ๋ฅผ ํ•œ ๋ฉ์–ด๋ฆฌ๋กœ. ์ „๋ถ€ ๋˜๊ฑฐ๋‚˜ ์ „๋ถ€ ์•ˆ ๋˜๊ฑฐ๋‚˜
MANDATORY ํŠธ๋žœ์žญ์…˜์ด ์ด๋ฏธ ์žˆ์–ด์•ผ๋งŒ ์‹คํ–‰. ์—†์œผ๋ฉด ์ฆ‰์‹œ ์˜ˆ์™ธ
REQUIRES_NEW ๊ธฐ์กด ํŠธ๋žœ์žญ์…˜๊ณผ ๋ณ„๊ฐœ๋กœ ์ƒˆ ํŠธ๋žœ์žญ์…˜์„ ์—ด๊ณ  ๋จผ์ € ์ปค๋ฐ‹
์ด๋ฒคํŠธ "๋ฌด์Šจ ์ผ์ด ์žˆ์—ˆ๋‹ค" ๋ฅผ Kafka ์— ๋‚จ๊ธฐ๋Š” ๊ฒƒ. ์ง์ ‘ ํ˜ธ์ถœ์˜ ๋Œ€์•ˆ
ํ† ํ”ฝ Kafka ์˜ ์šฐ์ฒดํ†ต ํ•˜๋‚˜. place.updated
๋ด‰ํˆฌ (EventEnvelope) ๋ชจ๋“  ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์‹ธ๋Š” ๊ณตํ†ต ๊ป๋ฐ๊ธฐ. eventId ยท data โ€ฆ
Outbox ์ด๋ฒคํŠธ๋ฅผ DB ์— ๋จผ์ € ์ €์žฅํ•˜๊ณ  ์ปค๋ฐ‹ ๋’ค ๋ณด๋‚ด๋Š” ํŒจํ„ด
Inbox ๊ฐ™์€ ์ด๋ฒคํŠธ๋ฅผ ๋‘ ๋ฒˆ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๊ฒŒ ์ฒ˜๋ฆฌ ์ด๋ ฅ์„ ๋‚จ๊ธฐ๋Š” ํŒจํ„ด
Relay outbox ํ‘œ์—์„œ ์•ˆ ๋‚˜๊ฐ„ ๊ฑด์„ ์ฃผ๊ธฐ์ ์œผ๋กœ ์ฃผ์›Œ ๋ณด๋‚ด๋Š” ์•ˆ์ „๋ง
DLQ ์žฌ์‹œ๋„๊ฐ€ ๋๋‚œ ๋ฉ”์‹œ์ง€๊ฐ€ ๊ฐ€๋Š” ํ† ํ”ฝ
๋ฉฑ๋“ฑ ์—ฌ๋Ÿฌ ๋ฒˆ ์ฒ˜๋ฆฌํ•ด๋„ ๊ฒฐ๊ณผ๊ฐ€ ํ•œ ๋ฒˆ๊ณผ ๊ฐ™์Œ
FOR UPDATE SKIP LOCKED ํ–‰์„ ์ž ๊ทธ๋˜ ์ด๋ฏธ ์ž ๊ธด ํ–‰์€ ๊ฑด๋„ˆ๋œ€. ์ค‘๋ณต ๋ฐœํ–‰ ๋ฐฉ์ง€
SecurityContext ์ด ์š”์ฒญ์˜ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋‹ด์•„ ๋‘๋Š” ์ž๋ฆฌ
principal ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž. ์šฐ๋ฆฌ๋Š” CustomUserPrincipal(accountId, role)
๊ฐ์‚ฌ (Auditing) ๋ˆ„๊ฐ€ ์–ธ์ œ ๋งŒ๋“ค๊ณ  ๊ณ ์ณค๋Š”์ง€ ์ž๋™์œผ๋กœ ๊ธฐ๋ก. BaseEntity 6์ปฌ๋Ÿผ
์†Œํ”„ํŠธ ๋”œ๋ฆฌํŠธ ํ–‰์„ ์ง€์šฐ์ง€ ์•Š๊ณ  deleted_at ๋งŒ ์ฑ„์›€
Flyway DB ์Šคํ‚ค๋งˆ๋ฅผ ๋ฒˆํ˜ธ ๋ถ™์ธ SQL ๋กœ ๊ด€๋ฆฌ. V1__outbox.sql
publish ์ด ๋ชจ๋“ˆ์„ GitHub Packages ์— ์˜ฌ๋ฆฌ๋Š” ๊ฒƒ. ๋ฒ„์ „ ํ•˜๋‚˜๋ฅผ ์˜๊ตฌ ์†Œ๋ชจ
mavenLocal() ๋‚ด ์ปดํ“จํ„ฐ์˜ ์ž„์‹œ ์ €์žฅ์†Œ. publish ์ „ ๊ฒ€์ฆ์šฉ

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages