๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
  • ์žฅ์›์ต ๊ธฐ์ˆ ๋ธ”๋กœ๊ทธ
๐Ÿ”ฌweb application/- distributed system

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์—์„œ ์„œ๋น„์Šค๊ฐ„ ํ†ต์‹ ์„ ์œ„ํ•œ 2๊ฐ€์ง€ ๋ฐฉ๋ฒ• ๋น„๊ต (2) [OpenFeign vs Rest Template] - ๊ฐ๊ฐ์˜ ๋น„๊ต

by Wonit 2021. 4. 29.

์ด ๊ธ€์€ 2๊ฐœ์˜ ๊ธ€๋กœ ๋‚˜๋ˆ„์–ด์ ธ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์—์„œ ์„œ๋น„์Šค๊ฐ„ ํ†ต์‹ ์„ ์œ„ํ•œ 2๊ฐ€์ง€ ๋ฐฉ๋ฒ• ๋น„๊ต [OpenFeign vs Rest Template] - ์„œ๋น„์Šค ๊ตฌํ˜„
  2. ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์—์„œ ์„œ๋น„์Šค๊ฐ„ ํ†ต์‹ ์„ ์œ„ํ•œ 2๊ฐ€์ง€ ๋ฐฉ๋ฒ• ๋น„๊ต [OpenFeign vs Rest Template] - ๊ฐ๊ฐ์˜ ๋น„๊ต

ํ•ด๋‹น ๊ธ€์—์„œ ๋‚˜์˜ค๋Š” ์‹ค์Šต ๋‚ด์šฉ์€ Spring Cloud๋ฅผ ์ด์šฉํ•œ MSA ๊ตฌ์„ฑ์˜ ์ „๋ฐ˜์ ์ธ ์ดํ•ด๊ฐ€ ํ•„์š”ํ•œ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค.
์‹ค์Šต์€ Eureka + Gateway + Microservices (2)๋ฅผ ์ด์šฉํ•œ ํ™˜๊ฒฝ์œผ๋กœ ํ•ด๋‹น ๊ธ€์—์„œ๋Š” Eureka์™€ Gateway ์„ค์ •์— ๋Œ€ํ•ด์„œ๋Š” ์–ธ๊ธ‰ํ•˜์ง€ ์ง€๋งŒ ๋งŒ์•ฝ Eureka๋‚˜ Gateway์˜ ์ดํ•ด๊ฐ€ ๋ถ€์กฑํ•˜์‹  ๋ถ„๋“ค์ด๋‚˜ ๋” ์•Œ์•„๋ณด๊ณ  ์‹ค์Šต ํ™˜๊ฒฝ์„ ๋”ฐ๋ผ ํ•ด๋ณด๊ณ ์‹ถ์€ ์‚ฌ๋žŒ์€ ์•„๋ž˜์˜ ์‹ค์Šต ๊ณผ์ •์— ์กด์žฌํ•˜๋Š” URL์—์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ชฉ์ฐจ

์ด์ „ ์‹œ๊ฐ„

  • ์„œ๋น„์Šค๊ฐ„ ํ†ต์‹ 
    • ์„œ๋น„์Šค ๊ตฌ์กฐ
    • ํ†ต์‹  ๊ณผ์ •
  • ์„œ๋น„์Šค ๊ตฌํ˜„
    • ์„œ๋น„์Šค ๊ตฌ์„ฑ
      • Eureka Service
      • Gateway Service
      • User Service
      • Team Service

์ด๋ฒˆ ์‹œ๊ฐ„

  • ์ง€๋‚œ ์‹œ๊ฐ„์˜ ์ •๋ฆฌ
  • Rest Template ์œผ๋กœ API ํ˜ธ์ถœํ•˜๊ธฐ
  • Spring Cloud OpenFeign ์œผ๋กœ API ํ˜ธ์ถœํ•˜๊ธฐ
  • ๋‘ ํ†ต์‹  ๋ฐฉ๋ฒ•์˜ ์ฐจ์ด
    • ์„ ์–ธ ๋ฐฉ์‹๊ณผ ์ฝ”๋“œ ๊ฐ€๋…์„ฑ
    • ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
    • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
  • ์ข…ํ•ฉ ์ •๋ฆฌ ๋ฐ ๊ฒฐ๋ก 
    • ํ‘œ๋กœ ์ •๋ฆฌ

์ง€๋‚œ ์‹œ๊ฐ„์˜ ์ •๋ฆฌ

 

์ง€๋‚œ ์‹œ๊ฐ„ ์šฐ๋ฆฌ๋Š” User-Service์™€ Team-Service ๋ฅผ ๊ตฌํ˜„ํ•˜์˜€์—ˆ๋‹ค.

 

 

