[TIL] 최종 프로젝트 (1) - Page redirection에 대하여

J쭈디·2025년 2월 11일
0

Sparta_프로젝트

목록 보기
12/35

1. 왔습니다. 최종이 왔습니다.

1. 동시성 제어를 위하여

어제 최종 프로젝트를 위해 파트분배를 했다. 이번 최종 프로젝트에서는 버전별로 나누어 작업을 시작할 예정이며, 우리의 주제는 취준생을 위한 웹페이지 만들기이다.

난 저번 과제에서 JWT 토큰과 Spring Security를 이용한 인증인가를 구현했고, 그와 더불어 AWS로 최종 배포까지 완료했었다.
그렇기 때문에 저번에 못 했던 큰 주제 중 하나인 동시성 제어가 탐이 났다. 팀원 분들과 파트 분배를 하면서 동시성 제어를 하고 싶다고 이야기했고, 다행히 팀원 분들이 오케이 해주셔서 해당 부분을 내가 진행하기로 했다.

2. Ver1. 페이지 리다이렉팅을 조사하라

동시성 제어를 하고 싶다고 하니 동시성 문제는 페이지 조회 및 리다이렉팅이랑 관련이 있다는 이야기를 들었다.

사실은 난 페이지 조회 파트 자체를 맡고 싶었으나, 여러 의견이 모인 결과 페이지 조회와 리다이렉팅이 쉽지 않을 것이라는 모두의 판단 하에 결국 Ver1 부분에서는 리다이렉팅만 공부하고 코드를 구현하기로 했다.

페이지 리다이렉팅이 그렇게 방법이 많다나? 요는 전체 방법을 다 문서화하고 어떤 페이지 리다이렉팅방법이 가장 현 프로젝트에 맞는가?를 찾아내라는
이야기였다.

2. Page redirection의 방법들?

일단 팀장님에게 받은 관련 방법들 예시를 정리해보기로 했다.

1. HttpURLConnection

  • Java의 표준 라이브러리(java.net.HttpURLConnection)로 제공됨
  • Spring을 없이도 사용 가능
  • 설정을 하기 전에는 리다이렉트를 수행하지 않고, POST요청이 GET으로 바뀌는 등의 문제가 있음.
  • 코드가 장황하고 유지보수가 어려움.
  • 직접 스트림을 열고 닫아야 해서 불편함.
    • 직접 InputStream이나 OutputStream을 열어서 요청을 보내야 하고, 응답을 받은 후에는 또 직접 close()disconnect()를 이용하여 닫아야 한다.
    • 제대로 닫지 않을 경우 리소스 누수가 발생할 가능성이 있다.
    • 스트림은 보통 데이터의 이동 통로를 말하며, InputStream이나 OutputStream은 추상클래스로, 오버라이딩을 하여 다양한 역할을 수행하는 것이 가능하다.

1-1. HttpURLConnection의 예시


public class HttpUrlConnectionExample {
    public static void main(String[] args) throws IOException {
        String url = "내가 쓸 API엔드포인트";
        HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); //URL 객체와 연결 객체 생성하여 캐스팅하여 HttpURLConnection 타입으로 저장
        
        /*
        API 호출 방식 및 서버에 보낼 헤더 종류와 형식 저장
        해당 코드에서는 GET 방식으로 호출하면 Accept헤더에 Json 형식으로 호출한다는 의미
        */
        connection.setRequestMethod("GET"); 
        connection.setRequestProperty("Accept", "application/json");
        
        /*
        connection으로 서버의 HTTP 응답 상태코드를 int 형으로 저장하여 출력
        */
        int responseCode = connection.getResponseCode();
        System.out.println("Response Code: " + responseCode);
        
        /*
        inpuStream을 이용해 서버 응답을 읽는데 이 때 BufferReader을 사용하여 저장하였고, 이를 readLine() 방식으로 한 줄씩 읽어오 게 했다. 
        StringBuilder의 경우 여러 줄의 데이터를 하나의 문자열로 만들 수 있다. 
        일반 String과 달리 불변객체가 아니라서 수정이 가능하기 때문에 보다 효율적이다.
        */
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
            String line;
            StringBuilder response = new StringBuilder();
            while ((line = reader.readLine()) != null) {
                response.append(line);
            }
            System.out.println("Response: " + response.toString());
        }

        connection.disconnect(); // 연결 종료
    }
}

