1. 개요

어떤 경우에는 시스템을 여러 프로세스로 분해해야 하며 각 프로세스는 애플리케이션의 다른 측면을 담당합니다. 이러한 시나리오에서는 프로세스 중 하나가 다른 프로세스에서 동기적으로 데이터를 가져와야 하는 경우가 일반적입니다.

Spring Framework는 Spring Remoting 이라는 포괄적인 도구를 제공 하여 원격 서비스를 적어도 어느 정도는 로컬에서 사용할 수 있는 것처럼 호출할 수 있습니다.

이 기사에서는 기본 Java 직렬화 및 HTTP를 활용하여 클라이언트와 서버 애플리케이션 간에 원격 메서드 호출을 제공하는 Spring의 HTTP 호출자 기반 애플리케이션을 설정합니다 .

2. 서비스 정의

사용자가 택시를 예약할 수 있는 시스템을 구현해야 한다고 가정해 보겠습니다.

또한 이 목표를 달성 하기 위해 두 가지 별개의 애플리케이션 을 구축하기로 선택했다고 가정해 보겠습니다 .

  • 택시 요청이 처리될 수 있는지 여부를 확인하는 예약 엔진 애플리케이션
  • 고객이 차량 서비스를 예약할 수 있는 프런트 엔드 웹 애플리케이션으로 택시 이용 가능 여부가 확인됨

2.1. 서비스 인터페이스

HTTP 호출자 와 함께 Spring Remoting 을 사용할 때 원격 호출의 기술을 캡슐화하는 클라이언트와 서버 측 모두에서 Spring이 프록시를 생성할 수 있도록 인터페이스를 통해 원격으로 호출 가능한 서비스를 정의해야 합니다. 택시를 예약할 수 있는 서비스의 인터페이스부터 시작하겠습니다.

public interface CabBookingService {
    Booking bookRide(String pickUpLocation) throws BookingException;
}

서비스가 택시를 할당할 수 있으면 예약 코드와 함께 Booking 개체를 반환합니다. 예약 은 Spring의 HTTP 호출자가 서버에서 클라이언트로 인스턴스를 전송해야 하기 때문에 직렬화 가능해야 합니다.

public class Booking implements Serializable {
    private String bookingCode;

    @Override public String toString() {
        return format("Ride confirmed: code '%s'.", bookingCode);
    }

    // standard getters/setters and a constructor
}

서비스가 택시를 예약할 수 없는 경우 BookingException 이 발생합니다. 이 경우 Exception 이 이미 구현 했기 때문에 클래스를 Serializable 로 표시할 필요가 없습니다 .

public class BookingException extends Exception {
    public BookingException(String message) {
        super(message);
    }
}

2.2. 서비스 패키징

인수, 반환 유형 및 예외로 사용되는 모든 사용자 정의 클래스와 함께 서비스 인터페이스는 클라이언트 및 서버의 클래스 경로 모두에서 사용할 수 있어야 합니다. 이를 수행하는 가장 효과적인 방법 중 하나는 나중에 서버와 클라이언트의 pom.xml 에 종속 항목으로 포함될 수 있는 .jar 파일에 모두 압축하는 것 입니다.

따라서 "api"라고 하는 전용 Maven 모듈에 모든 코드를 넣겠습니다. 이 예제에서는 다음 Maven 좌표를 사용합니다.

<groupId>com.baeldung</groupId>
<artifactId>api</artifactId>
<version>1.0-SNAPSHOT</version>

3. 서버 애플리케이션

Spring Boot를 사용하여 서비스를 노출하는 예약 엔진 애플리케이션을 빌드해 보겠습니다.

3.1. 메이븐 의존성

먼저 프로젝트에서 Spring Boot를 사용하고 있는지 확인해야 합니다.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.2</version>
</parent>

최신 Spring Boot 버전 은 여기에서 찾을 수 있습니다 . 그런 다음 웹 스타터 모듈이 필요합니다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

그리고 이전 단계에서 조립한 서비스 정의 모듈이 필요합니다.

<dependency>
    <groupId>com.baeldung</groupId>
    <artifactId>api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

3.2. 서비스 구현

먼저 서비스의 인터페이스를 구현하는 클래스를 정의합니다.

public class CabBookingServiceImpl implements CabBookingService {

    @Override public Booking bookPickUp(String pickUpLocation) throws BookingException {
        if (random() < 0.3) throw new BookingException("Cab unavailable");
        return new Booking(randomUUID().toString());
    }
}

이것이 가능한 구현이라고 가정해 봅시다. 랜덤의 값이 있는 테스트를 사용하면 사용 가능한 택시가 발견되고 예약 코드가 반환되는 경우의 성공적인 시나리오와 사용 가능한 택시가 없음을 나타내기 위해 BookingException이 발생하는 경우의 실패 시나리오를 모두 재현할 수 있습니다.

3.3. 서비스 노출

그런 다음 컨텍스트에서 HttpInvokerServiceExporter 유형의 빈으로 애플리케이션을 정의해야 합니다 . 나중에 클라이언트가 호출할 웹 애플리케이션의 HTTP 진입점 노출을 처리합니다.

