Skip to content
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

feat: DeepL API를 통해 뉴스를 번역하는 기능 추가 (#47) #58

Merged
merged 9 commits into from
Dec 26, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import kr.galaxyhub.sc.api.v1.translation.dto.TranslationRequest
import kr.galaxyhub.sc.common.support.toUri
import kr.galaxyhub.sc.translation.application.TranslationCommandService
import kr.galaxyhub.sc.translation.application.TranslationQueryService
import kr.galaxyhub.sc.translation.application.dto.TranslationCommand
import kr.galaxyhub.sc.translation.application.dto.TranslationResponse
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
Expand All @@ -28,7 +27,7 @@ class TranslationControllerV1(
@PathVariable newsId: UUID,
@RequestBody request: TranslationRequest,
): ResponseEntity<ApiResponse<UUID>> {
val command = TranslationCommand(newsId, request.destinationLanguage)
val command = request.toCommand(newsId)
val translateProgressionId = translationCommandService.translate(command)
return ResponseEntity.created("/api/v1/translation/${translateProgressionId}".toUri())
.body(ApiResponse.success(translateProgressionId))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
package kr.galaxyhub.sc.api.v1.translation.dto

import java.util.UUID
import kr.galaxyhub.sc.news.domain.Language
import kr.galaxyhub.sc.translation.application.dto.TranslationCommand
import kr.galaxyhub.sc.translation.domain.TranslatorProvider

data class TranslationRequest(
val destinationLanguage: Language
)
val sourceLanguage: Language,
val targetLanguage: Language,
val translatorProvider: TranslatorProvider,
) {

fun toCommand(newsId: UUID) = TranslationCommand(
newsId = newsId,
sourceLanguage = sourceLanguage,
targetLanguage = targetLanguage,
translatorProvider = translatorProvider
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package kr.galaxyhub.sc.news.application

import io.github.oshai.kotlinlogging.KotlinLogging
import kr.galaxyhub.sc.news.application.dto.NewsAppendContentEvent
import kr.galaxyhub.sc.news.domain.NewsRepository
import kr.galaxyhub.sc.news.domain.getOrThrow
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

private val log = KotlinLogging.logger {}

/**
* TranslationCommandService.translate 메시드 호출 시 News가 이미 생성되어 있으므로 예외 발생 가능성은 없음
* 하지만 혹시 모를 상황에 error 로그 남김
*/
@Component
@Transactional
class NewsAppendContentEventListener(
private val newsRepository: NewsRepository
) {

@EventListener
fun appendContent(event: NewsAppendContentEvent) {
runCatching {
newsRepository.getOrThrow(event.newsId)
}.onSuccess {
it.addContent(event.content)
}.onFailure {
log.error { "뉴스에 컨텐츠를 추가하는 중 예외가 발생했습니다. ${it.message}" }
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ class NewsCommandService(

fun create(command: NewsCreateCommand): UUID {
val news = newsRepository.findByOriginId(command.originId) ?: newsRepository.save(command.toNews())
val content = createContent(news, command)
val content = createContent(news.id, command)
news.addContent(content)
return news.id
}

private fun createContent(news: News, command: NewsCreateCommand) = Content(
news = news,
private fun createContent(newsId: UUID, command: NewsCreateCommand) = Content(
newsId = newsId,
language = command.language,
content = command.content,
newsInformation = NewsInformation(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kr.galaxyhub.sc.news.application.dto

import java.util.UUID
import kr.galaxyhub.sc.news.domain.Content

data class NewsAppendContentEvent(
val newsId: UUID,
val translateProgressionId: UUID,
val content: Content
)
8 changes: 3 additions & 5 deletions src/main/kotlin/kr/galaxyhub/sc/news/domain/Content.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ import jakarta.persistence.Embedded
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.FetchType
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import java.util.UUID

@Entity
class Content(
news: News,
newsId: UUID,
newsInformation: NewsInformation,
language: Language,
content: String,
Expand All @@ -24,9 +23,8 @@ class Content(
@GeneratedValue(strategy = GenerationType.IDENTITY)
val sequence: Long? = null

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "news_id", nullable = false)
var news: News = news
var newsId: UUID = newsId
protected set

@Embedded
Expand Down
9 changes: 6 additions & 3 deletions src/main/kotlin/kr/galaxyhub/sc/news/domain/Language.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package kr.galaxyhub.sc.news.domain

enum class Language {
ENGLISH,
KOREAN,
enum class Language(
val shortName: String,
) {

ENGLISH("EN"),
KOREAN("KO"),
}
8 changes: 6 additions & 2 deletions src/main/kotlin/kr/galaxyhub/sc/news/domain/News.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class News(
private val mutableSupportLanguages: EnumSet<Language> = EnumSet.noneOf(Language::class.java)
val supportLanguages: Set<Language> get() = mutableSupportLanguages.toHashSet()

@OneToMany(fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST], mappedBy = "news")
@OneToMany(fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST], mappedBy = "newsId")
private val contents: MutableList<Content> = mutableListOf()

fun addContent(content: Content) {
Expand All @@ -62,12 +62,16 @@ class News(
}

private fun validateAddContent(content: Content) {
validate(content.news != this) { "컨텐츠에 등록된 뉴스가 동일하지 않습니다." }
validate(content.newsId != id) { "컨텐츠에 등록된 뉴스가 동일하지 않습니다." }
validate(mutableSupportLanguages.contains(content.language)) { "이미 해당 언어로 작성된 뉴스가 있습니다." }
}

fun getContentByLanguage(language: Language): Content {
return contents.find { it.language == language }
?: throw NotFoundException("해당 언어의 컨텐츠가 존재하지 않습니다.")
}

fun isSupportLanguage(language: Language): Boolean {
return mutableSupportLanguages.contains(language)
}
}
3 changes: 3 additions & 0 deletions src/main/kotlin/kr/galaxyhub/sc/news/domain/NewsRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import org.springframework.data.repository.query.Param
fun NewsRepository.getByDetailByIdAndLanguage(id: UUID, language: Language) = findFetchByIdAndLanguage(id, language)
?: throw NotFoundException("식별자와 언어에 대한 뉴스를 찾을 수 없습니다. id=${id}, language=${language}")

fun NewsRepository.getOrThrow(id: UUID) = findById(id)
?: throw NotFoundException("식별자에 대한 뉴스를 찾을 수 없습니다. id=${id}")

interface NewsRepository : Repository<News, UUID> {

fun save(news: News): News
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package kr.galaxyhub.sc.translation.application

import io.github.oshai.kotlinlogging.KotlinLogging
import kr.galaxyhub.sc.news.application.dto.NewsAppendContentEvent
import kr.galaxyhub.sc.translation.application.dto.TranslatorFailureEvent
import kr.galaxyhub.sc.translation.domain.TranslationProgressionRepository
import kr.galaxyhub.sc.translation.domain.getOrThrow
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

private val log = KotlinLogging.logger {}

/**
* TranslationCommandService.translate 메서드의 트랜잭션이 API 호출보다 늦게 끝나면 예외 발생 가능성 있음.
* 따라서 log를 error로 발생시킴
* 해당 로그 파악하면 Retry 전략 구성할 것
* 혹은 TranslationCommandService.translate에서 TransactionalEventListener를 받는곳에서 WebClient 실행 고려
*/
@Component
@Transactional
class TranslationAppendContentEventListener(
private val translationProgressionRepository: TranslationProgressionRepository
) {

@EventListener
fun changeTranslationProgressionComplete(event: NewsAppendContentEvent) {
runCatching {
translationProgressionRepository.getOrThrow(event.translateProgressionId)
}.onSuccess {
it.changeComplete()
}.onFailure {
log.error { "번역 진행 상황의 상태를 변경 중 예외가 발생했습니다. message=${it.message}" }
}
}

@EventListener
fun changeTranslationProgressionFailure(event: TranslatorFailureEvent) {
runCatching {
translationProgressionRepository.getOrThrow(event.translateProgressionId)
}.onSuccess {
it.changeFailure(event.message)
}.onFailure {
log.error { "번역 진행 상황의 상태를 변경 중 예외가 발생했습니다. message=${it.message}" }
}
}
}

Original file line number Diff line number Diff line change
@@ -1,23 +1,48 @@
package kr.galaxyhub.sc.translation.application

import io.github.oshai.kotlinlogging.KotlinLogging
import java.util.UUID
import kr.galaxyhub.sc.common.support.validate
import kr.galaxyhub.sc.news.application.dto.NewsAppendContentEvent
import kr.galaxyhub.sc.news.domain.NewsRepository
import kr.galaxyhub.sc.news.domain.getByDetailByIdAndLanguage
import kr.galaxyhub.sc.translation.application.dto.TranslationCommand
import kr.galaxyhub.sc.translation.application.dto.TranslatorFailureEvent
import kr.galaxyhub.sc.translation.domain.TranslateProgression
import kr.galaxyhub.sc.translation.domain.TranslationProgressionRepository
import kr.galaxyhub.sc.translation.domain.TranslatorClients
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

private val log = KotlinLogging.logger {}

@Service
@Transactional
class TranslationCommandService(
private val translationProgressionRepository: TranslationProgressionRepository
private val translationProgressionRepository: TranslationProgressionRepository,
private val translatorClients: TranslatorClients,
private val newsRepository: NewsRepository,
private val eventPublisher: ApplicationEventPublisher,
) {

fun translate(command: TranslationCommand): UUID {
val newsId = command.newsId
val destinationLanguage = command.destinationLanguage
val translateProgression = TranslateProgression(newsId, destinationLanguage)
val (newsId, sourceLanguage, targetLanguage, translatorProvider) = command
val news = newsRepository.getByDetailByIdAndLanguage(newsId, sourceLanguage)
validate(news.isSupportLanguage(targetLanguage)) { "이미 뉴스에 번역된 컨텐츠가 존재합니다. targetLanguage=$targetLanguage" }
val content = news.getContentByLanguage(sourceLanguage)
val translateProgression = TranslateProgression(newsId, sourceLanguage, targetLanguage, translatorProvider)
translationProgressionRepository.save(translateProgression)
val translatorClient = translatorClients.getClient(translatorProvider)
translatorClient.requestTranslate(content, targetLanguage)
.doOnError {
log.warn { "뉴스 번역 요청이 실패하였습니다. newsId=${newsId}, translateProgressionId=${translateProgression.id}" }
eventPublisher.publishEvent(TranslatorFailureEvent(translateProgression.id, it.message))
}
.subscribe {
log.info { "뉴스 번역 요청이 완료되었습니다. newsId=${newsId}, translateProgressionId=${translateProgression.id}" }
eventPublisher.publishEvent(NewsAppendContentEvent(newsId, translateProgression.id, it))
}
return translateProgression.id
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package kr.galaxyhub.sc.translation.application.dto

import java.util.UUID
import kr.galaxyhub.sc.news.domain.Language
import kr.galaxyhub.sc.translation.domain.TranslatorProvider

data class TranslationCommand(
val newsId: UUID,
val destinationLanguage: Language
val sourceLanguage: Language,
val targetLanguage: Language,
val translatorProvider: TranslatorProvider,
)
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package kr.galaxyhub.sc.translation.application.dto

import java.util.UUID
import kr.galaxyhub.sc.news.domain.Language
import kr.galaxyhub.sc.translation.domain.TranslateProgression
import kr.galaxyhub.sc.translation.domain.TranslationStatus
import kr.galaxyhub.sc.translation.domain.TranslatorProvider

data class TranslationResponse(
val translateProgressionId: UUID,
val targetNewsId: UUID,
val sourceLanguage: Language,
val targetLanguage: Language,
val translationProvider: TranslatorProvider,
val translationStatus: TranslationStatus,
val message: String? = null,
) {
Expand All @@ -18,7 +23,10 @@ data class TranslationResponse(
translateProgressionId = translationProgression.id,
targetNewsId = translationProgression.newsId,
translationStatus = translationProgression.translationStatus,
message = translationProgression.message
message = translationProgression.message,
sourceLanguage = translationProgression.sourceLanguage,
targetLanguage = translationProgression.targetLanguage,
translationProvider = translationProgression.translatorProvider
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package kr.galaxyhub.sc.translation.application.dto

import java.util.UUID

data class TranslatorFailureEvent(
val translateProgressionId: UUID,
val message: String?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package kr.galaxyhub.sc.translation.config

import kr.galaxyhub.sc.translation.domain.TranslatorClient
import kr.galaxyhub.sc.translation.domain.TranslatorClients
import kr.galaxyhub.sc.translation.infra.DeepLTranslatorClient
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.http.HttpHeaders
import org.springframework.web.reactive.function.client.WebClient

@Configuration
class TranslatorClientConfig(
@Value("\${galaxyhub.translator.deepl-api-key}") private val deepLApiKey: String,
private val webClient: WebClient,
) {

@Bean
@Profile("!test")
fun deepLTranslatorClient(): TranslatorClient {
return DeepLTranslatorClient(
webClient.mutate()
.baseUrl("https://api.deepl.com")
.defaultHeader(HttpHeaders.AUTHORIZATION, "DeepL-Auth-Key $deepLApiKey")
.build()
)
}

@Bean
fun translatorClients(translatorClients: List<TranslatorClient>): TranslatorClients {
return TranslatorClients.from(translatorClients)
}
}
Loading