1-2. 공부 중 의문점과 공부하게된 객체 메모리 구조 이야기

StringBuilder가 불변객체가 아니란 말은 그럼 String은 불변객체란 말이 된다. 그래서 내가 그동안 비교를 할 때도 .equals를 써야 했던 거구나! 하는 깨달음이 생겼다.

예를 들자면 이러하다.

String str = "Hello";
str = str + " World";
System.out.println(str); // "Hello World"

여기서 보면 마치 str의 내부가 Hello World로 변경된 것으로 보인다.
하지만 실제로는 한 번 생성된 Hello가 사라지지 않고 남아있으며, str이 가리키는 메모리 위치만 바뀌어서 값이 Hello World로 변경되는 것이다.

쉽게 비유하자면 다음과 같이 예시를 들 수 있다.

  1. 나는 "쭈디스 컴퓨터" 호텔방 1234호에 투숙하는 String 형태의 값 "쭈디" 이고 최초로 Judy라는 객체명을 쓰고 있다.

  2. 그러나, 새로운 1002호에 투숙객 String 형태 값 "쭈디는 아니야"Judy라는 객체명을 쓰고 싶다고 하면서 나한테서 가져가버렸다.

  3. 하지만 1234호에 투숙한 "쭈디"가 사라지지는 않는 것이다.

그저 Judy라는 이름은 1234호를 가리키다가 현재는 1002호를 가리키고 있다.

1-3. 불변객체가 아닌 StringBuilder의 형태라면?

만약 위에 예시든 "쭈디스 컴퓨터" 호텔이 String 형태가 아니라 StringBuilder라면 다음과 같다.

StringBuilder sb = new StringBuilder("쭈디");
sb.append("는 아니야");
System.out.println(sb.toString()); // "쭈디는 아니야"

StringBuilder의 경우, 쭈디가 1234호에 투숙하다가 퇴실하고, 다시 쭈디는 아니야가 새로 투숙하는 방식이라고 할 수 있다.

2. RestTemplate

  • Spring에서 제공하는 동기식(Blocking) HTTP 클라이언트다
    • 동기식이란 먼저 시작된 작업이 끝나면 대기중인 새 작업이 시작하는 방식
  • Spring Boot 2.4부터 WebClient로 대체되어 비추천되지만, 여전히 많은 코드에서 사용됨.
  • 내부적으로 HttpURLConnection 또는 Apache HttpClient를 사용하여 HTTP 요청을 보냄.

2-1. RestTemplate - GET 요청 예시

		//RestTemplate을 사용하기 위한 인스턴스 생성
        RestTemplate restTemplate = new RestTemplate(); 
        String url = "사용할 API엔드포인트";

        ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
        System.out.println("Response: " + response.getBody());
    }
}

getForEntity(url, String.class)가 GET 요청을 보낸 후 ResponseEntity형태로 응답을 받고 문자열로 변환하여 반환하는 구조

2-2. RestTemplate - POST 요청 예시

        RestTemplate restTemplate = new RestTemplate();
        String url = "사용할 API엔드포인트";

        // 요청 바디 생성
        Map<String, String> requestBody = new HashMap<>();
        requestBody.put("값","값"); //필요에 따라 넣기

        // json 형태로 헤더를 반환하도록 세팅
        HttpHeaders headers = new HttpHeaders();
        headers.set("Content-Type", "application/json");

        // 위에서 생성한 바디와 셋팅된 헤더로 HttpEntity 객체 생성
        HttpEntity<Map<String, String>> request = new HttpEntity<>(requestBody, headers);

        // POST 요청으로 객체를 전달.
        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
        System.out.println("Response: " + response.getBody());
    }

요청 바디와 헤더를 포함한 HttpEntity 객체를 전달하는 방식

