Skip to content

Navigation Menu

Sign in
Sign up

Repository files navigation

ON SURVEY โ€” ํ† ์Šค ๋ฏธ๋‹ˆ์•ฑ ์„ค๋ฌธ ์กฐ์‚ฌ ํ”Œ๋žซํผ

์„ค๋ฌธ์„ ๋งŒ๋“ค๊ณ  ์‹ถ์€ ์‚ฌ๋žŒ๊ณผ ์‘๋‹ตํ•˜๊ณ  ์ฐธ์—ฌ ๋ณด์ƒ์„ ๋ฐ›๊ณ  ์‹ถ์€ ์‚ฌ๋žŒ์„ ์—ฐ๊ฒฐํ•˜๋Š” ์„œ๋น„์Šค. ํ† ์Šค ์•ฑ ์•ˆ์—์„œ ๋™์ž‘ํ•˜๋Š” ๋ฏธ๋‹ˆ์•ฑ(Apps in Toss)์œผ๋กœ, ๊ฒฐ์ œยท์ธ์ฆยทํฌ์ธํŠธ ์ง€๊ธ‰์ด ๋ชจ๋‘ ํ† ์Šค ํ”Œ๋žซํผ๊ณผ ์—ฐ๋™๋ฉ๋‹ˆ๋‹ค.

โš™๏ธ ๊ธฐ์ˆ  ์Šคํƒ

Language / Runtime Java 21, Spring Boot 3.5.6
Persistence MySQL 8(AWS RDS), Spring Data JPA, QueryDSL 7
Cache / Lock Redis 7, Redisson(๋ถ„์‚ฐ๋ฝ)
Security Spring Security
Infra Docker, Nginx, NCP Server, NCP Object Storage, AWS Lambda
CI/CD GitHub Actions
Observability Sentry, Discord Webhook, Spring Actuator

1. ์–ด๋–ค ์„œ๋น„์Šค์ธ๊ฐ€

flowchart LR
 A["์„ค๋ฌธ ์˜๋ขฐ์ž"] -->|"์ฝ”์ธ ์ถฉ์ „ (ํ† ์Šค IAP)"| B["์„ค๋ฌธ ์ƒ์„ฑ<br/>๋ฌธํ•ญยทํƒ€๊ฒŒํŒ…ยท๋ชฉํ‘œ ์‘๋‹ต ์ˆ˜ ์„ค์ •"]
 B --> C["๊ฒ€์ˆ˜ ํ›„ ๊ฒŒ์‹œ"]
 C --> D["์ฐธ์—ฌ์ž์—๊ฒŒ ๋…ธ์ถœ"]
 D --> E["์Šคํฌ๋ฆฌ๋‹ ํ†ต๊ณผ โ†’ ์‘๋‹ต"]
 E --> F["๋ชฉํ‘œ ์‘๋‹ต ์ˆ˜ ๋„๋‹ฌ ์‹œ ์ž๋™ ๋งˆ๊ฐ"]
 E --> G["์ฐธ์—ฌ์ž์—๊ฒŒ ํ”„๋กœ๋ชจ์…˜ ์ง€๊ธ‰"]
 F --> H["์˜๋ขฐ์ž: ๊ฒฐ๊ณผ ์ง‘๊ณ„ยทCSV ๋‹ค์šด๋กœ๋“œ"]
Loading

2. ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜

flowchart TB
 subgraph CLIENT["ํด๋ผ์ด์–ธํŠธ"]
 TOSS["ํ† ์Šค ๋ฏธ๋‹ˆ์•ฑ WebView"]
 BO["๋ฐฑ์˜คํ”ผ์Šค ๋ธŒ๋ผ์šฐ์ €"]
 end
 
 subgraph SERVER
 NGX["Nginx 1.25<br/>TLS ์ข…๋ฃŒ ยท ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ"]
 BLUE["onsurvey-blue :8080"]
 GREEN["onsurvey-green :8081"]
 REDIS[("Redis 7<br/>๋ถ„์‚ฐ๋ฝ ยท ์นด์šดํ„ฐ")]
 end
 
 subgraph DATA["๋ฐ์ดํ„ฐ"]
 RDS[("AWS RDS<br/>MySQL 8")]
 OBS[("NCP Object Storage<br/>์ด๋ฏธ์ง€ ยท ๋ณ€ํ™˜ JSON")]
 end
 
 subgraph EXT["์™ธ๋ถ€ ์—ฐ๋™"]
 TAPI["Toss Partner API (mTLS)<br/>OAuth2 ยท IAP ยท ํ”„๋กœ๋ชจ์…˜ ยท ํ‘ธ์‹œ"]
 LAMBDA["AWS Lambda<br/>๊ตฌ๊ธ€ํผ ํŒŒ์‹ฑ"]
 SENTRY["Sentry"]
 DISCORD["Discord Webhook"]
 end
 
 TOSS --> NGX
 BO --> NGX
 NGX -->|"active"| BLUE
 NGX -.->|"standby"| GREEN
 BLUE --> REDIS
 BLUE --> RDS
 BLUE --> OBS
 BLUE --> TAPI
 BLUE --> LAMBDA
 BLUE --> SENTRY
 BLUE --> DISCORD
 LAMBDA -->|"ํŒŒ์‹ฑ ๊ฒฐ๊ณผ ์ ์žฌ"| OBS
