반응형

 

 

이번 팀 플젝은 Spring Boot Cloud 기반 MSA 아키텍처로된 서비스를 개발하는데 맡은 부분이 프로젝트 MSA 초기 세팅과 유저, 인증/인가, 슬랙 연동을 하는 부분이다. 

 

이 과정속에서 어려웠고, 다음에 또 까먹지 않기 위해, 그리고 슬랙 연동하여 할 수 있는 것들을 스스로 생각해낸것들을 정리하려고 한다. 

 

Slack 이메일 인증 기능이고, 유저 서비스 <- -> 슬랙 서비스를 FeignClient를 통해 통신하였다. 

MSA 환경에서 이메일을 검증하고, Slack 워크스페이스 가입 여부를 확인하는 과정이다. 

 

 

전체 흐름 요약 


 

이 기능은 두 개의 MSA 서비스가 연동되어 동작합니다. 

 

User Service 사용자의 이메일을 Slack 쪽에 검증 요청함
Slack Service 실제 Slack API (users.lookupByEmail)를 호출해서 이메일 존재 여부를 판단함

 

 

동작 시나리오 


 

1. 사용자가 이메일 인증 요청 (/v1/users/slack/verify-member)

 

2. user-service -> slack-service 로 FeignClient 호출 

 

3. slack-service 가 slack API(users.lookupByEmail)로 이메일 존재 여부 조회

 

4. slack-service 가 결과 (ok=true/false)를 user-service로 응답 

 

5. user-service 가 이 결과를 보고 존재하면 유저 엔티티에 slackVerified값이 true로 변경(기본값 false) 

 

 

slack-service 


 

이메일 존재 여부 확인 API 

 

@PostMapping("/verify-member")
public BaseResponseDto<SlackVerifyResponse> verifySlackMember(
        @RequestBody SlackVerifyRequest req
) {
    SlackVerifyResponse response = slackService.verifySlackMember(req.email());
    return BaseResponseDto.success(
        "슬랙 워크스페이스 멤버 확인 완료",
        response,
        HttpStatus.OK
    );
}

 

@PostMapping("/verify-member) : 슬랙 내부용 API, user-service가 호출하는 엔드포인트

SlackVerifyRequest req : 사용자 이메일 정보를 담은 요청 DTO

slackService.verifySlackMember() : 실제 Slack API를 호출하는 핵심 로직 

 

@Service
@RequiredArgsConstructor
public class SlackService {

    private final WebClient webClient;

    @Value("${slack.bot.token}")
    private String slackToken;

    public SlackVerifyResponse verifySlackMember(String email) {
        return webClient.get()
            .uri("https://slack.com/api/users.lookupByEmail?email=" + email)
            .header("Authorization", "Bearer " + slackToken)
            .retrieve()
            .bodyToMono(SlackVerifyResponse.class)
            .block();
    }
}

 

WebClient : Slack API를 비동기로 호출하기 위한 Spring WebFlux 클라이언트

slackToken : 봇 인증용 토큰 (Slack 앱에서 발급받을 수 있음)

users.lookupByEmail : Slack 공식 API (이메일 존재 여부 확인용)

.block() : 동기 처리를 위해 block() 사용 (FeignClient로 응답 반환해야 하므로 사용함) 

 

public record SlackVerifyRequest(String email) {}
public record SlackVerifyResponse(boolean ok, SlackUser user) {}

public record SlackUser(String id, String name, String real_name) {}

 

SlackVerifyRequest : user-service -> slack-service 호출 시 이메일 전달용

SlackVerifyResponse : Slack API 응답(ok, user정보 포함)

 

 

user-service


 

Slack 인증 요청 처리 

 

@PostMapping("/slack/verify")
@Transactional
public ResponseEntity<BaseResponseDto<Void>> verifySlack(
        @AuthenticationPrincipal CustomUser customUser
) {
    slackVerifyService.verifySlackEmail(customUser.getId());
    return ResponseEntity.ok(
        BaseResponseDto.success("슬랙 이메일 인증 완료", null, HttpStatus.OK)
    );
}

 

@PostMapping("/slack/verify") : 이메일 인증 요청 API

slackVerifyService.verifySlackEmail() : 실제 이메일 검증 로직 수행 

 

@FeignClient(name = "slack-service")
public interface SlackFeignClient {

    @PostMapping("/verify-member")
    BaseResponseDto<SlackVerifyResponse> verifySlackMember(
        @RequestBody SlackVerifyRequest request,
        @RequestHeader("Authorization") String token
    );
}

 

@FeignClient(name ="slack-service") : 유레카 서버에 등록되어 있는 서비스명과 동일해야함.

verifySlackMember() : slack-service 쪽 엔드포인트 호출 

Authorization : 게이트웨이에서 전달받은 jwt를 전달 해줘야 한다. 

 

@Override
public void verifySlackEmail(UUID id) {
    User user = userRepository.findById(id)
        .orElseThrow(() -> new AppException(USER_NOT_FOUND));

    String token = request.getHeader("Authorization");

    SlackVerifyRequest slackRequest = new SlackVerifyRequest(user.getSlackEmail());
    BaseResponseDto<SlackVerifyResponse> response =
        slackFeignClient.verifySlackMember(slackRequest, token);

    if (response.getData().ok()) {
        user.updateSlackVerified(true);
    } else {
        throw new AppException(NOT_IN_WORKSPACE_EMAIL);
    }
}

 

slackFeignClient.verifySlackMember() : slack-service 호출 

response.getData().ok() : Slack API 응답 결과 검사 

user.updateSlackVerified(true) : 이메일 인증 처리 완료 

 

 

시퀀스 다이어그램


 

 

 

핵심 개념


 

 

Slack API users.lookupByEmail 로 이메일 존재 여부 확인
FeignClient 서비스 간 REST 호출 간소화
JWT 전달 게이트웨이 → UserService → SlackService 로 헤더 유지
예외 처리 Slack 응답이 ok=false 인 경우 명시적 예외 발생
동기 처리 .block() 사용으로 비동기 응답을 동기로 변환
DB 반영 인증 성공 시 slackVerified=true 필드 업데이트

 

 

왜 WebClient를 사용했나? 


 

이전에 게이트웨이에서 유저 서비스 간 통신을 FeignClient로 처리하려다가 순환 참조 문제가 발생한 경험이 있었다. 

FeignClient처럼 내부 서비스 간 동기 호출이 얽히면 또 다시 순환 의존이 생기지 않을까 하는 생각에 WebClient를 선택했다. 

 

WebClient는 비동기 방식이기도 하고, Slack API는 외부 API이기에 WebClient로 해야겠다 라는 판단을 내렸다. 

 

webClient.get()
    .uri("https://slack.com/api/users.lookupByEmail?email=" + email)
    .header("Authorization", "Bearer " + slackToken)
    .retrieve()
    .bodyToMono(SlackVerifyResponse.class)
    .block();

 

 

코드를 보면 block()이 보이는데,, 논블로킹을 버리고 RestClient와 동일하게 비동기 처리로 동작하게 만든 것이 의문이다. 

 

 

정리 및 이후 


 

우선 WebClient 대신 RestClient를 사용할 수 있는지 곰곰히 생각해보고 리팩토링을 해볼 생각이다. 

 

그리고, 유저 이메일 인증을 했으니 이메일 인증이 완료되면 해당 이메일이 인증되었다는 알림 메시지? 같은 것을 공유해주는 공간이 있으면 좋을거 같다는 생각을 했다. 

 

그럼 누가 언제 인증을 진행했는지 확인하는 용도로 쓸만하지 않을까 ?? 라는 내 생각이다.. 

 

728x90
반응형