Client-Server 연결해보기

이종찬·2023년 2월 16일
0

✅ 구현순서

  1. Client,Server 프로젝트 생성 ,client : port 8080, server : 9090으로 주소 설정
  2. 각각의 컨트롤러를 통해 서버를 통신 : HTTP요청을 수신, 응답을 반환하는 역할
  3. Service 클래스를 호출하여 데이터 처리
  4. ResponseEntity, RequestEntity를 활용하여 데이터 처리
  5. 공통 부분 묶기

🧑‍💻 1. Client, Server 주소 설정

각각의 프로젝트에서 주소를 설정해줍니다. 이 때 주소가 같으면 안됩니다. 이유는 클라이언트에서 호출하는 주소와 서버에서 응답하는 경우 충돌이 발생할 가능성이 있습니다. 클라이언트에서 동시에 여러 개의 서버 애플리케이션을 실행하려고 할 때 같은 포트 번호를 사용하면 서로 충돌하여 실행하지 않을 수 있습니다.

aplication.properties에서 server.port=9090 이런 식으로 설정할 수 있으며 아무것도 적히지 않은 경우 기본 8080으로 지정됩니다.

Client : 8080, server : 9090으로 설정하겠습니다.

🧑‍💻 2. 컨트롤러 구현

Dto

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResponseUser {
    private String name;
    private int age;
}

Server-Controller

@Slf4j
@RestController
@RequestMapping("/api/server")
public class ApiController{
	@GetMapping("/hello")
    public ResponseUser hello(@RequestParam String name, @RequestParam int age {
        return new ResponseUser(name, age);
    }
}

클라이언트-서버의 모델은 위의 것으로 동일하게 진행하며 서버 컨트롤러는 클라이언트에서 호출한 내용만 반환하도록 설정해보겠습니다.

Client-Controller

@RestController
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/api/client")
public class ApiController {
    private final RestTemplateService service;

    @GetMapping("/hello")
    public ResponseUser hello() {
        return service.hello();
    }
}

클라이언트의 경우 컨트롤러에서 서비스 계층의 메서드를 호출하려고 합니다.

🧑‍💻 3. Service

서비스 계층은 비즈니스 로직을 처리하며, 컨트롤러와 데이터 엑세스 계층 사이의 중간 계층으로 사용됩니다.

서비스 계층에서는 데이터 엑세스 계층에서 가져온 데이터를 가공하여 컨트롤러로 전달 또는 받은 요청에 대해 데이터 엑세스 계층으로 전달하기도 합니다.

서비스 계층의 메서드는 @Service를 사용하여 해당 클래스를 스프링에서 Bean으로 등록하여 관리할 수 있습니다. 다른 게층에서 필요한 Bean들을 사용할 수 있으며, 스프링에서 제공하는 다양한 기능을 사용할 수 있습니다. 또한 해당 어노테이션을 사용하므로써 다른 개발자가 서비스 계층의 클래스임을 쉽게 파악할 수도 있습니다.

Service

@Slf4j
@Service
public class RestTemplateService {
    public ResponseUser hello() {
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/api/server/hello")
                .queryParam("name", "Chan")
                .queryParam("age", 111)
                .encode()
                .build()
                .toUri();
        log.info("uri : {}", uri);
        //CRUD For Object , CRUD For ResponseEntity
        //String result = template.getForObject(uri, String.class); -> String

        RestTemplate template = new RestTemplate();
        ResponseUser result = template.getForEntity(uri, ResponseUser.class).getBody();
        log.info("result : {}",result);
        return result;
    }
}

UriComponentsBuilder는 URI를 동적으로 생성하고 조작하는데 사용되는 스프링 프레임워크 유틸리티 클래스입니다. URI를 생성하며 query,path,pathVariable,uriEndcode 등의 작업을 처리합니다.

Spring MVC에서 RESTful 웹 서비스는 리소스를 URI에 쿼리를 추가하여 리소스를 조작하는 방식입니다. 해당 클래스는 URI를 동적으로 생성하기 때문에 유연한 코드를 작성할 수 있습니다.

fromUriString은 호출함에 있어 고정 URI를 지정합니다. 한번에 컨트롤러에서 실행하는 URI를 넣어도 상관없지만 path()에 부가적으로 추가할 수 있기 때문에 기본 URI를 넣는게 좋습니다.

기본 uri를 설정하고 path,query를 추가로 설정해줍니다.build() 메서드를 사용하여 최종 UriComponent를 생성하고 toUri()로 Uri객체로 바꿔줍니다. 만들어진 uri는 다음과 같습니다.

uri : http://localhost:9090/api/server/hello?name=Chan&age=111

만들어진 Uri를 서버로 보내고 응답을 받아야합니다. 이 때 사용되는것이 RestTemplate입니다.

RestTemplate은 다양한 메서드를 제공하며, HTTP 요청을 보내고 응답을 받을 수 있습니다. 예를 들어 getForEntity() 메서드를 사용하여 GET 요청을 보내고 응답을 객체로 받을수 있습니다.

RestTemplate은 HTTP 요청을 보내기 전에 HttpMessageConverter를 사용하여 요청과 응답을 객체로 변환합니다. 즉, RestTemplate을 사용하면 객체와 JSON 데이터를 변환할 필요 없이 HTTP요청을 보낼 수 있습니다.

template을 이용하여 uri를 HTTP 요청 보내고 응답은 ResponseUser 모델클래스 형태로 받으면 결과는 다음과 같이 나오게 됩니다.

결과

result : ResponseUser(name=Chan, age=111)


🧑‍💻 4. RequestEntity, ResponseEntity

해당 클래스들은 HTTP 요청과 응답을 처리하는데 사용되는 클래스입니다.

ResponseEntity는 HTTP status, response header, response body등을 포함할 수 있으며 HTTP Response의 세부 정보를 커스텀할 수 있습니다.

RequestEntity는 HTTP request method, uri, request header, request body 등을 포함할 수 있으며 세부정보 역시 커스텀할 수 있습니다.

해당 클래스들을 사용하는 이유는 HTTP request,response를 커스텀하기 유용해서 입니다. 보다 더 세밀하게 조작할 수 있습니다.

public ResponseUser post() {
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/api/server/user")
                .encode()
                .build()
                .toUri();
        log.info("uri : {}", uri);

        //http body -> obejct -> object mapper -> json -> ...
        RestTemplate template = new RestTemplate();
        RequestUser user = new RequestUser("aaaaa", 100);

        RequestEntity<RequestUser> request = RequestEntity.post(uri)
                .contentType(MediaType.APPLICATION_JSON)
                .header("Authorization", "head")
                .body(user);

        ResponseEntity<ResponseUser> response = template.exchange(request,ResponseUser.class);

        log.info("response status : {}", response.getStatusCode());
        log.info("response body : {}", response.getBody());

        return response.getBody();
}

이전과 동일하게 URI를 만들고 RestTemplate를 사용하였습니다. 다른 점은 header, body를 추가하여 서버에서 POST로 받으려고 합니다.

그러기 위해서는 RequestEntity를 uri를 post메서드에 넣고 header와 body를 각각 설정해줍니다.

ResponseEntity에서 RestTemplate을 활용하여 보내야 합니다. 하지만 이 때 클라이언트 컨트롤러에서는 get으로 요청하지만 서버에서는 post로 받습니다. 서버에서 응답을 받기 때문에 post로 받습니다. 템블릿에서 제공하는 exchange() 메서드를 활용하여 get으로 받을 수 있습니다.

이번엔 서버에서 들어온 내용을 확인하도록 하겠습니다.

