Skip to content

삽질기

Sejin Jeon (Dean) edited this page Apr 25, 2023 · 74 revisions

REST API

PUT vs PATCH

  • 원문: https://williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot/
  • 요약
    • PUT은 대체, PATCH는 delta에 대한 수정. (따라서 PUT에서 생략된 필드는 '그대로 유지'하라는 의미가 아니라 '삭제'하라는 의미가 된다.)
    • PATCH를 사용할 때는 다음과 같이 변화분에 대한 operation, path(필드명), value를 배열로 넘겨줄 것을 RFC 문서에서 권고하고 있다.
      • bad example
        PATCH /users/123
        
        {
            "email": "new.email@example.org",
            "name": "newName",
        }
      • good example
        PATCH /users/123
        
        [
            { "op": "replace", "path": "/email", "value": "new.email@example.org" },
            { "op": "replace", "path": "/name", "value": "newName" },            
        ]
    • 관련 RFC 문서: https://tools.ietf.org/html/rfc6902
  • 현실성
    • 이제 저 글의 말대로 PATCH를 'idiot'처럼 쓰지 않으려 한다고 생각해보자.
    • 먼저 처음 드는 생각은 operation을 enum이나 상수로 정리해야 하냐는 것인데, Node.js 레벨에서 이를 따로 제공해주지 않는 것으로 보인다.
    • 저 body를 controller 단에서 파싱해주는 작업이 필요할텐데, 이에 대한 Node.js의 지원이 없는 것 같다. 이걸 또 유틸 함수 만들어서 처리하기도 좀...
    • 이 생각을 나만 하는 건가 싶어서 GitHub REST API를 참고해봤다. 나만 그런 생각을 한 게 아닌가보다.
  • 결론
    • 어플리케이션을 만들 때 로이 필딩 형님이 시키는거는 오지게 안 따라주는 게 국룰.. ㅎ

DELETE body

  • DELETE의 body는 명세에서 정의하는 바가 없다. RFC 문서에 따르면 DELETE에 body가 포함된 경우, 구현체에 따라 이 요청을 거부할 수도 있다고 나와있다.

    A payload within a DELETE request message has no defined semantics; sending a payload body on a DELETE request might cause some existing implementations to reject the request.

  • koa-body에서는 DELETE에 body가 들어가는 것을 허용하지 않는다.

DELETE multiple resources

  • https://stackoverflow.com/a/41539479/7258660
  • DELETE method는 기본적으로 한 번에 하나의 리소스만을 삭제하도록 고안되었다. 하지만, 여러 리소스를 동시에 삭제해야 하는 경우, 삭제 HTTP 요청을 여러번 보내는 건 서버에 부하를 쓸데없이 많이 주는 일이다.
  • 여러 리소스를 한 번에 삭제해야 하는 경우, REST에서는 POST {domain}/batch API를 만들어 사용할 것을 권고한다. batch API는 여러 HTTP 요청을 한번에 body에 담아 보내는 목적으로 쓴다.
    curl -X POST \
      -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
      -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
      -H "Content-Type: application/json" \
      -d '{
        "requests": [
          {
            "method": "POST",
            "path": "/1/classes/GameScore",
            "body": {
              "score": 1337,
              "playerName": "Sean Plott"
            }
          },
          {
            "method": "POST",
            "path": "/1/classes/GameScore",
            "body": {
              "score": 1338,
              "playerName": "ZeroCool"
            }
          }
        ]
      }' \
      https://api.parse.com/1/batch
    
  • GitHub에서 브랜치를 삭제해봐도 POST가 날아감을 알 수 있다. image

GET Body

  • 문제 상황
    • 서버에 아무 리소스도 생성하지 않지만, 서버의 기능을 사용해서 input을 가공하여 결과를 받아가기만 하는 경우.
    • 사이드이펙 없고 멱등한데 이거 GET 아님???? <- 맞음
  • 그럼 문제가 없는 게 아니냐 싶었는데...
    • 그런데 짜잔! Axios에서 불가능함.
    • 안되는 이유는 Axios가 fetch 또는 XHR을 사용하도록 구현되어 있는데 여기서 GET에 body 넣는 걸 허용해주지 않는다고 함.;;

