Rental Application (React & Spring boot Microservice) - 18 : auth-service(5: Feign Client)

#1 Feign Client

Feign Client는 Netflix에서 개발된 REST Call을 추상화한 라이브러리입니다. 사용방법은 호출하려는 Http Endpoint에 대한 Interface를 생성해서 @FeignClient라는 어노테이션으로 사용할 수 있죠.

예를 들자면 유저 서비스에서 포스트에 관한 정보를 불러오고 싶을 때 @GetMapping(/post-service/:postId)와 같은 Http Endpoint에 관한 Interface를 생성하고 이 Interface를 @FeignClient라는 어노테이션으로 호출하여 게시글 정보를 불러 올 수 있습니다.

#2 Feign Client 구현

저는 유저서비스에서 유저가 작성한 게시글 정보들과 대여 정보를 불러오고 싶으니 2개의 FeignClient를 작성하도록 하겠습니다.

  • auth-service - pom.xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • AuthServiceApplication
package com.microservices.authservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class AuthServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthServiceApplication.class, args);
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Feign Client를 사용하기 위한 설정을 마치고 clients라는 패키지를 만들어 여기에 Client인터페이스를 관리하도록 하겠습니다.

  • /clients/PostClient
package com.microservices.authservice.clients;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.List;

@FeignClient(name="post-service")
public interface PostClient {
    @GetMapping("/{userId}/posts")
    public List<ResponsePost> getPosts(@PathVariable("userId") String userId);
}
  • /clients/RentalClient
package com.microservices.authservice.clients;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.List;

@FeignClient(name="rental-service")
public interface RentalClient {
    @GetMapping("/{owner}/my_rentals")
    public List<ResponseRental> getRentalsByOwner(@PathVariable("owner") String owner);

    @GetMapping("/{borrower}/borrow_rentals")
    public List<ResponseRental> getRentalsByBorrower(@PathVariable("borrower") String borrower);
}

인터페이스의 코드들은 post-service, rental-service의 컨트롤러에 미리 구현해둔 REST Call들을 인터페이스 형식으로 가져온 것들입니다. 즉, Http Endpoint에 대한 Interface를 생성한 것이죠. 그러면 이 코드들의 오류 코드드들을 구현해보도록 하겠습니다.

  • ResponsePost
package com.microservices.authservice.vo;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Data;

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponsePost {
    private Long id;
    private String postType;
    private String title;
    private String content;
    private String createdAt;
    private String writer;
    private String status;

    @Builder
    public ResponsePost(
        Long id,
        String postType,
        String title,
        String content,
        String createdAt,
        String writer,
        String status
    ) {
        this.id = id;
        this.postType = postType;
        this.title = title;
        this.content = content;
        this.createdAt = createdAt;
        this.writer = writer;
        this.status = status;
    }
}
  • ResponseRental
package com.microservices.authservice.vo;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Data;

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseRental {
    private String rentalId;
    private Long postId;
    private Long price;
    private String owner;
    private String borrower;
    private String startDate;
    private String endDate;
    private String createdAt;

    @Builder
    public ResponseRental(
        String rentalId,
        Long postId,
        Long price,
        String owner,
        String borrower,
        String startDate,
        String endDate,
        String createdAt
    ) {
        this.rentalId = rentalId;
        this.postId = postId;
        this.price = price;
        this.owner = owner;
        this.borrower = borrower;
        this.startDate = startDate;
        this.endDate = endDate;
        this.createdAt = createdAt;
    }
}
  • ResponseUser
package com.microservices.authservice.vo;

import lombok.Builder;
import lombok.Data;

import java.util.List;

@Data
public class ResponseUser {
    private String email;
    private String nickname;
    private String phoneNumber;
    private String userId;
    private String encryptedPwd;
    private List<ResponsePost> posts;
    private List<ResponseRental> rentals;