๊ทธ๋ฆฌ๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์€ API Endpoint ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค.

 

  • Discovery Service
    • http://localhost:8761/eureka
  • Gateway Service
    • http://localhost:8000/
  • User Service
    • ์œ ์ € ์ƒ์„ฑ : http://localhost:50010/users POST
    • ์œ ์ € ์กฐํšŒ : http://localhost:50010/users/{userId} GET
  • Team Service
    • ํŒ€ ์ƒ์„ฑ : http://localhost:60010/teams POST
    • ํŒ€์› ์ถ”๊ฐ€ : http://localhost:60010/{userId}/teams POST
    • ์‚ฌ์šฉ์ž ๋ฒˆํ˜ธ๋กœ ํŒ€ ๊ฒ€์ƒ‰ : http://localhost:60010/{userId}/teams GET

 

์šฐ๋ฆฌ๋Š” API Gateway ํŒจํ„ด์„ ์ด์šฉํ•  ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— Gateway Service์˜ ํฌํŠธ์ธ 8000 ํฌํŠธ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  Gateway ๊ฐ€ ๊ฐ๊ฐ์˜ ์„œ๋น„์Šค์— ์š”์ฒญ์„ ๋ถ„์‚ฐ์‹œ์ผœ ์‘๋‹ต์„ ์ „ํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ด์ค€๋‹ค.

 

์œ„์˜ ์—”๋“œํฌ์ธํŠธ์—์„œ ํ•ต์‹ฌ์€ ์œ ์ € ์กฐํšŒ์ด๋‹ค.

 

์œ ์ €๋ฅผ ์กฐํšŒํ•  ๋•Œ ํ•ด๋‹น๋˜๋Š” ์‚ฌ์šฉ์ž์˜ Team ์ •๋ณด๋ฅผ ํ•จ๊ป˜ ์ถœ๋ ฅํ•˜๊ธฐ ์œ„ํ•ด์„œ User-Service ์—์„œ Team-Service ๋กœ API ์š”์ฒญ์„ ๋ณด๋‚ด๊ฒŒ ๋œ๋‹ค.

 

๊ทธ๋Ÿผ Team-Service ์—์„œ๋Š” ์š”์ฒญ์˜ Path Variable ๋กœ ๋„˜์–ด์˜จ ์‚ฌ์šฉ์ž์˜ ID ์— ๋”ฐ๋ผ์„œ Team ์„ ์กฐํšŒํ•˜๊ณ , Team ์˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ด์ค€๋‹ค.

ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ์ž ์‹œ ์ฐธ๊ณ ํ•ด๋ณด์ž.

 

  • User-Service ์—์„œ๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ Team ์ •๋ณด๋ฅผ ํ•จ๊ป˜ ๋ฐ›์•„์™€์„œ ๋ฐ˜ํ™˜ํ•ด์ค˜์•ผ ํ•œ๋‹ค.
  • Team-Service ์—์„œ๋Š” ์‚ฌ์šฉ์ž Id ๋ฅผ ๋ฐ›์•„์„œ Team ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ด์ค€๋‹ค.

์ž์„ธํ•œ ์ฝ”๋“œ๋Š” ์ด์ „ ๊ธ€, OpenFeign vs Rest Template - ์„œ๋น„์Šค ๊ตฌํ˜„ ์—์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์šฐ๋ฆฌ๊ฐ€ ๊ด€์‹ฌ๊ฐ€์ ธ์•ผ ํ•  ์ฝ”๋“œ๋Š” User-Service์˜ getUserById() ๋ฉ”์„œ๋“œ์ด๊ณ , ํ•ด๋‹น ๋ฉ”์„œ๋“œ์—์„œ Team-Service์˜ getTeamByUserId() ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.

 

๊ฐ๊ฐ์˜ ์„œ๋น„์Šค๋Š” Controller ์—์„œ Endpoint ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ๋Š”๋ฐ, ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

 


// User-Service-Application
@RestController
public class UserController {

    // ์ƒ๋žต

    @GetMapping("/users/{userId}")
    public UserResponseData getUser(@PathVariable("userId") Long id) {
        return userService.getUserById(id);
    }
}

// Team-Service-Application
@RestController
public class TeamController {

    // ์ƒ๋žต

    @GetMapping("/{userId}/teams")
    public TeamResponseData getTeamByUserId(@PathVariable("userId") Long userId) {
        return teamService.getTeamByUserId(userId);
    }
}

 

์ด์ œ ํ•ด๋‹น ์„œ๋น„์Šค์—์„œ API ํ˜ธ์ถœ์„ ์œ„ํ•ด RestTemplate๊ณผ OpenFeign์„ ์ด์šฉํ•ด๋ณด์ž.

 