PUT with multipart-form data

  • 문제 상황
    • PUT으로 이미지를 수정하고 싶은데.. 안 올라간다!
  • 원인
    • REST 규약에서 PUT에는 form 데이터를 허용하지 않음.
  • 히스토리
    • multipart-form의 태생은 <form> HTML 태그를 통해 데이터를 보낼 때 사용하는 헤더로부터 비롯되었다.
    • 2012년에 form이 GET과 POST만 지원하는 것에 대해서 이상하게 여긴 누군가가 PR을 올린 적이 있었다.
    • 하지만 Ian Hickson이라는 아조씨가 이상한 소리를 하면서 리젝해버렸다. (DELETE는 별 의미가 없다는 말이 맞다. 하지만 PUT에 payload를 넣으려는 사람이 없을 것이라는 판단은 어디서 나왔는지 잘 모르겠다.)
    • 해당 논의는 별도 이슈에서 진행하기로 하고 닫혔다.
    • 그리고 놀라울 만큼, 그 누구도 관심을 주지 않았다. (대충 어린 석가모니가 손가락 하나 들고 서있는 짤)

GET 글자 수 제한

  • GET에다가 ObjectId 같은 걸 파라미터로 넘겼다가 255자 넘어가면 어떡하지 하는 걱정에서 찾아봄.
  • GET 글자 수가 255자로 제한되어 있다는 건 도시전설이다.
  • 브라우저의 구현에 따라 제한되어 있고, 제일 짧은 게 익스플로러9 2053자라고 한다. 그 외의 브라우저들은 최소 몇만자 단위. 출처
  • 근데 엔진엑스나 아파치 등의 웹서버에서는 기본설정으로 GET 길이를 8KB로 제한하고 있으며, 이를 넘기면 414 에러를 뱉는다. (실제로 넘기는 걸 본 적이 있음)

Structure Design

DB 데이터 제약조건

  • 문제상황
    • Tag 삭제시, Post.tagList에서도 해당 태그를 하나 삭제해야 한다.
    • MongoDB에는 제약조건 규칙을 걸 수 있는 기능이 없다.
    • 이 제약조건 코드를 Service와 Repository 중 어느 쪽에 구현할 것인가.
  • 근거
    • 애시당초 비즈니스 로직이 아니라 데이터 설계단에서 걸리는 제약조건으로, 이 제약조건을 무시한 채로 데이터 변경이 일어나는 일은 있어서는 안 된다.
  • 결론
    • Repository에 구현한다.

DTO에 대하여

  • logic in getters
  • DTO as class vs interface
    • interface DTO에는 로직을 넣을 수 없어서 class로 전환하려 했었으나, DTO에 로직을 넣지 않기로 결정했으므로 그냥 interface를 사용하기로 했다.
    • 추가적으로 interface는 타입스크립트 차원에서 타입검사가 이루어지는 장점이 있다.
  • class-transformer
    • DTO를 클래스로 전환하려 했을 때 타입검사를 위해 도입하였으나, nested 객체의 경우 타입검사가 되지 않았다.
    • class-validator를 도입해봤으나 결과는 마찬가지. 방법이 있는지는 잘 모르겠다.
    • 이 라이브러리의 본래 용도는 TS -> TS로의 데이터 전달시 DTO 필드 검증 목적이 아니다. 외부 모듈에서 들어온 쌩 json 데이터가 우리가 원하는 포맷대로 들어왔는지 검사하는데에 한정적으로 사용된다. 근데 사실 interface로 받으면 될 것 같은데 무슨 의미가 있는지 잘 모르겠다.
  • Sharing DTO among projects?
    • ex) BE의 ResponseDto를 FE와 공유하여 코드 중복을 줄이고 DRY 원칙을 지켜야 하나?
    • BE - FE로만 생각해서 저런 의문이 든건데, 좀 더 일반화해서 MSA 상황을 생각해보자: FE, OpenAPI - BE - API1, API2, API3
    • FE, OpenAPI는 BE의 ResponseDto를 공유 받는 게 맞는가? API1 ~ 3은 각각 BE와 ResponseDto를 공유해야 하는가?
    • 각 프로젝트는 별도의 시스템이므로, 각자 필요한 내용만을 DTO로 가져가야 한다. FE - BE 상황에서 BE의 ResponseDto에 새로운 필드가 추가됐다 하더라도, FE는 그 필드를 사용할 때에만 필드를 추가해야 한다.
    • FE와 BE의 배포주기는 다를 수 있다. BE의 ResponseDto에서 기존 필드가 변경되는 일은 쉽사리 일어나지 않아야 하고, 새로운 필드가 추가되는 경우에는 FE 입장에서는 해당 필드를 사용하는 소스코드가 아무 것도 없는 상태기 때문에 자신의 DTO를 업데이트할 필요가 없다.
    • https://softwareengineering.stackexchange.com/a/366237

