[MSA - Spring Cloud] Eureka Client 개발하기(feat. FeignClient)

2025. 2. 7. 01:51·Spring/Spring Cloud
728x90
반응형
SMALL

이번에는 Service Registry에 등록할 Client 서버를 개발해보려고 한다.

User Service에서 회원가입후 로그인 API에 요청을 보내면 비즈니스 로직중 Auth 서비스에 토큰 생성을 요청하는 로직이 수행되어 두 클라이언트 서버간 통신이 이루어지는 작업을 함께 알아보려고 한다. 이때 사용하는 FeignClient에 대해서도 같이 알아보자.

우선 프로젝트를 모두 생성하고나서 비즈니스 로직과 함께 FeignClient에 대해 알아보겠다.

Dependency 설정

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.mysql:mysql-connector-j'
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    implementation 'org.springframework.boot:spring-boot-starter-actuator'

}

아직 Security 설정에 대한 고민이 남아있어 Security는 추가하지 않았다. 두 클라이언트 서비스 통신을 위한 openfeign과 클라이언트 서버로 지정하기 위해 netflix-eureka-client 의존성을 추가했다.

yml 설정

spring:
  application:
    name: user-service
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/[database이름]
    username: [username]
    password: [password]
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true


eureka:
  instance:
    prefer-ip-address: true
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/
    register-with-eureka: true
    fetch-registry: true

server:
  port: 19091
  • eureka.instance.prefer-ip-address : IP주소를 Eureka Server에 등록할지 여부
    • 기본값은 false, 해당 옵션을 true로 설정하지 않으면 각 서비스들이 호스트 이름으로 등록된다.
    • 서비스가 실제 배포 환경이라면 호스트 이름을 할당받고 정상적으로 작동되므로 false로 해줘도 무방하다
    • 컨테이너 기반(Docker) 배포 환경이라면 컨테이너가 임의의 호스트 이름이 부여되기 때문에 false로 한 경우 정상적으로 호스트 위치를 얻지 못하기도 한다.
  • eureka.client.service-url.defaultZone : 통신할 서버의 주소를 설정해준다. 이전에도 말했듯이 꼭 카멜케이스로 적을것!
  • eureka.client.register-with-eureka : 자기 자신을 등록할지 여부로 true로 설정해준다.
  • eureka.client.fetch-registry : 레지스트리에 서비스를 등록해야 하기에 true로 설정해준다.
  • spring.application.name : Eureka환경에서는 고유 ID로 통신하기 때문에 지정해준다.

Application에 FeignClients 적용

@SpringBootApplication
@EnableJpaAuditing
@EnableFeignClients
public class MovieReservationUserApplication {

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

}

이전에도 말했듯이 스프링부트의 강력한 기능인 PSA기능을 활용해서 어노테이션 @FeignClients를 적용해준다.

Entity 생성

회원가입을 하기 위한 엔티티를 생성해준다. 실습을 위한 프로젝트로 별도 refreshToken을 저장하지 않고 User 테이블에 저장해준다.
비밀번호 암호화는 BCryptPasswordEncoder를 사용해줬다.

import java.time.LocalDateTime;

@Entity
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id", nullable = false)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String userName;

    private String gender;

    private String phoneNumber;

    @Column(nullable = false)
    private String birthday;

    private UserRole role;

    private Boolean isDeleted = Boolean.FALSE;

    private String refreshToken;

    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    public void updateRefreshToken(String refreshToken) {
        this.refreshToken = refreshToken;
    }


    public static User fromSignUpRequest(UserSignUpRequest userSignUpRequest) {
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return User.builder()
                .email(userSignUpRequest.getEmail())
                .password(passwordEncoder.encode(userSignUpRequest.getPassword()))
                .userName(userSignUpRequest.getUserName())
                .birthday(userSignUpRequest.getBirthday())
                .role(userSignUpRequest.getRole())
                .phoneNumber(userSignUpRequest.getPhoneNumber())
                .gender(userSignUpRequest.getGender())
                .build();
    }

    public static UserResponse toResponse(User user) {
        return UserResponse.builder()
                .userId(user.getId())
                .email(user.getEmail())
                .userName(user.getUserName())
                .birthday(user.getBirthday())
                .phoneNumber(user.getPhoneNumber())
                .gender(user.getGender())
                .role(user.getRole())
                .build();
    }

}

Controller

@RestController
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {
    private final UserService userService;

    @PostMapping("/signUp")
    public ResponseEntity<Void> signUp(
            @RequestBody UserSignUpRequest userSignUpRequest
    ){
        userService.signUp(userSignUpRequest);

        return ResponseEntity.ok().build();
    }

    //로그인 후 토큰을 발급해서 반환해줘야 함 auth 서버 필요
    @PostMapping("/signIn")
    public ResponseEntity<Void> signIn(
            @RequestBody UserSignInRequest userSignInRequest
    ) {
        TokenResponse token = userService.signIn(userSignInRequest);

        return ResponseEntity.ok()
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + token.getAccessToken())
                .header("Refresh-Token", token.getRefreshToken())
                .build();
    }

    @PutMapping("/logout")
    public ResponseEntity<Void> logout(
            @RequestHeader("X-User-Id") String userId
    ){
        userService.logout(Long.valueOf(userId));

        return ResponseEntity.ok().build();
    }

}