๊ทธ๋ฆฌ๊ณ  ๊ฐ๊ฐ์˜ ์‹ค์Šต์„ ์œ„ํ•ด์„œ ๋จผ์ € ๋ฐ์ดํ„ฐ๋ฅผ ์ค€๋น„ํ•ด๋†“์ž.

์‹ค์Šต ์„ธํŒ…

์‹ค์Šต ํ™˜๊ฒฝ์„ ์œ„ํ•ด์„œ ๋ฏธ๋ฆฌ ์š”์ฒญ์„ ๋ณด๋‚ด์„œ ์‚ฌ์šฉ์ž ์ •๋ณด์™€ Team ์„ ์„ธํŒ…ํ•ด์ฃผ์ž.

 

์š”์ฒญ ์ˆœ์„œ

 

  • ์‚ฌ์šฉ์ž ์ƒ์„ฑ : http://localhost:8000/user/users POST
    • Request Body ์— {"username":"์žฅ์›์ต"} ์„ ๋‹ด์•„์„œ ์‚ฌ์šฉ์ž๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
  • ํŒ€ ์ƒ์„ฑ : http://localhost:8000/team/teams : POST
    • Request Body ์— { "name": "๋ฆฌ๋ฒ„ํ’€", "address": "์˜๊ตญ ๋จธ์ง€์‚ฌ์ด๋“œ"} ์„ ๋‹ด์•„์„œ ํŒ€์„ ์ƒ์„ฑํ•œ๋‹ค.
  • ํŒ€ ๋ฉค๋ฒ„ ์ถ”๊ฐ€ : http://localhost:8000/team/1/teams : POST
    • Request Body ์— { "name": "๋ฆฌ๋ฒ„ํ’€" } ์„ ๋‹ด์•„์„œ ํšŒ์›์„ ํŒ€์— ์ €์žฅํ•œ๋‹ค.

 

RestTemplate ์œผ๋กœ API ํ˜ธ์ถœํ•˜๊ธฐ

  • ์‚ฌ์šฉ๋ฒ•
    • Bean ์ถ”๊ฐ€
    • UserService.class ์—์„œ RestTemplate ์˜์กด์„ฑ ์ฃผ์ž…
    • ์„œ๋น„์Šค์—์„œ RestTemplate์œผ๋กœ ํ˜ธ์ถœ

Bean ๋“ฑ๋ก

Rest Template ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ UserServiceApplication.java ์—์„œ Bean์œผ๋กœ RestTemplate ์„ ๋“ฑ๋กํ•ด์ฃผ์ž.

 

@SpringBootApplication
@EnableDiscoveryClient
@RestController
public class UserServiceApplication {

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

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

 

์œ„์—์„œ @LoadBalanced ์–ด๋…ธํ…Œ์ด์…˜์€ RestTemplate ๋กœ ์ง์ ‘ URL์„ ํ˜ธ์ถœํ•˜์ง€ ์•Š๊ณ  Eureka ์— ์žˆ๋Š” ์ธ์Šคํ„ด์Šค๋ฅผ discovery ํ•˜์—ฌ ํฌํŠธ ๋ฒˆํ˜ธ์™€ uri ๋ฅผ ์ž๋™์œผ๋กœ ๋งคํ•‘ํ•ด์ค€๋‹ค.

 

RestTemplate ์˜์กด์„ฑ ์ฃผ์ž…

 

Bean์œผ๋กœ RestTemplate ์„ ๋“ฑ๋ก์‹œ์ผฐ์œผ๋‹ˆ Service.class์—์„œ RestTemplate ์— ๋Œ€ํ•œ ์˜์กด์„ฑ์„ ์ฃผ์ž…ํ•ด์ฃผ์ž

 

์„œ๋น„์Šค์—์„œ RestTemplate์œผ๋กœ ํ˜ธ์ถœ

 

์ด์ œ RestTemplate ์„ ์ด์šฉํ•ด์„œ Team-Service์˜ getTeamByUserId() ๋ฅผ ํ˜ธ์ถœํ•  ๊ฒƒ์ด๋‹ค.

 

@Service
@Transactional
public class UserService {

    private final UserRepository userRepository;
    private final RestTemplate restTemplate;

    public UserService(UserRepository userRepository, RestTemplate restTemplate) {
        this.userRepository = userRepository;
        this.restTemplate = restTemplate;
    }