Dependency Injection

  • 사용되어야만 하는 경우
    • 인터페이스가 있고, 여러 구현체가 있는 경우.
    • 상황에 맞게 적절한 구현체를 주입해주고, 주입받은 구현체가 무엇인지에 상관없이 인터페이스를 사용할 수 있음.
    • 전략 패턴, 추상 팩토리 패턴 등을 사용할 때.
  • 레이어 아키텍쳐에서 이게 필요한가?
    • Nope.
  • 그렇다면 Spring에서는 왜 굳이 여러 구현체가 있지도 않은 케이스에도 DI를 남발하는가?
    • 스프링은 IoC를 기본 컨셉으로 잡고 있어서 컨테이너가 빈을 관리하기 때문.
    • 일단 빈 등록된 건 다 의존성주입으로 넣어야 함.
  • 결론
    • 이 블로그에 들어간 DI 다 빼야 함. ㅎㅎ

@Transactional

  • js의 decorator로 구현하려다 잘 안되어 콜백 구조로 만들었었는데, 스프링의 AOP도 비슷함.
    • 타겟이 인터페이스인 경우: java.reflect의 다이나믹프록시 이용 → js 콜백함수 넘기는거랑 걍 똑같음(InvocationHandler 이용).
    • 타겟이 클래스인 경우: CGLib으로 프록시로 감싸진 모양새의 클래스를 직접 새로 생성하여 처리.

Javascript never works as I expect

