Spring MSA 구현하기 (WebClient적용하기)
Spring MSA 구현하기 (WebClient적용하기) — #SpringMSA #SpringWebClient #스프링클라이언트 #스프링서버개발 #개발자의도구들 🚨📝 📖 ...
#SpringMSA #SpringWebClient #스프링클라이언트 #스프링서버개발 #개발자의도구들
🚨📝 📖 📒✏️💡🔍
AI스쿨 msa기반 java 백엔드 코스 중에 공부한 내용을 작성하였습니다
\* 현재 Spring x 프로젝트를 진행하고 있습니다. 전체 목차를 보시려면 여기를 눌러주세요.
\* 해당 프로젝트 이전에 공부한 내용을 보시려면 여기를 눌러주세요.
좋아요 기능을 MSA로 구현하기
어전 글에서 좋아요 api를 설계했었는데요. 모놀리식 아키텍쳐를 적용해서, 클라이언트와 직접적으로 적용하는 방식으로 구했었습니다. 이번에는 이 좋아요 기능을 MSA로 변경하고자 합니다
Index
- Purpose
- What to do
- Principle
- FrontServer
- Setting
- Save
- Delete
- getUsers
- LikeApirServer
- Save
- Delete
- getUsers
- Question
- Answer
- Feedback
| ✅ Tips: |
|---|
Purpose
기존 좋아요 기능을 MSA로 설계합니다.
What to do
- 기존 서버를 FrontServer, LikeServer로 나누기
- FrontServer에 WebCleint를 연결하기
- MSA 소개글의 마지막 실습문제 해결하기
Principle
서버 통신에 있어서 다음 두가지 원칙을 지키고자 합니다. 미리 원칙을 세워두면, 코드 작성에 판단 시간을 줄이고 일관성을 줄 수 있기 때문에 좋습니다.
1. Server간의 Error사항은 정해진 Error Code를 사용하며, RFC7807형태로 에러를 상세히 기술한다.
2. DTO의 재사용성을 최소화하여 가능한 API당 DTO를 따로 제작한다.
Architecture
| 🧑💻 Logic |
|---|
단일 API
save Api (delete도 유사함)
다중 API(다중 server)
getUsers Api : 서로 다른 Table을 Join할 필요가 있는 경우
MSA 소개글에서 소개해드린 패턴입니다. 서로 다른 서비스 영역에서 Join이 필요한 경우에, FrontServer는 각 서비스 서버와 통신하여 필요한 데이터를 주고 받습니다. 이후 최종적으로 유저에게 필요한 정보를 제공합니다.
Front Server
| 🧑💻 Service |
|---|
프론트 서버를 Client 처럼 동작시키기 위해 WebCleint 를 사용하였습니다. 우선 이 글에서는 단일 API인 save만 구현하도록 하겠습니다. (UserServer는 아직 MSA를 적용하지 않았음)
| ✅ WebCleint는 HTTP 비동기식 Request Library입니다. 라이브러리에 대한 자세한 내용은 차후 따로 다루겠습니다 |
|---|
package com.example.frontServer.service
import com.example.frontServer.dto.like.LikeRequest
import com.example.frontServer.dto.like.LikeSaveResult
import com.example.frontServer.dto.like.LikeServerSaveResponse
import com.example.frontServer.enum.FrontServerError
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry
import org.springframework.http.HttpHeaders
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.util.UriBuilder
@Service
class LikeService(
private val client : WebClient,
) {
private val circuitBreaker = circuitBreakerRegistry.circuitBreaker("liveApiCircuitBreaker")
private val logger = KotlinLogging.logger {}
fun save(boardId: Long, userId: Long): LikeSaveResult {
val response = client.post()
.uri { uriBuilder: UriBuilder ->
uriBuilder
.path("/like")
.build()
}
.bodyValue(
LikeRequest(
boardId = boardId,
userId = userId,
)
)
.headers { headers ->
headers.remove(HttpHeaders.AUTHORIZATION)
}
.retrieve()
.bodyToMono(LikeServerSaveResponse::class.java)
.block()
if (response == null) {
return LikeSaveResult(
error = FrontServerError.UNEXPECTED_ERROR
)
// fallback으로 처리 할 수 는 없을까?
}
return LikeSaveResult.of(response)
}
}
이런식으로 Webclient를 사용하면 서버를 클라이언트 처럼 사용할 수 있습니다.
사용된 DTO입니다.
- FrontServer LikeService -> request Body : LikeRequest
package com.example.frontServer.dto.like
import org.jetbrains.annotations.NotNull
data class LikeRequest(
@field: NotNull
val boardId: Long,
@field: NotNull
val userId: Long,
@field: NotNull
val likeType: Int,
)
- LikeServer -> FrontServer LikeService : LikeServerSaveResponse
package com.example.frontServer.dto.like
import com.example.frontServer.dto.ServerErrorDetails
import com.example.frontServer.enum.ServiceServerError
data class LikeServerSaveResponse(
val error: ServiceServerError?,
val details: ServerErrorDetails?
)
\* server로 부터 에러가 있는 경우 details에 정보가 표기됩니다.
- FrontServer LikeService -> LikeController : LikeSaveResult
package com.example.frontServer.dto.like
import com.example.frontServer.enum.FrontServerError
import com.example.frontServer.enum.ServiceServerError
// service to controller
data class LikeSaveResult(
val error: FrontServerError?
) {
companion object {
fun of(serverResult: LikeServerSaveResponse): LikeSaveResult {
if (serverResult.error == null) {
return LikeSaveResult(null)
}
return if (serverResult.error == ServiceServerError.UNKNOWN_ID) {
LikeSaveResult(
error = FrontServerError.UNKNOWN_ID
)
} else {
LikeSaveResult(
error = FrontServerError.UNEXPECTED_ERROR
)
}
}
}
}
of 메서드를 사용하여 서버 에러 -> 클라이언트 에러간의 switching이 일어납니다.
| 🧑💻 Controller |
|---|
Controller를 통해 FrontServer는 CLient에게 Response를 전달합니다.
package com.example.frontServer.controller
import com.example.frontServer.dto.ResponseToClientDto
import com.example.frontServer.dto.like.LikeSaveResponse
import com.example.frontServer.security.AuthUserDetails
import com.example.frontServer.service.LikeService
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
class LikeController(
private val likeService: LikeService
) {
private val logger = KotlinLogging.logger {}
@PostMapping("/like")
fun save(
@Valid @RequestBody boardId: Long,
@AuthenticationPrincipal user: AuthUserDetails
): ResponseEntity<LikeSaveResponse> {
return ResponseEntity.ok().body(
LikeSaveResponse.of(
likeService.save(boardId, user.getUserId())
)
)
}
}
Response에는 아래 DTO가 사용되었습니다.
package com.example.frontServer.dto.like
import com.example.frontServer.enum.FrontServerError
// controller to client
data class LikeSaveResponse(
val error: FrontServerError?
) {
companion object{
fun of (result: LikeSaveResult): LikeSaveResponse {
return if (result.error != null) {
LikeSaveResponse(
error = result.error
)
} else {
LikeSaveResponse(
error = null
)
}
}
}
}
에러가 있는 경우에 LikeSaveResult에서 그대로 EroorCode를 가져와 전송합니다. 최종적으로 CLient는 FrontServerError 코드를 받아 볼 수 있습니다.
Like Server
LikeServer의 내부 로직(controller -> service -> repository)은 이전글과 동일합니다. 주요 변경사항인 FrontServer로 어떤 정보를 내보낼지에 관해서 기술하였습니다.
| 🧑💻 Controller |
|---|
package com.example.liveApiServer.controller
import com.example.liveApiServer.dto.LikeRequest
import com.example.liveApiServer.dto.LikeServerSaveResponse
import com.example.liveApiServer.dto.save.LikeRequest
import com.example.liveApiServer.dto.save.LikeServerSaveResponse
import com.example.liveApiServer.service.LikeService
import jakarta.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
class LikeController(
private val likeService: LikeService,
) {
@PostMapping("/like")
fun save(
@Valid @RequestBody request: LikeRequest
): ResponseEntity<LikeServerSaveResponse> {
return ResponseEntity.ok().body(
LikeServerSaveResponse.of(
likeService.save(request)
)
)
}
}
LikeServerSaveResponse 정보를 FrontServer에게 retrun.
| 🧑💻 Service |
|---|
package com.example.liveApiServer.service
import com.example.liveApiServer.dto.save.LikeRequest
import com.example.liveApiServer.dto.save.LikeServerSaveResult
import com.example.liveApiServer.entity.Like
import com.example.liveApiServer.repository.LikeRepository
import org.springframework.stereotype.Service
@Service
class LikeService(
private val likeRepository: LikeRepository,
) {
fun save(request: LikeRequest): LikeServerSaveResult{
return try {
likeRepository.save(
Like(
id = null, // 이렇게 명시하는게 좋아.
boardId = request.boardId,
userId = request.userId,
likeType = request.likeType,
)
)
LikeServerSaveResult(
details = null,
error = null
)
} catch {
// error 별로 LikeServerSaveResult 관리
// boardId userId가 없는 경우 어떻게 처리?
// likeType이 없는 type인 경우도 있을 수 있다.
}
}
}
현재 Service는 LikeServerResult를 반환 중입니다. 하지만, 여러가지 DB에러가 발생할 수 있는데요. 서버의 안정성을 높이기 위해서는 각 Exception을 catch하여 적절하게 Error 및 details를 작성하여 return 해야 합니다.
현재는 service에서 각 예외 상항을 처리하고 있습니다. 이렇게 되면 Service 코드가 굉장히 길어지게 되는데, 코드 가독성이 좋지 못합니다.
이를 위해 Spring에서는 Exception Handler 기능을 사용하고 있습니다. 이와 관련해서는 이후에 다루겠습니다. 현재는 Service에서 여러가지 예외 상황을 고려해야한다 정도만 알고 있으면 될 것 같습니다.
| 🧑💻 Others |
|---|
Error Handling
MSA에서는 두가지 종류의 에러처리를 고려해야 합니다. 이전글에서도 언급한적이 있는데, 클라이언트 측에 보내는 에러는 최소한의 에러정보만을 표시, 서버 사이에 전달되는 에러는 최대한 자세하게 기술하도록 구현합니다.
그래서 아래와 같은 형태로 에러를 관리하였습니다.
👉 FrontServer - MSA Servers
ServiceServerError : Service Server사이에서 사용되는 공용 에러코드 ex) 10001, 10002 ...
ServerErrorDetails : RFC 표준을 변형하여 사용하는 에러 Spec
👉 Client - FrontServer
FrontServerError : FrontServer에서 Client Server로 전달되는 에러코드 ex) 10001, 10002 ...
(클라이언트와 프론트 서버간에 미리 맞춰놓으면 됨)
앞으로 모든 에러는 이런식으로 처리할 예정입니다. 에러코드의 경우 Enum Class의 Ordinal 번호를 부여하였습니다 .
Question
| 🤔 write Question 1. 에러 DTO: 좋은 설계인가? |
|---|
Feedback
| 🙋♂️ |
|---|