    /**
     * ์‚ฌ์šฉ์ž๋ฅผ ์กฐํšŒํ•œ๋‹ค.
     *
     * @param id ์‚ฌ์šฉ์ž id
     * @return ์ €์žฅ๋œ ์‚ฌ์šฉ์ž์™€ ํŒ€ ์ •๋ณด
     */
    public UserResponseData getUserById(Long id) {
        User userOptional = userRepository.findById(id)
                .orElseThrow(RuntimeException::new);

        // Team team = GET team-service/{userId}/teams
        String url = String.format("http://team-service/%s/teams", id);

        ResponseEntity<TeamResponseData> responseData = restTemplate.exchange(url,
                HttpMethod.GET,
                null,
                TeamResponseData.class);

        TeamResponseData team = responseData.getBody();

        return UserResponseData.builder()
                .userId(userOptional.getId())
                .username(userOptional.getUsername())
                .team(team) // Team-Service ๋กœ ์กฐํšŒํ•œ Team ์ •๋ณด๋ฅผ ๋‹ด์•„์„œ ๋ฐ˜ํ™˜
                .build();
    }
}

 

์œ„์—์„œ RestTemplate ๋ฅผ bean ์œผ๋กœ ์ฃผ์ž…ํ•  ๋•Œ, @LoadBalanced ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜์˜€๊ธฐ ๋•Œ๋ฌธ์— ์ง์ ‘์ ์ธ team-service์˜ ์ฃผ์†Œ ์ฒด๊ณ„๋ฅผ ์ด์šฉํ•˜์ง€ ์•Š๊ณ  Microservice์˜ ์ด๋ฆ„ ์ฒด๊ณ„๋ฅผ ์ด์šฉํ•œ๋‹ค.

 

RestTemplate ์—์„œ๋Š” exchange ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํ†ต์‹ ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

 

ํ•ด๋‹น ๋ฉ”์„œ๋“œ์—์„œ๋Š” 4๊ฐœ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๋ฐ›๋Š”๋‹ค.

  1. url
  2. HTTP Method
  3. Request Body
  4. Response Data Type Reference

๊ฒฐ๊ณผ ํ™•์ธํ•˜๊ธฐ

๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๋ฉด ์ž˜ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

 

์ง€๊ธˆ์€ RestTemplate์˜ ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ๋ฒ•์„ ์ด์šฉํ•ด์„œ API๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์•„์™”๋‹ค.

 

์ด์ œ ์ด์™€ ๋น„์Šทํ•œ API๋กœ Spring Cloud์˜ Netflix OpenFeign ์„ ์ด์šฉํ•ด๋ณด๋„๋ก ํ•˜์ž.

 

Spring Cloud Netflix OpenFeign ์œผ๋กœ API ํ˜ธ์ถœํ•˜๊ธฐ

Feign Client ๋Š” REST ํ˜ธ์ถœ์„ ์ถ”์ƒํ™” ํ•œ Spring Cloud Netflix ์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค.

 

์ž์„ธํ•œ ์ด์•ผ๊ธฐ๋Š” ์•„๋ž˜ ๋น„๊ต ์—์„œ ํ•˜๋„๋ก ํ•˜๊ณ  ์ง€๊ธˆ์€ ์‚ฌ์šฉ ๋จผ์ € ํ•ด๋ณด์ž.

  • ์‚ฌ์šฉ๋ฒ•
    • ์˜์กด์„ฑ ์ถ”๊ฐ€
    • HTTP Endpoint์— ๋Œ€ํ•œ Client ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ
    • ํ˜ธ์ถœ

์˜์กด์„ฑ ์ถ”๊ฐ€

FeigntClient ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” OpenFeing ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์ค˜์•ผ ํ•œ๋‹ค.


build.gradle์— ๊ฐ€์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์˜์กด์„ฑ์„ ์ถ”๊ฐ€์‹œ์ผœ๋ณด์ž.

 

build.gradle

implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

 

๊ทธ๋ฆฌ๊ณ  Application.java ๋กœ ๊ฐ€์„œ @EnableFeignClients ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€์‹œ์ผœ์ฃผ์ž.

 

Http Endpint ์— ๋Œ€ํ•œ Client ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ

User-Service๊ฐ€ ํ˜ธ์ถœํ•ด์•ผํ•  Http Endpoint ๋Š” Team-Service์˜ http://localhost:8000/team/{userId}/teams ์ด๋‹ค.

์ด๋ฅผ ํ˜ธ์ถœํ•  ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ƒ์„ฑํ•ด๋ณด์ž.

 

@FeignClient(name = "team")
public interface TeamServiceClient {
    @GetMapping("/{userId}/teams")
    TeamResponseData getTeam(@PathVariable("userId") Long id);
}

 

FeignClient ๋ผ๋Š” ์–ด๋…ธํ…Œ์ด์…˜์„ ์ด์šฉํ•˜๋ฉด ์ง์ ‘ ํ•ด๋‹น URL์„ ๋ช…์‹œํ•˜์ง€ ์•Š๋”๋ผ๋„ Eureka ์— registerํ•œ Instance ์ด๋ฆ„์„ ์ฐพ์•„์„œ URL์„ ๋งคํ•‘ํ•ด์ค€๋‹ค.

 

ํ˜ธ์ถœ

์ด์ œ ์„œ๋น„์Šค ํด๋ž˜์Šค๋กœ ๋Œ์•„๊ฐ€์„œ Feign ์„ ์ด์šฉํ•ด ํ˜ธ์ถœ์„ ๋ณด๋‚ด๋ณด์ž.

 

UserService.class ์—๋Š” ์•„๊นŒ Feign ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ƒ์„ฑ์ž๋กœ ์˜์กด์„ฑ์„ ๋„ฃ์–ด์ค€๋‹ค.

 

@Service
@Transactional
public class UserService {

