티스토리 뷰

서버를 구축하다보면, 사용자 요청에 대한 로그를 남겨야 하는 작업이 필요합니다.
저에게 주어진 요구사항은 사용자의 IP, 접속한 브라우저, 사용자의 요청을 처리한 후 어떤 결과가 나왔는지에 대한 정보를 로그를 남기는 것입니다.

먼저 구축된 서버의 아키텍처를 아주 간단하게 나타내면 아래의 그림과 같습니다.

가장 먼저 기존의 코드를 건드리지 않고, 요구사항을 만족할 수 있는 방법이 없는지 고민했고 Nginx의 Access Log를 이용하는 방법을 생각해봤습니다.

172.18.0.1 - - [28/Sep/2022:07:04:30 +0000] "OPTIONS /api/v1/channels HTTP/1.1" 200 Chrome/

Nginx의 Access Log는 별도의 설정 없이 위와같은 로그를 Default로 저장하며 사용자의 IP 주소, HTTP Method, 접속한 브라우저의 정보를 저장합니다.

하지만, Nginx의 Access Log는 요청에 대한 정보는 저장할 수 있지만, 요청을 처리한 후 나온 결과를 얻을 수 없었습니다. 그래서 다음으로 생각한 방법은 아래의 그림과 같습니다.

로그를 남기는 용도의 서버를 별도로 구축하고, proxy_pass를 로그 서버로도 넘겨준다면, 기존의 코드를 하나도 건드리지 않고, 새로운 기능을 추가할 수 있습니다. 하지만, 여전히 문제는 존재하는데요, 요청의 처리 결과를 로그 서버에서 알아야 한다는 문제입니다. 만약 로그 서버에 같은 로직을 작성한다면 불필요한 중복이 생기는 것이죠.

그래서 Nginx의 Access 로그를 사용하지 않고, 사용자의 IP, 접속한 브라우저 등을 기존 서버에서 처리하는 방법을 알아보게 되었습니다. 이번에는 Spring에서 이 정보를 어떻게 얻을 수 있는지 알아보겠습니다.

요청을 보낸 사용자의 IP 주소와 Browser 정보를 얻기 위해서는 Spring HttpServletRequest를 사용하면 됩니다. Spring에서 제공하는 HttpServletRequest는 아래와 같습니다.

public interface HttpServletRequest extends ServletRequest {
	...
}

ServletRequest의 역할은 클라이언트 요청 정보를 제공하는 객체를 정의하는 것입니다. 그리고 HttpServletRequest는 HTTP 데이터를 포함한 클라이언트 요청 정보를 제공하는 객체입니다.

HTTP 데이터를 포함한 클라이언트 요청 정보를 얻을 수 있다는 것은 중요합니다. 왜냐하면 저희가 얻고 싶은 정보는 HTTP Header에 전부 들어있기 때문이에요.

 

HTTP 헤더 - HTTP | MDN

HTTP 헤더는 클라이언트와 서버가 요청 또는 응답으로 부가적인 정보를 전송할 수 있도록 해줍니다. HTTP 헤더는 대소문자를 구분하지 않는 이름과 콜론 ':' 다음에 오는 값(줄 바꿈 없이)으로 이루

developer.mozilla.org

위의 문서를 보면, 사용자의 IP 주소는 "X-Forwarded-For"에, Browser 정보는 "User-Agent"에 있다는 사실을 알 수 있습니다. 

이제 HttpServletRequest로 HTTP 정보가 추가된 request를 받고 Header를 열어봅시다. 먼저 Browser 정보를 얻는 방법입니다.

@Slf4j
@RestController("/api")
@RequiredArgsConstructor
public class XffController {

    private final XffService xffService;


    @GetMapping("/xff")
    public void receiveGet(HttpServletRequest request) {
        String browser = "";
        String userBrowser = request.getHeader("User-Agent");

        if(userBrowser.contains("Trident")) {												// IE
            browser = "ie";
        } else if(userBrowser.contains("Edge")) {											// Edge
            browser = "edge";
        } else if(userBrowser.contains("Whale")) { 										// Naver Whale
            browser = "whale";
        } else if(userBrowser.contains("Opera") || userBrowser.contains("OPR")) { 		// Opera
            browser = "opera";
        } else if(userBrowser.contains("Firefox")) { 										 // Firefox
            browser = "firefox";
        } else if(userBrowser.contains("Safari") && !userBrowser.contains("Chrome")) {	 // Safari
            browser = "safari";
        } else if(userBrowser.contains("Chrome")) {										 // Chrome
            browser = "chrome";
        }
        log.info("userBrowser = {} ", browser);

    }
}

조건문이 많기는 하지만, 어렵지는 않습니다. request의 Header에서 "User-Agent"에 저장된 value를 뽑으면 Client가 접속한 Browser를 알 수 있습니다. 

다음으로 IP 주소를 얻어봅시다. IP 주소를 Header에서 추출하기 전 Network 전공 지식이 필요한데요, Client부터 Server까지 패킷이 도착하는 과정에 대한 이해가 필요합니다. 이 주제만으로도 내용이 너무 길어지기 때문에 지금은 아래의 그림과 같은 문제가 존재한다고 이해하면 됩니다.