회원가입을 위한 signUp, 로그인을 위한 signIn, 로그아웃을 위한 logout 메서드가 존재한다.
회원가입은 다른 서비스와 별도 통신없이 회원가입 데이터를 받아 DB에 저장하는 역할.
로그인은 Auth 서비스 서버와 통신하여 토큰을 발급받고 토큰값을 헤더에 담아 반환하는 역할.
로그아웃은 user의 refreshToken을 삭제하여 로그아웃 처리를 해주는 역할이다.
이후에는 refreshToken을 쿠키에 저장하고 삭제하는 방식으로 수정할 계획이다.

그럼 비즈니스 로직을 살펴보자.

UserService

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final AuthClient authClient;

    //회원가입
    @Transactional
    public void signUp(UserSignUpRequest userSignUpRequest) {
        userRepository.save(User.fromSignUpRequest(userSignUpRequest));
    }

    //로그인
    @Transactional
    public TokenResponse signIn(UserSignInRequest userSignInRequest) {
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        TokenResponse token;

        //회원 조회
        User user = userRepository.findByEmail(userSignInRequest.getEmail()).orElseThrow(
                () -> new IllegalArgumentException("아이디 또는 비밀번호를 다시 확인해주세요")
        );

        //회원 존재할경우 비밀번호 매칭
        if(passwordEncoder.matches(userSignInRequest.getPassword(), user.getPassword())) {
            UserResponse response = User.toResponse(user);

            token = authClient.getToken(response);// auth 서비스 서버와 통신

        }else{
            throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
        }


        user.updateRefreshToken(token.getRefreshToken()); //리프레쉬 토큰 유저 엔티티에 저장

        return token;

    }

    @Transactional
    public void logout(Long userId) {
        User user = userRepository.findById(userId).orElseThrow(
                () -> new IllegalArgumentException("회원을 찾을 수 없습니다.")
        );

        user.updateRefreshToken(null);
    }
}

복잡한 로직은 존재하지 않고 여기서 눈여겨봐야 할 부분은

            token = authClient.getToken(response);// auth 서비스 서버와 통신

이 부분이다. 먼저 authClient 코드를 살펴보자.

authClient

@FeignClient(name = "auth-service")
public interface AuthClient {

    @PostMapping("/auth/token")
    TokenResponse getToken(@RequestBody UserResponse userRequest);

    @GetMapping("/auth/refresh-token")
    String renewToken(@RequestHeader("refreshToken") String refreshToken);

}

@FeignClient를 적용하고 통신할 서버의 이름을 지정해주면 Eureka서버에 해당 이름으로 질의를 날려 주소를 얻고 요청을 보낼 수 있다. 이후에는 Gateway가 관여를 하겠지만 이 부분은 차후에 추가된 게이트웨이와 함께 설명하겠다.

이때 주의해야 할 점은 Mapping 주소, 서버 이름, 요청시 보내는 데이터들의 일관성이다. 두 번 확인해서 통신중 실수한 부분이 없나 체크하자! 안그러면 또 삽질을 시작하게 된다....

위에서 사용한 FeignClient에 대해 알아보도록 하자
이 어노테이션은 어떻게 다른 서비스 서버와 통신할 수 있도록 해주는 걸까?

FeignClient

FeignClient는 REST Call을 추상화한 Spring Cloud Netflix 라이브러리다. Feign Client를 사용하면 HTTP Client를 직접 구현하지 않아도 REST API를 호출할 수 있다.
FeignClient와 동일한 기술인 RestTemplate이 있지만 FeignClient가 보다 직관적이고 코드가 간결하다는 장점이 있다. 또한 Spring Cloud의 다른 기능들과도 호환이 되기 때문에 서비스 디스커버리, 로드 밸런싱 등의 기능을 사용하면서 FeignClient를 활용할 수 있다.
특히 MSA에서는 FeignClient에는 Ribbon과 통합되어 있고, CircuitBraker를 사용해서 상태 확인 및 장애 시 대처가 가능하니 더 많은 장점이 있다고 생각한다. 하지만 단점으로는 RestTemplate보다 많은 설정 및 사용방법이 있기에 복잡성이 증가하고 직접적으로 HTTP 통신 제어가 어렵다. 이 기술에 대해서는 별도로 포스팅해서 공식 문서를 함께 참조해서 정리할 예정이다!

결과 확인하기