_.startsWith({}, '['

  • 이거 true 나옴.
  • {}.toString이 '[object Object]'를 반환하기 때문 ㅋㅋ

Node.js

require 개념

  • https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=jdub7138&logNo=221022257248
  • require 함수는 가져올 함수를 즉시실행함수로 한 번 감싼다. export 되지 않은 함수나 변수들은 클로저 영역에 들어가서 외부에서 접근이 막힌다.
    (function (exports, require, module, __filename, __dirname) {
        var myModule = function() {
            ...
        }
        module.exports = myModule;
    } (module.exports, require, module, filename, dirname);
    
    return module.exports;

require에 다른 의존성을 주입해서 두 번 이상 require해야 하는 경우

  • delete require.cache[require.resolve('@src/tag/TagRouter')];를 통해 캐시를 비워줘야 함.
  • Mocha의 코드들(describe, before, it 등)은 다른 코드들이 모두 실행된 이후에 실행되는 것으로 확인됨. 따라서, before 같은 곳에서 캐시를 비워줘봐야 이미 늦음.
    • Mocha 코드들의 실행시점에 대해 추가로 조사해봐야 할 것 같음.

inevitable usage of instanceof

  • 커스텀 에러에 대한 핸들링을 할 때, CustomError extends Error 형태로 구현이 된 경우, 발생할 에러가 내가 예측한 CustomError일지, 예상 밖의 그냥 Error일지 알 수 있는 방법이 없음.
  • 이에 대한 처리를 위해 BlogErrorHandlingUtil에서는 다음처럼 instanceof를 사용하였음.
    const blogError: BlogError = error instanceof BlogError
        ? error as BlogError
        : new BlogError(BlogErrorCode.UNEXPECTED_ERROR, [], error.toString());
  • instanceof가 다형성을 해치기 때문에 안티패턴이라는 시각에 동의하기 때문에 instanceof를 걷어낼 방법이 없을지 고민해봄.
  • 스프링의 ResponseEntityExceptionHandler 코드를 보면 피보탈 행님들이 예외 객체가 어떤 게 들어왔는지 판별하기 위해 instanceof와 분기문을 떡칠해놓은 장관을 볼 수 있음.
    public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {
    	HttpHeaders headers = new HttpHeaders();
    
    	if (ex instanceof HttpRequestMethodNotSupportedException) {
    		HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED;
    		return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, headers, status, request);
    	}
    	else if (ex instanceof HttpMediaTypeNotSupportedException) {
    		HttpStatus status = HttpStatus.UNSUPPORTED_MEDIA_TYPE;
    		return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, headers, status, request);
    	}
    	else if (ex instanceof HttpMediaTypeNotAcceptableException) {
    		HttpStatus status = HttpStatus.NOT_ACCEPTABLE;
    		return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, headers, status, request);
    	}
    	else if (ex instanceof MissingPathVariableException) {
    		HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
    		return handleMissingPathVariable((MissingPathVariableException) ex, headers, status, request);
    	}
    	else if (ex instanceof MissingServletRequestParameterException) {
    		HttpStatus status = HttpStatus.BAD_REQUEST;
    		return handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, headers, status, request);
    	}
    	else if (ex instanceof ServletRequestBindingException) {
    		HttpStatus status = HttpStatus.BAD_REQUEST;
    		return handleServletRequestBindingException((ServletRequestBindingException) ex, headers, status, request);
    	}
    	else if (ex instanceof ConversionNotSupportedException) {
    		HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
    		return handleConversionNotSupported((ConversionNotSupportedException) ex, headers, status, request);
    	}
    	else if (ex instanceof TypeMismatchException) {
    		HttpStatus status = HttpStatus.BAD_REQUEST;
    		return handleTypeMismatch((TypeMismatchException) ex, headers, status, request);
    	}
    	else if (ex instanceof HttpMessageNotReadableException) {
    		HttpStatus status = HttpStatus.BAD_REQUEST;
    		return handleHttpMessageNotReadable((HttpMessageNotReadableException) ex, headers, status, request);
    	}
    	else if (ex instanceof HttpMessageNotWritableException) {
    		HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
    		return handleHttpMessageNotWritable((HttpMessageNotWritableException) ex, headers, status, request);
    	}
    	else if (ex instanceof MethodArgumentNotValidException) {
    		HttpStatus status = HttpStatus.BAD_REQUEST;
    		return handleMethodArgumentNotValid((MethodArgumentNotValidException) ex, headers, status, request);
    	}
    	else if (ex instanceof MissingServletRequestPartException) {
    		HttpStatus status = HttpStatus.BAD_REQUEST;
    		return handleMissingServletRequestPart((MissingServletRequestPartException) ex, headers, status, request);
    	}
    	else if (ex instanceof BindException) {
    		HttpStatus status = HttpStatus.BAD_REQUEST;
    		return handleBindException((BindException) ex, headers, status, request);
    	}
    	else if (ex instanceof NoHandlerFoundException) {
    		HttpStatus status = HttpStatus.NOT_FOUND;
    		return handleNoHandlerFoundException((NoHandlerFoundException) ex, headers, status, request);
    	}
    	else if (ex instanceof AsyncRequestTimeoutException) {
    		HttpStatus status = HttpStatus.SERVICE_UNAVAILABLE;
    		return handleAsyncRequestTimeoutException((AsyncRequestTimeoutException) ex, headers, status, request);
    	}
    	else {
    		// Unknown exception, typically a wrapper with a common MVC exception as cause
    		// (since @ExceptionHandler type declarations also match first-level causes):
    		// We only deal with top-level MVC exceptions here, so let's rethrow the given
    		// exception for further processing through the HandlerExceptionResolver chain.
    		throw ex;
    	}
    }
  • 피보탈 형님들도 찾지 못한 답을 굳이 내가 찾을 필요는 없을 것 같아 그냥 나도 instanceof 쓰기로 함.
  • instanceof를 걷어낼 수 없는 케이스 판별 기준: 파라미터의 타입이 primitive하거나, 위 예시에서처럼 언어 자체에서 정의한 타입이라 인터페이스를 implement하도록 변경할 수 없는 경우.

