-
Notifications
You must be signed in to change notification settings - Fork 0
[WTH-236] 랜딩 문의하기 api 구현 #45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "feat/WTH-236-\uB79C\uB529-\uBB38\uC758\uD558\uAE30-API-\uAD6C\uD604"
Changes from all commits
f1a42ad
6b25d11
1fa51f5
01d6bc8
978b932
0e690f9
e484fec
4c44f81
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.weeth.domain.user.application.dto.request | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema | ||
| import jakarta.validation.constraints.Email | ||
| import jakarta.validation.constraints.NotBlank | ||
| import jakarta.validation.constraints.Size | ||
|
|
||
| data class CreateInquiryRequest( | ||
| @field:Schema(description = "이메일", example = "user@example.com") | ||
| @field:NotBlank | ||
| @field:Email | ||
| @field:Size(max = 255) | ||
| val email: String, | ||
| @field:Schema(description = "문의 내용", example = "서비스에 대해 문의드립니다.") | ||
| @field:NotBlank | ||
| @field:Size(max = 1000) | ||
| val message: String, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.weeth.domain.user.application.usecase.command | ||
|
|
||
| import com.weeth.domain.user.application.dto.request.CreateInquiryRequest | ||
| import com.weeth.domain.user.domain.port.InquiryNotifyPort | ||
| import com.weeth.domain.user.domain.port.InquirySavePort | ||
| import org.springframework.stereotype.Service | ||
|
|
||
| @Service | ||
| class CreateInquiryUseCase( | ||
| private val inquirySavePort: InquirySavePort, | ||
| private val inquiryNotifyPort: InquiryNotifyPort, | ||
| ) { | ||
| fun execute(request: CreateInquiryRequest) { | ||
|
Comment on lines
+8
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# command usecase의 `@Transactional` 적용 현황 확인
fd -t f -e kt 'UseCase' src/main/kotlin/com/weeth/domain \
| xargs rg -n -C1 '@Transactional|class .*UseCase|fun execute\('Repository: Team-Weeth/weeth-server Length of output: 26082 커맨드 UseCase에
수정 예시 import com.weeth.domain.user.domain.port.InquirySavePort
import org.springframework.stereotype.Service
+import org.springframework.transaction.annotation.Transactional
`@Service`
class CreateInquiryUseCase(
private val inquirySavePort: InquirySavePort,
private val inquiryNotifyPort: InquiryNotifyPort,
) {
+ `@Transactional`
fun execute(request: CreateInquiryRequest) {
inquirySavePort.save(request.email, request.message)
inquiryNotifyPort.notify(request.email, request.message)
}🤖 Prompt for AI Agents |
||
| inquirySavePort.save(request.email, request.message) | ||
| inquiryNotifyPort.notify(request.email, request.message) | ||
|
Comment on lines
+14
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저장 성공 후 알림 실패 시 요청 재시도로 중복 저장 위험이 있습니다. 현재 순서에서는 저장이 이미 성공한 상태에서 알림 실패 예외가 나면 클라이언트는 실패로 인식해 재시도할 수 있고, 그 경우 동일 문의가 다시 저장될 수 있습니다. 저장과 알림의 성공 기준을 분리(예: 알림 비동기/재시도 큐)하는 쪽이 안전합니다. 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.weeth.domain.user.domain.port | ||
|
|
||
| interface InquiryNotifyPort { | ||
| fun notify( | ||
| email: String, | ||
| message: String, | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.weeth.domain.user.domain.port | ||
|
|
||
| interface InquirySavePort { | ||
| fun save( | ||
| email: String, | ||
| message: String, | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package com.weeth.domain.user.infrastructure | ||
|
|
||
| import com.weeth.domain.user.domain.port.InquirySavePort | ||
| import com.weeth.global.config.properties.NotionProperties | ||
| import org.slf4j.LoggerFactory | ||
| import org.springframework.http.HttpHeaders | ||
| import org.springframework.scheduling.annotation.Async | ||
| import org.springframework.stereotype.Component | ||
| import org.springframework.web.client.RestClient | ||
| import java.time.LocalDate | ||
|
|
||
| @Component | ||
| class NotionInquirySaveAdapter( | ||
| private val notionProperties: NotionProperties, | ||
| restClientBuilder: RestClient.Builder, | ||
| ) : InquirySavePort { | ||
| private val restClient = restClientBuilder.baseUrl("https://api.notion.com").build() | ||
| private val log = LoggerFactory.getLogger(javaClass) | ||
|
|
||
| @Async | ||
| override fun save( | ||
| email: String, | ||
| message: String, | ||
| ) { | ||
| val body = | ||
| mapOf( | ||
| "parent" to | ||
| mapOf( | ||
| "type" to "database_id", | ||
| "database_id" to notionProperties.inquiryDatabaseId, | ||
| ), | ||
| "properties" to | ||
| mapOf( | ||
| "문의내용" to | ||
| mapOf( | ||
| "title" to listOf(mapOf("text" to mapOf("content" to message))), | ||
| ), | ||
| "이메일" to | ||
| mapOf( | ||
| "email" to email, | ||
| ), | ||
| "날짜" to | ||
| mapOf( | ||
| "date" to mapOf("start" to LocalDate.now().toString()), | ||
| ), | ||
| ), | ||
| ) | ||
|
|
||
| runCatching { | ||
| restClient | ||
| .post() | ||
| .uri("/v1/pages") | ||
| .header(HttpHeaders.AUTHORIZATION, "Bearer ${notionProperties.token}") | ||
| .header("Notion-Version", notionProperties.version) | ||
| .header(HttpHeaders.CONTENT_TYPE, "application/json") | ||
| .body(body) | ||
| .retrieve() | ||
| .toBodilessEntity() | ||
| }.onFailure { e -> log.warn("Notion 저장 실패: {}", e.message) } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package com.weeth.domain.user.infrastructure | ||
|
|
||
| import com.weeth.domain.user.domain.port.InquiryNotifyPort | ||
| import com.weeth.global.config.properties.SlackProperties | ||
| import org.slf4j.LoggerFactory | ||
| import org.springframework.scheduling.annotation.Async | ||
| import org.springframework.stereotype.Component | ||
| import org.springframework.web.client.RestClient | ||
|
|
||
| @Component | ||
| class SlackInquiryNotifyAdapter( | ||
| private val slackProperties: SlackProperties, | ||
| restClientBuilder: RestClient.Builder, | ||
| ) : InquiryNotifyPort { | ||
| private val restClient = restClientBuilder.build() | ||
| private val log = LoggerFactory.getLogger(javaClass) | ||
|
|
||
| @Async | ||
| override fun notify( | ||
| email: String, | ||
| message: String, | ||
| ) { | ||
| val text = "*[랜딩 문의하기]*\n*이메일:* $email\n*문의 내용:*\n```$message```" | ||
|
|
||
| runCatching { | ||
| restClient | ||
| .post() | ||
| .uri(slackProperties.webhookUrl) | ||
| .header("Content-Type", "application/json") | ||
| .body(mapOf("text" to text)) | ||
| .retrieve() | ||
| .toBodilessEntity() | ||
| }.onFailure { e -> log.warn("Slack 알림 전송 실패: {}", e.message) } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.weeth.global.config | ||
|
|
||
| import org.springframework.context.annotation.Bean | ||
| import org.springframework.context.annotation.Configuration | ||
| import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor | ||
| import java.util.concurrent.Executor | ||
|
|
||
| @Configuration | ||
| class AsyncConfig { | ||
| @Bean(name = ["taskExecutor"]) | ||
| fun taskExecutor(): Executor = | ||
| ThreadPoolTaskExecutor().apply { | ||
| corePoolSize = 5 | ||
| maxPoolSize = 10 | ||
| queueCapacity = 50 | ||
| setThreadNamePrefix("async-") | ||
| initialize() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.weeth.global.config.properties | ||
|
|
||
| import jakarta.validation.constraints.NotBlank | ||
| import org.springframework.boot.context.properties.ConfigurationProperties | ||
| import org.springframework.validation.annotation.Validated | ||
|
|
||
| @Validated | ||
| @ConfigurationProperties(prefix = "notion") | ||
| data class NotionProperties( | ||
| @field:NotBlank val token: String, | ||
| @field:NotBlank val version: String, | ||
| @field:NotBlank val inquiryDatabaseId: String, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.weeth.global.config.properties | ||
|
|
||
| import jakarta.validation.constraints.NotBlank | ||
| import org.springframework.boot.context.properties.ConfigurationProperties | ||
| import org.springframework.validation.annotation.Validated | ||
|
|
||
| @Validated | ||
| @ConfigurationProperties(prefix = "slack") | ||
| data class SlackProperties( | ||
| @field:NotBlank val webhookUrl: String, | ||
| ) |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Team-Weeth/weeth-server
Length of output: 131
🏁 Script executed:
Repository: Team-Weeth/weeth-server
Length of output: 16729
@EnableAsync활성화 시 TaskExecutor 설정이 누락되었습니다. 즉시 수정이 필요합니다.현재 코드베이스에서
@Async메서드가 실제로 사용되고 있습니다:NotionInquirySaveAdapter.save()(외부 Notion API 호출)SlackInquiryNotifyAdapter.notify()(외부 Slack API 호출)하지만 TaskExecutor 설정이 없어 Spring의 기본값인
SimpleAsyncTaskExecutor가 사용됩니다. 이는 각 비동기 작업마다 새로운 스레드를 생성하므로, 문의 폭주 시 스레드 무한 생성으로 인한 메모리 고갈 및 시스템 마비를 초래할 수 있습니다.다음 중 하나로 해결하세요:
TaskExecutorBean을 정의하고 스레드 풀 크기/큐 정책을 명시 (ThreadPoolTaskExecutor권장)spring.task.execution.*프로퍼티로 스레드 풀 설정 (예:spring.task.execution.pool.core-size,max-size,queue-capacity)🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AsyncConfig도 추가해두는게 조을 것 같아욤!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hyxklee동의해주셔서 감사합니다! 아래와 같이AsyncConfig를 추가하시면 됩니다.src/main/kotlin/com/weeth/global/config/AsyncConfig.kt파일을 새로 생성해 주세요:또는 별도 클래스 없이
application.yml에 다음을 추가하는 방법도 있습니다:설정값 근거 (Notion/Slack 같은 외부 API 호출 위주의 I/O-bound 작업 기준):
corePoolSizemaxPoolSizequeueCapacitythreadNamePrefixasync-inquiry-queueCapacity가 가득 차고maxPoolSize에 도달하면TaskRejectedException이 발생하므로, 필요 시RejectedExecutionHandler도 함께 설정하시면 더욱 안전합니다.🧠 Learnings used