User 서비스 서버와 Auth 서비스 서버가 통신하는 SignIn API를 실행시켜보겠다.

실행 후 로그를 살펴보자 정상적으로 User에서 요청이 들어왔고 로직 중간에 auth Server에 요청해 데이터를 받아 출력하는 것을 볼 수 있다.

  2025-02-08T00:55:49.008+09:00  INFO 1332 --- [auth-service] [io-19092-exec-1] c.s.c.m.AuthController                   : userResponse: UserResponse(userId=3, email=admin@test.com, userName=leesunro, birthday=1129, phoneNumber=01012341234, gender=MALE, role=ADMIN)
2025-02-08T00:55:49.009+09:00  INFO 1332 --- [auth-service] [io-19092-exec-1] c.s.c.m.AuthService                      : userRequest.getId =3

User의 refreshToken을 로그인 할 때마다 갱신해주기때문에 update쿼리가 날아간다.

 update user set birthday=?,created_at=?,email=?,gender=?,is_deleted=?,password=?,phone_number=?,refresh_token=?,role=?,updated_at=?,user_name=? where user_id=?

로그인 후 성공적으로 토큰을 헤더에 담아 클라이언트에게 넘겨준 것을 알 수 있었다.

728x90
반응형
SMALL

'Spring > Spring Cloud' 카테고리의 다른 글

[MSA - Resilienc4j] CircuitBreaker, fallback 메서드 개발하기  (0) 2025.02.11
[MSA - Spring Cloud] Spring Cloud Gateway 개발하기  (1) 2025.02.08
[MSA] 멀티 모듈에서 중복되는 코드를 서브모듈끼리 공유하는 방법  (0) 2025.02.08
[MSA - Spring Cloud] Eureka Server 개발하기(feat. Service Discovery란? 서버/클라이언트 사이드 디스커버리 전략)  (0) 2025.02.07
[MSA] 모노 레포와 멀티 레포 전략  (0) 2025.02.07
'Spring/Spring Cloud' 카테고리의 다른 글
  • [MSA - Spring Cloud] Spring Cloud Gateway 개발하기
  • [MSA] 멀티 모듈에서 중복되는 코드를 서브모듈끼리 공유하는 방법
  • [MSA - Spring Cloud] Eureka Server 개발하기(feat. Service Discovery란? 서버/클라이언트 사이드 디스커버리 전략)
  • [MSA] 모노 레포와 멀티 레포 전략
공부하고 기억하는 공간
공부하고 기억하는 공간
IT 비전공자로 시작하여 훌륭한 개발자가 되기 위해 공부하고 있는 공간입니다. 틀린 내용이나 부족한 부분이 있으면 댓글로 알려주세요 바로 수정하겠습니다.
    250x250
  • 공부하고 기억하는 공간
    IT - railroad
    공부하고 기억하는 공간
  • 전체
    오늘
    어제
    • 분류 전체보기 (325)
      • 면접 준비 (22)
        • OS (6)
        • Spring Security (0)
        • Java (3)
        • DB (11)
        • Network (3)
      • ElasticSearch (2)
      • Kafka (4)
      • Spring (22)
        • Spring Cloud (7)
        • Security6 (5)
        • JPA (12)
        • 프로젝트 리팩토링 회고록 (4)
        • Logging (8)
        • Batch (2)
      • Redis (17)
        • Redis 개념 (8)
        • Redis 채팅 (5)
        • Redis 읽기쓰기 전략 (1)
      • AWS (11)
      • 리눅스 (29)
        • 리눅스 마스터 2급 (5)
        • 네트워크(기초) (7)
        • 리눅스의 이해 (6)
        • 리눅스의 설치 (2)
        • 리눅스 운영 및 관리 (6)
      • JAVA-기초 (16)
        • JAVA기본 (11)
        • Design Pattern (5)
      • JSP (27)
        • JSP 기본 개념 (10)
        • JSP (1)
      • SQL (1)
      • TIL (36)
      • 문제 풀이 (2)
        • Programmers (9)
        • 백준 문제풀이 (28)
      • JavaScript (10)
      • HTML (17)
      • Ngrinder (1)
        • Ngrinder 문서 정리 (1)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      리눅스
      Springframework
      HTML
      자바기초
      JSP
      자바 면접질문
      CSS
      redis
      spring redis
      springsecurity
      JS
      Spring
      자바 반복문
      백준
      Spring Data Redis
      jsp기초
      자바
      java
      자바 면접
      레디스
      리눅스마스터2급
      리눅스마스터2급정리
      Til
      프로그래머스
      redis 채팅
      자바 알고리즘
      자바스크립트
      JavaScript
      스프링프레임워크
      jsp request
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.3
    공부하고 기억하는 공간
    [MSA - Spring Cloud] Eureka Client 개발하기(feat. FeignClient)
    상단으로

    티스토리툴바