decorator

  • method decorator example
export const testAnnotation = () => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
  console.dir(target);
  console.dir(propertyKey);
  console.dir(descriptor);
  const callback: Function = descriptor.value;
  descriptor.value = function() { // arrow function을 쓰면 this가 없기 때문에 반드시 그냥 function을 써야 함
    return callback.apply(this) + '!!'; // this를 가리키기 위해서는 `apply`를 사용해서 함수를 실행시켜야 함
  };
};
class HawiClass {
  prefix: string = 'Ay~ '

  @testAnnotation()
  public sayHawi(): string {
    return this.prefix + 'hawi';
  }
}

it('annotation test', () => {
  const test: string = new HawiClass().sayHawi();
  console.log(test);
});

Error Handling

  • https://fettblog.eu/typescript-typing-catch-clauses/
  • 자바스크립트의 throw는 자바의 throw와는 달리 '아무거나' 던질 수 있다. Error 타입이 아니어도 그냥 다 던짐.
  • 에러타입에 따라 다른 에러핸들링을 해야 할 때 자바처럼 여러 개의 catch를 쓰는 건 의미가 없음. 다음처럼 분기문과 instanceof의 조합을 이용하여 처리하라고 함. (자바와 애초에 문법이 다르기 때문에 안티패턴이 아님.)
    try {
      myroutine(); // There's a couple of errors thrown here
    } catch (e) {
      if (e instanceof TypeError) {
        // A TypeError
      } else if (e instanceof RangeError) {
        // Handle the RangeError
      } else if (e instanceof EvalError) {
        // you guessed it: EvalError
      } else if (typeof e === "string") {
        // The error is a string
      } else if (axios.isAxiosError(e)) {
        // axios does an error check for us!
      } else {
        // everything else  
        logMyErrors(e);
      }
    }

Koa

koa-router 404 이슈

  • Koa-router는 ctx.body에 뭔가 내용을 넣어줘야 200이 들어간다.
    • 문제
      • ctx.body = {something awsome};의 statement가 없으면 404가 반환된다.
    • 원인/해결책
      • Koa 공식 홈페이지를 찾아보면 다음과 같은 문장을 확인할 수 있다.

        Get response status. By default, response.status is set to 404 unlike node's res.statusCode which defaults to 200.

      • koa에서 요청을 처리하는 부분을 살펴보면 statusCode에 기본적으로 404를 넣는 것을 확인할 수 있다.
      • koa에서 body를 수정하는 setter 메소드를 보면 그냥 별 짓 안해도 알아서 200을 집어넣는 것을 볼 수 있다. 따라서 ctx.status = 200;만으로도 404 반환 이슈는 해결할 수 있다.
  • error handling
    • 문제
      • 모든 컨트롤러에서 try ~ catch 구문을 중복해서 집어넣는 건 잘못된 것 같다.
    • 원인/해결책
      • Koa Wiki을 보면, 다음과 같은 default error handler가 내장되어 있다고 한다.
        app.use(async (ctx, next) => {
          try {
            await next();
          } catch (err) {
            // will only respond with JSON
            ctx.status = err.statusCode || err.status || 500;
            ctx.body = {
              message: err.message
            };
          }
        })
      • 즉, 하위 layer에서 에러 발생시 그냥 에러객체에다가 statusCode 또는 statusmessage만 잘 넣어놓고 throw하면 알아서 FE로 에러응답을 던질 수 있다는 뜻이다.
  • middleware
    • KOA의 미들웨어는 Spring의 ArgumentResolver 쯤에 대응한다.
    • Spring의 filter처럼 모든 요청에 대해 router 인입에 앞서 동작하며, Spring의 interceptor처럼 요청/응답 객체에 접근할 수 있다.