    @Builder
    public ResponseUser(
        String email,
        String nickname,
        String phoneNumber,
        String userId,
        String encryptedPwd,
        List<ResponsePost> posts,
        List<ResponseRental> rentals
    ) {
        this.email = email;
        this.nickname = nickname;
        this.phoneNumber = phoneNumber;
        this.userId = userId;
        this.encryptedPwd = encryptedPwd;
        this.posts = posts;
        this.rentals = rentals;
    }
}
  • UserDto
package com.microservices.authservice.dto;

import com.microservices.authservice.vo.ResponsePost;
import com.microservices.authservice.vo.ResponseRental;
import lombok.Builder;
import lombok.Data;

import java.util.List;

@Data
public class UserDto {
    private String email;
    private String password;
    private String nickname;
    private String phoneNumber;
    private String userId;
    private String encryptedPwd;
    private List<ResponsePost> posts;
    private List<ResponseRental> rentals;

    @Builder
    public UserDto(
        String email,
        String password,
        String nickname,
        String phoneNumber,
        String userId,
        String encryptedPwd,
        List<ResponsePost> posts,
        List<ResponseRental> rentals
    ) {
        this.email = email;
        this.password = password;
        this.nickname = nickname;
        this.phoneNumber = phoneNumber;
        this.userId = userId;
        this.encryptedPwd = encryptedPwd;
        this.posts = posts;
        this.rentals = rentals;
    }
}

이 vo객체들은 FeignClient를 통해서 받아올 게시글과 대여에 관한 정보를 담을 클래스입니다. 그리고 최종적으로 유저의 정보를 반환할 때 이 정보들을 담을 클래스들을 리스트로 만들도록 하겠습니다.

  • AuthController
package com.microservices.authservice.controller;

import com.microservices.authservice.dto.UserDto;
import com.microservices.authservice.service.AuthService;
import com.microservices.authservice.vo.RequestRegister;
import com.microservices.authservice.vo.ResponseUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/")
@Slf4j
public class AuthController {
    ...

    @GetMapping("/{userId}/getUser")
    public ResponseEntity<?> getUser(@PathVariable("userId") String userId) {
        log.info("Auth Service's Controller Layer :: Call getUser Method!");

        UserDto userDto = authService.getUser(userId);

        return ResponseEntity.status(HttpStatus.OK).body(ResponseUser.builder()
                                                                     .email(userDto.getEmail())
                                                                     .nickname(userDto.getNickname())
                                                                     .phoneNumber(userDto.getPhoneNumber())
                                                                     .userId(userDto.getUserId())
                                                                     .posts(userDto.getPosts())
                                                                     .rentals(userDto.getRentals())
                                                                     .build());
    }

    @GetMapping("/{nickname}/my-rental-list")
    public ResponseEntity<?> getMyRentals(@PathVariable("nickname") String nickname) {
        log.info("Auth Service's Controller Layer :: Call getMyRentals Method!");

        UserDto userDto = authService.getRentalsByNickname(nickname);

        return ResponseEntity.status(HttpStatus.OK).body(ResponseUser.builder().rentals(userDto.getRentals()));
    }

    @GetMapping("/{nickname}/my-borrow-list")
    public ResponseEntity<?> getMyBorrow(@PathVariable("nickname") String nickname) {
        log.info("Auth Service's Controller Layer :: Call getMyRentals Method!");

        UserDto userDto = authService.getBorrowsByNickname(nickname);

        return ResponseEntity.status(HttpStatus.OK).body(ResponseUser.builder().rentals(userDto.getRentals()));
    }
}

저는 위 코드와 같이 Client들을 호출하기 위한 3개의 메서드를 만들었습니다. getUser는 유저의 정보를 불러오는 메서드입니다. 최종적으로 유저의 정보, 유저의 아이디 값을 이용한 게시물 정보를 불러올 수 있겠죠. getMyRentals, getMyBorrow 유저의 닉네임을 이용하여 대여 정보, 차용 정보를 불러 올 수 있는 메서드입니다. 앞서 RentalEntity에는 owner, borrower속성을 설정해두었는데 이 값과 nickname이 매칭되는 정보를 불러오게 할 메서드입니다.

  • AuthService
