ShootPointer๋ ๋๊ตฌ ์์์ ์ ๋ก๋ํ๋ฉด ๊ฐ์ธ ํ์ด๋ผ์ดํธ๋ฅผ ์์ฑํ๊ณ , ์์ฑ๋ ๊ฒฐ๊ณผ๋ฅผ ์ปค๋ฎค๋ํฐ ๊ฒ์๊ธ๊ณผ ๋ญํน์ผ๋ก ์ฐ๊ฒฐํ๋ ์๋น์ค์ ๋๋ค.
- ์ด ์๋น์ค๊ฐ ์ด๋ค ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ค๋์ง
- ์์ฒญ์ด ๋ค์ด์์ ๋ ๋ฐฑ์๋ ๋ด๋ถ์์ ์ด๋ค ๊ณ์ธต๊ณผ ์ ์ฅ์๋ฅผ ๊ฑฐ์น๋์ง
- ์ธ๋ถ OpenCV ์๋ฒ์ ์ด๋ป๊ฒ ํ์ ํ๋์ง
- ์ข์์/๋ญํน/๊ฒ์/์ค์๊ฐ ์งํ๋ฅ ๊ฐ์ ํต์ฌ ๊ธฐ๋ฅ์ด ์ด๋ป๊ฒ ๋์ํ๋์ง
- ๐ ํ๋ก์ ํธ ํ๋์ ๋ณด๊ธฐ
- ๐บ๏ธ ์๋น์ค ์ ์ฒด ํ๋ฆ
- ๐งฐ ๊ธฐ์ ์คํ๊ณผ ์ ์ฅ์ ์ญํ
- ๐๏ธ ๋ฐฑ์๋ ์ฒ๋ฆฌ ๊ตฌ์กฐ
- ๐ ์์ธ ๊ธฐ๋ฅ ์ค๋ช
- ๐๏ธ ๋ฐ์ดํฐ ๋ชจ๋ธ
- โ๏ธ ์ด์ / ๋ฐฐํฌ / CI
- โ ํ ์คํธ์ ํ์ง ๊ด๋ฆฌ
- ๐งฑ ๋ฉํฐ๋ชจ๋ ๋ฆฌํฉํฐ๋ง
๋๊ตฌ ์์ ์๋น์ค์์ ์ฌ์ฉ์๋ ๋ณดํต ๋ค์ ๋ฌธ์ ๋ฅผ ๊ฒช์ต๋๋ค.
- ๊ฒฝ๊ธฐ ์์์ ์ฌ๋ ค๋ ์ํ๋ ์ฅ๋ฉด๋ง ๋ค์ ๋ณด๊ธฐ ์ด๋ ต๋ค.
- ๋ด ํ๋ ์ด๋ง ๋ชจ์์ ์๋นํ๊ธฐ ์ด๋ ต๋ค.
- ์ฝํ ์ธ ๊ฐ ์์ ํ์ผ์ ๋จธ๋ฌด๋ฅด๊ณ ์ปค๋ฎค๋ํฐ ํ๋์ผ๋ก ์ ์ด์ด์ง์ง ์๋๋ค.
- ๋จ์ ์ ์ฅ์ด ์๋๋ผ "๊ฒ์", "๋ญํน", "์ค์๊ฐ ์ํ ํ์ธ"๊น์ง ์ด์ด์ ธ์ผ UX๊ฐ ์ข์์ง๋ค.
ShootPointer๋ ์ด ๋ฌธ์ ๋ฅผ ์๋ ํ๋ฆ์ผ๋ก ํ๊ณ ์ ํ์ต๋๋ค.
- ์นด์นด์ค ๋ก๊ทธ์ธ์ผ๋ก ํ์ ์๋ณ
- ๋ฑ๋ฒํธ ๋ฑ๋ก์ผ๋ก OpenCV ์๋ฒ๊ฐ ์ฌ์ฉ์๋ฅผ ์ธ์ํ ๊ธฐ์ค ํ๋ณด
- ์์ ์ ๋ก๋ ๊ถํ์ ๋ฐฑ์๋๊ฐ ๋ฐ๊ธ
- OpenCV ์๋ฒ๊ฐ ๊ฐ์ธ ํ์ด๋ผ์ดํธ๋ฅผ ์์ฑ
- ์์ฑ ์งํ๋ฅ ์ ์ค์๊ฐ์ผ๋ก ์ ๋ฌ
- ์์ฑ๋ ํ์ด๋ผ์ดํธ๋ฅผ ๊ฒ์๊ธ, ์ข์์, ๋๊ธ, ๋ญํน์ผ๋ก ํ์ฅ
flowchart LR
A["์นด์นด์ค ๋ก๊ทธ์ธ"] --> B["JWT ์ธ์ฆ"]
B --> C["๋ฑ๋ฒํธ ๋ฑ๋ก"]
C --> D["์์ ์
๋ก๋์ฉ ์๋ช
URL ๋ฐ๊ธ"]
D --> E["OpenCV ์๋ฒ๋ก ์์ ์
๋ก๋"]
E --> F["Redis Pub/Sub ์งํ๋ฅ ๋ฐํ"]
F --> G["SSE๋ก ํด๋ผ์ด์ธํธ์ ์ค์๊ฐ ์ ๋ฌ"]
E --> H["ํ์ด๋ผ์ดํธ ๊ฒฐ๊ณผ Webhook"]
H --> I["Highlight ์ ์ฅ"]
I --> J["๊ฒ์๊ธ ์์ฑ"]
J --> K["๋๊ธ / ์ข์์ / ๊ฒ์"]
K --> L["๋ญํน ์กฐํ ๋ฐ ์ง๊ณ"]
- OAuth2 ์์ ๋ก๊ทธ์ธ๊ณผ JWT ๊ธฐ๋ฐ ์ธ์ฆ/์ธ๊ฐ
- ์ธ๋ถ OpenCV ์๋ฒ์์ HTTP ์ฐ๋
- Redis Pub/Sub + SSE ๊ธฐ๋ฐ ์ค์๊ฐ ์ํ ์ ๋ฌ
- PostgreSQL / Redis / Elasticsearch / MongoDB๋ฅผ ์ญํ ๋ณ๋ก ๋ถ๋ฆฌํ ์ค๊ณ
- JPA ์ค์ฌ์ ๋๋ฉ์ธ ๋ก์ง๊ณผ Query ๋ถ๋ฆฌ
- ์ข์์ ๋์์ฑ ์ ์ด์ 10,000๊ฑด ๋์์ฑ ํ ์คํธ
- Docker Compose์ Jenkins ๊ธฐ๋ฐ ์ด์ ์๋ํ
- Spring Modulith ๊ธฐ๋ฐ ๋ชจ๋ ๋ถ๋ฆฌ ๋ฆฌํฉํฐ๋ง
flowchart LR
Client["Client"]
Api["API Server\nSpring Boot"]
Batch["Batch Server\n๋ถ๋ฆฌ ๊ตฌ์กฐ ์ค๊ณ"]
Kakao["Kakao OAuth"]
OpenCV["OpenCV Server"]
Postgres["PostgreSQL"]
Redis["Redis"]
ES["Elasticsearch"]
Mongo["MongoDB"]
Kibana["Kibana"]
Client --> Api
Api --> Kakao
Api --> Postgres
Api --> Redis
Api --> ES
Api --> Mongo
Api --> OpenCV
OpenCV -. "progress channel publish" .-> Redis
Batch -. "aggregation / snapshot" .-> Mongo
Batch -. "batch query" .-> Postgres
Kibana --> ES
| ์ ์ฅ์ | ์ญํ |
|---|---|
| PostgreSQL | ํ์, ๋ฑ๋ฒํธ, ํ์ด๋ผ์ดํธ, ๊ฒ์๊ธ, ๋๊ธ, ์ข์์ ๋ฑ ์ด์ ๋ฐ์ดํฐ ์ ์ฅ |
| Redis | Refresh Token, ์ ๋ก๋ Job TTL, ๋ญํน ZSet, OpenCV ์งํ๋ฅ Pub/Sub |
| Elasticsearch | ๊ฒ์๊ธ ๊ฒ์๊ณผ ์๋์์ฑ |
| MongoDB | ๊ธฐ๊ฐ๋ณ ๋ญํน ์ค๋ ์ท ๋ฌธ์ ์ ์ฅ |
์ด ํ๋ก์ ํธ์์ Redis๋ ๋จ์ ์บ์๊ฐ ์๋๋ผ 4๊ฐ์ง ์ญํ ์ ํฉ๋๋ค.
| ๋ชฉ์ | ์ฌ์ฉ ๋ฐฉ์ |
|---|---|
| ์ธ์ฆ | refresh:{email} ํํ๋ก Refresh Token ์ ์ฅ |
| ์ ๋ก๋ ์ถ์ | prefix + memberId์ jobId ์ ์ฅ |
| ์ค์๊ฐ ์ด๋ฒคํธ | OpenCV ์งํ๋ฅ Pub/Sub ์ฑ๋ ๊ตฌ๋ |
| ๋ญํน | ZSet ๊ธฐ๋ฐ ์ฃผ๊ฐ/์๊ฐ ํ์ฌ ๋ญํน ์ ์ฅ |
์ด ๋ฐฑ์๋๋ ํ์ผ์ ์ง์ ๋ถ์ํ์ง ์์ต๋๋ค.
๋์ OpenCV ์๋ฒ์ ์ญํ ์ ๋ถ๋ฆฌํด ์๋์ฒ๋ผ ํ์
ํฉ๋๋ค.
- ๋ฐฑ์๋๋ ์ฌ์ฉ์ ์ธ์ฆ, ์ ๋ก๋ ๊ถํ ๋ฐ๊ธ, ๊ฒฐ๊ณผ ์ ์ฅ, ์งํ๋ฅ ์ ๋ฌ์ ๋ด๋น
- OpenCV ์๋ฒ๋ ์์ ์ฒ๋ฆฌ, ๊ฐ์ธ ์๋ณ, ํ์ด๋ผ์ดํธ ์์ฑ, ์งํ๋ฅ ๋ฐํ์ ๋ด๋น
์ฆ, ์ด ์๋น์ค์ ํต์ฌ์ "๋ฏธ๋์ด ์ฒ๋ฆฌ ์์ฒด"๋ณด๋ค "๋ฏธ๋์ด ์ฒ๋ฆฌ ์๋ฒ์ ์์ ํ๊ฒ ํ์ ํ๋ ๋ฐฑ์๋ ์ค๊ณ"์ ์์ต๋๋ค.
| ๋ถ๋ฅ | ๊ธฐ์ |
|---|---|
| Language | Java 21 |
| Framework | Spring Boot 3.4.5 |
| Security | Spring Security, JWT, OAuth2 Client |
| ORM | Spring Data JPA |
| DB | PostgreSQL |
| Cache / Messaging | Redis, Redisson |
| Search | Elasticsearch |
| Document DB | MongoDB |
| Async / Client | WebClient, RestTemplate, SSE |
| Batch | Spring Batch |
| Docs | Swagger / OpenAPI |
| Test | JUnit5, Spring Boot Test, Testcontainers |
| Infra | Docker, Docker Compose, Jenkins, Azure |
- ์ ํฉ์ฑ์ด ์ค์ํ ์ด์ ๋ฐ์ดํฐ๋ PostgreSQL์ ์ ์ฅ
- ๋น ๋ฅธ ํ ํฐ ์ ์ฅ๊ณผ ์ด๋ฒคํธ ๋ธ๋ก์ปค ์ญํ ์ Redis์ ์์
- ๊ฒ์๊ธ ๊ฒ์ ํ์ง์ Elasticsearch๋ก ๋ถ๋ฆฌ
- ๊ณผ๊ฑฐ ๊ธฐ๊ฐ ๋ญํน ์ค๋ ์ท์ MongoDB์ ์ ์ฅ
์ฆ, ํ DB์ ๋ค ๋ชฐ์๋ฃ์ง ์๊ณ "๊ฐ ์ ์ฅ์์ ๊ฐ์ "์ ๋ง๊ฒ ์ญํ ์ ๋ถ๋ฆฌํ ์ค๊ณ์ ๋๋ค.
๋ง์ ๋๋ฉ์ธ ๊ธฐ๋ฅ์ด ์๋ ๊ณ์ธต ๊ตฌ์กฐ๋ฅผ ๋ฐ๋ณตํฉ๋๋ค.
flowchart LR
A["Controller"] --> B["Service"]
B --> C["Manager"]
C --> D["Helper"]
D --> E["Util / Validation / Mapper"]
E --> F["Repository"]
F --> G["DB / External System"]
| ๊ณ์ธต | ์ญํ |
|---|---|
| Controller | HTTP ์์ฒญ/์๋ต, ํ๋ผ๋ฏธํฐ ์์ง |
| Service | ํธ๋์ญ์ ์์์ , ์ ์ฆ์ผ์ด์ค ์์ |
| Manager | ์ค์ ๋น์ฆ๋์ค ํ๋ฆ ์กฐํฉ |
| Helper | Validation / Util ์กฐํฉ์ ์ํ ์ค๊ฐ ๊ณ์ธต |
| Util / Validation / Mapper | ์กฐํ, ์ ์ฅ, ๊ฒ์ฆ, DTO-Entity ๋ณํ |
| Repository | DB ์ ๊ทผ |
์ด ๊ตฌ์กฐ๋ ๋ค์ ๋ ๊ฑฐ์์ค๋ฝ์ง๋ง, ์ฑ ์์ ์ชผ๊ฐ์ ํ ์คํธํ๊ธฐ ์ข๊ณ , ๊ฐ ๋จ์๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ๊ต์ฒดํ๊ธฐ ์ฝ๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค.
๋ชจ๋ API๋ ๊ณตํต ์๋ต ํฌ๋งท์ ์ฌ์ฉํฉ๋๋ค.
- ์ฑ๊ณต ์:
status,success=true,data - ์คํจ ์:
status,success=false,error
์ด๋ฅผ ํตํด ํ๋ก ํธ์๋๊ฐ ๋๋ฉ์ธ๋ณ๋ก ์๋ต ํ์์ ๋ค๋ฅด๊ฒ ํ์ฑํ ํ์๋ฅผ ์ค์์ต๋๋ค.
GlobalExceptionHandler๊ฐ ๋ค์ ์์ธ๋ฅผ ์ผ๊ด๋ ์๋ต์ผ๋ก ๋ณํํฉ๋๋ค.
- ์กด์ฌํ์ง ์๋ endpoint
CustomException- ๊ทธ ์ธ ๊ธฐ๋ณธ ์์ธ
๋๋ถ๋ถ์ ์ํฐํฐ๋ ๊ณตํต ๋ฒ ์ด์ค ์ํฐํฐ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
created_at,modified_at์๋ ๊ด๋ฆฌis_deleted,deleted_at๊ธฐ๋ฐ ๋ ผ๋ฆฌ ์ญ์
ํนํ ๊ฒ์๊ธ๊ณผ ๋๊ธ์ ๋ ผ๋ฆฌ ์ญ์ ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋์ํฉ๋๋ค.
PostEntity๋@SQLRestriction("is_deleted = false")Comment๋ ๋์ผํ ๋ ผ๋ฆฌ ์ญ์ ํํฐ๋ฅผ ์ฌ์ฉ
์ฆ, ์ญ์ ์์ฒญ์ด ์๋ ์ค์ row๋ฅผ ๋ฐ๋ก ์์ ๋ ๋์ soft delete๋ก ์ฒ๋ฆฌํฉ๋๋ค.
ํ์ ์ด๋ฆ๊ณผ ์ด๋ฉ์ผ์ DB ์ปฌ๋ผ ์ปจ๋ฒํฐ๋ฅผ ํตํด ์ํธํ ์ ์ฅ๋ฉ๋๋ค.
member_nameemail
์ฆ, ๋ฏผ๊ฐํ PII๋ฅผ ํ๋ฌธ์ผ๋ก ์ง์ ์ ์ฅํ์ง ์๋๋ก ์ค๊ณํ์ต๋๋ค.
๊ณตํต ๊ด์ฌ์ฌ๋ฅผ ์ํด AOP๋ ์ผ๋ถ ์ฌ์ฉํฉ๋๋ค.
@CustomLog๊ธฐ๋ฐ ๋ฉ์๋ ๋ก๊นDistributedLockAspect๊ธฐ๋ฐ ๋ถ์ฐ๋ฝ ํ์ฅ ํฌ์ธํธ
๋ค๋ง ํ์ฌ "์ข์์ ์ ํฉ์ฑ"์ ์ต์ข ๊ตฌํ์ ๋ถ์ฐ๋ฝ์ด ์๋๋ผ DB ๋น๊ด์ ๋ฝ์ ์ ํํ์ต๋๋ค.
| Method | Endpoint | ํธ์ถ์ | ์ค๋ช |
|---|---|---|---|
| GET | /kakao/callback |
Kakao redirect | ์ธ๊ฐ ์ฝ๋๋ฅผ ๋ฐ์ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ํ JWT ๋ฐ๊ธ |
| GET | /member/me |
๋ก๊ทธ์ธ ์ฌ์ฉ์ | ๋ด ์ ๋ณด ์กฐํ |
| DELETE | /kakao |
๋ก๊ทธ์ธ ์ฌ์ฉ์ | ํ์ ํํด |
| PUT | /agree/highlight |
๋ก๊ทธ์ธ ์ฌ์ฉ์ | ํ์ด๋ผ์ดํธ ์ง๊ณ ๋์ |
| GET | /admin/token |
๋ด๋ถ/ํ ์คํธ | ๊ด๋ฆฌ์์ฉ ํ ํฐ ๋ฐ๊ธ |
| POST | /admin/token/check |
๋ด๋ถ/ํ ์คํธ | ํ ํฐ์ผ๋ก ํ์ ๋๊ธฐํ |
| GET | /admin/token/refresh/{email} |
๋ด๋ถ/ํ ์คํธ | Refresh Token ์กฐํ |
sequenceDiagram
participant Client
participant Kakao
participant Controller as MemberCommandController
participant Service as MemberCommandService
participant KakaoService
participant Manager as MemberManager
participant DB as PostgreSQL
participant Redis
Client->>Kakao: ์นด์นด์ค ๋ก๊ทธ์ธ
Kakao-->>Client: authorization code
Client->>Controller: GET /kakao/callback?code=...
Controller->>Service: processKakaoLogin(request)
Service->>Manager: callback ๊ฒ์ฆ
Service->>KakaoService: ์ฌ์ฉ์ ์ ๋ณด ์์ฒญ
KakaoService->>Kakao: access token ์์ฒญ
KakaoService->>Kakao: user info ์์ฒญ
Service->>Manager: ํ์ ์กฐํ ๋๋ ์ ๊ท ์์ฑ
Manager->>DB: member find/save
Service->>Redis: Refresh Token ์ ์ฅ
Service-->>Controller: KakaoDTO(access/refresh ํฌํจ)
Controller-->>Client: ๋ก๊ทธ์ธ ์ฑ๊ณต ์๋ต
/kakao/callback์ด ํธ์ถ๋๋ฉดMemberCommandServiceImpl.processKakaoLogin์ด ์์๋ฉ๋๋ค.- ์์ฒญ์์
code๋ฅผ ์ถ์ถํ๊ณ ,KakaoServiceImpl์ด ์ค์ ์นด์นด์ค API ํธ์ถ์ ์ํํฉ๋๋ค. KakaoApiHelperImpl์ด- ํ ํฐ endpoint๋ก access token ์์ฒญ
- ์ฌ์ฉ์ ์ ๋ณด endpoint๋ก ํ๋กํ ์กฐํ ๋ฅผ ์์ฐจ์ ์ผ๋ก ์ํํฉ๋๋ค.
MemberManager๋ ์ด๋ฉ์ผ ๊ธฐ์ค์ผ๋ก ํ์์ ์ฐพ๊ณ , ์์ผ๋ฉด ์ ๊ท ํ์์ ์์ฑํฉ๋๋ค.TokenServiceImpl์ด Access Token๊ณผ Refresh Token์ ๋ฐ๊ธํฉ๋๋ค.- Refresh Token์ Redis์ ์ ์ฅ๋ฉ๋๋ค.
- ์ต์ข
์ ์ผ๋ก
KakaoDTO์ access / refresh token์ ๋ด์ ๋ฐํํฉ๋๋ค.
sequenceDiagram
participant Client
participant Filter as JwtAuthenticationFilter
participant Jwt as JwtHandler/JwtUtil
participant UDS as CustomUserDetailsService
participant MQS as MemberQueryService
participant DB as PostgreSQL
participant SC as SecurityContext
participant Controller
Client->>Filter: API ์์ฒญ + Bearer Token
Filter->>Jwt: token resolve / validate
Jwt->>Jwt: email ์ถ์ถ
Filter->>UDS: loadUserByUsername(email)
UDS->>MQS: findByEmail(email)
MQS->>DB: member ์กฐํ
UDS-->>Filter: CustomUserDetails ๋ฐํ
Filter->>SC: Authentication ์ ์ฅ
SC-->>Controller: ํ์ฌ ์ฌ์ฉ์ ์ ๊ทผ ๊ฐ๋ฅ
/member/me๋ ๋จ์ ํ์ row ์กฐํ๊ฐ ์๋๋๋ค.
ํ์ ๊ธฐ๋ณธ ์ ๋ณด + ๋ฑ๋ฒํธ + ์ ํต๊ณ + ํ์ด๋ผ์ดํธ ๊ฐ์๋ฅผ ์กฐํฉํด ์๋ตํฉ๋๋ค.
flowchart LR
A["/member/me ์์ฒญ"] --> B["SecurityUtils.getCurrentMember()"]
B --> C["MemberManager.getMemberInfo(memberId)"]
C --> D["Member ์กฐํ"]
C --> E["ํ์-๋ฑ๋ฒํธ ์กฐํ"]
C --> F["Highlight 2์ ํฉ ์กฐํ"]
C --> G["Highlight 3์ ํฉ ์กฐํ"]
C --> H["Highlight ๊ฐ์ ์กฐํ"]
D --> I["MemberResponseDto ์กฐํฉ"]
E --> I
F --> I
G --> I
H --> I
- ํ์ ํํด
- ํ์ฌ ์ธ์ฆ๋ ํ์์ธ์ง ๊ฒ์ฆ
- DB์์ ํ์ ์ญ์
- Redis Refresh Token ์ญ์
- ์ง๊ณ ๋์
Member.agree()๋๋ฉ์ธ ๋ฉ์๋ ํธ์ถ- ์ด๋ฏธ ๋์ํ๋ค๋ฉด ์์ธ ์ฒ๋ฆฌ
์ฆ, ๋จ์ flag ์์ ์ด ์๋๋ผ ๋๋ฉ์ธ ์ํ ๋ณ๊ฒฝ์ผ๋ก ์ทจ๊ธํฉ๋๋ค.
| Method | Endpoint | ์ธ์ฆ | ์ค๋ช |
|---|---|---|---|
| POST | /api/backNumber |
์ฌ์ฉ์ JWT | ๋ฑ๋ฒํธ์ ๋ฑ๋ฒํธ ์ด๋ฏธ์ง ๋ฑ๋ก |
sequenceDiagram
participant Client
participant Controller as BackNumberController
participant Service as BackNumberCommandService
participant Manager as BackNumberManager
participant BackNumberHelper
participant MemberBackNumberHelper
participant OpenCV as OpenCVClient
participant DB as PostgreSQL
Client->>Controller: multipart ์์ฒญ(backNumberRequestDto, image)
Controller->>Service: create(member, backNumberEntity, image)
Service->>Manager: create(...)
Manager->>BackNumberHelper: ๋ฑ๋ฒํธ ์กฐํ ๋๋ ์์ฑ
BackNumberHelper->>DB: back_number ์ ์ฅ/์กฐํ
Manager->>MemberBackNumberHelper: ํ์-๋ฑ๋ฒํธ ๋งคํ ์กฐํ ๋๋ ์์ฑ
MemberBackNumberHelper->>DB: member_back_number ์ ์ฅ/์กฐํ
Manager->>OpenCV: ๋ฑ๋ฒํธ ์ด๋ฏธ์ง ์ ์ก
OpenCV-->>Manager: ๊ฒ์ฆ ์๋ต
Manager-->>Controller: ๋ฑ๋ก๋ ๋ฑ๋ฒํธ ๋ฐํ
- ๋ฑ๋ฒํธ ์์ฒด๋
back_number๋ง์คํฐ ํ ์ด๋ธ์์ ๊ด๋ฆฌํฉ๋๋ค. - ์ฌ์ฉ์์ ๋ฑ๋ฒํธ์ ๊ด๊ณ๋
member_back_numberํ ์ด๋ธ๋ก ๋ถ๋ฆฌํฉ๋๋ค. - ์ฆ, "๋ฑ๋ฒํธ ๊ฐ"๊ณผ "๋๊ฐ ๊ทธ ๋ฑ๋ฒํธ๋ฅผ ์ฐ๋๊ฐ"๋ฅผ ๋ถ๋ฆฌํ ๊ตฌ์กฐ์ ๋๋ค.
- ๋ง์ง๋ง ๋จ๊ณ์์ OpenCV ์๋ฒ์ ๋ฑ๋ฒํธ ์ด๋ฏธ์ง์ ๋ฑ๋ฒํธ ๊ฐ์ ํจ๊ป ์ ๋ฌํด ์ดํ ๊ฐ์ธ ํ์ด๋ผ์ดํธ ์์ฑ์ ๊ธฐ์ค ์ ๋ณด๋ฅผ ๋ง๋ญ๋๋ค.
OpenCVClientImpl.sendBackNumberInformation์ ์๋ ๋ฐฉ์์ผ๋ก ๋์ํฉ๋๋ค.
- multipart/form-data POST
- ํค๋์
X-Member-Id์ ๋ฌ - body์
image,backNumberํฌํจ - ์ต๋ 15์ด timeout
IOException,TimeoutException,WebClientRequestException๊ณ์ด์ ์ฌ์๋- ์๋ต์ด ์คํจ๊ฑฐ๋ ๋ฑ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ผ๋ฉด ์์ธ ์ฒ๋ฆฌ
์ฆ, ๋จ์ ์ธ๋ถ API ํธ์ถ์ด ์๋๋ผ ๋คํธ์ํฌ ์ค๋ฅ์ ๋๋ฉ์ธ ์ค๋ฅ๋ฅผ ๊ตฌ๋ถํด์ ๋ค๋ฃน๋๋ค.
| Method | Endpoint | ์ธ์ฆ | ์ค๋ช |
|---|---|---|---|
| POST | /api/pre-signed |
์ฌ์ฉ์ JWT | OpenCV ์ ๋ก๋์ฉ ์๋ช URL ๋ฐ๊ธ |
ํด๋ผ์ด์ธํธ๊ฐ ์์๋ก OpenCV ์ ๋ก๋ endpoint์ ๋ฐ๋ก ํ์ผ์ ๋์ง๋ฉด ๋ค์ ๋ฌธ์ ๊ฐ ์๊น๋๋ค.
- ๋๊ฐ ์ด๋ค ๊ถํ์ผ๋ก ์ ๋ก๋ํ๋์ง ๋ฐฑ์๋๊ฐ ํต์ ํ๊ธฐ ์ด๋ ต๋ค.
- ์ ๋ก๋ ์์ ๊ณผ ์ฌ์ฉ์/์์ ID ๋งคํ์ ์ถ์ ํ๊ธฐ ์ด๋ ต๋ค.
- ์งํ๋ฅ ๊ตฌ๋ ๊ณผ ๊ฒฐ๊ณผ ์กฐํ๋ฅผ job ๋จ์๋ก ์ฐ๊ฒฐํ๊ธฐ ์ด๋ ต๋ค.
๊ทธ๋์ ์ ๋ก๋ ์ ๋จ๊ณ์์ ๋ฐฑ์๋๊ฐ "์งง์ ์๋ช ์ ์ ๋ก๋ ๊ถํ"์ ๋ฐ๊ธํฉ๋๋ค.
sequenceDiagram
participant Client
participant Controller as PresignedUrlController
participant Service as PresignedUrlService
participant Validator as FileValidator
participant Encryptor as AesGcmEncryptor
participant Redis
participant OpenCV
Client->>Controller: POST /api/pre-signed(file metadata)
Controller->>Service: createPresignedUrl(memberId, request)
Service->>Validator: ํ์ผ ํฌ๊ธฐ/ํ์ฅ์ ๊ฒ์ฆ
Service->>Service: jobId ์์ฑ
Service->>Encryptor: expires:memberId:jobId ์ํธํ
Encryptor-->>Service: signature
Service->>Redis: memberId ๊ธฐ๋ฐ jobId TTL ์ ์ฅ
Service-->>Controller: preSignedUrl + signature + jobId
Controller-->>Client: ์
๋ก๋์ฉ URL ๋ฐํ
Client->>OpenCV: signed URL๋ก ์์ ์
๋ก๋
- ์์ฒญ์ด ์ค๋ฉด
SecurityUtils๋ก ํ์ฌ ํ์ ID๋ฅผ ๊ตฌํฉ๋๋ค. PresignedUrlService๋ ๋์ ๊ธฐ๋ฐjobId๋ฅผ ์์ฑํฉ๋๋ค.- ๋ง๋ฃ ์๊ฐ๊ณผ
memberId,jobId๋ฅผ ํ๋์ ๋ฌธ์์ด๋ก ๋ฌถ์ต๋๋ค. AesGcmEncryptor๊ฐ ์ด ๋ฌธ์์ด์ AES-GCM์ผ๋ก ์ํธํํฉ๋๋ค.- ์ํธ๋ฌธ์ query parameter๋ก ๋ฃ์ OpenCV ์ ๋ก๋ URL์ ๋ง๋ญ๋๋ค.
- ๊ฐ์ ์์ ์ Redis์
memberId -> jobId๋ฅผ TTL๊ณผ ํจ๊ป ์ ์ฅํฉ๋๋ค. - ํด๋ผ์ด์ธํธ๋ ์ด URL๋ก ์ง์ ์
๋ก๋ํ๊ณ , ์ดํ ๊ฐ์
jobId๋ก ์งํ๋ฅ ๊ณผ ๊ฒฐ๊ณผ๋ฅผ ์กฐํํฉ๋๋ค.
- ์๋ช
์๋ฌธ:
expires:memberId:jobId - ์ํธํ ๋ฐฉ์: AES-GCM
- IV๋ ๋งค ์์ฒญ๋ง๋ค ๋๋ค ์์ฑ
- ๊ฒฐ๊ณผ๋ URL-safe Base64 ๋ฌธ์์ด
- ํ์ผ ๋ฉํ๋ฐ์ดํฐ๋ ์ฌ์ ๊ฒ์ฆ
- ํ์ฅ์:
.mp4 - ํฌ๊ธฐ: ์ค์ ๊ฐ ๊ธฐ๋ฐ ์ ํ
- ํ์ฅ์:
์ฆ, ๋จ์ ํ ํฐ ๋ฌธ์์ด์ด ์๋๋ผ "๋ง๋ฃ ์๊ฐ๊ณผ ์ฌ์ฉ์/์์ ์๋ณ์๋ฅผ ๋ฌถ์ ์๋ช "์ ๋ฐ๊ธํฉ๋๋ค.
flowchart TD
A["memberId"] --> D["expires:memberId:jobId"]
B["jobId"] --> D
C["expires"] --> D
D --> E["AES-GCM Encrypt"]
E --> F["signature"]
F --> G["OpenCV upload URL"]
| Method | Endpoint | ์ธ์ฆ | ์ค๋ช |
|---|---|---|---|
| GET | /api/progress/subscribe?jobId=... |
์ฌ์ฉ์ JWT | SSE ๊ตฌ๋ |
| GET | /api/progress?jobId=... |
์ฌ์ฉ์ JWT | ์ต์ ์งํ๋ฅ polling ์กฐํ |
| POST | /api/highlight/upload-result |
OpenCV ์์คํ ํธ์ถ | ์์ฑ๋ ํ์ด๋ผ์ดํธ ๊ฒฐ๊ณผ ์ ์ฅ |
sequenceDiagram
participant Client
participant API as API Server
participant Redis
participant OpenCV
Client->>API: GET /api/progress/subscribe?jobId=...
API-->>Client: SSE emitter ์ฐ๊ฒฐ
OpenCV->>Redis: opencv-progress-upload:{jobId} publish
OpenCV->>Redis: opencv-progress-highlight:{jobId} publish
API->>Redis: channel subscribe
Redis-->>API: ์งํ๋ฅ payload ์ ๋ฌ
API-->>Client: SSE event ์ ์ก
flowchart LR
A["Redis Message"] --> B["ProgressSubscriber"]
B --> C["ProgressValidator"]
C --> D["ProgressSseEmitter.sendToClient"]
D --> E["eventCache ์ ์ฅ"]
D --> F["active emitter ์์ผ๋ฉด ์ฆ์ ์ ์ก"]
E --> G["์ฌ์ฐ๊ฒฐ ์ Last-Event-ID ๊ธฐ๋ฐ replay"]
์ด ๊ธฐ๋ฅ์ ํด๋ผ์ด์ธํธ๊ฐ ์
๋ก๋์ ํ์ด๋ผ์ดํธ ์์ฑ ์ํ๋ฅผ ๊ณ์ ํ์ธํด์ผ ํฉ๋๋ค.
Polling๋ง ์ฌ์ฉํ๋ฉด ๋ถํ์ํ ์์ฒญ ์๊ฐ ์ฆ๊ฐํฉ๋๋ค.
๊ทธ๋์ ์ด ํ๋ก์ ํธ๋ ๋ค์ ๋ ๊ฒฝ๋ก๋ฅผ ๋ชจ๋ ์ ๊ณตํฉ๋๋ค.
- ๊ธฐ๋ณธ ๊ฒฝ๋ก: SSE
- fallback ๊ฒฝ๋ก:
/api/progress?jobId=...
์ฆ, ์ค์๊ฐ์ฑ์ SSE๋ก ํ๋ณดํ๊ณ , ํด๋ผ์ด์ธํธ ์ ์ฝ์ด ์๋ ๊ฒฝ์ฐ polling์ผ๋ก๋ ๋์ํฉ๋๋ค.
์ด ์ปดํฌ๋ํธ๋ ๋จ์ emitter ์ ์ฅ์๊ฐ ์๋๋๋ค.
memberId:jobId์กฐํฉ์ผ๋ก emitter ๊ด๋ฆฌ- ์ต๊ทผ ์ด๋ฒคํธ๋ฅผ deque์ ์บ์
Last-Event-ID๊ฐ ์ค๋ฉด ๋๋ฝ ์ด๋ฒคํธ replay- TTL ์ดํ ์ค๋๋ ์ด๋ฒคํธ ์ ๋ฆฌ
- ์ต์ ์งํ๋ฅ ์ ๋ณ๋ map์ ์ ์ฅํด polling fallback ์ง์
์ฆ, "์ง๊ธ ์ฐ๊ฒฐ๋ ์ฌ์ฉ์์๊ฒ๋ง ๋ณด๋ด๊ธฐ"๊ฐ ์๋๋ผ "์ ๊น ๋๊ฒผ๋ค๊ฐ ๋ค์ ์ฐ๊ฒฐ๋ ์ฌ์ฉ์๋ ๋์น์ง ์๋๋ก" ์ค๊ณํ์ต๋๋ค.
sequenceDiagram
participant OpenCV
participant Controller as HighlightCommandController
participant Service as HighlightCommandService
participant Manager as HighlightManager
participant Factory as HighlightFactory
participant DB as PostgreSQL
OpenCV->>Controller: POST /api/highlight/upload-result
Controller->>Service: uploadHighlights(request, memberId)
Service->>Manager: saveHighlights(...)
Manager->>DB: Member ์กฐํ
Manager->>DB: Member์ BackNumber ์กฐํ
Manager->>Factory: HighlightEntity ๋ฆฌ์คํธ ์์ฑ
Factory-->>Manager: highlight entities
Manager->>DB: saveAll
Controller-->>OpenCV: ์ ์ฅ ์ฑ๊ณต ์๋ต
ํ์ด๋ผ์ดํธ๋ ๋จ์ URL ์ ์ฅ์ด ์๋๋๋ค.
highlightIdhighlightURLhighlightKeymemberbackNumbertwoPointCountthreePointCountvideoCreatedAtjobId
์ด ์ ๋ณด๊ฐ ์์ด์ผ ๋์ค์
- ๋ง์ดํ์ด์ง ํต๊ณ
- ๊ฒ์๊ธ ์ฐ๊ฒฐ
- ์บ๋ฆฐ๋ ์กฐํ
- ๋ญํน ๊ณ์ฐ
๊น์ง ํ์ฅํ ์ ์์ต๋๋ค.
| Method | Endpoint | ์ธ์ฆ | ์ค๋ช |
|---|---|---|---|
| GET | /api/highlight/list?page=&size= |
์ฌ์ฉ์ JWT | ๋ด ํ์ด๋ผ์ดํธ ํ์ด์ง ์กฐํ |
| GET | `/api/highlight?period=WEEKLY | MONTHLY` | ์ฌ์ฉ์ JWT |
| GET | /api/highlight/calendar?year=&month= |
์ฌ์ฉ์ JWT | ์๋ณ ์บ๋ฆฐ๋ ์กฐํ |
| GET | /api/highlight/latest?jobId= |
์ฌ์ฉ์ JWT | ํน์ job์ ์ต์ ๊ฒฐ๊ณผ ์กฐํ |
flowchart TD
A["HighlightQueryController"] --> B["HighlightManager"]
B --> C["listByPaging"]
B --> D["fetchAllMembersHighlights"]
B --> E["fetchCalendar"]
B --> F["fetchLatestCreatedHighlights"]
C --> C1["Pageable ์์ฑ"]
C1 --> C2["member์ highlight page ์กฐํ"]
C2 --> C3["HighlightInfoResponse ๋ณํ"]
D --> D1["WEEKLY / MONTHLY ๊ธฐ๊ฐ ๊ณ์ฐ"]
D1 --> D2["highlight-post-member-like join query"]
D2 --> D3["๊ธฐ๊ฐ ๋ด ์ธ๊ธฐ highlight ๋ฐํ"]
E --> E1["year/month ๊ฒ์ฆ"]
E1 --> E2["ํด๋น ์ flat data ์กฐํ"]
E2 --> E3["LocalDate ๊ธฐ์ค ๊ทธ๋ฃนํ"]
E3 --> E4["์บ๋ฆฐ๋ ์๋ต ์์ฑ"]
F --> F1["jobId + memberId ์กฐ๊ฑด ์กฐํ"]
F1 --> F2["์ต์ ์์ฑ highlight ๋ฆฌ์คํธ ๋ฐํ"]
์ด API๋ ๋ก๊ทธ์ธ ์ฌ์ฉ์์ ํ์ด๋ผ์ดํธ๋ฅผ ํ์ด์ง์ผ๋ก ๋ฐํํฉ๋๋ค.
PageRequest.of(page, size)์์ฑ- ํ์ ๊ธฐ์ค highlight page ์กฐํ
HighlightInfoResponse๋ก ๋งคํ
์ฆ, "๋ด ํ์ด๋ผ์ดํธ ๋ชฉ๋ก" ๊ธฐ๋ฅ์ ๊ธฐ๋ณธ API์ ๋๋ค.
์ด API๋ ๋จ์ํ highlight๋ง ๋ณด๋ ๊ฒ์ด ์๋๋ผ,
highlight + post + member + like๋ฅผ joinํด์ "๊ธฐ๊ฐ ๋์ ์ธ๊ธฐ ์์๋ ํ์ด๋ผ์ดํธ"๋ฅผ ๋ฐํํฉ๋๋ค.
์ ํํ๋ ๋ค์ ์๋ฏธ๋ฅผ ๊ฐ์ง๋๋ค.
- ์ง๊ณ ๊ธฐ๊ฐ: ์ด๋ฒ ์ฃผ ๋๋ ์ด๋ฒ ๋ฌ
- ์กฐ๊ฑด: ํด๋น ๊ธฐ๊ฐ ๋์ ๋๋ฆฐ ์ข์์
- ๊ฒฐ๊ณผ: ์ข์์ ์ ๊ธฐ์ค ์์ ํ์ด๋ผ์ดํธ
์ฆ, "๋ง์ด ๋ณธ ์์"์ด ์๋๋ผ "์ข์์ ํ๋์ด ๋ง์ด ๋ถ์ ํ์ด๋ผ์ดํธ"๋ฅผ ๋ณด์ฌ์ค๋๋ค.
์ด API๋ ์๊ฐ ์บ๋ฆฐ๋ UI๋ฅผ ์ํ API์ ๋๋ค.
๋์ ๋ฐฉ์์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- ์ฐ๋์ ์์ด ์ ํจํ์ง ๊ฒ์ฆ
- ํด๋น ์์ ์์ / ์ข ๋ฃ ์๊ฐ ๊ณ์ฐ
- ๊ทธ ๊ธฐ๊ฐ์ highlight๋ฅผ flat list๋ก ์กฐํ
LocalDate๋จ์๋ก ๊ทธ๋ฃนํ- ๋ ์ง๋ณ ๊ฐ์์ ์์ธ ๋ฆฌ์คํธ๋ฅผ ๋ฌถ์ด ์๋ต
์ฆ, DB์์ ๋ฐ๋ก ๋ฌ๋ ฅ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค์ง ์๊ณ , flat query ํ ์ ํ๋ฆฌ์ผ์ด์ ๋ ๋ฒจ์์ ๋ ์ง ๋จ์๋ก ๋ฌถ์ด ์๋ตํฉ๋๋ค.
์ด API๋ jobId ๋จ์ ๊ฒฐ๊ณผ ํ์ธ์ฉ์
๋๋ค.
- OpenCV๊ฐ ์์ ์ ๋๋ธ ์งํ
- ํด๋ผ์ด์ธํธ๊ฐ ๋ฐฉ๊ธ ์์ฑ๋ ๊ฒฐ๊ณผ๋ง ๋ฐ๋ก ๋ณด๊ณ ์ถ์ ๋
์ฌ์ฉํฉ๋๋ค.
์ฆ, ์ ์ฒด highlight ๋ชฉ๋ก์ ๋ค์ ๋ค์ง๋ ๋์ "์ด๋ฒ ์์ ๊ฒฐ๊ณผ"๋ง ๋น ๋ฅด๊ฒ ๊บผ๋ผ ์ ์์ต๋๋ค.
| Method | Endpoint | ์ธ์ฆ | ์ค๋ช |
|---|---|---|---|
| POST | /api/post |
์ฌ์ฉ์ JWT | ๊ฒ์๊ธ ์์ฑ |
| PUT | /api/post/{postId} |
์ฌ์ฉ์ JWT | ๊ฒ์๊ธ ์์ |
| DELETE | /api/post/{postId} |
์ฌ์ฉ์ JWT | ๊ฒ์๊ธ ์ญ์ |
| GET | /api/post/{postId} |
์ฌ์ฉ์ JWT | ๊ฒ์๊ธ ๋จ๊ฑด ์กฐํ |
| GET | /api/post |
์ฌ์ฉ์ JWT | ์ต์ ์/์ธ๊ธฐ์ ๋ชฉ๋ก ์กฐํ |
| GET | /api/post/mypage |
์ฌ์ฉ์ JWT | ๋ด๊ฐ ์์ฑํ ๊ฒ์๊ธ |
| GET | /api/post/my/like |
์ฌ์ฉ์ JWT | ๋ด๊ฐ ์ข์์ํ ๊ฒ์๊ธ |
| Method | Endpoint | ์ธ์ฆ | ์ค๋ช |
|---|---|---|---|
| POST | /api/comment |
์ฌ์ฉ์ JWT | ๋๊ธ ์์ฑ |
| PATCH | /api/comment/{commentId} |
์ฌ์ฉ์ JWT | ๋๊ธ ์์ |
| DELETE | /api/comment/{commentId} |
์ฌ์ฉ์ JWT | ๋๊ธ ์ญ์ |
| GET | /api/comment/{postId} |
์ฌ์ฉ์ JWT | ๊ฒ์๊ธ๋ณ ๋๊ธ ์กฐํ |
sequenceDiagram
participant Client
participant Controller as PostCommandController
participant Service as PostCommandService
participant Manager as PostManager
participant HighlightHelper
participant PostHelper
participant DB as PostgreSQL
participant ES as Elasticsearch
Client->>Controller: POST /api/post
Controller->>Service: create(request, member)
Service->>Manager: save(member, request)
Manager->>HighlightHelper: highlight ์กฐํ
Manager->>PostHelper: ํ์ ์์ highlight ๊ฒ์ฆ
Manager->>PostHelper: hashtag ๊ฒ์ฆ
Manager->>DB: post ์ ์ฅ
Manager->>ES: profile ํ์ฑ ์ document ์ ์ฅ
Controller-->>Client: postId ๋ฐํ
PostManager.save๋ ์๋ ์์๋ฅผ ๊ฐ์ง๋๋ค.
- ์์ฒญ highlight ์กฐํ
- ์ด highlight๊ฐ ํ์ฌ ์ฌ์ฉ์ ์์ ์ธ์ง ๊ฒ์ฆ
- ํด์ํ๊ทธ๊ฐ ์ ํจํ์ง ๊ฒ์ฆ
- PostEntity ์ ์ฅ
- Elasticsearch๊ฐ ํ์ฑํ๋ ๊ฒฝ์ฐ document ์ ์ฅ
์ฆ, ๊ฒ์๊ธ์ ๋ ๋ฆฝ ์ฝํ ์ธ ๊ฐ ์๋๋ผ "๋ด ํ์ด๋ผ์ดํธ๋ฅผ ์ฒจ๋ถํ ์ฝํ ์ธ "๋ก ์ค๊ณ๋์ด ์์ต๋๋ค.
- ์์
- ๊ฒ์๊ธ ์กด์ฌ ํ์ธ
- ๊ฒ์๊ธ ์์ ์ ๊ฒ์ฆ
- ์ highlight ๊ฒ์ฆ
- hashtag ๊ฒ์ฆ
- update
- ์ญ์
- ๊ฒ์๊ธ ์กด์ฌ ํ์ธ
- ๊ฒ์๊ธ ์์ ์ ๊ฒ์ฆ
- soft delete
๊ฒ์๊ธ ์ญ์ ๋ ๋ฌผ๋ฆฌ ์ญ์ ๊ฐ ์๋๋ผ ๋ ผ๋ฆฌ ์ญ์ ์ ๋๋ค.
/api/post
type=latest๋ฉด ์ต์ ์ ์กฐํtype=popular๋ฉด ์ธ๊ธฐ์ ์กฐํ- slice / no-offset ๋ฐฉ์์ผ๋ก ๋ค์ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ค๋ ค๋ ๊ตฌ์กฐ
/api/post/mypage
- ๋ด๊ฐ ์์ฑํ post id ๋ชฉ๋ก์ ๋จผ์ ์กฐํ
- ํ์ํ post๋ฅผ ๋ค์ fetch join์ผ๋ก ์กฐํ
- DTO ๋ณํ ํ ๋ฐํ
/api/post/my/like
- ๋ด๊ฐ ์ข์์ ๋๋ฅธ ๊ฒ์๊ธ์ like ์์ฑ ์๊ฐ ์์ผ๋ก ์กฐํ
๋๊ธ์ ๊ฒ์๊ธ๋ณด๋ค ๋จ์ํ์ง๋ง, ์๋ ๊ท์น์ด ๋ช ํํฉ๋๋ค.
- ์์ฑ ์: ๊ฒ์๊ธ ์กด์ฌ ํ์ธ ํ ์ ์ฅ
- ์์ ์: ๋๊ธ ์์ฑ์๋ง ์์ ๊ฐ๋ฅ
- ์ญ์ ์: ๋๊ธ ์์ฑ์๋ง ์ญ์ ๊ฐ๋ฅ
- ์ญ์ ๋ soft delete ๋ฐฉ์
flowchart LR
A["๋๊ธ ์์ฒญ"] --> B["CommentManager"]
B --> C["๊ฒ์๊ธ ์กด์ฌ ํ์ธ"]
B --> D["๋๊ธ ์์ฑ์ ๊ฒ์ฆ"]
B --> E["๋ด์ฉ ๊ณต๋ฐฑ ๊ฒ์ฆ"]
B --> F["save / update / soft delete"]
| Method | Endpoint | ์ธ์ฆ | ์ค๋ช |
|---|---|---|---|
| GET | /api/post/list?search= |
์ฌ์ฉ์ JWT | DB ๊ธฐ๋ฐ ๊ฒ์ |
| GET | /api/post/list-elastic?search= |
์ฌ์ฉ์ JWT | Elasticsearch ๊ธฐ๋ฐ ๊ฒ์ |
| GET | /api/post/suggest?keyword= |
์ฌ์ฉ์ JWT | ์๋์์ฑ |
์ด ํ๋ก์ ํธ๋ ๊ฒ์์ 2๋จ๊ณ๋ก ๋๋ด์ต๋๋ค.
- ๊ธฐ๋ณธ ๊ฒ์: DB LIKE ๊ฒ์
- ๊ณ ๊ธ ๊ฒ์: Elasticsearch ๊ฒ์
๊ทธ๋ฆฌ๊ณ Elasticsearch๊ฐ ๋นํ์ฑํ๋ ํ๊ฒฝ๋ ๊ณ ๋ คํด์ fallback์ ๋ฃ์์ต๋๋ค.
flowchart TD
A["/api/post/list-elastic ์์ฒญ"] --> B{"search ๋น์๋๊ฐ?"}
B -- ์ --> C["์ต์ ๊ฒ์๊ธ ๋ชฉ๋ก ๋ฐํ"]
B -- ์๋์ค --> D{"ES helper ํ์ฑํ?"}
D -- ์๋์ค --> E["DB LIKE ๊ฒ์ fallback"]
D -- ์ --> F{"ํด์ํ๊ทธ ๊ฒ์์ธ๊ฐ?"}
F -- ์ --> G["# ์ ๊ฑฐ ํ hashTag ๊ฒ์"]
F -- ์๋์ค --> H["title/content ๊ฒ์"]
G --> I["search_after ์ ๋ ฌ"]
H --> I
I --> J["PostListResponse ๋ฐํ"]
DB ๊ธฐ๋ฐ /api/post/list
title LIKE %search%content LIKE %search%post_id < lastPostIdORDER BY post_id DESC
์ฆ, ์ต์ ์์ผ๋ก ์๋ผ ๊ฐ์ ธ์ค๋ ์ ํ์ ์ธ no-offset ํจํด์ ๋๋ค.
Elasticsearch ๊ฒ์์ ์๋ ์ ๋ต์ ์ฌ์ฉํฉ๋๋ค.
- title ๊ฐ์ค์น:
40 - content ๊ฐ์ค์น:
10 - ์ ๋ ฌ ์ฐ์ ์์
_score DESClikeCnt DESCpostId DESC
- ํ์ด์ง๋ค์ด์
search_after
์ฆ, ๋จ์ full-text ๊ฒ์์ด ์๋๋ผ "์ ๋ชฉ์ ๋ ์ค์ํ๊ฒ ๋ณด๊ณ , ๋์ ์ด๋ฉด ์ข์์ ์์ ์ต์ ์ฑ์ผ๋ก ์ ๋ ฌ"ํ๋ ์ ๋ต์ ๋๋ค.
๊ฒ์์ด๊ฐ #๋ก ์์ํ๋ฉด ์ผ๋ฐ ๊ฒ์์ด๊ฐ ์๋๋ผ ํด์ํ๊ทธ๋ก ํด์ํฉ๋๋ค.
์์:
#2์ ์#๋ธ๋ฝ
๋์ ๋ฐฉ์:
- ๋งจ ์
#์ ๊ฑฐ - ๊ณต๋ฐฑ ์ ๊ฑฐ
hashTagํ๋ ๊ธฐ์ค ๊ฒ์
์๋์์ฑ๋ ์ผ๋ฐ ๊ฒ์๊ณผ ํด์ํ๊ทธ ๊ฒ์์ ๋ถ๋ฆฌํฉ๋๋ค.
- ์ผ๋ฐ ๊ฒ์์ด
title.keywordprefix ๊ธฐ๋ฐ ์๋์์ฑ
- ํด์ํ๊ทธ ๊ฒ์์ด
hashTagprefix ๊ธฐ๋ฐ ์๋์์ฑ- ์ค๋ณต ์ ๊ฑฐ ํ ์ต๋ 5๊ฐ
์ฆ, ๊ฒ์๊ธ ์ ๋ชฉ ์๋์์ฑ๊ณผ ํด์ํ๊ทธ ์๋์์ฑ์ ๊ฐ์ API ์์์ ๋ถ๊ธฐ ์ฒ๋ฆฌํฉ๋๋ค.
| Method | Endpoint | ์ธ์ฆ | ์ค๋ช |
|---|---|---|---|
| POST | /api/like/{postId} |
์ฌ์ฉ์ JWT | ์ข์์ ๋ฑ๋ก |
| DELETE | /api/like/{postId} |
์ฌ์ฉ์ JWT | ์ข์์ ์ทจ์ |
์ข์์๋ ์๋ 2๊ฐ๋ฅผ ๋์์ ๋ง์ถฐ์ผ ํฉ๋๋ค.
like_tablerow ์ ํฉ์ฑpost.like_cnt์ง๊ณ ์ ํฉ์ฑ
์ฌ๋ฌ ์ฌ์ฉ์๊ฐ ๊ฐ์ ๊ฒ์๊ธ์ ๋์์ ๋๋ฅด๋ฉด race condition์ด ์ฝ๊ฒ ๋ฐ์ํฉ๋๋ค.
์ต์ข ๊ตฌํ์ ๊ฒ์๊ธ row๋ฅผ ๋น๊ด์ ๋ฝ์ผ๋ก ์กฐํํ๋ ๋ฐฉ์์ ๋๋ค.
flowchart TD
A["์ข์์ ์์ฒญ"] --> B["Post ์กฐํ with PESSIMISTIC_WRITE"]
B --> C["์ด๋ฏธ ์ข์์ ๋๋ ๋์ง ๊ฒ์ฆ"]
C --> D["post.likeCnt ์ฆ๊ฐ"]
D --> E["LikeEntity ์ ์ฅ"]
E --> F["ํธ๋์ญ์
์ข
๋ฃ"]
์ทจ์๋ ์ญ์์ ๋๋ค.
flowchart TD
A["์ข์์ ์ทจ์ ์์ฒญ"] --> B["Post ์กฐํ with PESSIMISTIC_WRITE"]
B --> C["memberId + postId๋ก Like ์กฐํ"]
C --> D["post.likeCnt ๊ฐ์"]
D --> E["LikeEntity ์ญ์ "]
E --> F["ํธ๋์ญ์
์ข
๋ฃ"]
LikeManager.increase
- ๊ฒ์๊ธ์ ๋น๊ด์ ๋ฝ์ผ๋ก ์กฐํ
- ๊ฐ์ ์ฌ์ฉ์๊ฐ ์ด๋ฏธ ์ข์์ ๋๋ ๋์ง ํ์ธ
post.increase()- Like row ์ ์ฅ
LikeManager.decrease
- ๊ฒ์๊ธ์ ๋น๊ด์ ๋ฝ์ผ๋ก ์กฐํ
- Like row ์กฐํ
post.deleteLike()- Like row ์ญ์
LikeManagerConcurrencyTest์๋ ๋ค์ ์๋๋ฆฌ์ค๊ฐ ์์ต๋๋ค.
- ๋์์ 100๊ฑด ์ข์์ ์ฆ๊ฐ
- ๋์์ 1,000๊ฑด ์ข์์ ์ฆ๊ฐ
- ๋์์ 10,000๊ฑด ์ข์์ ์ฆ๊ฐ
- ๋์์ 100๊ฑด ์ข์์ ๊ฐ์
- ๋์์ 1,000๊ฑด ์ข์์ ๊ฐ์
- ๋์์ 10,000๊ฑด ์ข์์ ๊ฐ์
๊ทธ๋ฆฌ๊ณ ํ ์คํธ ์ฝ๋์๋ ์คํ ํ์ ๋ ๋จ์ ์์ต๋๋ค.
- ์ฆ๋ถ ์ฟผ๋ฆฌ ๋ฐฉ์ ์คํ
- Atomic ๋ฐฉ์ ์คํ
- Optimistic Lock ์คํ
- Distributed Lock ์คํ
์ฆ, ์ฌ๋ฌ ๋์์ ๋น๊ตํด ๋ณธ ๋ค ํ์ฌ ๊ตฌ์กฐ์์๋ ๋น๊ด์ ๋ฝ์ด ๊ฐ์ฅ ๋จ์ํ๊ณ ๋ช ํํ ์ ํ์ด์๋ค๋ ์ ์ ๋ณด์ฌ์ค๋๋ค.
| Method | Endpoint | ์ค๋ช |
|---|---|---|
| GET | /api/rank/last-week?date= |
์ง๋ ์ฃผ ๋ญํน ์กฐํ |
| GET | /api/rank/last-month?date= |
์ง๋ ๋ฌ ๋ญํน ์กฐํ |
| GET | /api/rank/this-week |
์ด๋ฒ ์ฃผ ๋ญํน ์กฐํ |
| GET | /api/rank/this-month |
์ด๋ฒ ๋ฌ ๋ญํน ์กฐํ |
๋ญํน์ ์ฑ๊ฒฉ์ด ๋ค๋ฅธ 2๊ฐ์ง๋ฅผ ๋ชจ๋ ์ฒ๋ฆฌํด์ผ ํฉ๋๋ค.
- "์ง๊ธ ์งํ ์ค์ธ ๋ญํน"
- "์ด๋ฏธ ์ข ๋ฃ๋ ๊ธฐ๊ฐ์ ๋ญํน"
์ด ๋์ ์กฐํ ์ ๋ต์ด ๋ค๋ฆ ๋๋ค.
flowchart LR
A["/api/rank/this-week or this-month"] --> B["RankingController"]
B --> C["RankingService"]
C --> D["RankingManager.fetchThisData"]
D --> E["RankingRedisRepository.getHighlightsWeeklyRanking"]
E --> F{"Redis ๊ฒฐ๊ณผ ์กด์ฌ?"}
F -- ์ --> G["RankingMapper -> Response"]
F -- ์๋์ค --> H["RankingJpaRepository.fetchThisWeekRankingTop10"]
H --> I["RankingUtil.calculateRanking"]
I --> G
flowchart LR
A["/api/rank/last-week or last-month"] --> B["RankingController"]
B --> C["RankingService"]
C --> D["RankingManager.fetchLastData"]
D --> E["periodKey ์์ฑ"]
E --> F["MongoDB RankingDocument ์กฐํ"]
F --> G{"๋ฌธ์ ์กด์ฌ?"}
G -- ์ --> H["๋ฌธ์ -> Response"]
G -- ์๋์ค --> I["ranking.sql JDBC ์ง๊ณ"]
I --> J["Mapper -> Response"]
๋ญํน์ ๋จ์ ์ด์ ๋ง ๋ณด์ง ์์ต๋๋ค.
- ์ด์ ์ฐ์
- ๋์ ์ด๋ฉด 3์ ์ ์ฐ์
- ๋ค์ ๋์ ์ด๋ฉด 2์ ์ ์ฐ์
Redis ์ชฝ์์๋ ์ด๋ฅผ ์ํด ๊ฐ์ค์น score๋ฅผ ์ฌ์ฉํฉ๋๋ค.
score = totalScore * 1_000_000
+ threeScore * 1_000
+ twoScore
์ฆ, total > three > two ์ฐ์ ์์๊ฐ ์์ฐ์ค๋ฝ๊ฒ ์ ์ง๋๋๋ก ์ค๊ณํ์ต๋๋ค.
ํ์ฌ ์ฃผ๊ฐ/์๊ฐ ๋ญํน์ Redis ZSet์ ์ฌ์ฉํฉ๋๋ค.
- ๋์ผ ํ์์ด ๋ค์ ๋ฐ์๋๋ฉด ๊ธฐ์กด ๊ฐ์ ์ ๊ฑฐ ํ ์ฌ์ฝ์
- ์กฐํ๋
reverseRangeWithScores๋ก top 10 - ์ฃผ๊ฐ / ์๊ฐ ํค๋ ์๋ก ๋ถ๋ฆฌ
๊ณผ๊ฑฐ ๋ญํน์ MongoDB ranking ์ปฌ๋ ์
๋ฌธ์๋ก ์ ์ฅํฉ๋๋ค.
typeperiodBegintypePeriodKeytop10
์ฆ, ๊ณผ๊ฑฐ ๋ญํน์ "๊ฒฐ๊ณผ ์ค๋ ์ท"์ผ๋ก ๋ค๋ฃจ๊ณ , ํ์ฌ ๋ญํน์ "์ค์๊ฐ ๊ฐ๋ณ ์ํ"๋ก ๋ค๋ฃน๋๋ค.
flowchart TD
A["์ฃผ๊ฐ/์๊ฐ ์ค์ผ์ค"] --> B["RankingRedisScheduler"]
B --> C["Redis ํ์ฌ ๋ญํน ์ด๊ธฐํ"]
D["์ผ๊ฐ/์ฃผ๊ฐ/์๊ฐ ๋ฐฐ์น ์ค์ผ์ค"] --> E["RankingBatchScheduler"]
E --> F["Spring Batch JobLauncher"]
F --> G["๋ญํน ์ค๋
์ท ์ ์ฅ ๊ตฌ์กฐ"]
apps/batch-server ์๋์๋ ๋ค์ ์ฝ๋๊ฐ ๋ถ๋ฆฌ๋์ด ์์ต๋๋ค.
- Reader
- Processor
- Writer
- Listener
- Validator
- Scheduler
์ฆ, API ์๋ฒ์์ ๋ญํน ์ง๊ณ๋ฅผ ๋ผ์ด๋ด ๋ณ๋ ๋ฐฐ์น ์๋ฒ๋ก ๋ถ๋ฆฌํ๋ ค๋ ๊ตฌ์กฐ๊ฐ ์ค๊ณ๋์ด ์์ต๋๋ค.
๋ค๋ง ํ์ฌ ์ํฌํธ๋ฆฌ ๊ธฐ์ค์ผ๋ก๋ ์ผ๋ถ Batch config ์ฝ๋๊ฐ ์ฃผ์ ์ฒ๋ฆฌ๋ ์ํ๋ผ, "๋ฐฐ์น ๊ตฌ์กฐ์ ๊ณจ๊ฒฉ์ ์กด์ฌํ์ง๋ง ์ ๋น ์ค"์ผ๋ก ๋ณด๋ ๊ฒ์ด ๊ฐ์ฅ ์ ํํฉ๋๋ค.
| ์ํฐํฐ | ์๋ฏธ |
|---|---|
| Member | ์๋น์ค ์ฌ์ฉ์. ์ด๋ฆ/์ด๋ฉ์ผ์ ์ํธํ ์ ์ฅ, ์ง๊ณ ๋์ ์ฌ๋ถ ํฌํจ |
| BackNumberEntity | ๋ฑ๋ฒํธ ๋ง์คํฐ |
| MemberBackNumberEntity | ํ์๊ณผ ๋ฑ๋ฒํธ์ ๋งคํ |
| HighlightEntity | OpenCV๊ฐ ์์ฑํ ๊ฐ์ธ ํ์ด๋ผ์ดํธ ๋จ์ |
| PostEntity | ํ์ด๋ผ์ดํธ๋ฅผ ์ฒจ๋ถํ ์ปค๋ฎค๋ํฐ ๊ฒ์๊ธ |
| Comment | ๊ฒ์๊ธ ๋๊ธ |
| LikeEntity | ๊ฒ์๊ธ ์ข์์ |
| RankingDocument | MongoDB์ ์ ์ฅ๋๋ ๊ธฐ๊ฐ๋ณ ๋ญํน ์ค๋ ์ท |
- ๊ฒ์๊ธ๊ณผ ๋๊ธ์ soft delete
- ํ์ด๋ผ์ดํธ๋ ๊ฒ์๊ธ๊ณผ 1:N ์ ์ฌ ๊ด๊ณ๋ก ์ฌ์ฌ์ฉ ๊ฐ๋ฅ
- Member๋ BackNumber์ ์ง์ ๊ฒฐํฉํ์ง ์๊ณ ๋ธ๋ฆฟ์ง ํ ์ด๋ธ ์ฌ์ฉ
- Ranking์ ์ด์ ํ ์ด๋ธ์ ๋งค๋ฒ ์ง๊ณํ์ง ์๊ณ ๋ฌธ์ ์ค๋ ์ท ๋ณด์กด
ํ์ฌ docker-compose.yml์ ์๋ ์๋น์ค๋ฅผ ํจ๊ป ์ฌ๋ฆด ์ ์๊ฒ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
- PostgreSQL
- MongoDB
- Elasticsearch
- Kibana
- API Server
- Batch Server
flowchart LR
A["docker-compose"] --> B["postgres"]
A --> C["mongo"]
A --> D["elasticsearch"]
A --> E["kibana"]
A --> F["api-server"]
A --> G["batch-server"]
F --> B
F --> C
F --> D
F --> G
flowchart TD
A["Jenkins Job ์์"] --> B["Git clone"]
B --> C["๋น๊ณต๊ฐ application / env ํ์ผ ์ฃผ์
"]
C --> D["๊ธฐ์กด ์ปจํ
์ด๋ ๋ฐ ํฌํธ ์ ๋ฆฌ"]
D --> E["Elasticsearch ๋ณผ๋ฅจ ๊ถํ ์์ "]
E --> F["docker-compose up -d --build"]
F --> G["API / DB / ES / Mongo ์ปจํ
์ด๋ ๊ธฐ๋"]
- main ๋ธ๋์น clone
- credential๋ก ๋น๊ณต๊ฐ ์ค์ ํ์ผ ๋ณต์ฌ
- ๊ธฐ์กด ํฌํธ/์ปจํ ์ด๋ cleanup
- Elasticsearch data/log ๊ถํ ์์
docker-compose up -d --build
์ฆ, ๋จ์ jar ๋ฐฐํฌ๊ฐ ์๋๋ผ "์ฌ๋ฌ ์ธํ๋ผ ์๋น์ค์ ์ฑ ์ปจํ ์ด๋๋ฅผ ํจ๊ป ๊ธฐ๋ํ๋ ๋ฐฐํฌ"๋ฅผ ์๋ํํ์ต๋๋ค.
๋ํ์ ์ผ๋ก ์๋ ์์ญ์ด ํ ์คํธ๋ฉ๋๋ค.
| ์์ญ | ์์ |
|---|---|
| ํ์ | MemberManagerTest, MemberCommandControllerTest, KakaoApiHelperImplTest |
| ๋ฑ๋ฒํธ | BackNumberManagerTest, BackNumberControllerTest |
| ํ์ด๋ผ์ดํธ | HighlightManagerTest, HighlightQueryControllerTest, HighlightFactoryTest |
| ๊ฒ์๊ธ | PostManagerTest, PostCommandControllerTest, PostQueryControllerTest |
| ๋๊ธ | CommentManagerTest, CommentCommandControllerTest |
| ์ข์์ | LikeManagerTest, LikeManagerConcurrencyTest |
| ๋ญํน | RankingManagerTest, RankingControllerTest, RankingRedisSchedulerTest |
- ์ปจํธ๋กค๋ฌ / ๋น์ฆ๋์ค / ๋ฆฌํฌ์งํ ๋ฆฌ ํ ์คํธ ๋ถ๋ฆฌ
- ๋์์ฑ ํ ์คํธ ๋ณ๋ ์์ฑ
- Elasticsearch ์ฐ๋ ํ ์คํธ ๋ถ๋ฆฌ
- JaCoCo ๋ฆฌํฌํธ ์์ฑ ์ค์ ๊ด๋ฆฌ
.
โโโ apps
โ โโโ api-server
โ โโโ batch-server
โโโ libs
โ โโโ common
โ โโโ support
โโโ modules
โ โโโ community-context
โ โโโ global
โ โโโ media-processing-context
โ โโโ member-context
โโโ src/main/java/com/midas/shootpointer
โโโ domain
โโโ global
โโโ infrastructure
๊ธฐ์กด src/main/java/com/midas/shootpointer ๊ตฌ์กฐ๋ ๊ธฐ๋ฅ์ด ๋์๋ก ๋ค์ ๋ฌธ์ ๊ฐ ์๊น๋๋ค.
- ๋๋ฉ์ธ ๊ฒฝ๊ณ๊ฐ ํ๋ ค์ง
- ๊ณตํต ์ฝ๋๊ฐ ์ฌ๊ธฐ์ ๊ธฐ ํฉ์ด์ง
- API ์๋ฒ์ ๋ฐฐ์น ์๋ฒ์ ์ฑ ์์ด ์์
- ํ ์คํธ์ ๋ฐฐํฌ ๋จ์๊ฐ ์ปค์ง
๊ทธ๋์ ๋ค์ ๋ฐฉํฅ์ผ๋ก ๋ถ๋ฆฌํ๊ณ ์์ต๋๋ค.
- ์ง์
์ ๋ถ๋ฆฌ:
api-server,batch-server - ์ปจํ
์คํธ ๋ถ๋ฆฌ:
member-context,community-context,media-processing-context - ๊ณตํต ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ถ์ถ:
libs/common,libs/support
flowchart LR
A["Legacy src/main/java"] --> B["apps"]
A --> C["modules"]
A --> D["libs"]
B --> B1["api-server"]
B --> B2["batch-server"]
C --> C1["member-context"]
C --> C2["community-context"]
C --> C3["media-processing-context"]
C --> C4["global"]
D --> D1["common"]
D --> D2["support"]