2016년 4월 15일 금요일

Spring RestController 의 응답결과에 값 추가하기

Spring RestController 을 이용해서 쉽게 JSON 이나 XML 형태의 값을 클라이언트에게 전송할 수 있습니다. 하지만, 때론 Controller 까지 그 값을 넣어주지 않았는데 일괄적으로 그 값을 보내줘야 할 때가 있습니다.

그럴 경우 하나의 방법으로 WebMvcConfigurerAdapter 을 상속받은 클래스를 만들어 configureMessageConverters 을 Override 하는 방법이 있습니다.

JSON 의 예를 들어보면, Spring 에서 JSON 은 Jackson 을 기본으로 합니다. 그럼 아래와 같은 형태의 Override 을 구현해야 합니다.

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {

    }

매개변수로 받게 되는 converters 에 새로운 MessageConverter 을 추가(add method 이용)하는 방식입니다. Jackson 은 AbstractJackson2HttpMessageConverter 을 제공하기 때문에, 이것을 이용해서 writeInternal 을 Override 해서 구현하면 됩니다. 물론 wirteInternal 에 대한 소스는 오픈소스이기 때문에 쉽게 구할 수 있기 때문에, objectWriter.writeValue(generator, value); 이전에 value 에 원하는 값을 추가하면 JSON 에 값이 추가됩니다.

configureMessageConverters 일 이용하면 JSON 뿐만 아니라 Spring 에서 기본 제공하지 않는 MessagePack 같은 것도 converter 로 추가하는 등의 작업이 가능합니다.

2016년 4월 8일 금요일

Spring Web 에서 request parameter 추가

Spring Web 에서 Client 가 요청한 주소에 기반해서 값을 request 에서 가져올 수 있지만, 가끔 그 매개변수에 서버 쪽에서 어떤 값을 추가해서 받아야 할 필요가 있을 때가 있습니다. 저의 경우는 Spring MVC 에서 실제 요청을 받은 Controller 의 method 명을 알고자 할 때 강제로 Exception 을 발생시켜 첫번째 라인의 method 명을 알아내는 방식보다는, 이 방식으로 요청되는 주소에서 값을 얻어서(주소에 method 명이 유추되도록 구성되어야 하겠죠) request 에 추가하도록 처리하였습니다.

원리는 간단하면서도 살짝 복잡합니다. 일단 HttpServletRequest 라는 개체는 setParameter 라는 것이 존재하지 않습니다. 이와 같이 매개변수를 개발자가 마음대로 조작할 수 있을 경우 문제가 더 클 수 있기 때문입니다. 그래서 약간의 우회적인 방법이 필요합니다.

먼저, Filter 을 등록해야 합니다. Interceptor 는 불가능한 것으로 압니다. Filter 에서 doFilter method 등을 Override 받아 구현할 때 HttpServletRequest, HttpServletResponse, FilterChain 을 매개변수를 받을 것입니다. 이 때 매개변수로 받은 HttpServletRequest 개체를 생성자로 받아서 이를 확장할 HttpServletRequestWrapper 클래스를 상속받아 구현하는 것입니다. 통상적으로 request 의 getParameterMap() 을 이용해 Map 에 매개변수들을 저장한 뒤 getParameter(), getParameterMap(), getParameterNames(), getParameterValues() 등을 Override 해서 구현하고, setParameter() 을 새롭게 구현(setParameter() 는 2 개의 오버로드된 method 로 구현합니다)하는 형태가 됩니다.

이렇게 구현된 HttpServletRequestWrapper 는 HttpServletRequest 의 구현체 중 하나이기 때문에 Controller 로 전송되어도 사용에 크게 문제가 없습니다(OOP Design Pattern 의 장점).



저는 보통 이렇게 구현해놓고 씁니다.


public class HttpRequestWithModifiableParameters extends HttpServletRequestWrapper {

    Map<String, String[]> params;

    public HttpRequestWithModifiableParameters(HttpServletRequest request) {

        super(request);
        this.params = request.getParameterMap();

    }

    @Override
    public String getParameter(String name) {

        String[] paramArray = getParameterValues(name);

        if (paramArray != null && paramArray.length > 0) {

            return paramArray[0];

        } else {

            return null;

        }

    }

    @Override
    public Map<String, String[]> getParameterMap() {

        return Collections.unmodifiableMap(params);

    }

    @Override
    public Enumeration<String> getParameterNames() {

        return Collections.enumeration(params.keySet());

    }