Typescript

tsc 설정

tsc with path alias

ts-mockito

sinon vs ts-mockito

  • ts-mockito가 나은 점
    • ts-mockito가 when/then/given의 BDD 스타일을 제공하기 때문에 가독성이 좋다. (주관적)
    • 모듈의 함수를 아예 재정의하는 것이 아니라, 자바의 Mockito처럼 mock 객체를 하나 생성하여 사용하는 형태다. 따라서 각 테스트 파일마다 beforesinon.restore()와 같은 작업을 안 해줘도 된다.
  • sinon을 써야만 하는 경우
    • ts-mockito로 생성한 mock 객체가 DI 방식으로 주입되지 않는 경우에는 사용할 수 없다.
    • ts-mockito는 verificator가 부실하다. 얘를 들어, Number.isSafeInteger()로 정수임을 검증하는 로직이 있는 모듈은 anyNumber() 사용해서 모킹하려 할 때, validation에 걸린다. (i.e. crypto.randomInt(min: number, max: number))

verify 쓸 때 주의할 점.

let mockedFoo:Foo = mock(Foo);
let foo:Foo = instance(mockedFoo);  // 빼먹으면 verify 정상동작 안 함!
 
foo.getBar(3);
foo.getBar(5);
 
verify(mockedFoo.getBar(3)).called();
verify(mockedFoo.getBar(5)).called();

위 코드에서 instance화 하는 작업이 빠지면 아무리 foo의 함수들 호출해봤자 verify에서 감지를 못한다.

MongoDB

replica set 세팅할 때

  • storage.dbPath에 쓰기 권한 들어가야 함.

    $ chmod g+w primary
    $ chmod g+w arbiter
    $ chmod g+w secondary0
    
  • 레플리카셋 초기화 (레플리카셋에 접속이 안될 때) 최초 한 번 초기화가 필요하다.

    $ brew services start mongodb-community
    $ mongo --port 27017
    > config = {
        _id : "blog",
        members : [
            {_id : 0, host : "127.0.0.1:27017"},
            {_id : 1, host : "127.0.0.1:27018"},
            {_id : 2, host : "127.0.0.1:27020"},
        ]
    }    
    > rs.initiate(config)
    

    config 수정할 때는 rs.initiate(config, {force: true}) 이렇게 force 옵션 줘서 덮어써주면 된다.

    참고: https://knight76.tistory.com/entry/mongodb-replica-set-%EB%A7%8C%EB%93%A4%EA%B8%B0

(Mongoose) create vs insertMany

  • FYI
    • MongoDB에서는 Collection.insertOneCollection.insertMany를 지원한다.
    • Mongoose에서는 createinsertMany를 지원한다.
  • create vs insertMany
    • 둘 다 collection에 새로운 document 추가하는 애들이다.
    • create는 하나만 추가하고 하나만 리턴한고, insertMany는 여러개 추가하고 여러개 리턴한다. 그 외의 동작 차이는 없다.
    • insertManycreate에 비해 성능상 이점이 있다. Mongoose 저자의 말에 따르면 insertMany는 여러 건의 데이터를 저장할 때, hook과 validation들을 건너뛰고 MongoDB driver를 직접 호출하기 때문이라고 한다. 참고
  • 결론
    • insertMany 쓰면 됨.

