ํจ๊ปํ๊ฐ์ ๊ณตํต ๋ชจ๋์ ๋๋ค. ๋๋ฉ์ธ ์๋น์ค 14๊ฐ๊ฐ ์ ๋ถ ์ฐ๋ ๊ฒ๋ง ๋ด์ต๋๋ค.
๋จผ์ ์ ์ฒด ๊ทธ๋ฆผ์ ๋ณด๊ณ , ์ด ๋ ํฌ๊ฐ ๊ทธ ์ ์ด๋์ ์๋์ง ๋ณธ ๋ค ์ฝ์ต๋๋ค.
โ ์ ์ฒด ๊ตฌ์กฐ โ ์ธต์ผ๋ก ๋ณธ ๊ฒ. ์์์ ์๋๋ก ์์ฒญ์ด ๋ด๋ ค๊ฐ๊ณ , ์ด๋ ์ธต์ ๋ฌด์์ด ์๋์ง.
โก ์ ์ฒด ๊ตฌ์กฐ โ ์๋น์ค๋ผ๋ฆฌ ๋ฌด์์ ์ฃผ๊ณ ๋ฐ๋์ง. ์ด๋ก ์ค์ ์ด /internal ํธ์ถ, Kafka ํ๊ฐ ์ด๋ฒคํธ, ํ๋์ ์ ์ ์ด VPC ๊ฒฝ๊ณ.
โข ์ด ๋ ํฌ๋ฅผ ์ค์ฌ์ผ๋ก. ์ง์ ์ฐ๊ฒฐ๋ ๊ฒ๋ง ๋จ๊ธด ๊ทธ๋ฆผ.
โ โก ๋
service-template/docs์ ์๋ ๊ฒ์ ๊ฐ๋ฆฌํต๋๋ค. ์๋น์ค๊ฐ ๋์ด๋ ๊ทธ์ชฝ ํ ๊ณณ๋ง ๊ณ ์นฉ๋๋ค.
[ GitHub Packages ]
โ
โ com.pawtrail:common:0.0.13 (jar)
โผ
๋๋ฉ์ธ ์๋น์ค 14๊ฐ โโโถ ๊ณตํต ๋ชจ๋ ์๋ ์ค์ 7๊ฐ๊ฐ ์กฐ๊ฑด์ ๋ง์ผ๋ฉด ์ผ์ง
โ โ
โ โโโโถ ์๋ต ํ์ ยท ์์ธ ์ฒ๋ฆฌ
โ โโโโถ ์ธ์ฆ ํค๋ โ SecurityContext
โ โโโโถ BaseEntity ยท ๊ฐ์ฌ ์ปฌ๋ผ
โ โโโโถ Outbox ยท Inbox
โ โโโโถ ๋ค๋ฅธ ์๋น์ค๋ฅผ ๋ถ๋ฅด๋ RestClient ๋น๋
โ โโโโถ Flyway V1 ยท V2
โ
โโโโถ ๊ฐ์ ๋๋ฉ์ธ ์ฝ๋๋ง ์
์๋น์ค๋ ์์กด์ฑ ํ ์ค๋ง ์ถ๊ฐํ๋ฉด ๋ฉ๋๋ค. ๋๋จธ์ง๋ ์๋ ์ค์ ์ด ์์์ ์ผญ๋๋ค.
๊ณตํต ๋ชจ๋์ด ์์ผ๋ฉด ์ด๋ ๊ฒ ๋ฉ๋๋ค.
| ์์ผ๋ฉด | ์์ผ๋ฉด | |
|---|---|---|
| ์๋ต ํ์ | ํ ๊ณณ์์ ์ ์ | ์๋น์ค 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 (๊ฒ์ดํธ์จ์ด๋ง ์)
ํ ํฝ ์ด๋ฆ์ ์ฌ๊ธฐ ๋์ง ์๋ ์ด์ โ ํ ํฝ์ ๊ฐ๋ฐ ๋์ค ์ถ๊ฐยท๋ณ๊ฒฝ๋๋๋ฐ ๊ณตํต ๋ชจ๋์ ์์ผ๋ฉด ๊ทธ๋๋ง๋ค ์ฌ๋ฐฐํฌ์ ์ ์๋น์ค ๋ฒ์ ์ ์ด ํ์ํฉ๋๋ค. ์ด ๋ชจ๋์ ๊ธฐ์ค์ "๊ฑฐ์ ๋ฐ๋์ง ์๋ ๊ฒ" ์ ๋๋ค.
์๋๋ฅผ ๋ชจ๋ฅด๋ฉด ์ด ๋ฌธ์๊ฐ ์ ์ฝํ๋๋ค. ํ ๋ฌธ๋จ์ฉ๋ง ๋ณด๊ณ ๊ฐ๋๋ค.
โ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ โ ์ด ์ ์ฅ์๋ ์คํ๋์ง ์์ต๋๋ค
์๋น์ค ์ ์ฅ์ (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 โ ์ปจํ
์คํธ ๋ชจ๋ธ์ ๋ชป ๋ง๋ค์ด์
๋น๋๋ ํต๊ณผํจ
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๊ฐ๊ฐ ์กฐ๊ฑด์ ๋ณด๊ณ ์ผ์ง
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
์ด ์ค๋ฅ๊ฐ ๋๋ฉด ํ ํฐ ๋ฌธ์ ์ ๋๋ค. ๋ฉ์์ง์ ๊ทธ๋ ๊ฒ ์ ์ ํ ์์ต๋๋ค.
@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 ์ฐธ๊ณ .
# 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๊ฐ ์ผ์ ธ ์์ต๋๋ค.
๊ธฐ๋ ๋ก๊ทธ์์ ๋ด ๋๋ค.
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 ๊ฐ ์ฑ์์ง
์๋น์ค๊ฐ ์๋ฌด๊ฒ๋ ๋ฑ๋กํ์ง ์์๋ ์ผ์ง๋๋ค. ํด๋์คํจ์ค์ ๋ฌด์์ด ์๋์ง๋ฅผ ๋ณด๊ณ ์คํ๋ง์ด ํ๋จํฉ๋๋ค.
ํด๋์คํจ์ค์ ๋ฌด์์ด ์๋ โ ์ด๋ ์๋ ์ค์ ์ด ์ผ์ง๋
์๋ธ๋ฆฟ ์น + 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๊ฐ)
| ์๋ ์ค์ | ์กฐ๊ฑด | ๋ฑ๋กํ๋ 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 ์ด ์์ด ํญ์ ์ผ์ง๋๋ค.
EntityManager ๋ก ํ๋จํ๋ฉด ์ ๋ฉ๋๋ค.
๋ฌด์ํ ์๋น์ค๊ฐ JPA ์คํํฐ๋ฅผ ์ง์
โ
โโโ spring-data-jpa โ
ํจ๊ป ์ฌ๋ผ์ง
โโโ jakarta.persistence-api โ ๋จ์ ์์
โฒ
โโโ hibernate-spatial ์ด hibernate-core ๋ฅผ ๊ฑฐ์ณ ์ ์ด๋ก ๋๊ณ ์ด
(์ขํ ํ์
์ ์ฐ๋ ์๋น์ค๊ฐ ์์ด ์ง์ธ ์ ์์)
EntityManager ๋ก ํ๋จํ๋ฉด ์กฐ๊ฑด์ด ์ฐธ์ด ๋์ด ๋ฌด์ํ ์๋น์ค์์ JPA ์ค์ ์ด ์ผ์ง๊ณ ,
entityManagerFactory ๋น์ ๋ชป ์ฐพ์ ๊ธฐ๋์ ์คํจํฉ๋๋ค.
๋ฌธ์์ด๋ก ์ฐ๋ ์ด์ ์ ๋๋ค.
@ConditionalOnClass(name = "org.springframework.data.jpa.repository.JpaRepository")
โฒ
โโโ ํด๋์ค ๋ฆฌํฐ๋ด๋ก ์ฐ๋ฉด ์กฐ๊ฑด ํ๊ฐ ์์ ์ ๋ก๋ฉ์ ์๋ํด
NoClassDefFoundError ๊ฐ ๋ ์ ์์์คํ๋ง ์์ฒด ์๋ ์ค์ ๋ ์ด ํจํด์ ์๋๋ค.
๋๋ถ๋ถ์ @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์ ๊ฒฐ๋ก ์ด ์ ๋ฐ๋๋ผ ํท๊ฐ๋ฆฌ๊ธฐ ์ฌ์ด ์๋ฆฌ์ ๋๋ค.๊ทธ์ชฝ์ ์๋น์ค๊ฐ ์๊ธฐ ๊ฒ์ ๋ง๋ค๋ฉด ๋น์ด ๋์ด ๋์ด ์ด๋ ์ชฝ๋ ์ ์ฉ๋์ง ์๋๋ค ๋ผ ๋ง๋ค์ง ์๋ ๊ฒ์ด ๊ท์น์ด๊ณ , ์ด์ชฝ์ ์กฐ๊ฑด์ ๋ถ์ด์ง ์๋ ๊ฒ์ด ๋ต์ ๋๋ค.
๊ธฐ์ค์ ํ๋์ ๋๋ค. ๊ทธ ํ์ ์ ๋น์ ์ด๋ฏธ ๋๊ฐ ๋ง๋ค์ด ๋๋๋์ ๋๋ค.
์ฐ์์ด ์ ์ผ๋ก ๊ฐ๋ฆฝ๋๋ค. ์ฐ๋ฆฌ ์ฝ๋๊ฐ ์ฐ๋ ๊ฒ์ ์๋ ๋๋ฟ์ ๋๋ค.
| ๋น | ๋ฌด์์ด ๋ถ์ด ์๋ | ๋๊ฐ ์ฐ๋ |
|---|---|---|
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 ์ ์์ต๋๋ค.
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 ๋ฅผ ๋ถ๋ฆ ๋๋ค.
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 ํด์ ์
์ปจํธ๋กค๋ฌ๊ฐ 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() + ํ์ด์ง ์ ๋ณด ์กฐ๋ฆฝ์ด ํ์ด์ง ์๋ํฌ์ธํธ๋ง๋ค ๋ฐ๋ณต๋๊ณ ,
ํ๋ ์์๋ฅผ ๋ฐ๊ฟ ๋ฃ์ด๋ ์ปดํ์ผ์ด ํต๊ณผํฉ๋๋ค.
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 ์ผ๋ก ๋ฐ๋ ์์ด ๋๊ฐ ์ ์ด ์์ด
๊ทธ ํธ๋ค๋ฌ๋ฅผ ๋ง๋ค์์ต๋๋ค.
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 ๊ณตํต ์กฐ๊ฑด์ด๊ณ , ์ค์ ์กฐํ๋ฅผ ์ง๋ ์๋น์ค์์ ์ ํฉ๋๋ค.
๊ฒ์ดํธ์จ์ด โโโถ 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๊ฐ๊ฐ ์ ๋ถ ๋ฌด์๋ฏธํฉ๋๋ค.
ํธ๋์ญ์ ์ด๋ "์ฌ๋ฌ ์ฐ๊ธฐ๋ฅผ ํ ๋ฉ์ด๋ฆฌ๋ก โ ์ ๋ถ ๋๊ฑฐ๋ ์ ๋ถ ์ ๋๊ฑฐ๋" ์ ๋๋ค. ๊ณ์ 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-templateREADME 7-7 ์ ๋ง๋๋ ๋ฒ์ด ์์ต๋๋ค.
์นดํ์นด์์ ์์
โ
โโโโถ 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) ์ธ๋ฑ์ค๋ฅผ
๋ฏธ๋ฆฌ ๋ง๋ค์ด ๋์์ต๋๋ค.
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 ๋ก๋ง ๋ํ๋ฉ๋๋ค.
์์ฃผ ์ฐ๋ ์ฌ์ฏ ๊ฐ์ง์ ๋๋ค. ์๋น์ค ์ฝ๋์์ ์ค์ ๋ก ์ด๋ ๊ฒ ์๋๋ค.
@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)์ ๋๋ค. ๋ฌด์ธ์ ๋ฒ์ ์ ๋ง๋ค์ง ์์์ต๋๋ค.
์๋น์ค๋ง๋ค ์๊ธฐ 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" ๊ท์น์ ๋น์ฆ๋์ค ์์ธ๋ฅผ
๋งํ๋ ๊ฒ์ด๊ณ , ๊ณ์ฝ ์๋ฐ์ ์ฑ๊ฒฉ์ด ๋ค๋ฆ
๋๋ค.
@GetMapping
public CommonApiResponse<List<PetOutput>> getMyPets(
@CurrentUser CustomUserPrincipal principal) {
return CommonApiResponse.success(petService.findByAccount(principal.accountId()));
}CustomUserPrincipal ์ ๋๋ง ๊ฐ์ง๋๋ค.
record CustomUserPrincipal(UUID accountId, Role role)๊ฒ์ดํธ์จ์ด๊ฐ ํค๋๋ก ๋ฃ์ด ์ค ๊ฐ์ด๋ผ ํ ํฐ์ ํ์ฑํ๋ ์ฝ๋๊ฐ ์๋น์ค์ ์์ต๋๋ค.
@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๋ผ ์ด๊ธ๋๋ฉด ๊ธฐ๋์ด ์คํจํฉ๋๋ค.
์ด๋ฒคํธ๋ฅผ ์ ์ํฉ๋๋ค.
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๋ก ๋ค์ ์ฝ์ ์ ์๋ ๊ฐ์ ์ฃ์ง ์๊ณ , ๊ฐ์ธ์ ๋ณด๋ ๋์ฑ ์ฃ์ง ์์ต๋๋ค.
์๋น ์ ์ฉ 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 ๋ก ์ผํค๋ฉด ์คํจํ ์ด๋ฒคํธ๊ฐ ์ฑ๊ณต์ผ๋ก ์ฒ๋ฆฌ๋์ด ์ฌ๋ผ์ง
๋น๋๋ฅผ ์ฃผ์
๋ฐ์ 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 ์ ๋ถ๋ฅด๋ ์๋ฆฌ๋ ์์ญ ์ด๊ฐ ๊ฑธ๋ฆฝ๋๋ค.
์ด ๋ชจ๋์ ๊ณ ์น ๋๋ง ๋ด ๋๋ค. ์์ฃผ ์๋ ์ผ์ด ์๋๋๋ค.
โ ์ฝ๋๋ฅผ ๊ณ ์น๊ณ build.gradle ์ version ์ ์ฌ๋ฆผ
โ
โผ
โก ./gradlew publishToMavenLocal *๋ก์ปฌ์ ๋จผ์ ๋ฃ์
โ
โผ
โข ์๋น ์๋น์ค์์ ๊ฒ์ฆ
build.gradle ์ repositories ๋งจ ์์ mavenLocal() ์์ ์ถ๊ฐ
gradle.properties ์ commonVersion ์ ์ ๋ฒ์ ์ผ๋ก
./gradlew build ๊ฐ ํต๊ณผํ๋์ง
โ
โผ
โฃ ./gradlew publish GitHub Packages ๋ก
โ
โผ
โค mavenLocal() ์ ์ง์ฐ๊ณ ๋ค์ ๋น๋ *์์ ์ฃผ์
โ
โผ
โฅ ๋ฒ์ ํ๊ธฐ๋ฅผ ์ฌ๋ฌ ๊ณณ์ ๋ฐ์
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 ์์ ์ ๋ฒ์ ์ ๋ฐ์ ๊ทธ๋ฅ ํต๊ณผํฉ๋๋ค.
ํ๊ฒฝ๋ณ์๊ฐ ํฐ๋ฏธ๋ ์ธ์ ๋ง๋ค ์ฌ๋ผ์ง๋๋ค.
$env:GPR_USER = "<GitHub ์์ด๋>"
$env:GPR_TOKEN = (Get-Content C:\Tour_Prj\GPR_TOKEN.txt).Trim()
./gradlew publishexport GPR_USER=<GitHub ์์ด๋>
export GPR_TOKEN=$(cat ~/GPR_TOKEN.txt | tr -d '\n')
./gradlew publish| ๊ฑธ๋ฆฌ๋ ๊ฒ | ์ฆ์ |
|---|---|
| ์ ํฐ๋ฏธ๋์์ ์คํ | 401 Unauthorized |
.Trim() ์ ์ ํจ |
ํ์ผ ๋ ๊ฐํ์ด ๊ฐ์ ์์ฌ ๋ 401 |
ํ ํฐ์ write:packages ๊ฐ ์์ |
401 |
๋งค๋ฒ ๋ฃ๊ธฐ ์ฑ๊ฐ์๋ฉด OS ํ๊ฒฝ๋ณ์๋ก ๋ก๋๋ค. ํฐ๋ฏธ๋ ์ฌ์์์ด ํ์ํฉ๋๋ค.
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์์ ๋ฐ๊ฒฌํ์ต๋๋ค.
์๋น์ค๊ฐ ๋ฒ์ ์ ์ ์ฌ๋ฆฌ๋ฉด ์๊ฒ์ผ๋ก ๊ณ์ ๋น๋๋ฉ๋๋ค.
์ปดํ์ผ์ ์ ์์ ์ผ๋ก ๋๊ธฐ ๋๋ฌธ์ ์์์ฑ๊ธฐ ์ด๋ ต์ต๋๋ค. ๊ณตํต ๋ชจ๋์ด ๋ฐ๋์๋ค๋ ๊ณต์ง๋ฅผ ๋ฐ์ผ๋ฉด ์ด ๊ฐ๋ถํฐ ํ์ธํฉ๋๋ค.
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์์ ์ค์ ๋ก ๊ฒช์์ต๋๋ค.
// ์คํ ๊ฐ๋ฅ 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 ๊ฐ ์์ด ๊ธฐ๋์ ์คํจํฉ๋๋ค.
์ด ์ ์ฅ์์ ํ
์คํธ๋ ํ๋๋ฟ์
๋๋ค. 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๊ฐ์์ ๋๋ ์ด ๋ธ๋ก์ด ์์ด๋ ๋๋ฌ๋์ง ์์์ต๋๋ค.
| ์ฆ์ | ์์ธ |
|---|---|
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 ์ค์ ์ด ๋ด๋ ค์ค๋์ง
| ์ฆ์ | ์์ธ |
|---|---|
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;"| ์ฆ์ | ์์ธ |
|---|---|
MessageConversionException |
RecordMessageConverter ๊ฐ ์์. ์๋น์ค๊ฐ ์๊ธฐ ๊ฒ์ ์ ์ํ๋์ง |
| ๋ฆฌ์ค๋๊ฐ ์์ ์ ๋ถ๋ฆผ | ํ ํฝ ์ด๋ฆ์ด ๋ฐํ ์ชฝ๊ณผ ๋ค๋ฆ. ์ค๋ฅ๊ฐ ์ ๋๊ณ ์กฐ์ฉํจ |
data ๊ฐ LinkedHashMap |
์๋น DTO ํ์ ์ ์ ์ ์์ |
| ๋ฐํ ์ชฝ์ด ํ๋๋ฅผ ์ถ๊ฐํ์ ๊นจ์ง | ์๋น DTO ์ @JsonIgnoreProperties(ignoreUnknown = true) ๊ฐ ์์ |
| ๊ฐ์ ์ด๋ฒคํธ๊ฐ ๋ ๋ฒ ์ฒ๋ฆฌ๋จ | processOnce ๋ก ์ ๊ฐ์์ |
| ์คํจํ ์ด๋ฒคํธ๊ฐ ์ฌ๋ผ์ง | try-catch ๋ก ์ผ์ผฐ์. ์์ธ๋ฅผ ๊ทธ๋๋ก ๋์ง ๊ฒ |
RecordMessageConverter๋ฅผ ์๋น์ค๊ฐ ์ ์ํ๋ฉด ๋์ด ๋์ด ๋ ๋ค ์ ์ฉ๋์ง ์์ต๋๋ค. Boot ์ดgetIfUnique()๋ก ์ง์ด๊ฐ๊ธฐ ๋๋ฌธ์ ๋๋ค.
| ์ฆ์ | ์์ธ |
|---|---|
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 ๋ฌธ์์ด์ด๋ผ ์ฐ๋ฆฌ ๋ช ์ธ์ ์ค์ ์์ด ๋ง์ต๋๋ค.
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 ์ฒดํฌ ํด์
์ค์ ์ ๋ฐ๊พธ๊ธฐ ์ ์ ๊ทธ ํ์ผ์ ์ ์ฅํ์ง ์์ต๋๋ค. ํ๋ฉด๋ง ๊นจ์ง ์ํ์์ ์ ์ฅํ๋ฉด ์๋ณธ์ด ๋ฌผ์ํ๋ก ๋ฐ๋์ด ๋ณต๊ตฌ๋์ง ์์ต๋๋ค.
๊ฐ ํญ๋ชฉ์ ๋ฌธ์ โ ๊ณ ๋ฅธ ๊ฒ โ ๋ฒ๋ฆฐ ๊ฒ ์์์ ๋๋ค.
| ๊ณ ๋ฆ | ๋ฒ๋ฆผ |
|---|---|
AutoConfiguration.imports ๋ก๋ง ๋ฑ๋ก |
์๋น ์๋น์ค๊ฐ scanBasePackages ์ ๋ฃ๊ธฐ |
@ConditionalOnMissingBean ์ด ์ ๋ขฐ๋จ |
์ผ๋ฐ @Configuration ์ ์ฒ๋ฆฌ ์์๊ฐ ์ ๋ณด์ฅ๋ผ ์กฐ๊ฑด์ด ๋ค์งํ |
| ์ฑ ํด๋์ค์ ์ ์ ๊ฒ์ด ์ค์ด๋ฆ | 14๊ฐ ๋ ํฌ๊ฐ ์ ํํ ์ ์ด์ผ ํ๋ ๊ตฌ์กฐ |
auth ๊ฐ ์๊ธฐ SecurityFilterChain ์ ์ ์ํ๋ ์๋ฆฌ๊ฐ ์ค๊ณ์ ์ด๋ฏธ ์์๊ณ ,
๊ทธ๊ฒ์ด ์ฑ๋ฆฝํ๋ ค๋ฉด ์กฐ๊ฑด์ด ์ ๋ขฐ๋ผ์ผ ํ์ต๋๋ค.
์์ชฝ์ ๊ฑธ๋ฉด ์ ๋ฉ๋๋ค. ์๋ ์ค์ ํด๋์ค๊ฐ ์ค์บ์ ์กํ๋ฉด ์ฒ๋ฆฌ ์์๊ฐ ๊นจ์ ธ ์กฐ๊ฑด ํ๊ฐ๊ฐ ๋ค์งํ๊ณ ๊ฐ์
@Configuration์ด ๋ ๋ฒ ๋ฑ๋ก๋ ์ ์์ต๋๋ค. ์คํ๋ง ๋ถํธ๊ฐ ๋ช ์์ ์ผ๋ก ๊ธํ๋ ํํ์ ๋๋ค.
๊ทธ๋์ @Component ๋ฅผ ์ ๋ถ @Bean ์ผ๋ก ์ฎ๊ฒผ์ต๋๋ค.
imports ๋ก๋ง ๋ฑ๋กํ๋ฉด
โ
โโโ @Component ๋ ์ค์บ์ด ์ก๋ ๊ฒ์ด๋ผ ๋น์ด ์ ๋จ
AuditorProvider ยท GlobalExceptionHandler ยท InboxProcessor ...
โ
โโโ ์๋ ์ค์ ์ @Bean ์ผ๋ก ์ ์ธ
@RestControllerAdvice๋ ๋ผ๋ฉด ์ ๋ฉ๋๋ค.GlobalExceptionHandlerยทTraceIdResponseAdvice๋ ์ ๋ ธํ ์ด์ ์ผ๋ก ์ฐพ์์ง๋ฏ๋ก@Bean์ผ๋ก ๋ฑ๋กํด๋ ํด๋์ค์ ์ ๋ ธํ ์ด์ ์ ์ ์งํด์ผ ์ด๋๋ฐ์ด์ค๋ก ๋์ํฉ๋๋ค.
์กฐ๊ฑด ์ถ์ด ์ ํํ ์ด ๊ฐ์๋ก ๊ฐ๋ฆผ
์๋ธ๋ฆฟ ์น + 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๊ณผ ์ถฉ๋ํ ์ฌ์ง๊ฐ ์๊น๋๋ค.
๋ ๋ค ๊ธฐ๋ณธ order ๊ฐ LOWEST_PRECEDENCE
โ
โโโ ํ๋ก์ ์ ์ฉ ์์๊ฐ ๋ณด์ฅ๋์ง ์์
@Transactional ์ด ๋ฐ๊นฅ์ด๋ฉด ํธ์ถ ์ค๋ ๋์์ ํธ๋์ญ์
์ ์ด๊ณ
๋น๋๊ธฐ๋ก ๋์ด๊ฐ๋ฉฐ ๋๊น
๋น์ ๋๋ก ์ชผ๊ฐฐ์ต๋๋ค.
OutboxCommitListener @Async + @TransactionalEventListener ํธ๋์ญ์
์์
โ
โโโโถ OutboxPublisher @Transactional(REQUIRES_NEW) ๋ฐํ + ์ํ ๊ฐฑ์
๋ถ์ ํจ๊ณผ โ Relay ์ Listener ์ ๋ ๋ฒ ๋ณต์ฌ๋ผ ์๋ ๋ฐํ ๋ก์ง์ด ํ ๊ณณ์ผ๋ก ๋ชจ์์ต๋๋ค.
publish(UUID) ๊ฐ ์ํฐํฐ๊ฐ ์๋๋ผ ID ๋ฅผ ๋ฐ๋ ์ด์ ์
๋๋ค.
๋ค๋ฅธ ํธ๋์ญ์
์์ ์จ ์ํฐํฐ๋ ์ค์์์ด๋ผ dirty checking ์ด ์ ๊ฑธ๋ฆฝ๋๋ค.
์ ํธ๋์ญ์
์ด ์ง์ ์กฐํํด์ผ save() ์์ด UPDATE ๊ฐ ๋๊ฐ๋๋ค.
๊ณตํต ๋ชจ๋์ ๋ฃ์์ง๋ ๋งค๋ฒ ํ๋จํด์ผ ํ๋ ๋ฌผ์์ ๋๋ค. ๋ฃ์ผ๋ฉด 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๊ฐ๊ฐ ์ง๊ณ ๊ฐ๋ ๋น์ด ํ๋ ๋์ด๋ฉ๋๋ค.
common-web ยท common-jpa ยท common-messaging ์ผ๋ก ๋๋๋ฉด ์กฐ๊ฑด๋ถ ๋ก๋ฉ์ด
๊ฑฐ์ ๋ถํ์ํด์ง๋๋ค. ๊ทธ๋ฐ๋ฐ ์ ํ์ต๋๋ค.
| ์ชผ๊ฐ๋ฉด | ์ง๊ธ | |
|---|---|---|
| ๋ ํฌ ยท publish ยท ๋ฒ์ | 3๋ฐฐ | 1๊ฐ |
| ์กฐ๊ฑด๋ถ ๋ก๋ฉ | ๊ฑฐ์ ๋ถํ์ | ์ ๋ ธํ ์ด์ ํ ์ค |
| ๊ฒฝ๊ณ๋ฅผ ์ ํ๋ ๊ทผ๊ฑฐ | ์์ โ PageResponse ๊ฐ Page ๋ฅผ ์ฐธ์กฐํ๋๋ฐ web ์ธ๊ฐ jpa ์ธ๊ฐ |
โ |
| ๋๋๋ฆฌ๊ธฐ | ๋น์ | โ |
์๋น์ค๊ฐ 0๊ฐ์๋ ์์ ์ ๊ฒฝ๊ณ๋ฅผ ์ ํ ๊ทผ๊ฑฐ๊ฐ ์์์ต๋๋ค.
verdict ๊ฐ common ์ ์์ ์ ์ฐ๋ ์๋ ๊ฒํ ํ์ต๋๋ค.
verdict ๋ common 7๊ฐ ํจํค์ง ์ค 6๊ฐ๋ฅผ ์
โ
โโโ entity/ + audit/ ํ๋๋ง ์ ์
์ ์ฐ๋ฉด 6๊ฐ๋ฅผ ๋ณต์ฌํด์ผ ํ๊ณ
ํ์ชฝ๋ง ๊ณ ์ณ์ง๋ฉด ์๋ต ํํ๊ฐ ์กฐ์ฉํ ๊ฐ๋ฆผ
์ตํ ์๋จ์ผ๋ก ๋จ๊ฒจ ๋์์ต๋๋ค. ์กฐ๊ฑด๋ถ ๋ก๋ฉ์ด ๊ณ์ ๋ฌธ์ ๋ฅผ ์ผ์ผํค๋ฉด ๊ทธ๋ ๋ด ๋๋ค.
ํ ํฝ์ ๊ฐ๋ฐ ๋์ค ์ถ๊ฐยท๋ณ๊ฒฝยท์ญ์ ๋จ
โ
โโโ ๊ณตํต ๋ชจ๋์ ์์ผ๋ฉด ๊ทธ๋๋ง๋ค
์ฌ๋ฐฐํฌ + ์ ์๋น์ค ๋ฒ์ ์
์ด ํ์ํจ
์ด ๋ชจ๋์ ๊ธฐ์ค์ "๊ฑฐ์ ๋ฐ๋์ง ์๋ ๊ฒ" ์ ๋๋ค. 7-4 ์ ๊ธฐ์ค โ ์ ๊ฑธ๋ฆฐ ์ฌ๋ก์ ๋๋ค.
๋์ ๋ฌธ์์ด์ด ๋ ์ ์ฅ์์ ์กด์ฌํฉ๋๋ค. ๋ฐํ ์ชฝ getTopic() ๊ณผ ๋ฐ๋ ์ชฝ
@KafkaListener(topics = ...) ์
๋๋ค.
์ด๊ธ๋๋ฉด ์ค๋ฅ๊ฐ ๋์ง ์๊ณ ์ด๋ฒคํธ๋ง ์ค์ง ์์ต๋๋ค. ์ํ์ฑ ์
service-templateREADME 9-2 ๋ฅผ ๋จ์ผ ์ฐธ์กฐ๋ก ์ผ๊ณ Kafka UI ๋ก ์ค๋ฌผ์ ํ์ธํ๋ ๊ฒ์ ๋๋ค.
AuthContextHeaders ๋ ๊ฐ์ ๋ถ๋ฅ์
๋๋ค โ ๊ฒ์ดํธ์จ์ด๊ฐ common ์ ์ ์จ์
4๋ฒ์งธ ์ฌ๋ณธ์ด ๋ฉ๋๋ค.
์ปจ์๋จธ๊ฐ ๋ง๋๋ ํ์ 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 ๊ฐ ์ด๋ฏธ ๋น์ด ์์
๊บผ๋ผ ๊ฐ์ด ์์
PK Hibernate @UuidGenerator(style = VERSION_7)
์ด๋ฒคํธ ID EventEnvelope.generateUuidV7() ์์ฒด ๊ตฌํ
Hibernate ๊ฒ์ ์ฌ์ฌ์ฉํ๋ฉด ์ ๋ฉ๋๋ค.
UuidVersion7Strategy ๋ hibernate-core ์ ์์
โ
โโโ ๋ฌด์ํ ์๋น์ค(verdict ยท congestion)์๋ Hibernate ๊ฐ ์์
๊ทธ๋ฐ๋ฐ verdict ๋ ์ด๋ฒคํธ๋ฅผ ์๋นํ๋ฉฐ EventEnvelope ๋ฅผ ๋ค๋ฃธ
โ
โโโ ์ฌ์ฌ์ฉํ๋ฉด message/ ๊ฐ JPA ์ ๋ฌถ์ฌ
์กฐ๊ฑด๋ถ ๋ก๋ฉ์ ์๋ฏธ๊ฐ ์ฌ๋ผ์ง
์คํ๋ง ์นดํ์นด Observation ์ ๋งก๊น๋๋ค.
์ฐ๋ฆฌ๊ฐ ๋ง๋ ํค๋ ์ฐ๋ฆฌ๊ฐ ๋ฃ์ X-User-Id ยท X-User-Role
W3C ํ์ค ํค๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ traceparent
์์ผ๋ก ๋ฃ์ผ๋ฉด ํค๋๊ฐ ์ค๋ณต๋์ด ํ์ค์ ๋ฌดํจ ์ฒ๋ฆฌ๋๊ณ ๋ฐ๋ ์ชฝ์ด ์ trace ๋ฅผ ์์ํฉ๋๋ค.
โ ๋จ Kafka ๋ Observation ์ ์ผ์ผ๋ง ๋์ํฉ๋๋ค.
spring: kafka: template: { observation-enabled: true } listener: { observation-enabled: true }ํ๋ก๋์๋ง ์ผ๋ฉด ํค๋๋ ์ค๋ ค ๊ฐ์ง๋ง ์ปจ์๋จธ๊ฐ ์ฝ์ง ์์ ๋ฐ์ชฝ์ ๋๋ค.
| ๋ฒ์ | ๋ฌด์์ด | ์ |
|---|---|---|
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 ๋ ์ฐ๋ ค๋ ์๊ฐ ๋งํ๋
๊ฒ์ด๋ผ ์ฑ๊ฒฉ์ด ๋ค๋ฆ
๋๋ค.
โ
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 ์ธ ์คํจ ์๋ต
์๋น์๊ฐ ์๋ ์๋น์ค๊ฐ ์๊ฒจ์ผ ํ์ธ๋ฉ๋๋ค.
โฌ Inbox ์ ์ฒด ํ๋ฆ InboxProcessor ์ ํธ๋์ญ์
์ด ์ค์ ๋ก ๋ฌถ์ด๋
โฌ KafkaSecurityInterceptor ๋ฆฌ์ค๋ ์ปจํ
์ด๋์ ์ค์ ๋ก ๋ถ๋
โฌ ์ฌ์๋ ยท DLQ 1s โ 2s โ 4s ๋ค {ํ ํฝ}.dlq ๋ก ๊ฐ๋
โฌ RecordMessageConverter EventEnvelope<T> ๋ก ๋ฐ์์ง๋
auth ๋ก๋ ํ์ธํ ์ ์์ต๋๋ค.
@KafkaListener๊ฐ ํ ๊ฐ๋ ์์ด ์ธํฐ์ ํฐ๊ฐ ๋ถ์ ์๋ฆฌ ์์ฒด๊ฐ ์์ต๋๋ค.user ยท pet ยท search ยท review ยท notification ์ค ํ๋๊ฐ ์๊ธฐ๋ฉด ํ์ธ๋ฉ๋๋ค.
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 ์ผ๋ก ๋๊ฐ๋๋ค.
| ๋ฌด์ | ์ธ์ |
|---|---|
| ์ํํธ ๋๋ฆฌํธ ์กฐํ ๋ฐฉ์ | ์ค์ ์กฐํ๋ฅผ ์ง๋ ์๋น์ค์์ |
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์ผ๋ก ๋จ์ต๋๋ค.
๊ณตํต ์ฉ์ด๋ 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 ์ ๊ฒ์ฆ์ฉ |