2-3. RestTemplat - 수동 처리 예시

  • 기본적으로 리다이렉션을 자동 수행함
  • POST 요청이 3xx번 응답을 받을 경우 GET으로 변경될 가능성이 있음. (HTTP에서는 302, 303은 기본적으로 GET요청으로 변경되기 때문)
    • HTTP/1.1에서 302가 리다이렉트를 의미하지만 요청 메서드의 유지 여부가 명확하게 정해져있지 않고, 이로 인해 POST 요청인데도 리다이렉트 되는 URL은 GET으로 자동 변환이 된다.
    • 303은 그냥 GET로 바꾸라는 의미의 상태코드
    • 이는 302, 302가 나오면 RestTemplate를 사용하여도 리다이렉트 시 내부적으로는 HttpURLConnection을 사용하기 때문이기도 하다.
    • 반면 307,308은 POST가 유지되기 때문에 POST가 유지되는데 서버가 아래의 응답처럼 나오면 POST가 유지되고 있다는 의미이다.
      HTTP/1.1 307 Temporary Redirect
      Location: /new-url
  • 리다이렉트 없이 HTTP 메서드를 유지하려면 Apache HttpClient를 설정해야 한다는 데 본 프로젝트에서는 리다이렉트가 있어야 하니 이 부분은 설정 안 할 것으로 예상됨.
        RestTemplate restTemplate = new RestTemplate();
        String originalUrl = "기존 URL주소";

        ResponseEntity<String> response = restTemplate.getForEntity(originalUrl, String.class);
        
        /*
        
        */
        if (response.getStatusCode().is3xxRedirection()) {
            String newUrl = response.getHeaders().getLocation().toString();
            ResponseEntity<String> newResponse = restTemplate.getForEntity(newUrl, String.class);
            /*
            만약 여기서  
            ResponseEntity<String> newResponse = restTemplate.exchange(newUrl, HttpMethod.POST, request, String.class); 
            이러한 형태를 사용했다면 기존정보를 유지하면서 새 URL로 POST요청 또한 가능
            */
            System.out.println("Redirected Response: " + newResponse.getBody());
        }
  • 리다이렉트로 300번대 응답이 올 때는 헤더에서 Location값을 읽어서 직접 새 요청을 보낸다.
  • RestTemplate은 기본적으로 리다이렉트를 자동으로 따라가지만, POST가 GET으로 변환되는 등 문제 발생 시나 API 요청 후 상태가 변경되어서 데이터가 유실될 위험이 있을 때 위와 같이 수동적인 처리를 해야 할 필요가 있다.

결론, 간단한 HTTP 요청에는 유용하게 쓰일 수 있지만 비동기 지원이 필요할 경우에는 WebClient를 사용해야 한다.

3. WebClient + Mono

  • WebClient는 Spring WebFlux에서 제공하는 비동기(Non-blocking) HTTP 클라이언트
    • 비동기형은 먼저 요청된 것이 응답되지 않아도 새 요청을 받아 처리한다.
    • Spring Boot WebFlux는 반응형 및 비동기적 웹 애플리케이션 개발을 지원하는 모듈이고, Reactive Streams를 기반으로 하여 이벤트지향 프로그래밍을 할 수 있기 때문에 높은 확장성과 고성능에 유리
    • 동시에 많은 요청을 처리하기에도 적합

3-1. WebClient - GET 요청 예시

        WebClient webClient = WebClient.create("기본 주소");

        Mono<String> response = webClient.get() //GET요청
                .uri("엔드포인트") 
                .retrieve() 
                .bodyToMono(String.class); 
                //응답을 Mono<String> 타입으로 변환

        response.subscribe(System.out::println); // 비동기 실행 결과출력 
  • Mono가 Reactive Streams 중에 하나에 해당된다.
  • Mono는 단 하나의 값을 비동기적으로 반환한다. 예시로 들자면 알림서비스와 비슷하다. 연락이 올 때까지 기다리지 않고 알림이 오면 알 수 있는 그런 것이다.
  • 응답이 될 준비가 되면 subscribe으로 인해 결과가 그 때 출력되는 것이다.