No join for MongoDB

  • 왜 분산 DB 환경에서는 join이 구현되기 어려운가
    • 이 블로그 글에 몽고디비의 전반적인 특징이 잘 설명돼있는데, 보면 분산 시스템을 만들기 위해 조인과 트랜잭션을 포기했다고 한다.
    • https://qr.ae/pN0iin
      • NoSQL은 기본적으로 복잡한 SQL 없이 key를 사용해서 get/set하는 것만을 목적으로 하는 데이터베이스다. 여기서 얻은 구조적 단순함으로부터 높은 확장성을 꾀한다.
      • 다른 샤드에 데이터를 나눠서 저장한다. 정확한지는 모르겠는데, 데이터의 50%는 A 샤드의 데이터노드에, 나머지는 B 샤드의 데이터노드에 나눠서 저장하는 식. 이렇게 하여, 조회시간을 O(N/샤드수)로 줄일 수 있다.
      • 데이터가 여러 샤드에 나뉘어있으므로 인덱스 걸기 힘들어진다. 인덱스의 성능도 샤드 수만큼 줄어들게 되고, 구현도 복잡해진다.
      • 데이터가 여러 샤드에 나뉘어있으므로 걍 카테시안 곱 만드는 것부터 쉽지 않다. '브로커'라는 것이 구현되어야 하는데 이는 NoSQL의 'simplicity'와 'easy scalability'에 맞지 않는다.
    • 참고로, 트랜잭션은 나중(4.x)에 클러스터 기능이 추가되면서 지원하기 시작했다. 본 프로젝트에서도 잘 쓰고 있다.
  • population
    • Mongoose에서는 join을 대신해서 population을 제공한다.
    • 하지만 얘는 조인이 아니다.
    • 그리고 심지어 MongoDB에서 실행되는 것도 아니다.
    • 몽구스 문서에 따르면 그냥 find 쿼리 이후에 다시 참조 도큐먼트에다가 N번 실행 때리는 것이라고 한다.

      Paths are populated after the query executes and a response is received. A separate query is then executed for each path specified for population. After a response for each query has also been returned, the results are passed to the callback.

    • 즉, 이건 성능이 쓰레기다. 그냥 데이터 연결성을 정확히 표현하는 수단 정도로만 쓰고, 다른 collection에 있는 연관 데이터를 읽어와야 하는 상황이라면 직접 생각해서 쿼리를 두 번 호출하거나, 읽어온 데이터에서 Node.js 로직으로 필터링하는 게 좋겠다.

ObjectId 다루는 법

MongoDB ObjectId 극혐...

  • find해서 얻어왔을 때
    • 다음과 같은 Types.ObjectId 타입의 객체를 얻을 수 있다.
    • ObjectId { _bsontype: "ObjectID", id: { 0: 96, 1: 146, ... 11: 46 } }
    • id string 추출하는 법: ObjectId.toString() 하면 나온다. 따라서 위 객체에다가 바로 .toString() ObjectId 타입 객체를 넣어주면 에러난다.
  • create 등 CUD 액션 후에 리턴값을 얻어왔을 때 (UD는 실험적으로 확인된 건 아님)
    • { ..., _doc: { _id: { _bsonType: "ObjectID", id: { 0: 96, ... } }, ... } }
    • 방법1: ObjectId를 직접 구해서 .toString()
      • returnObj._doc._id.toString()
    • 방법2: 방법1과 동일. 하지만, _doc은 생략할 수 있다.
      • returnObj._id.toString()
    • 방법3: 다음처럼 호출해도 위와 동일한 결과가 나온다.
      • returnObj.id
  • update해야 할 때
    • 내부적으로 new Types.ObjectId()로 감싸는 것 같다. id string으로 넣어줘야 하고,
  • insert해야 할 때
    • 모르겠음.
  • delete해야 할 때
    • 모르겠음.

n + 1 problem?

  • 그런거 없다. 왜냐하면 MongoDB가 JOIN을 지원하지 않기 때문.
  • lazy loading 정도는 고려해볼 수 있지만, 결국 N + 1번 조회해야 함. 상황에 따라 populate를 하느냐 마냐 정도..