    @PostMapping("/user")
    public ResponseUser user(
            @RequestHeader("Authorization") String auth,
            @RequestBody ResponseUser user
            ) {
        log.info("header : {}", auth);
        log.info("body : {}", user);
        return user;
    }

결과

header : head
body : ResponseUser(name=aaaaa, age=100)


👨‍💻 5. 공통 부분 묶기

컨트롤러에서 서비스 객체의 메서드를 호출하는 방식으로 앞선 예제들이 구현되어있습니다. URI를 생성하고 Request,Response Entity를 만들어서 통신하는데 겹치는 코드들이 많은 것을 확인할 수 있습니다.

우선 모델 클래스 부분을 간단하게 정리해보면 다음과 같습니다.

BaseDTO

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Req<T> {
    private HttpHeaders headers;
    private T responseBody;
}

모델 클래스는 주로 header, body로 이루어져 있어 여러게의 헤더를 담을 수 있는 headers, 제네릭을 사용하여 body값을 받겠습니다.


BaseURI

public class BaseUri {
    private static final String baseUri = "http://localhost:9090";
    public static URI uri(String path) {
        return UriComponentsBuilder
                .fromUriString(baseUri)
                .path(path)
                .encode()
                .build()
                .toUri();

    }
}

URI 생성 메서드도 구현할 때 매번 비슷한 코드를 작성하여 BaseUri를 만들었습니다.

Entity

@Slf4j
public class BaseEntity<T, K> {
    private Req<T> t;
    private Req<K> k;

    public RequestEntity<T> requestEntity(URI uri, HttpHeaders headers, T body) {

        return RequestEntity.post(uri)
                .contentType(MediaType.APPLICATION_JSON)
                .headers(headers)
                .body(body);
    }
}

Request,Response Entity의 경우 마찬가지로 정형화되어있기 때문에 재사용하기 좋을 거 같아서 분리하였습니다. ResponseEntity같은 경우에 Request값에 의존하여 응답받기 때문에 따로 만들지는 않았습니다.

genericExchange

public ResponseUser genericExchange() {
        RequestUser request = new RequestUser("SSSSSSS", 1111);
        URI uri = BaseUri.uri("/api/server/exchange");

        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "auth");
        headers.add("Custom", "custom");

        Req<RequestUser> req = new Req<>();
        req.setHeaders(headers);
        req.setResponseBody(request);

        RestTemplate template = new RestTemplate();
        
        BaseEntity<RequestUser, ResponseUser> entity = new BaseEntity<>();
        RequestEntity<RequestUser> reqEntity = entity.requestEntity(uri, req.getHeaders(), req.getResponseBody());
        ResponseEntity<ResponseUser> resEntity = template.exchange(reqEntity, new ParameterizedTypeReference<ResponseUser>() {});

        return resEntity.getBody();
}

URI,Request,Response Entity,Request,Response 모델 클래스까지 실행에 문제없이 코드를 줄였습니다.

실행 결과

header : [accept:"application/json, application/*+json", content-type:"application/json", authorization:"auth", custom:"custom", user-agent:"Java/17.0.6", host:"localhost:9090", connection:"keep-alive", content-length:"29"]
2023-02-16T22:59:38.493+09:00 INFO 1382 --- [nio-9090-exec-1] c.e.restserver.controller.ApiController : body : ResponseUser(name=SSSSSSS, age=1111)

header, body가 모두 잘 들어온 것을 확인할 수 있습니다.

profile
왜? 라는 질문이 사라질 때까지

0개의 댓글