개발/프로젝트

[세션 / 토큰 로그인 프로젝트] 고도화(2) 멱등성 검사 미들웨어(2) 구현 과정

prpn97 2023. 7. 24. 19:53

구현하게 된 계기, 기획은 아래 링크에서 확인할 수 있다.

https://prpn97.tistory.com/123

 

[세션 / 토큰 로그인 프로젝트] 고도화(2) 멱등성 검사 미들웨어(1) 계기 및 기획

계기 기본적으로 get, put, delete는 멱등하고, 멱등하지 않은 메서드는 post, patch로 알고 있다. 동일한 요청을 여러 번 반복했을 때 결과값이 동일하면 멱등한 것이라고 볼 수 있는데, 단순하게 로그

prpn97.tistory.com

구현 과정

 

앞서 설명했듯, 클라이언트의 요청이 들어오는 시점에서 가장 먼저 해당 미들웨어를 두고

로직이 작동하기 전에 로그를 기록하고, 작동한 후를 직전 로그와 비교하는 방법으로 구현하였다. 

 

1. saveRequestToDB

요청시점의 로그를 기록하는 미들웨어

 

    const key = req.sessionID;
    res.cookie('iKey', key, {
      httpOnly: true,
      maxAge: 3600000, //1시간
    });
    
    const exceptPassword = { ...req.body };
    delete exceptPassword.password;
    
    const requestData = {
      method: req.method,
      url: req.url,
      body: JSON.stringify(exceptPassword) || null,
      cookie: JSON.stringify(req.cookies) || null,
      key: key,
    };

요청이 들어온 직후 express-session을 통해 계속 바뀌는 sessionID값을

iKey(idempotency key)라는 이름의 쿠키로 발급한다. 

 

그리고 요청이 들어오면 method, url, body, cookie, key로 구분하여 db에 넣어준다. 

멱등성 검사를 위해서는 cookie로 가능하지만, 모든 요청의 시점을 기록하기 때문에 

참고할 수 있도록 추가로 요청시점의 내용을 db로 저장했다. 

 

위에서 언급했듯, 모든 요청이 들어오는 시점에 iKey가 발급되기 때문에 각 요청마다 iKey가 다르게 db에 저장된다. 

그리고 해당 시점에 클라이언트가 가지고 있는 cookie를 기록한다.

여기서 중요한 부분은 cookie에 기록이 되는 시점에 클라이언트는 당연히 ikey가 담긴 cookie를 받지 못한 상태다.

그렇기 때문에 cookie에는 당시 시점에서 기존에 클라이언트가 가지고 있는 cookie값, 혹은 빈 객체가 담기게 된다. 

db에 들어갈 때 requestData는 다음과 같이 확인되었다. 

Request data saved to DB: {
  method: 'POST',
  url: '/token/login',
  body: '{"username":"sh5080"}',
  cookie: '{}',
  key: 'FCkOz1IrJSR7Hd1qPEqq-huf_ph7j_bC'
}

 

 

2. checkIdempotency

모든 로직이 작동한 후 멱등성을 검사하는 미들웨어로, requestData와 같은 폼으로 비교할 수 있도록 하였다.

    const current = {
      method: req.method,
      url: req.url,
      body: JSON.stringify(exceptPassword),
      cookie: JSON.stringify(req.cookies),
      key: req.sessionID,
    };

문제점 

해당 미들웨어(saveRequestToDB)가 가장 먼저 실행되고, 이후 api가 작동한 후, 

멱등성을 검사하는 미들웨어(checkIdempotency)를 구현했는데,

req에는 요청이 들어온 시점 즉, saveRequestToDB의 req 와 동일하게 확인되었고,

무엇보다.. 검사할 내용이 넘어오질 않았다. 

무엇이 문제일까? 혹시 공부중에 이 포스트를 본다면, 같이 1분정도 고민해보고 결과를 확인하면 좋겠다. 

 

결과를 확인하기 전에 checkIdempotency의 코드를 통해 힌트를 확인해보자. 

간단하게 이전 요청내용과 현재 요청내용을 담았고, 객체로 비교하면 데이터를 소모하기 때문에

동일한 값도 false로 확인되어 json으로 파싱해서 값을 비교했다. 

    const exceptPassword = { ...req.body };
    delete exceptPassword.password;
    const token = res.locals.responseData.token;
    if (token) {
      req.cookies.tokenID = token;
    }    

const previousJSON = JSON.stringify(previousData);
    const currentJSON = JSON.stringify(current);
    if (previousRequest) {
      // 이전 요청과 현재 요청을 비교하여 멱등성 검사
      if (previousJSON === currentJSON) {
        // 같은 요청이면 멱등하다고 판단
        console.log('멱등성을 만족합니다.');
        console.log('이전 요청: ', previousJSON);
        console.log('현재 요청: ', currentJSON);
        console.log('쿠키?: ', req.cookies);
      } else {
        // 다른 요청이면 멱등하지 않다고 판단
        console.log('멱등성이 깨졌습니다.');
        console.log('이전 요청: ', previousJSON);
        console.log('현재 요청: ', currentJSON);
      }
    } else {
      // 이전 요청이 없는 경우
      console.log('첫 번째 요청입니다.');
    }

    res.send(res.locals.responseData);

 

문제점 1) 왜 헤더에 쿠키가 보이는데 req.cookies에는 쿠키가 보이지 않을까?

사실 당연한 것인데,, req는 클라이언트에서 요청한 값이기 때문에 

먼저 1. 클라이언트에서 요청받고 2. 서버에서 로직이 실행되고 3. 다시 클라이언트로 보냈을 때

