Nginx 란?

Nginx 란 러시아의 이고르 시쇼브란 개발자가 Apache 의 C10K Problem (하나의 웹서버에 10,000개의 클라이언트의 접속을 동시에 다룰 수 있는 기술적인 문제) 을 해결하기 위해 Event-Driven 구조로 만든 오픈 소스 소프트웨어다.


Apache 와의 차이점

Apache

Client가 HTTP 요청을 보낼 때, Apache는 MPM (Multi Processing Module) 을 사용하여 처리한다.


1. Prefork 방식 (멀티 프로세스 방식)

자식 프로세스를 미리 생성해두고 클라이언트 요청에 하나에 대해 한 프로세스가 담당한다.

따라서 한 자식 프로세스가 알 수 없는 원인으로 정지하더라도 다른 자식 프로세스에 영향을 주지 않는다.

자식 프로세스의 수는 최대 1024 개다.

프로세스 당 한 개의 스레드만 존재하기 때문에 스레드간 메모리 공유를 하지 않아서 안정적인 대신에 메모리 사용량이 많다.

시작 시 생성한 프로세스의 수보다 요청이 많아지면 실행 중인 프로세스를 복제하여 실행한다. 이 때, 메모리 영역까지 같이 복제된다.


2. Worker 방식 (멀티 프로세스 & 멀티 스레드 방식)

자식 프로세스마다 멀티 스레드로 실행하며 각 클라이언트의 요청을 스레드가 처리한다.

하나의 프로세스가 여러 요청을 담당하며 Prefork 와 비교해서 시작 프로세스 수를 줄일 수 있다.

스레드 간 메모리를 공유하기 때문에 메모리 사용량이 적다.

한 프로세스 당 최대 64 개의 스레드 처리가 가능하다.


Apache 한계

Apache는 접속마다 Process 또는 Thread를 생성하는 구조이다.

동시 접속 요청이 10,000 개라면 그 만큼 Process or Thread 생성 비용이 들 것이고 대용량 요청을 처리할 수 있는 웹서버로서의 한계를 드러내게 된다.


Nginx

Nginx 는 Event-Driven 방식으로 동작한다.

한 개 또는 고정된 프로세스만 생성 하고, 그 프로세스 내부에서 비 동기 방식으로 효율적으로 작업들을 처리한다.

따라서 동시 접속 요청이 많아도 Process 또는 Thread 생성 비용이 존재하지 않는다.

Event-Driven 방식에선 작업을 하다 I/O, socket read/write 등 CPU가 관여하지 않는 작업이 시작되면 기다리지 않고 바로 다른 작업을 수행한다.


Config

Nginx 블록

server {
    root /home/a;

    location / {
        root /home/b;
        index main.html;
    }
}

server 블록에 root 정의해두면 하위 블록에 전부 적용된다.

location 블록에 root 를 재정의하면 location 블록에 있는게 우선시된다.

위 nginx 서버에 접근하면 /home/b/main.html 파일을 연다.


error_page 설정

server {
    location / {
        error_page 404 main.html;
        ..
    }
}

www.a.com 에 접속했을 때 파일을 찾지 못해 404 에러가 발생하면 www.a.com/main.html 로 URL 을 이동시켜준다.


error_page 커스터마이징

server {
    location / {
        error_page 404 main.html;
    }

    location /main.html {
        root /home/c;
    }
}

error_page 로 이동시킬 때 location 블록으로 다시 root 경로를 설정해 줄 수 있다.

www.a.com/main.html URL 로 이동할 때 띄우는 html 경로를 강제로 지정할 수 있다.

위 예시에서는 /home/c/main.html 페이지를 띄워준다. (URL 에 맞는 html 파일로 이동)


Reverse Proxy 설정

server {
    server_name aa.bb.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        error_page 404 502 main.html;
    }
}

aa.bb.com 도메인에 접근하면 http://127.0.0.1:8080 로 연결해준다.

404 나 502 에러가 발생하면 aa.bb.com/main.html URL 로 이동시킨다.


명령어

nginx 디렉토리로 가서 명령어 타이핑 (권한이 필요하면 sudo)

# nginx 시작
$ ./nginx

# nginx 중지
$ ./nginx -s stop

