회사 업무/리팩토링편

[리팩토링] RestTemplate을 활용한 API 호출 서비스 계층 리팩토링

코드몬스터 2025. 2. 10. 20:44
728x90

 

목차

1. 들어가기 앞서

2. 변경 전 코드

3. 변경 후 코드

  • 1차 리팩토링 - 클래스 변수
  • 2차 리팩토링 - 유틸 클래스 분리
  • 3차 리팩토링  - 서비스 계층 추가

 들어가기 앞서

코드를 작성하다 보면 중복되는 로직이 존재한다‼️

그 중 RestTemplate 클래스를 사용하여 다른 서버 API를 호출하는 부분이 계속 중복되었다.

 

이렇게 중복되게 사용하는 코드 처리는 아래와 같은 방법 중 하나를 선택해서 처리한다.

  1. 클래스 변수로 빼서, 각 메서드에서 공용으로 사용한다.
  2. 로직 의미에 따라서 common 또는 util 이라는 패키지에 넣어서 처리한다.

이번 리팩토링은 1번과 2번의 과정을 모두 시도하면서 변화 되는 과정을 보여주려고 한다.


 변경 전 코드

각 메서드마다 new RestTemplate()로 restTemplate 인스턴스를 생성하고 다른 서버를 호출하는 코드가 들어간다.

// Before Refactoring
public class Test {
   
    public void testB() {
    	
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(APPLICATION_JSON);
        
    	RestTemplate restTemplate = new RestTemplate.getForEntinty()
        
        // 그외 코드...
    }

}

✅ 변경 후 코드

1️⃣ 1차 리팩토링 - 클래스 변수

클래스 변수를 사용해서 각 메서드 마다 동일한 인스턴스를 사용하도록 변경했다.

장단점

장점

  • 싱글톤 패턴 구현: RestTemplate 인스턴스를 static으로 선언하여, 모든 인스턴스가 동일한 RestTemplate 객체를 사용하게 됩니다. 불필요한 객체 생성을 줄이고 메모리 효율을 높일 수 있습니다.
  • 불변성: final 키워드를 사용함으로써 RestTemplate 객체를 변경하지 않도록 보장합니다. 특히 다중 스레드 환경에서 안전한 사용이 가능합니다.
  • 재사용성: 여러 메서드에서 이 RestTemplate 객체를 재사용할 수 있어 코드가 효율적이고 간결해집니다.

단점

  • 여전히 여러 로직에서 사용되는 부분을 분리해내지 못했다.

클래스 변수 구현

// Forst Refactoring
public class Test {
   
    private static final RestTemplate restTemplate = new RestTemplate();

    public void testB() {
    	
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(APPLICATION_JSON);
        
    	RestTemplate restTemplate = restTemplate.getForEntinty()
        
    }

}

2️⃣  2차 리팩토링 - 유틸 클래스 분리

유틸 클래스 분리 방법

Spring context에 new RestTemplate()을 빈으로 등록하여 싱글톤으로 관리되도록 함.

장단점

장점(나의 생각)

  • RestTemplate를 util 패키지로 빼서 클래스로 작성하여 중복되는 부분을 제거할 수 있다.

문제점

  1. 싱글턴 빈으로 관리된다고 해도, new RestTemplate()을 직접 사용하면 내부적으로 SimpleClientHttpRequestFactory를 기본으로 사용하기 때문에 여전히 HTTP 연결을 새로 생성하는 방식으로 성능이 저하될 수 있음.
  2. 커넥션 풀을 활용하지 않으면 빈번한 연결 생성 및 해제로 인해 성능이 떨어지고, 리소스 낭비가 발생할 수 있음.

유틸클래스 구현

① 스프링 빈 등록

// 스프링 빈 등록
@Configuration
public class RestTemplateConfig {

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

}

 

② 유틸 클래스 작성

@Slf4j
@Component
public class ApiClient {

    private final RestTemplate restTemplate;

    public ApiClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public <T> ResponseEntity<T> sendRequest(String url, HttpMethod method, HttpEntity<?> requestEntity, ParameterizedTypeReference<T> responseType) {
        try {
            return restTemplate.exchange(url, method, requestEntity, responseType);
        } catch (HttpStatusCodeException e) {
            if (e.getStatusCode().is4xxClientError()) {
                throw new HttpClientErrorException(e.getStatusCode(), e.getStatusText());
            } else if (e.getStatusCode().is5xxServerError()) {
                throw new HttpServerErrorException(e.getStatusCode(), e.getStatusText());
            } else {
                throw new RuntimeException("HTTP 상태 코드 오류: " + e.getStatusCode(), e);
            }
        } catch (Exception e) {
            throw new RuntimeException("오류 발생: " + e.getMessage(), e);
        }
    }

    public <T> HttpEntity<T> createRequestEntity(@Nullable T body, @Nullable MultiValueMap<String, String> customHeader) {

        HttpHeaders headers = new HttpHeaders();
        if (customHeader != null) { // 헤더 값을 따로 작성 안 하는 경우
            customHeader.forEach((key, values) -> values.forEach(value -> headers.add(key, value)));
        } else { // 헤더 값을 따로 작성 하는 경우
            headers.set("Content-Type", "application/json");
        }

        return new HttpEntity<>(body, headers);
    }
}

3️⃣  3차 리팩토링  - 서비스 계층 추가

리팩토링 방법

  1. Spring context에 new RestTemplate()을 빈으로 등록하여 싱글톤으로 관리되도록 함.(2차 리팩토링이랑 동일)
  2. API 호출하는 로직을 새로운 서비스 계층으로 분리시켜 API 호출하는 책임만 담당하도록 처리.

장단점

장점(나의 생각)

  • RestTemplate를 util 패키지로 빼서 클래스로 작성하여 중복되는 부분을 제거할 수 있다.
  • API를 호출하는 부분을 서비스 계층으로 분리하여 한 번 더 깔끔하게 정리할 수 있다.

단점

  • 어떻게 보면 번거로운 작업인 것 같기도 하다.
  • 서비스 계층이 추가되면서 구조가 복잡해질 수 있다.

서비스 계층 구현

 어노테이션 생성

  • 어노테이션 생성에서 서비스 어노테이션을 추가했다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service
public @interface ApiRequestLayer {
}

 

 인터페이스 클래스 생성

public interface TestAPIInterface {
	// 이하 생략
}

 

③ 구현 클래스 생성

  • 해당 서비스가 호출하는 API를 정의한다.
@Slf4j
@ApiRequestLayer
public class TestAPIImpl implements TestAPIInterface {

	// 이하 생략...
    
}