@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Server {

    @Bean(name = "/booking") HttpInvokerServiceExporter accountService() {
        HttpInvokerServiceExporter exporter = new HttpInvokerServiceExporter();
        exporter.setService( new CabBookingServiceImpl() );
        exporter.setServiceInterface( CabBookingService.class );
        return exporter;
    }

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

Spring의 HTTP 호출자 가 HttpInvokerServiceExporter 빈 의 이름을 HTTP Endpoints URL의 상대 경로로 사용 한다는 점은 주목할 가치가 있습니다.

이제 서버 애플리케이션을 시작하고 클라이언트 애플리케이션을 설정하는 동안 계속 실행할 수 있습니다.

4. 클라이언트 신청

이제 클라이언트 애플리케이션을 작성해 보겠습니다.

4.1. 메이븐 의존성

우리는 서버 측에서 사용한 것과 동일한 서비스 정의와 동일한 Spring Boot 버전을 사용할 것입니다. 여전히 웹 스타터 의존성이 필요하지만 포함된 컨테이너를 자동으로 시작할 필요가 없으므로 의존성에서 Tomcat 스타터를 제외할 수 있습니다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

4.2. 클라이언트 구현

클라이언트를 구현해 봅시다:

@Configuration
public class Client {

    @Bean
    public HttpInvokerProxyFactoryBean invoker() {
        HttpInvokerProxyFactoryBean invoker = new HttpInvokerProxyFactoryBean();
        invoker.setServiceUrl("http://localhost:8080/booking");
        invoker.setServiceInterface(CabBookingService.class);
        return invoker;
    }

    public static void main(String[] args) throws BookingException {
        CabBookingService service = SpringApplication
          .run(Client.class, args)
          .getBean(CabBookingService.class);
        out.println(service.bookRide("13 Seagate Blvd, Key Largo, FL 33037"));
    }
}

@Bean 어노테이션 이 달린 invoker () 메서드는 HttpInvokerProxyFactoryBean 인스턴스를 생성합니다 . setServiceUrl() 메서드 를 통해 원격 서버가 응답하는 URL을 제공해야 합니다 .

서버에 대해 수행한 것과 유사하게 setServiceInterface() 메서드 를 통해 원격으로 호출하려는 서비스의 인터페이스도 제공해야 합니다 .

HttpInvokerProxyFactoryBean 은 Spring의 FactoryBean 을 구현 합니다. FactoryBean 은 빈 으로 정의되지만 Spring IoC 컨테이너는 팩토리 자체가 아니라 생성하는 객체를 주입합니다. FactoryBean 기사 에서 FactoryBean대한 자세한 내용을 확인할 수 있습니다 .

main() 메서드 는 독립 실행형 응용 프로그램을 부트스트랩하고 컨텍스트에서 CabBookingService 인스턴스를 가져 옵니다. 내부적으로 이 개체는 원격 호출 실행과 관련된 모든 기술을 처리 하는 HttpInvokerProxyFactoryBean 에 의해 생성된 프록시일 뿐입니다. 덕분에 이제 서비스 구현이 로컬에서 사용 가능한 경우처럼 프록시를 쉽게 사용할 수 있습니다.

응용 프로그램을 여러 번 실행하여 택시를 사용할 수 있을 때와 그렇지 않을 때 클라이언트가 어떻게 작동하는지 확인하기 위해 여러 원격 호출을 실행해 보겠습니다.

5. 주의 사항

원격 호출을 허용하는 기술로 작업할 때 잘 알고 있어야 할 함정이 있습니다.

우리는 신뢰할 수 없는 리소스를 네트워크로 사용할 때 항상 예상치 못한 상황을 예상해야 합니다.

네트워크 문제로 인해 또는 서버가 다운되어 클라이언트가 서버에 연결할 수 없는 동안 서버를 호출한다고 가정하면 Spring Remoting은 RuntimeException 인 RemoteAccessException 을 발생 시킵니다.

그런 다음 컴파일러는 try-catch 블록에 호출을 포함하도록 강제하지 않지만 네트워크 문제를 적절하게 관리하기 위해 항상 그렇게 하는 것을 고려해야 합니다.

5.2. 개체는 참조가 아닌 값으로 전송됩니다.

Spring Remoting HTTP 는 메서드 인수와 반환 값을 마샬링하여 네트워크에서 전송합니다. 즉, 서버는 제공된 인수의 복사본에 따라 작동하고 클라이언트는 서버에서 생성된 결과의 복사본에 따라 작동합니다.

따라서 예를 들어 클라이언트와 서버 간에 공유 객체가 없기 때문에 결과 객체에 대한 메서드를 호출하면 서버 측에서 동일한 객체의 상태가 변경될 것이라고 기대할 수 없습니다.

5.3. 세분화된 인터페이스에 주의

네트워크 경계를 넘어 메서드를 호출하는 것은 동일한 프로세스의 개체에서 메서드를 호출하는 것보다 훨씬 느립니다.

이러한 이유로 더 번거로운 인터페이스를 희생하더라도 적은 수의 상호 작용이 필요한 비즈니스 트랜잭션을 완료할 수 있는 더 거친 인터페이스로 원격으로 호출해야 하는 서비스를 정의하는 것이 좋습니다.

6. 결론

이 예제를 통해 Spring Remoting을 사용하여 원격 프로세스를 호출하는 것이 얼마나 쉬운지 확인했습니다.

이 솔루션은 REST 또는 웹 서비스와 같은 다른 널리 퍼진 메커니즘보다 약간 덜 개방적이지만 모든 구성 요소가 Spring으로 개발되는 시나리오에서는 실행 가능하고 훨씬 빠른 대안을 나타낼 수 있습니다.

평소와 같이 GitHub 에서 소스를 찾을 수 있습니다 .

Generic footer banner