# nginx 재시작
$ ./nginx -s reload


Reference


'공부 > Server' 카테고리의 다른 글

RabbitMQ  (0) 2020.11.09
Forward Proxy, Reverse Proxy 정의와 차이점  (2) 2020.05.21

Proxy

프록시 서버는 클라이언트가 자신을 통해서 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해 주는 컴퓨터 시스템이나 응용 프로그램을 가리킨다.

서버와 클라이언트 사이에 중계기로서 대리로 통신을 수행하는 것을 가리켜 '프록시', 그 중계 기능을 하는 것을 프록시 서버라고 부른다.

프록시 서버 중 일부는 프록시 서버에 요청된 내용들을 캐시를 이용하여 저장해 둔다.

프록시는 크게 Forward ProxyReverse Proxy 로 나뉜다.


Forward Proxy

클라이언트(사용자)가 인터넷에 직접 접근하는게 아니라 포워드 프록시 서버가 요청을 받고 인터넷에 연결하여 결과를 클라이언트에 전달 (forward) 해준다.

프록시 서버는 Cache 를 사용하여 자주 사용하는 데이터라면 요청을 보내지 않고 캐시에서 가져올 수 있기 때문에 성능 향상이 가능하다.


Reverse Proxy

클라이언트가 인터넷에 데이터를 요청하면 리버스 프록시가 이 요청을 받아 내부 서버에서 데이터를 받은 후 클라이언트에 전달한다.

클라이언트는 내부 서버에 대한 정보를 알 필요 없이 리버스 프록시에만 요청하면 된다.

내부 서버 (WAS) 에 직접적으로 접근한다면 DB 에 접근이 가능하기 때문에 중간에 리버스 프록시를 두고 클라이언트와 내부 서버 사이의 통신을 담당한다.

또한 내부 서버에 대한 설정으로 로드 밸런싱(Load Balancing) 이나 서버 확장 등에 유리하다.


차이점

1. End Point

Forward Proxy 는 클라이언트가 요청하는 End Point 가 실제 서버 도메인이고 프록시는 둘 사이의 통신을 담당해준다.

Reverse Proxy 는 클라이언트가 요청하는 End Point 가 프록시 서버의 도메인이고 실제 서버의 정보는 알 수 없다.


2. 감춰지는 대상

Forward Proxy 는 클라이언트가 감춰진다.

요청 받는 서버는 포워드 프록시 서버를 통해서 요청을 받기 때문에 클라이언트의 정보를 알 수 없다.

Reverse Proxy 는 반대로 서버가 감춰진다.

클라이언트는 리버스 프록시 서버에게 요청하기 때문에 실제 서버의 정보를 알 수가 없다.


Reference

'공부 > Server' 카테고리의 다른 글

RabbitMQ  (0) 2020.11.09
Nginx  (0) 2020.05.21

템플릿 리터럴

템플릿 리터럴 (template literals)은 변수를 이용해서 동적으로 문자열을 생성할 수 있다.

기존에는 문자열에 변수를 추가하려면 더하기 기호를 반복해서 사용해야했다.

const year = 22;
const name = 'alice';
const text = 'Hi! My name is ' + name + ' and I am ' + year * 2 + ' years old';
console.log(text);      // Hi! My name is alice and I am 22 years old


이런식으로 코드를 작성할 경우 시간도 너무 오래걸리고 따옴표로 인한 가독성도 굉장히 떨어지게 된다.

ES6 에서는 템플릿 리터럴로 바꾸어 표현할 수 있다.

템플릿 리터럴은 백틱(` `) 을 사용하며 변수나 식은 ${variable} ${expression} 으로 입력한다.

const text = `Hi! My name is ${name} and I am ${year * 2} years old`


여러 줄을 입력할 때는 엔터를 입력하면 된다.

// 기존: \n 을 사용
const text = 'Hi! My name is ' + name + '\n and I am ' + year * 2 + ' years old';

// ES6: 엔터 추가
const text = `Hi! My name is ${name} 
and I am ${year * 2} years old`


async await

Promise 를 사용해도 어쩔 수 없는 한계가 존재한다.

콜백 함수에 비해서 조금 나아졌다고는 하나 중첩해서 쓰거나 return 으로 함수 호출하면서 로직이 복잡해지면 가독성이 떨어진다.