3-2. WebClient - POST 요청 예시

        WebClient webClient = WebClient.create("기본 주소");

        Mono<String> response = webClient.post()
                .uri("엔드포인트")
                .bodyValue(Map.of("title", "foo", "body", "bar", "userId", 1))
                .retrieve()
                .bodyToMono(String.class);

        response.subscribe(System.out::println); // 비동기 실행

GET방식과 유사하지만 .bodyValue(Map.of("title", "foo", "body", "bar", "userId", 1)) 이 부분이 맵핑되어 저장되는 Requset값들이다.

3-3. WebClient - 수동 처리 예시

기본적으로 리다이렉트를 자동으로 따라가지 않아서 수동 처리를 해야한다.

WebClient webClient = WebClient.create();
Mono<String> response = webClient.get()
    .uri("기존 주소")
    .exchangeToMono(clientResponse -> clientResponse.headers().location()
        .map(newUrl -> webClient.get().uri(newUrl).retrieve().bodyToMono(String.class))
        .orElseGet(() -> clientResponse.bodyToMono(String.class)) // 리다이렉트가 없으면 기존 응답 반환
    );

response.subscribe(System.out::println);

3xx 응답이 오면 Location 헤더를 읽어서 새로운 요청을 보냄.
자동 리다이렉트가 불가능하므로, 직접 처리해야 함.

3. 현재 프로젝트에 적합성 여부

1. 특정 사이트로 리다이렉트하기

우리는 취준생을 위한 페이지를 만들면서 취업 관련 사이트로 리다이렉팅이 되도록 할 예정이다. 그런데 위의 방법들은 API로 호출하는 것인데 이게 어떻게 되는 거지?
라는 의문이 들었다. 그래서 다시 한 번, 어떤 로직인지 팀장님에게 물어봤다.

페이지를 크롤링 > 크롤링한 페이지 id가 존재하고 그것을 pathVariable로 Api 호출 시, 해당 페이지로 리다이렉팅이 되는 구조가 되어야 한다고 한다.

2. 무엇을 쓸 것인가?

위에 있는 것들 중 HttpURLConnection는 열심히 정리했지만 좀 아닌 것 같다.
그리고 그나마 RestTemplate이나 WebClient를 쓸 거 같은데 이것도 둘 중 무엇이 더 나을지 고민을 더 해봐야 할 거 같다.

그리고 결정적으로 현재는 우리의 개발 단계가 Ver1이다.
이 부분에서는 크롤링을 제외하고 개발하기로 했기 때문에 일단은 RestTemplate, WebClient 모두 제외되고 제 3의 방법을 써야할 거 같다.

3. Ver1 현재 단계에서 적절한 리다이렉트 방법

3-1. RedirectView

RedirectView 인스턴스를 생성하고 setUrl을 이용하여 원하는 주소를 써주면 된다. 이 부분은 더미 데이터를 이용할 예정이니 매우 간편할 것 같다.

사용자 요청: GET /redirect/{id}  
서버 응답: 302 Found + Location: https://velog.io  

만약 위와 같이 진행된다면 페이지는 velog.io로 자동으로 이동되는 것이다.

3-2. sendRedirect

httpServletResponse를 이용하는 방법이다. 컨트롤러 단에서 HttpServletResponse를 가져와서 .sendRedirect("주소"); 를 써주는 걸로 사용이 가능하다. 이 부분은 그런데 서블릿 API를 직접 사용하기 때문에 더미데이터를 사용하는 것과는 거리가 있는 방법이다. 그래서 Ver1에서는 아마 RedirectView를 쓰는 게 그나마 적절한 것 같다는 게 내 생각이다.

4. Ver2 이후에서 고민할 것

크롤링이 들어가는 Ver2 이후에는 실시간 크롤링이라면 아무래도 RestTemplate이나 WebClient를 사용하면서 동시에 RedirectView나 sendRedirect()를 사용해야 할 거 같다는 생각이 든다.

전자를 사용하여 크롤링 데이터를 가져오고, 후자를 이용하여 리다이렉팅을 진행하는 것이다.

profile
언제 어느 위치에 있더라도 그 자리의 최선을 다 하는 사람이 되고 싶습니다.

0개의 댓글