package com.microservices.authservice.service;

import com.microservices.authservice.dto.UserDto;
import org.springframework.security.core.userdetails.UserDetailsService;

public interface AuthService extends UserDetailsService {
    UserDto registerUser(UserDto userDto);

    UserDto getUserDetailsByEmail(String email);

    UserDto getUser(String userId);

    UserDto getRentalsByNickname(String nickname);

    UserDto getBorrowsByNickname(String nickname);
}
  • AuthServiceImpl
package com.microservices.authservice.service;

import com.microservices.authservice.clients.PostClient;
import com.microservices.authservice.clients.RentalClient;
import com.microservices.authservice.dto.UserDto;
import com.microservices.authservice.entity.UserEntity;
import com.microservices.authservice.repository.AuthRepository;
import com.microservices.authservice.util.DateUtil;
import com.microservices.authservice.vo.ResponsePost;
import com.microservices.authservice.vo.ResponseRental;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Service
@Slf4j
public class AuthServiceImpl implements AuthService {
    private AuthRepository authRepository;
    private BCryptPasswordEncoder passwordEncoder;
    private PostClient postClient;
    private RentalClient rentalClient;

    @Autowired
    public AuthServiceImpl(
        AuthRepository authRepository,
        BCryptPasswordEncoder passwordEncoder,
        PostClient postClient,
        RentalClient rentalClient
    ) {
        this.authRepository = authRepository;
        this.passwordEncoder = passwordEncoder;
        this.postClient = postClient;
        this.rentalClient = rentalClient;
    }

    ...

    @Transactional
    @Override
    public UserDto getUser(String userId) {
        log.info("Auth Service's Service Layer :: Call getUser Method!");

        UserEntity userEntity = authRepository.findByUserId(userId);

        if(userEntity == null) throw new UsernameNotFoundException(userId);

        return UserDto.builder()
                      .email(userEntity.getEmail())
                      .nickname(userEntity.getNickname())
                      .phoneNumber(userEntity.getPhoneNumber())
                      .userId(userEntity.getUserId())
                      .encryptedPwd(userEntity.getEncryptedPwd())
                      .posts(postClient.getPosts(userId))
                      .build();
    }

    @Transactional
    @Override
    public UserDto getRentalsByNickname(String nickname) {
        log.info("Auth Service's Service Layer :: Call getRentalsByNickname Method!");

        return ResponseEntity.status(HttpStatus.OK).body(ResponseUser.builder()
                                                                     .rentals(userDto.getRentals())
                                                                     .build());
    }

    @Transactional
    @Override
    public UserDto getBorrowsByNickname(String nickname) {
        log.info("Auth Service's Service Layer :: Call getBorrowsByNickname Method!");

        return ResponseEntity.status(HttpStatus.OK).body(ResponseUser.builder()
                                                                     .rentals(userDto.getRentals())
                                                                     .build());
    }

    ...
}

client들을 이용하여 서비스레이어에서 정보를 호출할 수 있도록 구현하였습니다.

  • AuthRepository
package com.microservices.authservice.repository;

import com.microservices.authservice.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AuthRepository extends JpaRepository<UserEntity, Long> {
    UserEntity findByEmail(String email);

    UserEntity findByUserId(String userId);
}

마지막으로 api-gateway에 해당 설정을 추가하도록 하겠습니다.

- id: rental
  uri: lb://RENTAL-SERVICE
  predicates:
    - Path=/rental-service/**
  filters:
    - RemoveRequestHeader=Cookie
    - RewritePath=/rental-service/(?<segment>.*), /$\{segment}

#3 테스트

1) /user-service/:userId/getUser

2) /user-service/:nickname/getMyRentals

3) /user-service/:nickname/getMyBorrow

위의 화면처럼 테스트 결과가 잘 나오는 모습을 볼 수 있습니다. 다음 포스트에서는 FeignClient의 오류, 마이크로서비스의 분산추적에 관해 다루도록 하겠습니다.

참고

인프런: Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) - 이도원

좋은 웹페이지 즐겨찾기