async await 을 사용하면 then 체이닝보다 가독성이 좋아진다.

하지만 async await 도 프로미스를 활용하는 개념이기 때문에 프로미스를 완전히 대체할 수는 없다.


1. 사용하기

1.1. async

async 키워드를 이용해서 정의된 함수는 항상 프로미스를 반환한다.

따라서 함수 호출 뒤에 then 으로 체이닝이 가능하다.

리턴값이 프로미스라면 그대로 반환된다.

async function getData1() {
  return 111;
}

const getData2 = async () => {
  throw new Error(222);
}

async function getData3() {
  return Promise.resolve(333);
}

getData1().then(data => console.log(data));     // 111
getData2().catch(err => console.log('getData2: ' + err));   // getData2: Error: 222
getData3().then(data => console.log(data));     // 333


1.2. await

await 키워드는 async 함수 내부에서만 사용된다.

await 키워드를 붙여서 프로미스를 사용하면 해당 프로미스가 처리됨 상태가 될 때까지 기다린다.

function printAfter(seconds) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('print ' + seconds);
      resolve(seconds);
    }, seconds * 1000)
  },);
}

async function log() {
  const data1 = await printAfter(3);    // 3 초 뒤 출력
  const data2 = await printAfter(5);    // 8 초 뒤 출력
  console.log(data1, data2);
}

log();


2. 활용하기

2.1. 비동기 함수 병렬 실행

await 키워드가 붙으면 해당 함수가 끝날때까지 기다리게 된다.

하지만 여러 개의 await 함수를 호출할 때 함수들 간의 의존성이 없다면 굳이 순차적으로 실행할 필요가 없다.

프로미스는 생성과 동시에 실행된다는 점을 활용하여 프로미스 생성을 먼저 하고 await 키워드를 나중에 호출하는 방법을 사용하면 된다.

async function log() {
  const p3 = printAfter(3);     // 3초 뒤 출력
  const p5 = printAfter(5);     // 5초 뒤 출력

  const data1 = await p3;
  const data2 = await p5;
  
  console.log(data1, data2);
}


아니면 Promise.all 을 사용한다면 더 간단하게 표현 할 수 있다.

async function log() {
  const [data1, data2] = await Promise.all([printAfter(3), printAfter(5)]);

  console.log(data1, data2);
}


2.2. 예외 처리

try catch 문으로 감싸면 async / sync 함수 구분하지 않고 발생하는 예외들을 모두 잡는다.

async function log() {
  try {
    const data = printAfter(3);
    console.log(data);
  } catch (error) {
    console.log(error);
  }
}


프로미스 (Promise)

프로미스는 비동기 상태를 값으로 다룰 수 있는 객체다.

프로미스가 사용되기 전에는 콜백 패턴이 많이 사용되었다.

그러다가 여러 가지 프로미스 라이브러리가 등장하면서 널리 사용되었고 ES6 에서는 프로미스가 자바스크립트 언어에 포함되었다.


1. 콜백(Callback) 패턴의 문제

자바스크립트에서는 비동기 프로그래밍의 한 가지 방법으로 콜백(callback) 패턴을 많이 사용했다.

하지만 콜백 패턴은 조금만 중첩되어도 코드가 복잡해지는 콜백 지옥(Callback Hell)을 만들어낸다.

function display(data) {
  console.log("result: " + data);
}

function conactData(data) {
  parsedData = data.concat(" parsing");
  console.log(parsedData);
  display(parsedData);
}

function getData(callback) {
  data = "This is new Data";
  console.log(data);
  callback(data, display);
}

getData(parsing);
// This is new Data
// This is new Data parsing
// result: This is new Data parsing


위 코드는 프로미스를 사용하면 아래와 같이 좀더 보기 쉽게 바꿀 수 있다.

const getData = new Promise((resolve, reject) => {
  data = "This is new Data";
  console.log(data);
  resolve(data);
})

getData
  .then(data => {
    // concatData
    parsedData = data.concat(" parsing");
    console.log(parsedData);
    return parsedData;
  })
  .then(data => {
    // display
    console.log("result: " + data);
    return data;
  })
// This is new Data
// This is new Data parsing
// result: This is new Data parsing