    private final UserRepository userRepository;
    private final RestTemplate restTemplate;
    private final TeamServiceClient teamServiceClient;

    public UserService(UserRepository userRepository,
                       RestTemplate restTemplate,
                       TeamServiceClient teamServiceClient) {
        this.userRepository = userRepository;
        this.restTemplate = restTemplate;
        this.teamServiceClient = teamServiceClient;
    }

    public UserResponseData getUserById(Long id) {
        User userOptional = userRepository.findById(id)
                .orElseThrow(RuntimeException::new);

        TeamResponseData team = teamServiceClient.getTeam(id);


        return UserResponseData.builder()
                .userId(userOptional.getId())
                .username(userOptional.getUsername())
                .team(team) // Team-Service ๋กœ ์กฐํšŒํ•œ Team ์ •๋ณด๋ฅผ ๋‹ด์•„์„œ ๋ฐ˜ํ™˜
                .build();
    }
}

๊ฒฐ๊ณผ ํ™•์ธํ•˜๊ธฐ

 

๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๋ฉด ์ž˜ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

์ด์ œ ์ด ๋‘˜์„ ์‹ค์งˆ์ ์œผ๋กœ ๋น„๊ตํ•ด๋ณด๋„๋ก ํ•˜์ž.


์„ ์–ธ ๋ฐฉ์‹๊ณผ ๊ฐ€๋…์„ฑ

๋‘ ๋ฐฉ์‹์˜ ์„ ์–ธ์„ ๋ด๋ณด์ž.

 

RestTemplate๋‚˜ Feign ๋ชจ๋‘ ์œ„์—์„œ ๋ณธ ๋ฐ”์™€ ๊ฐ™์ด URL์„ ์ง์ ‘ ๋ช…์‹œํ•ด์ค˜์•ผ ํ•œ๋‹ค.


ํ•˜์ง€๋งŒ ๋‹ค๋ฅธ ์ ์ด ์žˆ๋‹ค๋ฉด ๋ฐ”๋กœ ๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ์ด๋‹ค.

 

์ฝ”๋“œ๋ฅผ ๋ด๋ณด์ž.

 

@Service
@Transactional
public class UserService {

    // ์ƒ๋žต

    public UserResponseData getUserById(Long id) {
        User userOptional = userRepository.findById(id)
                .orElseThrow(RuntimeException::new);

        TeamResponseData team = teamServiceClient.getTeam(id);

        return // ์ƒ๋žต
    }