Loading

3. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌ์กฐ

ํŒจํ‚ค์ง€๋ฅผ domain(๋น„์ฆˆ๋‹ˆ์Šค) ๊ณผ global(๊ณตํ†ตยท์™ธ๋ถ€์—ฐ๋™) ๋กœ ๋‚˜๋ˆ„๊ณ , ์™ธ๋ถ€ ์‹œ์Šคํ…œ์— ์˜์กดํ•˜๋Š” ์˜์—ญ์€ ํฌํŠธ-์–ด๋Œ‘ํ„ฐ ๋ฐฉ์‹์œผ๋กœ ์ธํ„ฐํŽ˜์ด์Šค์™€ ๊ตฌํ˜„์„ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

OneQ/OnSurvey/
โ”œโ”€โ”€ domain/ # ๋ฐ”์šด๋””๋“œ ์ปจํ…์ŠคํŠธ
โ”‚ โ”œโ”€โ”€ survey/ # ์„ค๋ฌธ ์ƒ์„ฑยท๊ฒŒ์‹œยท๊ด€๋ฆฌยท์ง‘๊ณ„ยทํ™˜๋ถˆ
โ”‚ โ”œโ”€โ”€ question/ # ๋ฌธํ•ญ 11์ข… ๋ฐ ๋ณด๊ธฐยท์„น์…˜
โ”‚ โ”œโ”€โ”€ participation/ # ์Šคํฌ๋ฆฌ๋‹ยท์‘๋‹ต ์ €์žฅยท์‘๋‹ต ์™„๋ฃŒ
โ”‚ โ”œโ”€โ”€ member/ # ํšŒ์›ยท์ฝ”์ธยทํฌ์ธํŠธ
โ”‚ โ”œโ”€โ”€ discount/ # ํ• ์ธ ์ฝ”๋“œ
โ”‚ โ””โ”€โ”€ admin/ # ๋ฐฑ์˜คํ”ผ์Šค (port/adapter ๊ตฌ์กฐ)
โ””โ”€โ”€ global/
 โ”œโ”€โ”€ auth/ # ์ธ์ฆ ์ „๋žต ยท ํ•„ํ„ฐ ยท ํ† ํฐ
 โ”œโ”€โ”€ payment/ promotion/ push/ # ํ† ์Šค ์—ฐ๋™ ์œ ์Šค์ผ€์ด์Šค (port/out)
 โ”œโ”€โ”€ infra/ # toss ยท redis ยท ncp ยท discord ยท transaction
 โ””โ”€โ”€ common/ # config ยท exception ยท response ยท util
flowchart TB
 subgraph L1["Presentation"]
 C1["REST Controller"]
 C2["Backoffice (Thymeleaf SSR)"]
 end
 subgraph L2["Security Filter Chain"]
 F1["#1 /connect-out โ€” ๊ณ ์ • ํ—ค๋” ๊ฒ€์ฆ"]
 F2["#2 ๋ฐฑ์˜คํ”ผ์Šค โ€” ์„ธ์…˜ + RateLimiter"]
 F3["#3 ๊ทธ ์™ธ โ€” STATELESS + AuthStrategy"]
 end
 subgraph L3["Application"]
 A1["Facade/UseCase"]
 A2["Command/Query ์„œ๋น„์Šค ๋ถ„๋ฆฌ"]
 end
 subgraph L4["Domain"]
 D1["Entity"]
 D2["Port(out) ์ธํ„ฐํŽ˜์ด์Šค"]
 end
 subgraph L5["Infrastructure"]
 I1["JPA + QueryDSL"]
 I2["TossApiClient (WebClient / mTLS)"]
 I3["RedisAgent (Redisson)"]
 I4["NcpS3Client / Discord Notifier"]
 end
 C1 --> F3 --> A1
 C2 --> F2 --> A1
 A1 --> A2 --> D1
 A1 --> D2
 D2 -. ๊ตฌํ˜„ .-> I2
 D2 -. ๊ตฌํ˜„ .-> I1
 A2 --> I3
 A1 --> I4