2. 프로미스의 세가지 상태

프로미스는 다음 세 가지 상태 중 하나로 존재한다.

상태설명
대기 중 (pending)결과를 기다리는중
이행됨 (fulfilled)수행이 정상적으로 끝났고 결과값을 갖고 있음
거부됨 (rejected)수행이 비정상적으로 끝났음

그리고 이행됨, 거부됨 상태를 처리됨(settled) 상태라고 한다.

프로미스는 처리됨 상태가 되면 더 이상 다른 상태로 변경되지 않는다.

대기 중 상태일때만 이행됨 또는 거부됨 상태로 변할 수 있다.


3. 프로미스 생성

3.1. new 키워드로 생성

// 1. new 키워드로 생성
const promise = new Promise((resolve, reject) => {
  // ..
  // resolve() or reject('error')
})

new 키워드로 생성한 프로미스는 대기 중 상태가 된다.

생성자에 입력되는 resolve 와 reject 는 콜백 함수이다.

resolve 를 호출하면 promise 는 이행됨 상태가 된다.

반대로 reject 를 호출하면 거부됨 상태가 된다.

만약 생성자에 입력된 함수 안에서 예외가 발생하면 거부됨 상태가 된다.

new 키워드로 생성된 프로미스의 내부는 즉시 실행된다.

만약 API 요청을 보내는 비동기 코드가 있다면 프로미스가 생성되는 순간에 요청을 보낸다.


3.2. Promise.reject 생성

// Promise.reject 로 생성
const promise = Promise.reject('error');

거부됨 상태인 프로미스를 생성한다.


3.3. Promise.resolve 생성

// Promise.resolve 로 생성
const promise = Promise.resolve(params);

const p1 = Promise.resolve(123);
const p2 = Promise.resolve(p1);   // return p1
console.log(p2 === p1);           // true

입력값이 프로미스라면 그 객체가 그대로 반환되고, 프로미스가 아니라면 이행됨 상태인 프로미스가 반환된다.


4. 프로미스 사용

4.1. then

then 은 처리됨 상태가 된 프로미스를 처리할 때 사용되는 메소드다.

프로미스가 처리됨 상태가 되면 then 메소드의 파라미터로 전달된 함수가 호출된다.

then 메소드는 항상 프로미스를 반환하기 때문에 하나의 프로미스에서 연속적으로 then 메소드를 호출할 수 있다.

getData().then(onResolve, onReject);


만약에 함수 수행 중에 예외가 발생해서 거부됨 상태가 되면 onReject 함수가 존재하는 then 까지 이동한다.

onReject 함수가 실행되고 나면 프로미스는 다시 이행됨 상태가 되어 4 다음에는 5 를 출력한다.

// print: 4 5
Promise.reject('error')
  .then(() => console.log('1'))
  .then(() => console.log('2'))
  .then(() => console.log('3'), () => console.log('4'))
  .then(() => console.log('5'));


4.2. catch

catch 는 프로미스 수행 중에 발생한 예외를 처리한다.

then 함수의 onReject 와 같은 역할을 한다.

예외 처리는 then 보다 catch 함수를 사용하는 게 가독성이 더 좋다.

Promise.reject('error').then(null, error => {
  console.log(error);
});

Promise.reject('error').catch(error => {
  console.log(error);
})


then 은 onResolve 에서 에러가 발생했을 때 같은 함수 내에 있는 onReject 함수에서 처리할 수 없다.

하지만 catch 를 사용하면 처리가능하고 함수도 좀 더 직관적이다.

catch 도 마찬가지로 프로미스를 반환하기 때문에 계속해서 체이닝을 이어나갈 수 있다.

// onResolve 에서 에러가 발생해도 onReject 가 아닌 뒤에서 처리해야됨
Promise.resolve().then(onResolve, onReject);

// print: this is Error: then error
// print: success then 2
Promise.resolve()
  .then(() => {
    throw new Error('then error');
  })
  .catch(error => {
    console.log('this is ' + error);
    return 2;
  })
  .then(data => {
    console.log('success then: ' + data);
  });


4.3. finally

finally 는 프로미스가 처리됨(settled) 상태일 때 호출되는 함수이다.

프로미스 체인의 가장 마지막에 사용된다.

