2 tire 아키텍처
상품 정보같은 리소스가 존재하는 곳과, 리소스를 사용하는 앱을 분리시킨 것을 2티어 아키텍처 또는, 클라이언트-서버 아키텍처라고 부른다.
리소스를 사용하는 앱 -> "클라이언트"
리소스가 제공(serve)하는 곳 -> "서버"
상품 정보는 서버에서 다루고, 클라이언트는 단지 상품 정보를 조회할 뿐이다.
3 tire 아키텍처
기존 2 티어 아키텍처에 데이터베이스가 추가된 형태를 3 티어 아키텍처라고 부른다.
일반적으로 서버는 리소스를 전달해주는 역할만 담당한다.
리소스를 저장하는 공간을 별도로 마련해 두는데, 이 공간을 "데이터베이스"라고 부른다.
데이터베이스는 창고와 같은 역할을 한다.
장점:
만약 서버에서 다수의 클라이언트와 연결을 계속 유지해야 한다면, 이에 따른 많은 리소스가 발생하게 됩니다. 따라서 연결을 유지하기 위한 리소스를 줄이면 더 많은 연결을 할 수 있으므로 비연결적인 특징을 갖습니다.
단점:
서버는 클라이언트를 기억하고 있지 않으므로 동일한 클라이언트의 모든 요청에 대해, 매번 새로운 연결을 시도/해제의 과정을 거쳐야하므로 연결/해제에 대한 오버헤드가 발생한다는 단점이 있습니다.
KeepAlive: 이에 대한 해결책으로 오버헤드를 줄이기 위해 HTTP의 KeepAlive 속성을 사용할 수 있습니다. KeepAlive는 지정된 시간동안 서버와 클라이언트 사이에서 패킷 교환이 없을 경우, 상대방의 안부를 묻기 위해 패킷을 주기적으로 보내는 것을 말합니다. 이 때 패킷에 반응이 없으면 접속을 끊게 됩니다. 이것도 메모리를 많이 사용하게 되므로 주의를 요합니다.
무상태란?
Connectionless로 인해 서버는 클라이언트를 식별할 수가 없는데, 이를 Stateless라고 합니다.
2) 세션
쿠키는 사용자 정보가 브라우저에 저장되기 때문에 공격자로부터 위변조의 가능성이 높아 보안에 취약합니다.
이와 달리 세션은 브라우저가 아닌 서버단에서 사용자 정보를 저장하는 구조입니다.
따라서 쿠키보다는 안전하다고 할 수 있습니다.
그런데 세션 정보도 중간에 탈취 당할 수 있기 때문에 보안에 완벽하다고 할 수 없습니다.
또한 세션을 사용하면 서버에 사용자 정보를 저장하므로, 서버의 메모리를 차지하게 되고, 만약 동시 접속자 수가 많은 서비스의 경우에는 서버 과부하의 원인이 됩니다.
3) 토큰을 사용하는 OAuth, JWT
토큰 기반의 인증 방식의 핵심은 보호할 데이터를 토큰으로 치환하여 원본 데이터 대신 토큰을 사용하는 기술입니다. 그래서 중간에 공격자로부터 토큰이 탈취되더라도 데이터에 대한 정보를 알 수 없으므로, 보안성이 높은 기술이라 할 수 있습니다. 꼭 토큰 기반 인증이 좋은 것은 아니다.
REST 성숙도 모델 - 0단계
REST 성숙도 모델에 따르면, 0단계에서는 단순히 HTTP 프로토콜을 사용하기만 해도 됩니다. 0단계는 좋은 REST API를 작성하기 위한 기본 단계입니다. 단순히 HTTP 프로토콜을 사용하는 것이 REST API의 출발점입니다.
REST 성숙도 모델 - 1단계
개별 리소스와의 통신을 준수
모든 자원은 개별 리소스에 맞는 엔드포인트(Endpoint)를 사용해야 한다는 것과 요청하고 받은 자원에 대한 정보를 응답으로 전달해야 한다는 것이 1단계에서 의미하는 바입니다. 1단계에서는 요청하는 리소스가 무엇인지에 따라 각기 다른 엔드포인트로 구분하여 사용해야 합니다. 엔드포인트 작성 시에는 동사, HTTP 메서드, 혹은 어떤 행위에 대한 단어 사용은 지양하고, 리소스에 집중해 명사 형태의 단어로 작성하는 것이 바람직한 방법입니다.
요청에 따른 응답으로 리소스를 전달할 때에도 사용한 리소스에 대한 정보와 함께 리소스 사용에 대한 성공/실패 여부를 반환해야 합니다.
REST 성숙도 모델 - 2단계
CRUD에 맞게 적절한 HTTP 메서드를 사용하는 것에 중점을 둡니다.
조회(Read)를 하기 위해서는 GET 메서드를 사용하여 요청을 보내고, 이 때 GET 메서드는 body를 가지지 않기 때문에 query parameter를 사용하여 필요한 리소스를 전달합니다.
예약을 생성(CREATE)하기 위해서는 POST 메서드를 사용하여 요청을 보내는 것이 바람직합니다. 그리고 2단계에서는 POST 요청에 대한 응답이 어떻게 반환되는지도 중요합니다.
이 경우 응답은 새롭게 생성된 리소스를 보내주기 때문에, 응답 코드도 201 Created 로 명확하게 작성해야 하며, 관련 리소스를 클라이언트가 Location 헤더에 작성된 URI를 통해 확인할 수 있도록 해야, 완벽하게 REST 성숙도 모델의 2단계를 충족한 것이라고 볼 수 있습니다.
GET 메서드 같은 경우는 서버의 데이터를 변화시키지 않는 요청에 사용해야 합니다.
POST는 요청마다 새로운 리소스를 생성하고 PUT 은 요청마다 같은 리소스를 반환합니다. 이렇게 매 요청마다 같은 리소스를 반환하는 특징을 멱등(idempotent)하다고 합니다. 그렇기 때문에 멱등성을 가지는 메서드 PUT과 그렇지 않은 POST는 구분하여 사용해야 합니다.
PUT은 교체, PATCH는 수정의 용도로 사용합니다.
REST 성숙도 모델 - 3단계
마지막 단계는 HATEOAS(Hypertext As The Engine Of Application State)라는 약어로 표현되는 하이퍼미디어 컨트롤을 적용합니다. 3단계의 요청은 2단계와 동일하지만, 응답에는 리소스의 URI를 포함한 링크 요소를 삽입하여 작성한다는 것이 다릅니다.
이때 응답에 들어가게 되는 링크 요소는 응답을 받은 다음에 할 수 있는 다양한 액션들을 위해 많은 하이퍼미디어 컨트롤을 포함하고 있습니다.
이렇게 응답 내에 새로운 링크를 넣어 새로운 기능에 접근할 수 있도록 하는 것이 3단계의 중요 포인트입니다.
규칙들을 통해 리소스 중심의 올바른 앤드포인트 작성, 적절한 응답 코드와 리소스에 대한 정보 기재, CRUD에 적합한 HTTP 메서드 사용 등을 고려해야 좋은 REST API를 디자인할 수 있다는 것을 배웠습니다.
HTTP 메시지
HTTP는 HTML과 같은 문서를 전송하기 위한 Application Layer 프로토콜입니다. HTTP는 웹 브라우저와 웹 서버의 소통을 위해 디자인되었습니다. 전통적인 클라이언트-서버 모델에서 클라이언트가 HTTP messages 양식에 맞춰 요청을 보내면, 서버도 HTTP messages 양식에 맞춰 응답합니다. HTTP는 특정 상태를 유지하지 않는 특징이 있습니다. Stateless(무상태성)
HTTP messages는 클라이언트와 서버 사이에서 데이터가 교환되는 방식입니다. HTTP messages에는 다음과 같은 두가지 유형이 있습니다.
요청(Requests), 응답(Responses)
HTTP messages는 몇 줄의 텍스트 정보로 구성됩니다. 그러나 개발자느 이런 메시지를 직접 작성할 필요가 거의 없습니다. 구성 파일, API, 기타 인터페이스에서 HTTP messages를 자동으로 완성합니다.
요청(Requests)과 응답(Responses)은 다음과 같은 구조를 가집니다.
1. start line: start line에는 요청이나 응답의 상태를 나타냅니다. 항상 첫 번째 줄에 위치합니다. 응답에서는 status line이라고 부릅니다.
2. HTTP headers: 요청을 지정하거나, 메시지에 포함된 본문을 설명하는 헤더의 집합입니다.
3. empty line: 헤더와 본문을 구분하는 빈 줄이 있습니다.
4. body: 요청과 관련된 데이터나 응답과 관련된 데이터 또는 문서를 포함합니다. 요청과 응답의 유형에 따라 선택적으로 사용합니다.
이 중 start line과 HTTP headers를 묶어 요청이나 응답의 헤드(head)라고 하고, payload는 body라고 이야기합니다.
Last Modified를 이용해 캐시의 수정 시간을 알 수 있습니다. Last Modified는 데이터가 마지막으로 수정된 시간 정보를 헤더에 포함합니다. 이로 인해 응답 결과를 캐시에 저장할 때 데이터 최종 수정일도 저장됩니다.
캐시 유효시간이 초과되더라도 If-Modified-Since 헤더를 이용해 조건부 요청을 할 수 있음.
ETag: 서버에서 완전히 캐시를 컨트롤하고 싶은 경우
서버에서 헤더에 ETag를 작성해 응답합니다.
클라이언트의 캐시에서 해당 ETag 값을 저장합니다.
만약 캐시 시간이 초과돼서 다시 요청을 해야 하는 경우라면 이때 ETag 값을 검증하는 If-None-Match를 요청 헤더에 작성해서 보낸다. (조건부 요청임)
서버에서 데이터가 변경되지 않았을 경우 ETag는 동일하기에 그래서 If-None-Match는 거짓이 됩니다.
이 경우 서버에서는 304 Not Modified를 응답하며 이때 역시 HTTP Body는 없습니다.
브라우저 캐시에서는 응답 결과를 재사용하고 헤더 데이터를 갱신합니다.
1. 데이터가 수정되었는지 ETag를 이용해 검증
2. 수정되지 않았다면 바디를 제외한 HTTP 헤더만 전송
3. 브라우저 캐시에서 응답 결과를 재사용, 헤더 메타데이터 또한 갱신
4. 브라우저는 캐시에서 조회한 데이터를 렌더링
Cache-Control: max-age
캐시 유효 시간, 초 단위
Cache-Control: no-cache
데이터는 캐시해도 되지만, 항상 원(Origin) 서버에 검증하고 사용
Cache-Control: no-store
데이터에 민감한 정보가 있으므로 저장하면 안됨
(메모리에서 사용하고 최대한 빨리 삭제)
Expires: 캐시 만료일을 정확한 날짜로 지정
HTTP 1.0부터 사용
지금은 더 유연한 Cache-Control:max-age 권장
Cache-Control: max-age와 함께 사용하면 Expires는 무시됨
검증 헤더 (Validator)
ETag: "v1.0", ETag: "845eed07c5887cf"
Last-Modified: Wed, 26 Dec 2020 12:01:29 GMT
조건부 요청 헤더
If-Match, If-None-Match: ETag 값 사용
If-Modified-Since, If-Unmodified-Since: Last-Modified 값 사용
Last-Modified:
브라우저가 서버로 요청한 파일의 최종 수정 시간을 알려주는 헤더, Last-Modified 헤더를 쓸 경우 브라우저가 다음에 다시 접속할 때 서버에게 파일이 또 수정되었는지 여부를 물어보게 되는데 이때 서버가 수정여부를 내려주는 헤더가 If-Modified-Since 헤더임, 이 헤더를 사용해 캐싱을 해 성능을 향상시킬 수 있는데 이미지/CSS/JS와 같은 정적파일들은 아파치에서 자동적으로 Last-Modified, If-Modifieid-Since헤더를 붙여준다. php파일과 같은 동적파일들에는 로직상에서 헤더를 붙여주면 된다.
ETag 헤더와 Last-Modified 헤더의 사용시점
클라이언트의 입장에서 서버가 ETag 헤더를 보내고 있으면 If-None-Match 헤더를 이용하면 된다. ETag와 Last-Modified는 둘다 캐싱에 사용할 수 있지만 조금 다르다. ETag는 파일 수정 여부만 판별하고 Last-Modified는 시간값을 기준으로 하기 때문에 짧은 시간 내에 변경되는 리소스등에는 ETag보다는 Last-Modified헤더를 쓰는게 더 적합한 것 같다.
Cache-Control: private
응답이 해당 사용자만을 위한 것, private 캐시에 저장해야 함(기본 값)
Cache-Control: s-maxage
프록시 캐시에만 적용되는 max-age
Age: 60 (HTTP 헤더)
오리진 서버에서 응답 후 프록시 캐시 내에 머문 시간(초)
Cache-Control: no-store
데이터에 민감한 정보가 있으므로 저장하면 안됨
(메모리에서 사용하고 최대한 빨리 삭제)
Cache-Control: must-revalidate
캐시 만료 후 최초 조회 시 원 서버에 검증해야함
원 서버 접근 실패 시 반드시 오류가 발생해야함 - 504(Gateway Timeout)
must-revalidate는 캐시 유효 시간이라면 캐시를 사용함
Pragma: no-cache
HTTP 1.0 하위 호환
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
캐시 무효화를 확실하게 해야 하는 경우 Pragma와 같은 하위 호환까지 포함하여 위와 같이 적용해야 합니다. 확실한 캐시 무효화 응답을 하고 싶다면 위에 있는 캐시 지시어를 모두 넣어야합니다.
no-cache와 must-revalidate 모두 원 서버에 검증해야 하지만 그에 대한 응답에 대해 다른 점이 있습니다.
no-cache
캐시 서버 요청을 하면 프록시 캐시 서버에 도착하면 no-cache인 경우 원 서버에 요청을 하게 됩니다. 그리고 원 서버에서 검증 후 304 응답을 하게 됩니다.
만약 프록시 캐시 서버와 원 서버 간 네트워크 연결이 단절되어 접근이 불가능하다면, no-cache에서는 응답으로 오류가 아닌 오래된 데이터라도 보여주자라는 개념으로 200OK으로 응답을 합니다.
must-revalidate
하지만 must-revalidate라면 원 서버에 접근이 불가할 때 504 Gateway Timeout 오류를 보냅니다.
통장 잔고 등 중요한 정보가 원 서버를 못 받았다고 해서 예전 데이터로 뜬다면 큰 문제가 생기기 때문에 이런 경우 must-revalidate를 써야 합니다.
보안/인증
1. CORS에 대해 설명해주세요.
교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다. 웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때 교차 출처 HTTP 요청을 실행합니다.
교차 출처 요청의 예시: https://domain-a.com의 프론트 앤드 JavaScript 코드가 XMLHttpRequest를 사용하여 https://domain-b.com/data.json을 요청하는 경우.
보안 상의 이유로, 브라우저는 스크립트에서 시작한 교차 출처 HTTP 요청을 제한합니다. 예를 들어, XMLHttpRequest와 Fetch API는 동일 출처 정책을 따릅니다. 즉, 이 API를 사용하는 웹 애플리케이션은 자신의 출처와 동일한 리소스만 불러올 수 있으며, 다른 출처의 리소스를 불러오려면 그 출처에서 올바른 CORS 헤더를 포함한 응답을 반환해야 합니다.
서로 다른 Origin 간에 리소스를 공유하는 것이다.
SOP란
CORS 에러는 SOP라는 브라우저의 원칙으로 인해 발생한다.
SOP는 Same Origin Policy로, 동일 출처가 아닌 경우 발생한다.
여기서 동일 출처는 프로토콜, 호스트, 포트가 동일한 경우이다.
SOP가 필요한 이유
그렇다면 동일 출처가 아닌 경우 브라우저가 접근을 차단하는 이유는 뭘까?
사용자가 웹사이트에 접근할 때는 브라우저의 쿠키에 로그인 세션 토큰을 남기게 된다.
해당 토큰을 사용해 사용자는 매번 로그인할 필요없이 서비스를 이용할 수 있다.
웹 사이트는 토큰을 받아 파싱, 해석하여 로그인되었음을 인지한다.
그러나 로그인 토큰이 남아있는 브라우저에서,
사용자가 악성사이트에 접속한 경우, 악성사이트는 사용자 브라우저의 토큰에 접근할 수 있다.
악성사이트가 이 토큰으로 네이버와 같은 웹사이트에 로그인을 요청할 경우, 사용자는 악의적인 상황에 놓일 수 있다.
이런 상황에서 브라우저는 SOP에 따라 이런 악의적 상황을 막습니다.
브라우저는 악성 사이트의 요청서버와, 요청받고 응답을 주는 응답서버의 출처가 다른걸 보고 CORS에러를 내립니다.
웹사이트 서버에서 악성사이트를 허가해주지 않는 이상, 악성사이트는 CORS에러로 정상적으로 로그인할 수 없습니다.
따라서 사용자가 보는 피해를 방지할 수 있습니다.
간단 정리
악의적인 상황
1. 내가 A 사이트에 로그인하면 브라우저에 ID등 세션정보(토큰)를 쿠키에 저장함
2. 그 상황에서 우연히 특정 악성 사이트에 접근하게 된다
3. 악성사이트는 브라우저에게 시켜 내 쿠키를 읽어 토큰을 갈취함
4. 악성사이트에서 A 사이트에 내 세션 토큰으로 요청해 로그인하고 나쁜짓함
브라우저의 SOP에 의한 해결
1. 악성사이트에 대한 A사이트의 응답을 분석한다.
2. 두 사이트의 출처가 다르다면 악성사이트에게 CORS 에러 내림
3. 악성사이트는 나쁜짓 못한다.
CORS란
CORS란 Cross origin resource sharing으로,
이런 sop로 발생하는 에러를 해결해주는 일종의 방법입니다.
sop로 사용자를 보호할 수 있지만,
서버는 점차 확장되고, 때로는 다른 회사의 서버부를 이용해야(API 사용 등등) 하는 상황이 자주 발생합니다.
따라서 조건에 따라 이런 다른 출처를 허가해줘야 합니다.
CORS의 흐름
1. 단순요청
요청만으로 서버에 영향을 주지 않는 단순요청의 경우,
1. 서버에게 본요청을 전달한다.
2. 서버가 OK랑 Access-Control-Allow-Origin를 보낸 경우 응답을 전달한다.
3. Access-Control-Allow-Origin이 안오면 본요청이 fail되고 브라우저는 CORS에러를 발생시킨다.
단순요청이 아닌 경우, 예비요청을 통해 허가를 받은 후, 본 요청을 할 수 있습니다.
1. OPTIONS 메소드로 Origin(호스트 uri)을 실어서 보낸다. (예비 요청)
2. 서버가 요청을 허가할 경우 OK랑 Access-Control-Allow-Origin을 보낸다.
3. 브라우저는 서버의 허가를 확인 후 본 요청을 보내게 된다.
CORS의 해결
CORS는 크게 두 부분으로 해결할 수 있습니다.
1. 프론트엔드의 웹서버에서 요청을 받아서 백엔드서버로 요청을 보내는 것
2. 백엔드 서버에서 자체적으로 요청을 허가해주는 것
프론트엔드에서 프록시 이용
우선 CORS에러는 브라우저를 통과하며 발생합니다.
따라서 온전한 서버끼리의 통신에서는 발생하지 않습니다.
프론트엔드 단의 웹서버에서 요청을 프록시 형태로 감싸 백엔드 서버로 바로 요청할 경우, 에러가 발생하지 않습니다.
백엔드 서버에서 허가
백엔드 서버에서 Access-Control-Allow-Origin 응답을 설정하게 하여, 임의로 허가를 내려줄 수 있습니다.
Preflight 요청에 대해 설명해주세요.
실제 요청을 보내도 안전한지 판단하기 위해 preflight 요청을 먼저 보낸다.
Preflight Request는 actual 요청 전에 인증 헤더를 전송하여 서버의 허용 여부를 미리 체크하는 요청이다.
이 요청으로 트래픽이 증가할 수 있는데 서버의 헤더 설정으로 캐쉬가 가능
브라우저에서는 다른 도메인으로 보내게 될 때 해당 도메인에서 CORS를 허용하는지 알아보기 위해 preflight 요청을 보내는데 이에 대한 처리가 필요
preflight 요청은 OPTIONS 메서드를 사용하며 "Access-Control-Request-" 형태의 헤더를 전송
Access-Control-Request-
Access-Control-Request-Method: actual 요청 시에 사용하는 메서드를 지정
Access-Control-Request-Headers: actual 요청 시에 전송하는 헤더를 지정