Loading

์˜๋„

  • Command / Query ๋ถ„๋ฆฌ โ€” ์กฐํšŒ๋Š” @Transactional(readOnly = true)๋กœ ์žก๊ณ  QueryDSL Projection์œผ๋กœ ํ•„์š”ํ•œ ์ปฌ๋Ÿผ๋งŒ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ์“ฐ๊ธฐ ๊ฒฝ๋กœ๋Š” ์ •ํ•ฉ์„ฑ, ์ฝ๊ธฐ ๊ฒฝ๋กœ๋Š” ์‘๋‹ต์†๋„๋ฅผ ์ค‘์ ์œผ๋กœ ๋ชจ๋ธ์„ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
  • RedisAgent๋ฅผ ์ธํ„ฐํŽ˜์ด์Šค(RedisLockAction, RedisCacheAction)๋กœ ๋…ธ์ถœ์‹œํ‚ต๋‹ˆ๋‹ค.
  • ํ•„ํ„ฐ ์ฒด์ธ 3๋ถ„๋ฆฌ โ€” ์‚ฌ์šฉ์ž์™€ ์šด์˜์ž์˜ ์ธ์ฆ ๋ฐฉ์‹์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ๊ฐ ์ธ์ฆ ๋ฐฉ์‹์— ๋”ฐ๋ผ@Order๊ฐ€ ๋‹ค๋ฅธ SecurityFilterChain ์„ธ ๊ฐœ๋กœ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

4. ๋„๋ฉ”์ธ ๋ชจ๋ธ