finally 는 이전에 사용된 프로미스를 그대로 반환하기 때문에 처리됨 상태인 프로미스의 데이터를 건드리지 않고 추가 작업을 할 수 있다.

// print: 123
// print: finish promise
Promise.resolve(123)
  .then(data => {
    console.log(data);
    return data;
  })
  .catch(error => {
    console.log(error);
    return error;
  })
  .finally(() => {
    console.log('finish promise');
  });


5. 프로미스 활용

다음 예제를 통해 Promise 를 활용하는 방법들을 알아본다.

const getData1 = Promise.resolve(1);
const getData2 = new Promise((resolve, reject) => 
  setTimeout(function() {
    console.log('3');
    resolve(2);
  }, 5000));


5.1. Promise.all: 병렬 처리

then 함수를 체인으로 연결하면 각각의 비동기 로직이 병렬로 처리되지 않고 순차적으로 실행된다.

서로 간의 의존성이 없다면 병렬로 처리하는 게 더 빠르다.

Promise.all 함수는 여러 개의 프로미스를 동시에 실행하며 프로미스가 모두 처리되면 처리됨 상태가 되고 하나라도 거부된다면 거부됨 상태가 된다.

// print: 3   (5초 뒤 출력)
// print: 1 2
Promise
  .all([getData1, getData2])
  .then(([data1, data2]) => {
    console.log(data1, data2);
  });


5.2. Promise.race: 가장 빨리 처리된 프로미스

Promise.race 는 여러 개의 프로미스 중 가장 빨리 처리된 프로미스를 반환한다.

여러 개의 프로미스 중 하나라도 처리되면 처리됨 상태가 된다.

프로미스 간의 격차가 커서 먼저 Promise.race 가 실행 되더라도 다른 프로미스의 작업은 중지되지 않는다.

// print: 1
// print: 3   (5초 뒤 출력)
Promise
  .race([getData1, getData2])
  .then(data => {
    console.log(data);
  });


Problem


2차원 배열 matrix 안에 target 값이 있으면 true

없으면 false 를 return 하는 문제입니다.

matrix 안의 숫자는 왼쪽에서 오른쪽으로 증가하고 위쪽에서 아래쪽으로 증가합니다.



Solution

왼쪽에서 오른쪽으로, 위에서 아래로 증가한다는 사실을 이용하면 O(n + m) 에 풀 수 있습니다.

오른쪽 위 혹은 왼쪽 아래 에서 시작하며 현재 값보다 target 이 크면 커지는 방향으로 이동하고 작으면 작아지는 방향으로 이동하면 됩니다.

예를 들어 왼쪽 아래에서 시작한다면 row = matrix.length - 1 이고 col = 0 에서 시작합니다.

만약 찾는 값이 현재 위치에 있는 값보다 작으면 작은쪽 (위쪽) 으로 이동합니다. (row--)

현재 위치에 있는 값보다 크면 커지는 쪽 (오른쪽) 으로 이동합니다. (col++)



Java Code

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix.length == 0) return false;

        int row = matrix.length - 1;
        int col = 0;

        while (row >= 0 && col < matrix[0].length) {
            int value = matrix[row][col];

            if (target == value) {
                return true;
            } else if (target < value) {
                row--;
            } else {
                col++;
            }
        }

        return false;
    }
}

Problem


징검다리를 건널 수 있는 프렌즈 인원수를 구하는 문제입니다.

징검다리는 stones 배열로 주어지며, 한번 발을 디딜때마다 1 씩 줄어듭니다.

프렌즈들은 0 인 돌을 만나면 최대 k 만큼 가장 가까운 돌로 점프할 수 있습니다.



Solution

이분탐색을 응용한 파라메트릭 서치(Parametric Search) 를 활용하는 문제입니다.

건널 수 있는 프렌즈들의 최대, 최소값은 stones 배열의 최대, 최소값과 동일합니다.

stones 배열의 최대값이 5, 최소값이 1 이라고 할 때 1 과 5 로 이진탐색을 돌립니다.

중간값 mid 가 3 이기 때문에 프렌즈 인원을 3 으로 정해두고 전부 다 건널 수 있는지 탐색합니다.