다음 클라이언트의 요청이 들어오게 되면 다시 1번의 과정에서 보이는 것이지,

클라이언트로 요청을 보내주는 과정에서는 내가 전달하는 내용들을 확인할 수가 없는 것이다. 

 

 

예를 들어 내가 놀이공원 알바생인데, 티켓을 확인하고 도장을 찍었다고 하자.

나는 로직을 통해 도장을 찍었는데, 도장을 찍기 전의 티켓만 확인이 되는 것이다. 

그 이유는 req는 클라이언트에서 요청을 하는 내용이기 때문에

도장을 찍은, 그러니까 로직이 작동한 후의 req를 보기 위해서는 내가 도장을 찍어서

손님에게 티켓을 주고 나서 손님이 도장이 찍힌 티켓을 보여줘야 알 수 있는 것이다. 

 

멱등성을 검사한다는 것은 행위에 있어 변화가 있는지 확인해야 하는데,

행위가 마쳐지기 전에 변화를 확인하려 했던 것이다. 

그렇다면 행위가 마쳐지기 전, 그러니까 프론트에 내용을 전송하기 전에

결과값을 예상하고 검사하는 식으로 구현해보겠다. 

 

 

문제점 2) 왜 값이 checkIdempotency로 넘어오지 않을까?

 

값이 넘어오지 않는 이유를 캐치했다면.. 이미 내가 구현한 내용들을 내가 설명하기도 전에 파악했을 것 같다. 

위에 작성한 코드 가장 아래를 보면 res.send(res.locals.responseData)로 클라이언트에게 응답데이터를 보내는데,

각 라우터에서 res.json, res.send 등으로 이미 클라이언트에게 전송할 데이터를 보내는데 왜 라우터가 지나간 후에

또 같은 데이터를 보내는 것일까? 그리고 res.locals는 무엇일까?

 

먼저 값이 넘어오지 않은 이유는 다음과 같다. 

이미 해당 미들웨어로 진입하기 전에 각 라우터에서 데이터를 전송하고 오류가 났을 때만 에러핸들러로 보냈기 때문에

각 라우터 뒷단의 미들웨어로 올 데이터가 없는 상태였다. 

 

그래서 각 라우터에서 로직을 거쳐 전송해야 할 데이터가 있는 경우 res.locals로 보내고, 

해당 미들웨어에서 res.locals의 데이터를 받아서 프론트로 전송했다. 

res.locals로 보내게 되면 바로 클라이언트로 보내는 것이 아니라 전역변수로 request하는 시점에만 유효하도록 한다.

 

결국 프론트로 전송하기 전에 한 단계를 더 거치도록 한 것이다. 

 

 

해결 방법

로그인을 할 때 요청이 오는 시점의 쿠키를 확인하여 (아직 로그인이 완료되지 않았기 때문에 쿠키가 없는 상태)

로그인한 후에 받는 쿠키를 비교하여 차이가 있는지를 확인해야 한다. 

그러나 쿠키를 비교하기 위해서는 결과를 알고있어야 하는데 아직 클라이언트에서 결과를 제시하지 않았기 때문에

req.cookies로는 알 수가 없다. 아니, 어떻게 해도 미래를 확인하고 알 수는 없다. 

어떻게 보내게 되는지 결과를 받아서 미들웨어에서 확인을 하고 값을 프론트로 보내도록 한 것이다. 

 

 

코멘트

현재는 req.cookies에 res.cookie로 추가가 될 쿠키를 더해서 (사실 위 코드에 나와있다..)

req.cookies.tokenID로 넣어주고, req.cookies의 전후를 비교하도록 하였다. 

그런데.. 다 생각하고 고민하고 구현하고 나서 느끼는 것인데,

사실 이렇게 구현하면 당연히 변화가 있는 부분을 넣어서 전후를 비교하니까

변화가 있다는 것은 너무나도 당연하다. 

 

내가 구현한 메커니즘으로 생각해보면, 차라리 쿠키가 생기기 전과 후를 비교하는 것보다

쿠키가 생길 때처럼 무언가 변화가 생기는 api에서 res보내줄 때 res.locals에 false 하나 넣어주고

true면 멱등하고, false면 멱등하지 않도록 하면 그만이지 않을까 싶다. 

변화 전 데이터는 심지어 db에 저장하면서 값을 불러와서 비교하는 작업을 하고 있는데,

res.localse로 전역으로 구분한다면 db에 저장해서 확인하는게 차이가 없는 것 같다. 

 

 

나중에 테스트코드를 작성할 때 로직으로 추가해주는 방식으로 구현하는 것이 훨씬 깔끔할 것 같다. 

멱등하지 않은 것이 문제나 에러가 아니다.

다만, 조회를 하는데 값이 바뀐다던지 값이 바뀌는 도중 에러가 생겼을 때

중복요청이 된다고 생각하면 참 아찔하다. 점검은 늘 중요하다. 

 

값이 바뀌기 직전에 변화의 유무를 체크한다면 오류를 확인하기 전에도 변화가 되었는지를 알 수 있다.

물론 트랜잭션을 통해 롤백한다면 결과론적으로 알 바 아닐 수 있지만.. 왜 해야 하는지는 중요한 것 같다.

 

 

개인적으로 아쉬운 부분은 통신에 관한 내 이해도인 것 같다. 

req.cookies를 단순히 개발자도구를 열어서 쿠키가 확인이 되는데 왜 안보이는지

한참을 생각했는데, 시공간의 개념을 확고히 해야함을 다시금 깨닫는다. 

728x90