메인 서버에서 팔로잉 요청을 처리하는 파트를 맡아 코드를 작성했다. following server가 있었기 때문에, 메인 서버에서 할 일은 WebClient로 following server에 필요한 데이터와 함께 작업을 요청하는 정도만 수행하면 된다. 따라서 WebClient 작동 방식만 알면 간단한 작업이 된다.
WebClient는 HTTP request를 수행하기 위한 client 모듈 중 하나이다. 자세한 사용 방법은 아래 공식 문서를 참고하면 알 수 있다.
https://docs.spring.io/spring-framework/reference/web/webflux-webclient.html
WebClient :: Spring Framework
Spring WebFlux includes a client to perform HTTP requests with. WebClient has a functional, fluent API based on Reactor, see Reactive Libraries, which enables declarative composition of asynchronous logic without the need to deal with threads or concurrenc
docs.spring.io
Following에 관한 기능을 크게 세 가지로 잡고 작업을 시작했다.
1. 새롭게 팔로우를 하는 경우 (newFollowing)
2. 팔로우하고 있는 목록을 조회하는 경우 (getFollowing)
3. 팔로우 취소 (unfollowing)
1. 새롭게 팔로우하는 경우
/* FollowingRequestDto.kt */
package com.flowery.flowerygateway.dto
import java.util.*
data class FollowingRequestDTO(
val followerId: UUID,
val followingId: UUID
)
Following Request를 보낼 Dto에는 following 요청을 보낸 사용자(팔로워)의 id, following을 받을 사용자의 id를 담고 있다. following server api에서 요청한 데이터 형식을 따랐다.
/* FollowingController.kt */
@PostMapping("gardener/newfollowing")
fun newFollowing(followingRequestDTO: FollowingRequestDTO): Mono<ResponseEntity<String>> {
return followingService.addFollowing(followingRequestDTO)
.flatMap { response ->
if (response.body == null || response.body!!.get("ok") == false) {
Mono.just(ResponseEntity.status(404).body("Following failed"))
} else if (response.statusCode != HttpStatus.OK) {
Mono.just(ResponseEntity.status(response.statusCode).body("Following failed"))
} else {
Mono.just(ResponseEntity.ok("Following succeeded"))
}
}
}
cotroller에서는 오류처리 작업만 해주었다. 뒤에서 나올 FollowingService 내의 addFollowing을 호출해 주어서, 팔로잉 중인 유저를 추가하는 작업을 수행하고, 리턴값에 따라 오류 처리를 해준다. 이때 flatMap은 addFollowing의 리턴값에 다른 값을 map해주는 목적으로 쓰인다. map과의 차이점은 flatMap은 원소를 추출해 작업한다는 점이다. 위 코드에서는 addFollowing의 리턴값이 Mono<ResponseEntity<>>인데, 만약 Map을 사용하면 newFollowing의 리턴값이 Mono<Mono<ResponseEntity<>>>가 될 수 있다. 하지만 faltMap에서는 Mono 내의 원소인 ResponseEntity를 it으로 정하여 작업할 수 있게 하기 때문에 위 함수의 리턴값이Mono<ResponseEntity<>>가 될 수 있는 것이다.
map과 flatMap의 더 자세한 차이점: https://kchanguk.tistory.com/56
.map()과 .flatMap()의 차이
1. .map() .map()은 단일 스트림의 원소를 매핑시킨 후 매핑시킨 값을 다시 스트림으로 변환하는 중간 연산을 담당합니다. 객체에서 원하는 원소를 추출해는 역할을 한다고 말할 수 있습니다. 아래
kchanguk.tistory.com
/* FollowingService.kt */
fun addFollowing(followingRequestDTO: FollowingRequestDTO) : Mono<ResponseEntity<Map<String, Boolean>>> {
val response = webClient.put() //put 요청 전송
.uri { builder -> builder.path("/rel").build() }
.contentType(MediaType.APPLICATION_JSON) //json 형식으로 dto 전달
.bodyValue(followingRequestDTO) //body value로 dto 전달
.retrieve() //요청에 대한 응답 받기
.bodyToMono(object : ParameterizedTypeReference<Map<String, Boolean>>() {})
.map { body -> ResponseEntity.ok(body) } //요청이 정상처리 되었을 경우
.onErrorResume { throwable -> Mono.just(ResponseEntity.status(500).body(null)) } //오류발생한 경우
return response
}
마지막으로 FollowingService에서는 주로 WebClient를 이용해 following server에 요청을 보내는 작업을 했다. 이 작업을 작성하면서 WebClient를 자세히 공부했기 때문에 한 줄 한 줄 자세히 기록해두려 한다.
우선 WebClient 객체는 아래와 같이 Service 클래스의 인자로 선언해두었다.
class FollowingService(@Qualifier("followingServiceClient") private val webClient : WebClient) {}
그리고 following server와 연결을 하기 위해서 사전에 WebClientConfig 파일 내에 이렇게 builder를 미리 작성해 두었다. 따라서 @Qualifier 어논테이션을 통해 followingServiceClient에서 빌드한 WebClient를 사용할 것이라고 알려주면, 자동으로 빈에 등록된 아래 config를 이용해 following server에 요청을 보낼 수 있는 webClient가 생성된다.
/* WebClientConfig.kt */
@Bean
fun followingServiceClient() : WebClient {
return WebClient.builder()
.baseUrl("http://localhost:/생략")
.build()
}
이렇게 기본적인 준비를 해주면, WebClient를 통해서 다른 서버에 요청을 보낼 수 있게 된다. 그래서 이제 FollowingService에서 위와 같이 Following server에 요청을 보내게 되는 것이다. 지금 수행하고자 하는 작업은 follower를 추가하는 작업이므로 PUT 요청을 보내게 된다. (이 역시 following server api에서 요청한 대로 작성한다.) path를 api에 기재된 대로 작성해 주는데, 이때 앞서 WebClientConfig에서 baseUrl을 작성해 두었기 때문에 그다음부터 작성하면 된다. 그리고 bodyValue로 controller로부터 받은 followingRequestDTO를 전달해 준다. 이때, Json형식으로 전달해줘야 하므로, 그 위에서 Media type을 APPLICATION_JSON 으로 설정해 준 것이다. 마지막으로 이를 상황에 맞게 우리가 원하는 리턴값으로 mapping해준 후 리턴하면 된다.
2. 팔로잉 목록 조회
여기서부터는 사실 상 1번과 비슷한 작업의 반복이다.
/* FollowingController.kt */
@GetMapping("gardener/followings")
fun getFollowing(@RequestParam id : UUID): Mono<ResponseEntity<List<UUID>>> {
return followingService.followingList(id)
.flatMap { list ->
if (list.statusCode != HttpStatus.OK) {
Mono.just(ResponseEntity.status(list.statusCode).body(emptyList()))
} else if (list.body == null) {
Mono.just(ResponseEntity.status(404).body(emptyList()))
} else {
val uuidList = list.body!!.flatMap { user ->
user.values
}
Mono.just(ResponseEntity.ok(uuidList))
}
}
}
controller를 먼저 보면, 1번과 다른 점은 GetMapping이라는 점과, dto 대신 @RequestParam으로 id를 받는 것이다. 사실 팔로잉 목록을 가져오는 데에는 유저 아이디 하나만 있으면 된다. 해당 유저의 db에 현재 팔로잉하고 있는 목록이 모두 저장되어 있기 때문이다. 그리고 결괏값으로 uuidList, 즉 팔로잉하고 있는 유저들의 id 목록을 받게 될 것이다. 그래서 리턴값의 원소가 List<UUID>가 되는 것이다.
/* FollowingService.kt */
fun followingList(id : UUID) : Mono<ResponseEntity<List<Map<String, UUID>>>> {
val response = webClient.get()
.uri { builder -> builder.path("/rel/followings").queryParam("userId", id).build() }
.retrieve()
.bodyToMono(object : ParameterizedTypeReference<List<Map<String, UUID>>>() {})
.map { body -> ResponseEntity.ok(body) }
.onErrorResume { throwable -> Mono.just(ResponseEntity.status(500).body(emptyList()))}
return response
}
service 코드도 상당히 유사하다. 다른 점은 GET request를 보낸다는 점과, body value로 정보를 전달하는 것이 아닌 path에 id를 함께 전달해야 하기 때문에 .queryParam을 이용한다는 점이다. builder.path에 baseUrl 다음 path를 작성해 준 다음 queryParam으로 userId={id}를 넘겨준다. 이렇게 되면 전달하는 url이 http://localhost/baseurl/rel/followings?userId={id}와 같은 형식이 된다.
3. 팔로우 취소
팔로우 취소 역시 앞선 두 가지 기능과 유사하다.
/* UnfollowingRequestDTO.kt */
package com.flowery.flowerygateway.dto
import java.util.*
class UnfollowingRequestDTO(
val followerId: UUID,
val followingId: UUID
)
FollowingRequestDTO처럼 현재 follow하고 있는 follower의 id와 팔로우를 끊을 상대의 id인 followingId를 전달한다.
/* FollowingController.kt */
@DeleteMapping("gardener/unfollowing")
fun unfollowing(unfollowingRequestDTO: UnfollowingRequestDTO): Mono<ResponseEntity<String>> {
return followingService.deleteFollowing(unfollowingRequestDTO)
.flatMap { response ->
if (response.body == null || response.body!!.get("ok") == false) {
Mono.just(ResponseEntity.status(404).body("Unfollowing failed"))
} else if (response.statusCode != HttpStatus.OK) {
Mono.just(ResponseEntity.status(response.statusCode).body("Unfollowing failed"))
} else {
Mono.just(ResponseEntity.ok("Unfollowing succeeded"))
}
}
}
controller의 구조 역시 newFollowing과 같다. 다만, PostMapping이 아닌 DeleteMapping을 사용한다.
fun deleteFollowing(unfollowingRequestDTO: UnfollowingRequestDTO) : Mono<ResponseEntity<Map<String, Boolean>>> {
val response = webClient.method(HttpMethod.DELETE)
.uri { builder -> builder.path("/v1/rel").build() }
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(unfollowingRequestDTO)
.retrieve()
.bodyToMono(object : ParameterizedTypeReference<Map<String, Boolean>>() {})
.map { body -> ResponseEntity.ok(body) }
.onErrorResume { throwable -> Mono.just(ResponseEntity.status(500).body(null)) }
return response
}
마지막으로 Service 구조도 같다. 여기서도 다른 점은 .method(HttpMethod.DELETE)의 형식으로 요청을 보낸다는 점이다. .delete()가 .bodyValue()를 지원하지 않지만, 우리가 보낼 delete 요청에서는 bodyValue를 필요로 하기 때문에 method에 DELETE 요청을 넣는 방식으로 대체했다.