Client의 요청은 결국 Nginx를 거쳐 Spring WAS로 넘어옵니다. 이때 WAS 입장에서 HTTP 요청을 보낸 Client는 Nginx가 됩니다. 따라서 WAS에서 단순히 요청을 보낸 호스트의 IP 주소를 열어보면 Nginx의 IP 주소가 되는 것이죠. 실제 요청을 보낸 Client의 IP 주소를 모르는 것입니다. 

이 문제를 해결하기 위해서는 X-Forwarded-For이 필요합니다.

 

HTTP 헤더 및 Application Load Balancer - Elastic Load Balancing

클라이언트 포트 보존 속성(routing.http.xff_client_port.enabled)을 활성화하고 routing.http.xff_header_processing.mode 속성에 preserve 또는 remove을(를) 선택할 경우 Application Load Balancer는 클라이언트 포트 보존 속

docs.aws.amazon.com

X-Forwarded-For는 여러 모드로 요청을 보낸 Client의 IP를 얻을 수 있게 도와주는데, 아래와 같이 지금까지 지나온 호스트의 IP 주소를 저장하는 방법으로 동작합니다.

X-Forwarded-For : client, proxy1, proxy2

지나온 호스트의 IP 주소를 스택 자료구조 같이 저장한다면, 마지막에 남은 IP 주소는 Client의 IP 주소라고 특정할 수 있습니다. 하지만 실제 Client의 IP 주소는 아닙니다.

XFF를 이용한다고 정확하게 Client의 IP 주소를 특정할 수는 없습니다. 애초에 대부분의 인터넷 사용자는 인터넷이라는 거대한 네트워크에서 유일한 존재가 아니고 사설망에 속하기 때문이에요. XFF는 '어느정도' 사용자를 특정 지을 수 있고 어떤 외부 IP를 거쳐 요청을 보냈는지 정보를 얻을 수 있게 해주는 기술입니다.

@Slf4j
@RestController("/api")
@RequiredArgsConstructor
public class XffController {

    private final XffService xffService;


    @GetMapping("/xff")
    public void receiveGet(HttpServletRequest request) {
    
        String ip = request.getHeader("X-Forwarded-For");
        String userIp = request.getRemoteUser();
     
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {

            ip = request.getHeader("Proxy-Client-IP");

        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {

            ip = request.getHeader("WL-Proxy-Client-IP");

        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {

            ip = request.getHeader("HTTP_CLIENT_IP");

        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {

            ip = request.getHeader("HTTP_X_FORWARDED_FOR");

        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {

            ip = request.getHeader("X-Real-IP");

        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {

            ip = request.getHeader("X-RealIP");

        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {

            ip = request.getHeader("REMOTE_ADDR");

        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {

            ip = request.getRemoteAddr();

        }


        log.info("ip = {} ", ip);

    }
}

XFF를 코드로 구현하면 위와 같습니다. XFF에 들어있는 정보를 순차적으로 확인하면서, 마지막에 남은 IP 주소로 Client IP 주소를 유추하는 것입니다.

그럼 실제로 동작하는지 확인해봅시다.

aws EC2를 띄우고, Public IP를 할당받았습니다. 여기에 구축한 서버를 띄워보겠습니다.

서버가 정상적으로 띄워졌습니다. 이제 만들어놓은 controller로 요청을 보내보면 아래와 같은 결과를 얻을 수 있습니다.

2022-10-02 02:40:37.105  INFO 13186 --- [nio-8080-exec-1] example.xff.controller.XffController     : ip = 118.235.41.108 
2022-10-02 02:40:37.114  INFO 13186 --- [nio-8080-exec-1] example.xff.controller.XffController     : userBrowser = chrome

IP 주소를 얻었습니다. 서버는 제가 보낸 요청이 118.235.41.108 라는 주소로부터 왔다고 판단하는 것인데, 이 IP 주소는 어떤 장치가 할당받은 IP 주소일까요? IP 주소로 지리적 위치를 알려주는 서비스를 이용해봤습니다.

위에서 예상한대로 정확한 위치를 나타내지 않습니다. 하지만, 사용자의 지리적 위치를 어느정도 유추할 수 있는 정도의 정보입니다. 왜 제가 보낸 요청의 IP 주소가 '부산 수영구' 인지 유추할 수도 있습니다. 저는 현재 휴대폰 핫스팟과 연결해 네트워크에 접속중입니다. 그리고 수영구에는 kt 기지국이 있기 때문입니다.

이전에 깃허브에 오픈된 소스 코드를 돌아다니다 XFF를 이용해 사용자의 IP 주소를 찾아내고, 이 IP 주소를 pk로 잡아 관리하는 프로젝트를 본적이 있습니다. 이 서비스는 모든 사용자가 구별되는 Public IP를 할당받지 않는 한 문제가 생기는 구조였습니다. 그리고 실제로 모든 호스트들은 public IP를 할당받지 않습니다.

결론은 HTTP 정보가 포함된 객체를 제공하는 HttpServletRequest를 이용하면, 요청에 대한 다양한 정보를 얻을 수 있고, 여기에는 접속한 Browser, 사용자의 IP 주소 등등이 들어있습니다.

'BackEnd > Spring' 카테고리의 다른 글

checkstyle로 코드 컨벤션 관리하기  (0) 2023.01.06
[Spring] @Async 비동기 처리 방법  (2) 2022.10.02
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함