    public UserResponseData getUserById(Long id) {
        User userOptional = userRepository.findById(id)
                .orElseThrow(RuntimeException::new);

        // Team team = GET team-service/{userId}/teams
        String url = String.format("http://team-service/%s/teams", id);
        ResponseEntity<TeamResponseData> responseData = restTemplate.exchange(url,
                HttpMethod.GET,
                null,
                TeamResponseData.class);

        TeamResponseData team = responseData.getBody();


        return // ์ƒ๋žต
    }
}

 

Service ์˜ ํ–‰๋™์— ๋Œ€ํ•œ ๊ด€์‹ฌ์‚ฌ๋Š” Team-Service์—๊ฒŒ ํ˜ธ์ถœ์„ ๋ณด๋‚ด๋Š” ๊ฒƒ์œผ๋กœ Feign ์ด๋‚˜ RestTemplate์ด๋‚˜ ๋™์ผํ•˜๋‹ค.

 

ํ•˜์ง€๋งŒ Uri ์— ๋Œ€ํ•œ ์ง์ ‘์ ์ธ ์„ค์ • ์ •๋ณด๋Š” UserService๊ฐ€ ๊ฐ€์ ธ์•ผ ํ•˜๋Š”๊ฒŒ ๋งž์„๊นŒ?

 

์ฑ…์ž„์˜ ๊ด€์‹ฌ์‚ฌ๋กœ ๋ณธ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ?

 

๋งŒ์•ฝ Team-Service์˜ ํ˜ธ์ถœ ๊ฒฝ๋กœ๊ฐ€ ๋‹ฌ๋ผ์กŒ๋‹ค๋ฉด ๊ทธ์— ๋Œ€ํ•œ ์ฑ…์ž„์€ UserService ๊ฐ€ ์•„๋‹ˆ๋ผ ํ˜ธ์ถœ์„ ํ•˜๋Š” ๋กœ์ง ์ž์ฒด์— ์กด์žฌํ•œ๋‹ค.


ํ•˜์ง€๋งŒ RestTemplate ์—์„œ๋Š” ์„ค์ • ์ •๋ณด๊ฐ€ UserService.class ๋‚ด์— ์žˆ๊ธฐ ๋•Œ๋ฌธ์— UserService๊ฐ€ ๊ทธ ์ฑ…์ž„์„ ์ง€๊ณ  ์žˆ๋‹ค.


๊ทธ์— ๋ฐ˜ํ•ด์„œ Feign์€ ์–ด๋–จ๊นŒ?


์•„์˜ˆ Feign์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ํ˜ธ์ถœ์— ๊ด€ํ•œ ์„ค์ •์„ ๋‹ค FeignClient.interface ์—์„œ ์ˆ˜ํ–‰ํ•˜๋„๋ก ๊ฐ•์ œํ™”๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ด€์‹ฌ์‚ฌ๊ฐ€ ๋ถ„๋ฆฌ๋˜์–ด์žˆ๋‹ค.

 

๊ฒฐ๊ตญ ์ด๋ฅผ ๊ฐ€์ ธ๋‹ค ์“ฐ๋Š” UserService ์—์„œ๋Š” ๋ฐ˜ํ™˜์— ๋Œ€ํ•œ ๊ฒฐ๊ณผ๋งŒ์„ ์ฑ…์ž„์œผ๋กœ ๊ฐ–๊ณ  ์žˆ๋Š” ๊ฒƒ์œผ๋กœ ์ ์ ˆํ•˜๋‹ค๊ณ  ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๊ฐ€๋…์„ฑ์€ ์ด์•ผ๊ธฐ ํ•˜์ง€ ์•Š๋”๋ผ๋„ Feign ์ด ์ข‹๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

 

์˜ˆ์™ธ ์ฒ˜๋ฆฌ

์ด ์ƒํ™ฉ์„ ๋ด๋ณด์ž.

 

๋งŒ์•ฝ User-Service ์—์„œ Team-Service ์˜ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž ์š”์ฒญ์„ ๋ณด๋‚ธ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ณด์ž.

 

๊ทธ๋Ÿผ ์‘๋‹ต์œผ๋กœ 500 ์—๋Ÿฌ๊ฐ€ ๋‚˜์˜ค๊ฒŒ ๋œ๋‹ค.

 

 

์‚ฌ์‹ค 500 ์—๋Ÿฌ๋Š” Team-Service ์—์„œ ์ œ๋Œ€๋กœ๋œ ๊ฐ’์„ ๋ฐ˜ํ™˜๋ฐ›์ง€ ๋ชปํ•ด์„œ ๋ฐœ์ƒํ•˜๋Š” User-Service์˜ ์—๋Ÿฌ์ด๋‹ค.


Team-Service ์—์„œ๋Š” ์‹ค์ œ๋กœ 404 Not Found ๊ฐ€ ๋ฐœ์ƒํ–ˆ๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์ฒ˜๋ฆฌํ•˜์ง€ ๋ชปํ•˜๋‹ˆ User-Service ์—์„œ 500 ์—๋Ÿฌ๋กœ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์žˆ๋‹ค.

 

๊ทธ๋Ÿผ User-Service ์—์„œ๋Š” 500 ์—๋Ÿฌ๊ฐ€ ์•„๋‹Œ ํ˜ธ์ถœํ•˜๋Š” Team-Service ์— 404 ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ทธ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•œ๋‹ค.

 

์ด๋Ÿด ๋•Œ RestTemplate๊ณผ Feign์€ ๊ฐ๊ฐ์„ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ• ๊นŒ?

 

RestTemplate์€ try-catch ๋ฅผ ์ด์šฉํ•˜์—ฌ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์•ผ ํ•œ๋‹ค.

public UserResponseData getUserById(Long id) {
    User userOptional = userRepository.findById(id)
            .orElseThrow(RuntimeException::new);

    // Team team = GET team-service/{userId}/teams
    String url = String.format("http://team-service/%s/teams", id);

    try {

      ResponseEntity<TeamResponseData> responseData = restTemplate.exchange(url,
            HttpMethod.GET,
            null,
            TeamResponseData.class);

      TeamResponseData team = responseData.getBody();

    }catch (Excepotion e) {
        return new UserNotEnrolledTeamException("์‚ฌ์šฉ์ž๋Š” ํŒ€์— ๊ฐ€์ž…๋˜์–ด์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
    }
    return UserResponseData.builder()
            .userId(userOptional.getId())
            .username(userOptional.getUsername())
            .team(team) // Team-Service ๋กœ ์กฐํšŒํ•œ Team ์ •๋ณด๋ฅผ ๋‹ด์•„์„œ ๋ฐ˜ํ™˜
            .build();
}

 

Feign ์˜ ์žฅ์  ์ค‘ ํ•˜๋‚˜๋Š” Microservice ์—์„œ ๋‚ด๋ถ€์ ์œผ๋กœ API ํ˜ธ์ถœ์„ ์ˆ˜ํ–‰ํ–ˆ์„ ๋•Œ, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ํ•ธ๋“ค๋งํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ErrorDecoder๋กœ ์ œ๊ณตํ•œ๋‹ค.

 

์ž์„ธํ•œ ErrorDecoder์— ๋Œ€ํ•ด์„œ๋Š” ๋‹ค์Œ ์‹œ๊ฐ„์— ๊นŠ๊ฒŒ ์•Œ์•„๋ณด๋„๋ก ํ•˜๊ณ  ํ˜„์žฌ๋Š” ์ฝ”๋“œ๋งŒ ๋ณด๋„๋ก ํ•˜์ž

 

๊ฐ๊ฐ์˜ ํ†ต์‹ ์—์„œ ์—๋Ÿฌ๋ฅผ ๋ณ€ํ™˜์‹œ์ผœ์ค„ ErrorDecoder ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ƒ์†๋ฐ›๋Š” Concrete ํด๋ ˆ์Šค๋ฅผ ํ•˜๋‚˜ ์ƒ์„ฑํ•˜๋ฉด ๋œ๋‹ค.

 

public class FeignError implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {

        switch(response.status()) {
            case 404:
                if(methodKey.contains("getOrders")) {
                    return new UserNotEnrolledTeamException("์‚ฌ์šฉ์ž๋Š” ํŒ€์— ๊ฐ€์ž…๋˜์–ด์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
                }
        }

        return null;
    }
}

 

๊ทธ๋ฆฌ๊ณ  service ์—์„œ์˜ ์‚ฌ์šฉ์€ ์ฝ”๋“œ ๋ณ€ํ™” ์—†์ด ์‚ฌ์šฉํ•˜๋‹ค๊ฐ€ ๋งŒ์•ฝ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋ฉด ํ•ด๋‹น ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜์‹œํ‚ค๊ฒŒ ๋œ๋‹ค.

 

public UserResponseData getUserById(Long id) {
    User userOptional = userRepository.findById(id)
            .orElseThrow(RuntimeException::new);

    // without try - catch
    TeamResponseData team = teamServiceClient.getTeam(id);

    return UserResponseData.builder()
            .userId(userOptional.getId())
            .username(userOptional.getUsername())
            .team(team) // Team-Service ๋กœ ์กฐํšŒํ•œ Team ์ •๋ณด๋ฅผ ๋‹ด์•„์„œ ๋ฐ˜ํ™˜
            .build();
}

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ์ •๋ง ์ค‘์š”ํ•˜๋‹ค.


์‚ฌ์‹ค ์ด๊ฑฐ ํ•˜๋‚˜ ๋งŒ์œผ๋กœ ์œ„์˜ ๋ชจ๋“  ์žฅ์ ๊ณผ ๋‹จ์ ๋“ค์„ ๋ฌด์‹œํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ธ๋ฐ, UnitTest ์—์„œ๋Š” ์™ธ๋ถ€ ๋ชจ๋“ˆ๊ณผ์˜ ์˜์กด ๊ด€๊ณ„๋ฅผ ๋Š์–ด ํ™•์‹คํ•œ ๊ณ ๋ฆฝ์ด ์ค‘์š”ํ•˜๋‹ค.


๋ณดํ†ต ํ•˜๋‚˜์˜ Unit ์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋‹ค๋ฅธ ์˜์กด ๊ด€๊ณ„๋ฅผ ๋Š๊ณ  mock ๊ฐ์ฒด๋ฅผ stubbing ํ•˜์—ฌ ์‚ฌ์šฉํ•œ๋‹ค.

 

๊ทธ๋Ÿผ UserService ์—์„œ RestTemplate ์„ ์ฃผ์ž…๋ฐ›๊ฒŒ ๋œ๋‹ค๋ฉด RestTemplate ์˜ exchange ๋Š” ์–ด๋–ป๊ฒŒ stubbing ํ•ด์•ผํ• ๊นŒ?

 

๊ฐ„๋‹จํ•˜๊ฒŒ stubbing ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๋น„๊ตํ•ด๋ณด์ž.

 

class UserServiceTest {

    private UserService userService;

    private final UserRepository userRepository = mock(UserRepository.class);
    private final TeamServiceClient teamServiceClient = mock(TeamServiceClient.class);

    @Autowired
    private RestTemplate restTemplate;

    @BeforeEach
    void setUp() {
        userService = new UserService(userRepository, restTemplate, teamServiceClient);

        User user = User.builder()
                .id(1L)
                .username("name")
                .build();

        TeamResponseData responseData = TeamResponseData.builder()
                .id(1L)
                .name("team name")
                .address("address")
                .build();

        given(userRepository.findById(anyLong())).willReturn(Optional.of(user));

        // Feign Test
        given(teamServiceClient.getTeam(anyLong())).willReturn(responseData);


        // RestTemplate Test
        given(restTemplate.exchange(eq("http://localhost:8000/team/1L/teams"),
                HttpMethod.GET, null, UserResponseData.class))
                .will(invocation ->
                    ResponseEntity.status(HttpStatus.OK).body(responseData)
                );
    }
}

 

๋งŒ์•ฝ ์—ฌ๊ธฐ์„œ RestTemplate ์ด ํ˜ธ์ถœํ•˜๋Š” URL์ด ๋‘ ๊ฐœ๋ผ๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ?

 

๊ทธ๋Ÿผ RestTemplateBuilder ๋ฅผ ์ด์šฉํ•ด์„œ ํ•˜๋‚˜ ํ•˜๋‚˜ ๋งคํ•‘์„ ํ•ด์•ผํ•œ๋‹ค.

 

์šฐ๋ฆฌ๋Š” Eureka ๋ฅผ ์ด์šฉํ•ด์„œ API ํ˜ธ์ถœ Endpoint ๋ฅผ MSA ์ธ์Šคํ„ด์Šค ์ด๋ฆ„์œผ๋กœ ์ง€์ •ํ–ˆ๋Š”๋ฐ, ์ด๋ ‡๊ฒŒ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ๋Š” ์ผ์ผ์ด ์ ์–ด์ค˜์•ผ ํ•˜๊ณ , ๋งŒ์•ฝ ํ•ด๋‹น URI๊ฐ€ ๋ณ€๊ฒฝ๋œ๋‹ค๋ฉด ์ด์™€ ๊ด€๋ จ๋œ ๋ชจ๋“  API ๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ๋ณ€๊ฒฝํ•ด์•ผ ํ•œ๋‹ค.

 

๊ทธ์— ๋ฐ˜ํ•ด Feign ์€ ์ธํ„ฐํŽ˜์ด์Šค์˜ ๋ฐ˜ํ™˜ ๊ฐ์ฒด๋งŒ ๋งคํ•‘ํ•ด์ค€๋‹ค๋Š” ์ ์—์„œ ์•„์ฃผ ๋งค๋ ฅ์ ์ด๋‹ค.

์ข…ํ•ฉ ์ •๋ฆฌ ๋ฐ ์ถ”๊ฐ€ ์‚ฌํ•ญ

์ง€๊ธˆ๊นŒ์ง€ ์•„์ฃผ ๋งŽ์€ ์ด์•ผ๊ธฐ๋ฅผ ํ–ˆ์—ˆ๋‹ค.

 

์ด์ œ ์œ„์—์„œ ๋‚ด์šฉํ•œ ์ด์•ผ๊ธฐ๋“ค์„ ์ •๋ฆฌํ•ด๋ณด์ž๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

 

์ด๋ฆ„ ์ฝ”๋“œ ๊ฐ€๋…์„ฑ, ์ง๊ด€์„ฑ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ ๋Ÿฌ๋‹ ์ปค๋ธŒ
Open Feign ๊ฐ€๋…์„ฑ ์ข‹์Œ  ErrorDecoder ์ œ๊ณต ์ผ๋ฐ˜์ ์ธ ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ฐ„ํŽธํ•œ stubbing ๋‚ฎ์Œ
Rest Template ๊ฐ€๋…์„ฑ์ด ์ข‹๊ฒŒ ๋˜๊ธฐ ์œ„ํ•ด ๋‹ค๋ฅธ ์ž‘์—… ํ•„์š” try-catch Spring ์ด ๊ตฌํ˜„ํ•ด๋†“์€ ๊ฐ์ฒด์˜ ๋ณต์žกํ•œ stubbing ๋‚ฎ์Œ

๋Œ“๊ธ€