    @Override
    public String[] getParameterValues(String name) {

        String[] result = null;
        String[] temp = params.get(name);

        if (temp != null) {

            result = new String[temp.length];
            System.arraycopy(temp, 0, result, 0, temp.length);

        }

        return result;
    }

    public void setParameter(String name, String value) {

        String[] oneParam = {value};
        setParameter(name, oneParam);

    }

    public void setParameter(String name, String[] values) {

        params.put(name, values);

    }

}


그래도 남용은 하지 말아야겠습니다.

2016년 4월 7일 목요일

채팅 서버 구상 (Netty + RabbitMQ)

곧 출시할 서버에서 임시로 쓸 채팅 서버를 구상하였습니다.

네트워크 엔진(프라우드넷 같은)이 도입되면 거기 것을 쓸 것이고, 채팅이 중요한 부분을 차지한다고 생각도 들지 않아서 일단 땜빵 비스무리하게 구현했네요.

요구 조건은 다음과 같습니다.
  • 사용자가 주기적으로 새로운 메세지가 있는지 확인하는 방식은 안됨. (주기적 확인)
  • 많은 사용자가 접속할 수 있으므로 메세지 전송은 최대한 가벼워야 함. (가벼움)
  • 채팅 내용이 많이 전송되지 않을 수 있지만, 사용자가 매우 많이 접속할 수 있으므로 수평적 확장(Scale Out)이 가능해야 함. (수평적 확장)
  • 사용자 인증이 된 접속에게만 메세지 내용을 전파해야 함. (인증)
  • 메세지 내용을 필터링 할 수 있어야 함. (필터링)

채팅방을 따로 만들어서 채팅하고 이런 기능은 일단 고려 대상에서 제외하고, 서버마다 개별적으로 내용이 전달되는 형태보다는 모든 서버가 논리적으로 하나의 서버인 것처럼 보이고 싶었던 것이 나의 고민이었습니다.

혼자서 이것저것 고민해봤으나 가장 괜찮은 방법은 클러스터링 된 Queue 을 이용해서 사용자들이 L4 을 통해 어떤 서버에 접속을 해 있든 자신이 받을 메세지를 Queue 에서 뽑아오는 형태가 좋을 것 같았습니다. 물론 상대가 메세지를 보내는 순간 접속이 되어 있지 않다면 그 메세지는 받을 필요가 없기 때문에 반드시 사용자별로 Queue 를 구현할 필요는 없다는 생각도 있었습니다.

그래서 구상한 물리적 구성은 다음과 같습니다.

L4 - 채팅서버 애플리케이션 - Cluster 된 RabbitMQ 서버

서버는 아주 단순합니다. 다만, RabbitMQ 서버는 Cluster 된 상태여야 합니다. 1 개의 RabbitMQ 만 있다가 서버가 중지되면 곤란하니까요. 그리고 채팅서버 애플리케이션이 동작하는 서버와 RabbitMQ 서버는 물리적으로 분리되어 있어도 상관없으나, 퍼블리셔에게 신청한 채팅서버는 분리된 형태가 아니다보니, 물리적으로 하나의 서버에 설치하는 것으로 할 계획입니다. 어짜피 채팅서버 애플리케이션은 매우 가벼울테니...

실제 채팅서버 애플리케이션은 다음과 같은 흐름을 가질 것입니다.

  1. 애플리케이션이 실행되면 RabbitMQ 에 애플리케이션 독자적인 Queue 을 선언합니다. 채팅서버 애플리케이션마다 한 개씩 가지게 됩니다. Topic 형태에 맞게 이름을 부여하겠죠.
  2. RabbitMQ 에 TopicExchange 을 생성합니다. 위에서 만든 Queue 에 모두 전파할 수 있도록 말이죠.
  3. 위에서 만든 Queue 와 Exchanger 을 이용해 RabbitMQ 에 접속합니다.
  4. 사용자 접속 요청을 받을 수 있도록 Netty 로 decoder 와 encoder 을 생성합니다.
  5. 사용자가 접속을 하면 접속자 풀(Singleton 에 있는 Map)에 등록을 합니다.
  6. 사용자의 첫 메세지가 인증에 관련된 것이라면 인증을 처리하고 사용자 풀(Singleton 에 있는 Map)에 사용자 정보(Netty 의 Channel 이 있어야겠죠)를 입력합니다. 인증 실패 혹은 인증에 관련된 것이 아니라면 접속을 강제로 끊어야겠죠.
  7. 사용자에게 채팅 메세지가 전송되어 오면, 메세지 내용과 상황에 맞는 변조를 거쳐 RabbitMQ 에 데이터를 전송합니다. 물론 위에서 선언한 Exchanger 을 통해 전송해야하고, 이 작업을 통해 채팅 메세지는 모든 Queue 에 하나씩 전송됩니다.
  8. 각 서버는 자신이 바라보고 있는 하나의 Queue 에서 채팅 내용을 순서대로 하나씩 꺼내옵니다. 이 때 Spring Boot AMQP 같은 것을 이용하면 구현이 매우 쉽게 되겠죠.
  9. 꺼내온 내용을 처리하여 사용자 풀에 있는 모든 사용자에게 채팅 내용을 전송합니다. 사용자 풀에 Channel 이 있을 것이므로 Map 의 values 을 Collection 으로 받아와 pararellStream() 으로 처리하면 병렬 처리도 될 것입니다.