erDiagram
 MEMBER ||--o{ SURVEY : "์ƒ์„ฑ"
 MEMBER ||--o{ RESPONSE : "์ฐธ์—ฌ"
 MEMBER ||--o{ PAYMENT : "๊ฒฐ์ œ"
 MEMBER ||--o{ PROMOTION_GRANT : "ํ”„๋กœ๋ชจ์…˜ ์ˆ˜๋ น"
 SURVEY ||--|| SURVEY_INFO : "ํƒ€๊ฒŒํŒ…/๋ชฉํ‘œ์‘๋‹ต์ˆ˜"
 SURVEY ||--o{ SECTION : "๊ตฌ์„ฑ"
 SURVEY ||--o{ QUESTION : "ํฌํ•จ"
 SURVEY ||--o| SCREENING : "์‚ฌ์ „์งˆ๋ฌธ"
 SURVEY ||--o{ RESPONSE : "์‘๋‹ต ์ด๋ ฅ"
 QUESTION ||--o{ CHOICE_OPTION : "๊ฐ๊ด€์‹ ๋ณด๊ธฐ"
 QUESTION ||--o{ GRID_OPTION : "๊ทธ๋ฆฌ๋“œ ํ–‰/์—ด"
 QUESTION ||--o{ QUESTION_ANSWER : "๋‹ต๋ณ€"
 SCREENING ||--o{ SCREENING_ANSWER : "๋‹ต๋ณ€"
 SURVEY_INFO }o--o| DISCOUNT_CODE : "ํ• ์ธ ์ ์šฉ"
 MEMBER {
 bigint member_id PK
 bigint user_key UK "ํ† ์Šค ์‚ฌ์šฉ์ž ์‹๋ณ„์ž"
 string name
 enum gender
 enum status "ACTIVE / WITHDRAWN ๋“ฑ"
 bigint coin "์ถฉ์ „ ์ฝ”์ธ"
 bigint promotion_point "์ง€๊ธ‰ ํ”„๋กœ๋ชจ์…˜"
 bool onboarding_completed
 }
 SURVEY {
 bigint id PK
 bigint member_id FK
 text title
 datetime deadline
 enum status "WRITING/ONGOING/CLOSED/REFUNDED/REVIEW"
 bool is_temporary
 bool is_free
 int total_coin
 }
 SURVEY_INFO {
 bigint info_id PK
 bigint survey_id FK
 int due_count "๋ชฉํ‘œ ์‘๋‹ต ์ˆ˜"
 int completed_count
 enum gender "ํƒ€๊ฒŒํŒ… ์กฐ๊ฑด"
 int promotion_amount
 bool refundable
 }
 QUESTION {
 bigint question_id PK
 bigint survey_id FK
 string type "๋‹จ์ผ ํ…Œ์ด๋ธ” ์ƒ์† ํŒ๋ณ„์ž"
 int question_order
 int section
 bool is_required
 }
 RESPONSE {
 bigint survey_id PK "๋ณตํ•ฉ PK"
 bigint member_id PK
 bool is_responded
 bool is_screened
 }
 PROMOTION_GRANT {
 bigint id PK
 bigint user_key "UK(user_key, survey_id, promotion_code)"
 bigint survey_id
 string promotion_code
 enum status "PENDING/SUCCESS/FAILED"
 string exec_key "ํ† ์Šค ์‹คํ–‰ ํ‚ค"
 bool point_granted "ํฌ์ธํŠธ ๋ฐ˜์˜ ์—ฌ๋ถ€"
 }
 PAYMENT {
 bigint payment_id PK
 string order_id "ํ† ์Šค ์ฃผ๋ฌธ ๋ฒˆํ˜ธ"
 bigint user_key
 int total_amount
 enum status "PENDING/PAID/REFUNDED"
 enum purpose "COIN_CHARGE/HOME_REGISTRATION"
 }
Loading
  • ๋ฌธํ•ญ 11์ข…์„ ๋‹จ์ผ ํ…Œ์ด๋ธ” ์ƒ์†(SINGLE_TABLE)์œผ๋กœ โ€” ๊ฐ๊ด€์‹, ํ‰์ , NPS, ๊ทธ๋ฆฌ๋“œ, ๋‚ ์งœ, ์‹œ๊ฐ„ ๋“ฑ ํƒ€์ž…๋งˆ๋‹ค ํ•„์š”ํ•œ ์†์„ฑ์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ์กฐ์ธ ์ „๋žต์„ ์“ฐ๋ฉด ๋ฌธํ•ญ ์กฐํšŒ๋งˆ๋‹ค ์กฐ์ธ์ด ๋ถ™๋Š”๋ฐ, ์„ค๋ฌธ ์ฐธ์—ฌ ์‹œ ๋‹ค๊ฑด์˜ ๋ฌธํ•ญ ์กฐํšŒ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฏ€๋กœ ์กฐ์ธ ๋น„์šฉ์„ ์ตœ์†Œํ™”ํ•˜๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์— ๋Œ€ํ•œ ํŠธ๋ ˆ์ด๋“œ ์˜คํ”„๋กœ ํ•˜์œ„ ํƒ€์ž…์˜ ์ปฌ๋Ÿผ์ด nullable์ด ๋˜๋Š” ๊ฒƒ์„ ๊ฐ์ˆ˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • RESPONSE์˜ ๋ณตํ•ฉ PK (survey_id, member_id) โ€” ํ•œ ์‚ฌ๋žŒ์€ ํ•œ ์„ค๋ฌธ์— ํ•œ ๋ฒˆ๋งŒ ์‘๋‹ตํ•˜๋„๋ก ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ ˆ๋ฒจ์—์„œ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.
  • PROMOTION_GRANT์˜ ๋ณตํ•ฉ ์œ ๋‹ˆํฌ ์ œ์•ฝ โ€” ํฌ์ธํŠธ ์ค‘๋ณต ์ง€๊ธ‰์„ ๋ง‰๋„๋ก ์œ ๋‹ˆํฌ ์ œ์•ฝ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • SURVEY์™€ SURVEY_INFO ๋ถ„๋ฆฌ โ€” ์„ค๋ฌธ ๋ณธ๋ฌธ(์ œ๋ชฉ, ๋ฌธํ•ญ)์€ ์ž‘์„ฑ ์ค‘ ์ž์ฃผ ๋ณ€๊ฒฝ๋˜๊ณ  ํƒ€๊ฒŒํŒ…, ๊ฐ€๊ฒฉ ์ •๋ณด ๋“ฑ์€ ๊ฒฐ์ œ ๋ฐ ๊ฒŒ์‹œ ์‹œ์ ์— ํ™•์ •๋ฉ๋‹ˆ๋‹ค. ๋ณ€๊ฒฝ ์ฃผ๊ธฐ๊ฐ€ ๋‹ค๋ฅธ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„๋ฆฌํ•ด ์ž ๊ธˆ ๊ฒฝํ•ฉ์„ ์ค„์˜€์Šต๋‹ˆ๋‹ค.

About

๐ŸŒŠ๐Ÿ“ข OnSurvey Server ๐Ÿ“ข๐ŸŒŠ

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

AltStyle ใซใ‚ˆใฃใฆๅค‰ๆ›ใ•ใ‚ŒใŸใƒšใƒผใ‚ธ (->ใ‚ชใƒชใ‚ธใƒŠใƒซ) /