만약 다 건널 수 있다면 3 보다 작은 1, 2 도 전부 건널 수 있기 때문에 3 ~ 5 사이의 값을 탐색합니다.

이런식으로 건널 수 있는 프렌즈 인원 중 가장 큰 값이 남을때까지 계속 하면 됩니다.

건널 수 있는지 판단하는 canCross 함수는 돌에서 프렌즈 값을 뺀 숫자를 갖고 음수가 최대 k 만큼 연속하지 않은지 확인하면 됩니다.



Java Code

import java.util.*;

class Solution {
    public int solution(int[] stones, int k) {
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        
        for (int stone : stones) {
            max = Math.max(max, stone);
            min = Math.min(min, stone);
        }
        
        return binarySearch(stones, k, min, max);
    }
    
    private int binarySearch(int[] stones, int k, int lo, int hi) {
        if (hi == lo) return lo;
        
        while (lo < hi) {
            int mid = lo + (hi - lo) / 2;
            
            if (canCross(stones, k, mid)) {
                lo = mid + 1;
            } else {
                hi = mid;
            }
        }
        
        return lo - 1;
    }
    
    private boolean canCross(int[] stones, int k, int friends) {
        int passCount = 0;
        
        for (int stone : stones) {
            if (stone - friends < 0) {
                passCount++;
            } else {
                passCount = 0;
            }
            
            if (passCount == k) return false;
        }
        
        return true;
    }
}


Problem


총 방의 갯수 k 와 고객들이 선택한 방이 주어질 때 각 방별로 배정된 사람들의 목록을 return 하는 문제입니다.

방은 고객들이 선택한 순서대로 정해집니다.

A 고객이 선택한 방을 이미 B 고객이 선택했다면 A 고객은 다음 방을 배정받습니다.

A 고객이 선택한 방번호보다 크고 아무도 선택하지 않은 가장 작은 방번호가 다음 방입니다.



Solution

정확성 뿐만 아니라 효율성까지 고려해야 하는 문제입니다.

단순하게 풀이한다면 Set 자료구조에 선택된 방들을 담아두고 중복된 방을 선택한 경우 +1 하며 전체 스캔하는 것으로 정확성은 통과할 수 있다.

하지만 k 가 최대 10^12 이기 때문에 O(n)으로 매번 검색을 해버리면 효율성을 하나도 통과할 수 없습니다.

다음 방을 선택하는 과정에서 O(n) 으로 스캔하는 건 어쩔수가 없습니다.

그렇다면 한번 선택한 방은 다음번에 선택할 때 스캔을 최소 로 한다면 정확성을 통과할 수 있습니다.

접근법은 우선 HashMap 을 선언하여 <방 번호 : 다음 방 번호> 이렇게 저장해두는 겁니다.

고객이 방을 선택했을 때 map.containsKey() 로 다음 방을 바로 찾을 수가 있죠.

다음으로 할 일은 다음 방을 갱신해주는 겁니다.

예를 들어 방이 6 번까지 있고 [1, 2, 3, 4] 는 이미 선택되었다고 가정합니다.

다음 고객이 1 을 선택하였을 때 5 번 방이 비어있는 걸 알기 위해 1 -> 2 -> 3 -> 4 -> 5 순서대로 가야 하지만

다음 방을 미리 저장해둔다면 1 -> 5 로 바로 스캔할 수 있습니다.

그리고 1 번을 갱신하는 김에 2, 3, 4 방도 똑같이 다음 방이 5 니까 갱신해줍니다.

이것을 재귀로 구현한 게 long findEmptyRoom(long room) 함수입니다.



Java Code

import java.util.*;

class Solution {
    Map<Long, Long> map = new HashMap<>();

    public long[] solution(long k, long[] room_number) {
        int n = room_number.length;
        long[] answer = new long[n];

        for (int i = 0; i < n; i++) {
            answer[i] = findEmptyRoom(room_number[i]);
        }

        return answer;
    }
    
    private long findEmptyRoom(long room) {
        if (!map.containsKey(room)) {
            map.put(room, room + 1);
            return room;
        }
        
        long nextRoom = map.get(room);
        long emptyRoom = findEmptyRoom(nextRoom);
        map.put(room, emptyRoom);
        return emptyRoom;
    }
}


+ Recent posts