이렇게 프로그래밍 된 채팅서버를 가지고 테스트를 진행해볼 것입니다. 물론 채팅 클라이언트는 제가 만드는게 아니라서...언제 테스트를 진행할지는 의문이지만요
실제 코드는 그리 길지 않습니다. 자잘한거 다 빼면 30 라인 조금 넘으려나...




일단, 조건에 대한 만족 여부를 판단해보겠습니다.

  • 주기적 확인 : 사용자가 메세지를 보내면 서버에서 모든 사용자에게 Channel 을 이용해서 메세지를 전송하기 때문에 연결만 유지한 상태면 됩니다. 그러므로 클라이언트에서 주기적으로 새로운 메세지가 있는지 확인할 필요가 없습니다.
  • 가벼움 : HTTP 전송 등을 사용하지 않기 때문에 가볍습니다. Netty 에서 StringDecoder/StringEncoder 을 제공하기 때문에 평문으로 그냥 보내도 되고, ProtoBuffer 을 이용해서 보낼 수도 있겠죠. 다만, 클라이언트가 Unity 이다 보니, ProtoBuffer 는 못쓸 것 같네요. FlatBuffer 가 제공되면 좋겠는데...일단은 장기적으로 쓸 서버가 아니라서 String 으로 해보고 판단하겠습니다.
  • 수평적 확장 : L4 만 버텨준다면 채팅서버 애플리케이션은 몇 대가 있더라도 상관 없습니다. 자신에게 접속된 사용자를 Singleton Map 으로 관리하고, 메세지가 있을 경우 그 사용자들에게 메세지만 보내주면 되니까요. RabbitMQ 역시 한 대든 여러 대든 상관 없습니다. Queue 가 어마어마하게 생길 리도 없고(채팅서버 애플리케이션이 그렇게 많이 실행될 리가 없으니까요) 서버가 늘어나도 Cluster 연결만 하면 문제 없습니다. 다만 전파속도가 얼마나 빠를지는 테스트를 안해봤네요. RabbitMQ 에 대한 분석이 조금 부족한게 문제겠네요.
  • 인증 : Netty 에서 TCP 나 UDP(UDT) 등이 연결될 때 channelActive() 가 호출됩니다. 여기서 접속된 사용자를 저장하는게 어떤 의미가 있을지...사실 모르겠네요. 그래도 메세지를 보내면 channelRead() 가 호출되는데, 인증된 사용자 풀에 저장되어 있지 않을 경우 로그인 처리를 시도하고, 실패하면 접속을 강제로 끊어버리기 때문에(Channel.close() 을 호출) 연결만 하고 멍하니 있는 시도 등은 한 번 걸러낼 수 있겠지요. 인증된 사용자 풀이 따로 있기 때문에, 여기서 메세지 보낼 사용자 목록을 담아둘 수 있겠습니다. 좀 더 응용하면, 방에 해당하는 Map 도 만들어서 방 사용자에게만 메세지를 보내는 형태도 구현가능하겠죠. 물론, 채팅방 위주의 채팅서버라면 이런 구성보다는 매칭서버와 채팅서버를 분리하는게 좋겠죠.
  • 필터링 : 인증된 사용자가 메세지를 보내면(channelRead() 호출) 메세지 내용을 필터링 해버리면 되지 않을까 싶네요. 보낸 메세지에 대한 로그도 남길 수 있을 것이구요.

이렇게 되겠네요.

아직 확인을 못한 부분은, 채팅서버 애플리케이션에서 RabbitMQ 서버로 접속할 때 Cluster 된 서버 목록을 지정해서 살아있는 서버에 계속 연결이 될 수 있느냐겠네요. RabbitMQ 분석을 다른 사람에게 맡겨놔서 저는 잘 몰라요.