Persistence Context in MongoDB

  • JPA에서 영속성 컨텍스트의 존재 이유: 트랜잭션 내에서 DB 요청 횟수를 최소화하기 위해
  • JS 진영의 대표적인 ODM인 Mongoose에서 이를 지원하지는 않는 듯 하다.

    Mongoose provides a straight-forward, schema-based solution to model your application data. It includes built-in type casting, validation, query building, business logic hooks and more, out of the box.

    • 다음의 목적으로 사용하는 ODM이다.
      • 직관적 사용
      • 스키마로 관리
      • 타입 캐스팅
      • 밸리데이션
      • 쿼리 빌딩..은 당연한거고;;
      • 훅 <- 간단히 공부해봐야겠음
    • 그래서 영속성을 유사하게 사용하기 위해 cachegoose라는 것도 있다.
    • 참고로, Hibernate를 사용하는 Hibernate OGM 같은 것도 있는데 얘는 된다.
  • MongoDB에서 영속성의 목적인 속도와 내구성을 대체하는 개념으로 이런 게 있다고 한다.
    • 내구성 - journaling: 트랜잭션 수행 전에 일종의 백업본을 메모리에 만들어서 실패시 리트라이하는데 쓴다고 한다.
    • 속도 - write semantics: 문서상의 설명은 여기인 듯 한데, 같이 알아야 하는 개념이 있고 꽤나 복잡하다. ReplicaSet의 write concern, write concern에 대해 먼저 알아보도록 하자.. 어쩌면 레포지터리 테스트의 간헐적 실패 원인과 관련이 있을지 모른다.

Testing

Mocha Performance

  • spec: 'test/**/*.ts' <= 요거 넣는 순간 테스트 로딩속도가 개느려짐.
  • **이 문제.
  • 나중에 Mocha에다가 이슈나 PR 남겨보자. <= 조금 봤는데 코드가 가이드 없이 따라가기 쉽지 않았음. 그냥 말자...

CI/CD

Travis CI 요금체계

React

비즈니스 로직은 어디에 들어가야 하는가

  • 의문
    • 계층 구조에서는 service layer또는 application layer라 불리는 계층에 비즈니스 로직을 때려박는다.
    • Flux 구조에서는 어디에 들어가야 하는가.
    • component에 다 때려박으면 간단은 하다. 하지만 이는 BE에서 컨트롤러에 다 때려박는 것과 다를 바가 없어보인다. 컴포넌트가 너무 복잡해지면 코드 읽기가 어려울 듯 하다.
    • reducer에 넣으면 되긴 하는데, 로직이 복잡해지면 reducer가 너무 커지지 않을까..
  • 1안: component에 다 때려박기
  • 2안: reducer에 넣기
    • reducer는 순수함수로만 작성되어야 한다. 따라서 비동기처리가 불가능하다. 참고

ETC

License

  • GPL
    • 제약: GPL 라이센스 명시, 반드시 코드 전문 오픈소스로 공개
    • 허용: 걍 다 됨.
  • BSD (2-clause)
    • 제약: 프로그램 원본에 실린 BSD 라이센스 전문 명시(이 때 원 저작자 정보가 같이 실리게 됨).
    • 허용: 사용, 수정, 재배포
    • 참고: FreeBSD, 3-clause, 4-clause도 있다. clause가 늘어날 수록 제약이 심해진다. 2-clause는 사실상 MIT 라이센스와 동일하다.
  • MIT
    • 제약: MIT 라이센스 명시(이 때 원 저작자 정보가 같이 실리게 됨)
    • 허용: 가져다 쓰든 개조를 하든 소프트웨어의 복사본을 판매하든 걍 다 됨.
  • Apache
    • 제약: Apache 라이센스 명시, 아파치재단 샤라웃, 코드 수정시 외부에 그 사실을 밝혀야 함.
    • 허용: 걍 다 됨. (아파치 라이센스 적용시에는 특허권 보호가 불가능함)