SOP(Same Origin Policy)

다른 출처의 리소스를 사용하는 것을 제한을 거는 보안 방식



SOP를 사용해야하는 예시

  1. 어떤 유저가 FaceBook에 로그인을 하고 토큰을 받아 로그인 과정을 완료했다.
  2. 유저는 로그인한 상태로 메일(http://hacker.com)을 받았다.
  3. 메일은 페이스북에 악의적으로 실행되는 script 코드를 작성하였다.
  4. 이때 페이스북에 유저의 토큰을 가지고 해커의 출처(origin) 을 통해 글을 작성하려고 한다.
  5. 이때 SOP 정책에 의해 요청을 확인하고 다른 출처(origin)을 가지게 되므로 페이스북에서 자신의 출처와 다른 출처를 가지므로 SOP 정책에 의해 해당 요청을 거부하게 된다.



CORS란?

Cross-Origin Resource Sharing으로서 다른 출처(origin)의 자원을 공유할 수 있게 허용하는 정책을 뜻한다. 여기서 Origin은 protocol + host + port를 합한 것으로 이 세가지가 동일하면 같은 origin이라고 판단한다. 예를 들어 http://123.456.789.123:3000http://123.456.789.123:8001은 다른 Origin이다.

 

과거 front와 back이 분리되어 있지 않던 시절에는 front와 back이 같은 Origin에 있었다. 하지만 둘이 분리되면서 각기 다른 서버에 존재하기 시작하면서 문제가 발생하기 시작했다. 별도로 존재하는 frontend server와 backend server로 요청(request)이 이뤄져야 하는 상황이 되었다. 동시에 어떤 곳에서 bacnend server는 request가 들어왔을 때 신뢰할 수 있는 것인지 알 수 없게 됬다. 따라서, backend server 쪽에서 어떤 Origin에서 request가 들어왔을 때, 받아줄 것인지 허용 Origin에 대해서 정의해줄 필요가 생겼다.

 

backend: "나는 이 Origin에서 이뤄지는 요청만 처리해줄거야. 다른 Origin은 믿을 수 없어. 해커가 날 공격할 수도 있잖아?"

 

그런데 일단 frontend에서 backend 쪽으로 요청을 보내기 전 미리 preflight request를 보낸다. 그러면 backend는 이를 통해 허용된 origin인지를 판단하고 응답한다.



preflight request란?

preflight request는 실제 request 전에 browser에서 보내는 작은 request이다. 지금 request을 보내는 frontend가 backend server에서 허용한 Origin이 맞는지, 그리고 해당 endpoint에서 어떤 HTTP method들을 허용하는지 등을 확인한다. 만약 허용되는 Origin이고 요청하는 메소드도 허용되는 것이라면 실제 request을 할 수 있게 해준다. 그렇지 않다면, 실제 request를 보내기도 전에 보내지 못하게 막는 것이다.

만일 preflight request가 이루어질려면 server에서 OPTIONS method를 허용해줘야 한다. preflight request는 OPTIONS method에 의해 만들어지기 때문이다.



CORS 작동 방식

위에서 언급한 preflight reqeust는 CORS의 작동 방식 중 하나이다. CORS 작동 방식은 아래 3가지와 같은 것들이 있다.



1. preflight request

preflight는 CORS 상황에서 보안을 확인하기 위해 브라우저가 제공하는 기능이다.
preflight는 미리 통신을 함으로써 문제가 있는 요청에 대해 일부러 ERROR를 발생시킨다.
요새는 크롬 개발자도구에서 preflight를 표기해서 어디서 preflight가 발생했는지 확인하기 쉬워졌다.

request를 바로 보내지 않고 preflight request를 보내서 허용되는 method가 무엇인지, 허용되는 Origin인지 등을 먼저 확인한다. 브 라우저와 서버는 다음과 같은 방식으로 통신한다.

 

  1. 브라우저는 서버로 HTTP OPTIONS method로 preflight request를 보낸다.
    • Origin header에는 자신의 출처를 넣는다.
    • Access-Control-Request-Method header에 실제 request에 사용할 method를 설정한다.
    • Access-Control-Request-Headers header에 실제 request에 사용할 header들을 설정한다.

 

  1. server는 이 preflight request에 대한 response으로 허용되는 것들에 대한 정보를 header에 담아서 browser로 보낸다.
    • Access-Control-Allow-Origin header에 허용되는 Origin들을 알려준다.
    • Access-Control-Allow-Methods header에 허용되는 Method들을 알려준다.
    • Access-Control-Allow-Headers header에 허용되는 header들을 알려준다.
    • Access-Control-Max-Age header에 해당 preflight request가 브라우저에 캐시 될 수 있는 시간을 초 단위로 알려준다.

 

- Access-Control-Max-Age header: 이 preflight 요청에 대한 결과를 브라우저가 얼마나 오랫동안 cache 할 수 있는지를 지정한다.
    만일 지정된 시간 동안에는 동일한 조건의 요청이 있을 때, preflight request를 다시 보내지 않고, cache된 정보를 사용하여 본     request를 바로 수행할 수 있다. 이는 네트워크 트래픽을 줄이고, 성능을 향상시키는 데 도움이 된다.

 

// 요청
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-Custom-Header
  1. 브라우저는 preflight request/response를 통해 본 요청이 이뤄질 수 있는지 미리 확인한 후 본 요청을 보낸다.

 

// 응답 
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 600

 

  1. 서버는 본 요청에 대해 응답해준다.
POST /api/data HTTP/1.1
Host: api.example.com // 요청을 받는 서버의 호스트
Origin: https://example.com // 요청을 보내는 클라이언트의 출처(도메인)
X-Custom-Header: value 
Content-Type: application/json // 요청 본문의 내용 유형을 JSON을 명시

{
  "data": "example" // 실제 데이터
}

 

preflight는 다음과 같은 상황에서 발생한다.

  1. OPIONS: 브라우저에서 OPTIONS를 던져 해당 사이트에서 사용가능한 method정보를 가져오게 될 때 preflight가 일어난다. 따라서 개발자는 실제 원하는 요청에 대해 작성해주면 되고 OPTIONS request를 보내는 코드를 작성하지 않아도 된다.
methods : GET, POST, OPTIONS, HEAD, PUT, DELETE 
  1. Simple Request(단순 요청): 사용자 정의 Header 정보를 추가, 수정하게 되면 단순요청에 preflight가 발생하게 된다. 예외적으로 사용자 정의 Header가 content-type일 때 type이 text/plain, multipart/form-data, x-www-form-urlencoded 일 경우엔 preflight가 일어나지 않는다.
사용자 정의 Header : Accept, Accept-language, content-language, content-type

Simple Request : GET, POST, HEAD
  1. Simple Request를 제외한 나머지 요청
  2. 쿠키 세팅: 내 쿠키를 다른 third party에 보내고 싶을 때 with Credential을 이용하게 되는데 이 때 preflight가 발생하게 된다.



2. simple request

preflight request를 생략하고 서버에 바로 실제 request를 보낸 후 server가 이에 대한 응답을 헤더에 Access-Control-Allow-Origin header를 보내주면 브라우저가 CORS 정책 위반 여부를 검사하는 방식이다. preflight request를 생략하려면 아래와 같은 조건들이 모두 만족되어야 한다.

 

  1. request의 method가 GET, POST, HEAD 중 하나여야 한다.
  2. request의 header가 Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width인 경우에만 적용된다.
  3. Conttent-Type 헤더가 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나여야 한다.

 

기본적으로 요즘 HTTP 요청은 application/json 또는 text/xml로 이루어지기에 대부분 3번째 Content-Type 헤더 조건을 만족시키지 못한다. 따라서, preflight request가 이루어지는 경우가 대다수라고 볼 수 있다.



3. credentialed request

client가 server로 요청할 때 자격 인증 정보(Credential) 를 담아서 요청할 때 사용되는 방식이다. 여기서 말하는 자격 인증 정보는 session ID가 저장되어 있는 cookie나 Authorization header에 설정하는 token value 등을 의미한다.

 

기본적으로 브라우저가 제공하는 요청 API 들은 별도의 option없이 브라우저의 쿠키와 같은 data를 함부로 request data에 담지 않도록 설정되어 있다. 요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 바로 credentials 옵션이다. 이 옵션에는 3가지 값이 있는데 각각 다음과 같은 의미를 갖는다.

 

  1. same-origin: 같은 출처 간 요청에만 인증 정보를 담을 수 있음
  2. include: 모든 요청에 인증 정보를 담을 수 있음
  3. omit: 모든 요청에 인정 정보를 담을 수 없음

 

credentials가 include로 설정되어야 Cross Origin 요청이 가능하다.

 

또한 서버에서도 응답 헤더를 다음과 같이 설정해줘야 한다.

  1. 응답 헤더의 Access-Control-Allow-Credentials 항목을 true로 설정해야한다.
  2. 응답 헤더의 Access-Control-Allow-Origin을 *(와일드카드)로 설정하면 안된다
  3. 응답 헤더의 Access-Control-Allow-Method를 *로 설정하면 안된다.
  4. 응답 헤더의 Access-Control-Allow-Headers를 *로 설정하면 안된다.

 

참고로 credentialed request 역시 preflight request가 선행된다.



주의할 점

CORS는 웹 어플리케이션이 출처 간 요청을 안정하게 수행할 수 있도록 하는 메커니즘이다. 브라우저는 보안 상의 이유로 동일 출처 정책을 강제한다. 따라서, 웹 페이지에서 다른 출처의 자원에 접근하려 할 때 CORS 규칙을 따르지 않으면 브라우저는 이를 차단한다.

 

브라우저는 특정 조건이 충족될 때 preflight request을 서버로 보낸다. 이는 OPTIONS method로 이루어지며, server가 실제 요청을 허용하는지 미리 확인한다. 서버는 이 요청에 대해 허용되는 출처, 메서드, 헤더 등을 응답한다.

 

그러나 모든 요청이 preflight 요청을 필요로 하지는 않는다. 특히 간단한, GET, POST, HEAD 요청의 경우 특정 조건을 만족하면 preflight 없이 바로 실제 요청이 이루어질 수 있다. 이러한 경우 브라우저는 요청을 보내고, 서버는 응답을 반환한다.

 

브라우저가 preflight 요청을 생략하고 실제 요청을 보내는 경우, 서버는 이를 처리하고 응답을 반환할 수 있다. 그러나 응답이 CORS 규칙을 위반하면 브라우저는 그 응답을 차단하고 CORS 에러를 발생시킨다.

 

여기서 중요한 점은, server는 요청을 정상적으로 처리하고 응답을 반환했을 수 있지만, 브라우저는 이를 수신하고 처리하지 않는다는 것이다. 이로 인해 클라이언트 측에서는 요청이 실패한 것처럼 보이지만, 실제로는 서버에서 작업이 이루어졌을 수 있다.

 

예시 상황
클라이언트가 서버에 POST 요청을 보내 데이터를 업데이트한다고 가정해보자

 

  1. 클라이언트가 브라우저에서 server로 POST 요청을 보낸다.
  2. 서버는 요청을 받아 DB를 업데이트하고, 성공 응답을 반환한다.
  3. 그러나 서버 응답에 필요한 CORS 헤더가 누락되어 있다.
  4. 브라우저는 서버의 응답을 CORS 위반으로 간주하고, 응답을 차단하며 CORS 에러를 표시한다.

 

이 경우, 서버 측에서는 데이터 업데이트가 정상적으로 이루어졌지만, 클라이언트 측에서는 요청이 실패한 것 처럼 보인다. 이는 다음과 같은 문제를 야기할 수 있다.

  • 클라이언트는 서버에 대한 작업이 실패했다고 잘못 인식할 수 있다.
  • 잘못된 응답 처리를 통해 중복된 요청이 발생할 수 있다.
  • 사용자는 잘못된 오류 메시지를 볼 수 있다.

 

이러한 문제를 방지하려면

  • 서버는 CORS 설정을 정확히 구성하여 모든 필요한 헤더를 포함해야 한다.
  • 클라이언트는 서버와의 상호작용이 제대로 이루어졌는지 확인하기 위해 네트워크 트래픽을 모니터링할 수 있어야 한다.
  • 개발자는 브라우저 콘솔의 CORS 에러 메시지를 주의 깊게 분석하여 문제를 정확히 파악해야 한다.

 

이와 같은 이해를 통해 CORS 에러가 발생했을 때, 단순히 요청이 실패했다고 가정하지 않고, 서버와 클라이언트 사이의 실제 상호작용을 면밀히 조사해야 한다.

출처

https://velog.io/@cloudlee711/CORS-%EC%99%80-preflight
https://bskyvision.com/entry/CORS%EC%99%80-%EA%B4%80%EB%A0%A8-%EC%9E%88%EB%8A%94-preflight-request%EB%9E%80
https://yoonlangcow.tistory.com/44

'인터넷 기본 지식' 카테고리의 다른 글

#10. JWT  (0) 2024.06.24
#9. RESTful 웹 API 디자인  (1) 2024.06.19
#8. LSP  (2) 2024.06.10
#7. 프로그래밍 패러다임  (1) 2024.06.09
#6. DNS(Domain Name System)  (0) 2024.05.31

JWT에 대하여

JWT의 정의

Json Web Token의 줄임말이다. RFC 7519에 명세되어 있는 국제 표준으로써, 통신 양자간의 정보를 JSON 형식을 사용하여 안전하게 전송하기 위한 방법이다. JWT는 정보가 토큰 자체에 포함된(Self-Container) 클레임(Claim) 기반 토큰이다.

 

Claim은 JWT 내부에 포함된 정보의 단위로, 특정 주체(사용자, 시스템 등)에 대한 속성이나 권한을 나타낸다. 클레임은 JWT의 payload 부분에 JSON 형식으로 포함되며, 토큰이 발급될 때 함께 서명되어 변경이 불가능하게 된다.

 

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022
}

 

위의 경우, sub, name, admin, iat와 같은 클레임이 포함되어 있다. 각 클레임은 토큰을 사용하는 어플리케이션이나 시스템에서 특정 주체에 대한 정보를 표현하고, 토큰을 수신한 쪽에서 이를 기반으로 인증 및 인가 과정을 수행할 수 있다.

 

JWT는 인증(Authentication)권한부여(Authorization) 에 사용되는 것이 가장 일반적이다. 인증 절차를 거쳐 서버에서 JWT를 발급해주면, 클라이언트는 이를 잘 보관하고 있다가 API 등을 사용할 때에 서버에 JWT를 함께 제출하며 서버로 부터 행위에 대해 인가 받을 수 있다.

 

JWT는 해시 혹은 비대칭키 방식을 사용하여 서명(Signatrue) 하기 때문에 무결성을 검증할 수 있다는 특징이 있다. 또한 토큰 자신이 정보를 직접 포함하고 있는 특징 덕분에, 통신 양자간 정보를 안전하게 전송할 때에도 사용된다.

해시(Hash): 해시 함수는 임의의 길이를 가진 입력 데이터를 고정된 길이의 값으로 변환하는 함수이다. 이 값은 해시값(Hash Value) 또는 해시코드(Hash Code)라고 불린다. 해시 함수의 주요 특징은 다음과 같다.
    - 고정된 길이의 출력: 입력 데이터의 길이에 관계없이 항상 동일한 길이의 출력을 생성
    - 단방향성: 해시값으로부터 원래의 입력 데이터를 복원하는 것은 거의 불가능
    - 충돌 회피: 서로 다른 두 입력이 동일한 해시값을 갖는 경우(충돌)가 극히 드물다.

비대칭키(Asymmetric Key): 비대칭키 암호화는 공개키 암호화라고도 하며, 두 개의 키 쌍(공개키와 개인키)을 사용한다.
    - 공개키(Public Key) : 누구나 알 수 있는 키로, 데이터를 암호화하거나 서명을 검증하는데 사용된다.
    - 개인키(Private Key): 소유자만 알고 있는 비밀 키로, 데이터를 복호화하거나 서명하는 데 사용된다.

    비대칭키 암호화의 주요 특징은 개인키로 서명한 데이터를 공개키로 검증할 수 있다는 점이다. 

서명(Signature): 서명은 데이터의 출처를 확인하고 무결성을 검증하는 데 사용된다. JWT는 다음과 같이 이루어진다.
    1. JWT의 header와 payload를 결합한 후 hash function을 사용하여 hash value를 생성한다. 
    2. 이 hash value를 private key로 암호화하여 서명을 생성한다.

    서명된 JWT를 받은 수신자는 다음과 같이 서명을 검증한다.

    1. JWT의 header와 payload를 결합한 후 동일한 hash function을 사용하여 hash value를 생성한다.
    2. public key를 사용하여 JWT에 포함된 서명을 복호화하여 hash value을 얻는다.
    3. 두 hash value가 일치하면 데이터가 변경되지 않았음을 확인할 수 있다. 

무결성(Integrity) : 무결성은 데이터가 전송 중에 변조되지 않았음을 보장하는 특성이다. JWT의 무결성은 서명을 통해 보장된다. 서명이 포함된 JWT는 수신자가 서명을 검증하여 토큰의 payload(클레임 데이터)가 전송 중에 변조되지 않았는지 확인할 수 있다. 

 

또한 JWT는 URL에 대한 안전한(URL-Safe) 문자열로 구성되어 있어 어떤 경로로든 전송할 수 있다.



서버기반 인증 VS 토큰기반 인증

 

HTTP는 무상태(Stateless) 프로토콜의 일종이다. 무상태 프로토콜이란, 각 요청을 독립적인 트랜잭션으로 취급하여 모든 상태가 어디에도 저장되지 않는다는 특성이다. 즉, 이전 요청과 현재 요청은 서로 관련이 없음을 뜻한다.

 

따라서 HTTP 그 자체만으로는 사용자가 아무리 인증에 성공한다고 하더라도, 그 인증 상태가 어디에도 저장되지 않는다. 로그인하고, 다음 페이지로 이동한 뒤, 또 로그인을 해야하는 이상한 서비스는 아무도 사용하지 않을 것 이다.

 

이러한 한계를 극복하기 위해 여러 인증 방법이 등장하게 되었고, 그 중 서버기반 인증과 토큰기반 인증 두가지에 대해 알아보겠다.



서버기반 인증

 

서버기반 인증에서는 사용자가 성공적으로 로그인한 이후 서버에서 사용자에 대한 세션(Session) 을 생성한다. 또한 이와 동시에 사용자의 브라우저에서 세션 ID를 저장하는 쿠키가 생성된다. 서버는 이 세션 ID를 통해 사용자를 식별하고, 사용자에 대한 정보를 저장, 관리한다.

 

서버에서 사용자에 대한 모든 정보를 갖고 있다. 라는 특징을 살펴보자. 만일 우리가 운영하는 서비스의 사용자가 증가하게 되어 서버를 확장해야 하는 상황에 직면했다고 가정하자.

 

이 경우 CPU를 더 좋은 것으로 교체하고, HDD/SDD 용량을 더 큰 것으로 교체하는 등의 방법을 가장 먼저 생각할 수 있다. 허나 단일 컴퓨터의 장비를 고성능으로 교체하는 방식의 확장을 수직 확장(Scale Up) 이라고 한다.

 

하지만, 이는 경제적으로 부담이 된다. 또한 한대의 서버만으로 운영하기에, 이 서비스는 단일 장애 지점(SPOF: Single Point of Failure) 를 갖게 되며, 서버가 다운되면 모든 서비스는 접속이 불가능해 질 것 이다.

 

단일 장애 지점: 시스템 내에 단일 요소가 고장 날 경우, 전체 시스템이 중단되는 지점을 의미한다. 이는 신뢰성과 가용성을 크게 저하시킬 수 있는 잠재적인 약점이다.

 

이런 이슈가 존재해 여러 대의 서버를 한 번에 운용하는 방법을 선택하는 것으로 방향을 틀었다고 가정해보자. 이렇게 서버를 여러 대 두어 확장하는 방식을 수평 확장(Scale Out) 이라고 한다.

 

수직 확장의 경제적 부담과 비교하여, 수평 확장은 동일한 사양의 컴퓨터 한대만 추가하고 제거하면서 스케일 관리를 할 수 있어 경제적으로 훨씬 부담이 덜하며 확장에 유연하다.

 

또한 하나의 서버 컴퓨터가 죽더라도, 다른 컴퓨터가 그대로 역할을 이어받으면 그만이다. 안전성 측면에서 바라봐도 우위에 있다고 할 수 있다. 이런 특징은 특히 사용자 증가폭 예측이 어려운 서비스에서 빛을 발하게 된다.

 

이런 Scaling 이슈로 인해 현대의 대규모 트래픽 처리와 데이터 저장이 필요한 서비스들은 일반적으로 수평적으로 서버를 확장한다.

 

하지만, 여러 대의 서버를 사용하면 데이터 불일치의 문제가 발생할 수 있다. 즉, 모든 서버가 메모리에 동일한 세션 정보를 가지고 있는 것이 아니다. 유저가 서비스 사용 중에 로그인이 풀리는 경험을 겪을 수 있다는 것이다.

 

이를 해결하기 위해 운용중인 모든 서버 컴퓨터에서 유저의 세션 ID를 모두 공유해야 한다. 이는 매우 번거로운 일이다.



토큰기반 인증

세션기반 인증에서는 세션 정보는 서버 메모리 위에 저장된다고 하였다. 세션 정보에는 유저의 ID, 이름, 권한 등 유저의 여러 정보가 포함될 수 있다.

 

이에 반해 토큰기반 인증방식은 유저의 정보를 서버에 저장하지 않는다. 유저가 성공적으로 로그인하면, 서버는 클라이언트로 토큰(가장 일반적으로 JWT가 사용됨)을 발급한다.

 

클라이언트는 토큰을 받아 저장하고, 서버에 요청할 때 HTTP header에 실어 함께 전송한다. 서버는 이를 검증(Verification) 하고, 유저를 인가(Authorization)한다. 이와 같이 서버는 '발급''검증' 두 가지 역할만 할 뿐 직접 정보를 갖고 있지 않다. 유저 상태의 저장 책임이 서버에서 클라이언트로 이동된 것이다.

 

이 말인 즉슨, 수평 확장의 환경에서 여러 대의 서버 컴퓨터가 모두 유저에 대한 정보를 기억하고 있을 필요가 없다는 뜻이다.

 

하지만, JWT 와 같이 토큰 자체에 정보가 저장되는 형태의 토큰은 세션과 달리 클라이언트에 유저의 정보가 저장되므로 노출되기 매우 쉽다. 따라서 민감한 정보를 담아서는 절대 안된다. 또한 토큰의 사이즈는 세션 ID에 비해 굉장히 비대하다. 토큰 기반 인증을 사용하면 이런 토큰을 사용하여 통신하면서 발생하는 오버헤드를 감안해야 한다는 단점이 존재한다.

 

뭐든지 '절대적으로' 좋은 것은 존재하지 않는다. 좋은 개발자는 존재하는 기술을 적재적소에 사용할 수 있어야 한다.

 

예전에는 의미없는 랜덤 문자열등을 생성해서 토큰 기반 인증을 구현하였는데, 이 토큰에는 만료시각 등의 정보를 담을 수 없어, 따로 만료시킬 수단이 존재하지 않는다. JWT 같은 경우 데이터를 직접 갖고 있는 클레임(Claim) 기반 토큰이므로 토큰의 만료를 구현할 수 있게 되었다.



JWT의 구조

토큰은 헤더(Header), 페이로드(Payload), 서명(Signature) 세 부분으로 구성되어 있다. 각 구성요소는 점'.'으로 분리된다. 따라서 JWT는 헤더, 페이로드, 서명의 형태를 갖는다.

 

각각의 구성요소는 JSON 형태로 표현된다. 다만, JSON의 경우 개행을 포함할 수 있어, 이를 한 줄로 나타내기 위해 최종적으로는 각 구성요소를 Base64로 인코딩한다.

JSON 형식의 데이터는 사람이 읽기 쉽게 작성될 수 있기에 개행 문자(줄바꿈 문자)를 포함할 수 있다. 이는 데이터를 여러 줄로 표현하는 것이다.



헤더(Header)

헤더는 일반적으로 토큰의 유형과 암호화 알고리즘 두 가지 정보를 아래와 같이 JSON의 형태로 담고 있다.

 

{
  "alg": "HS256", // algorithm: JWT를 서명하는 데 사용된 알고리즘
  "typ": "JWT" // type: 토큰의 타입을 지정
}

 

alg에 넣어둔 암호화 알고리즘은 주로 HMAC SHA256, RSA가 사용된다. 이는 후술할 서명(Signature)에서 사용된다.



페이로드(Payload)

Payload는 사용자의 정보 혹은 데이터 속성 등을 나타내는 클래임(Claim)이라는 정보 단위로 구성된다. 클래임도 3가지로 구분할 수 있는데 각각 등록된 클레임(Registered Claim), 공개 클레임(Public Claim), 비공개 클레임(Private Claim) 으로 구성되어 있다.



등록된 클레임(Registered Claim)

JWT 사양에 이미 정의된 클레임이다. 아래의 7개의 등록된 클레임이 정의되어 있다. 모든 클레임은 선택적이다. token size를 작게 유지하기 위해 이름이 3글자로 축약되어 있는 것을 확인할 수 있다.

 

  • iss: Issuer, 토큰 발급자를 나타낸다.
  • sub: Subject, 토큰 제목을 나타낸다.
  • aud: Audience, 토큰 대상자를 나타낸다.
  • exp: Expiration Time, 토큰 만료 시각을 나타낸다. Numeric Date 형식으로 나타낸다.
  • nbf: Not Before, 토큰의 활성 시각을 나타낸다. 쉽게 말해, 이 시각 적에는 토큰이 유효하지 않다는 의미이다. Numeric Date 형식으로 나타낸다.
  • iat: Issued At, 토큰이 발급된 시각을 나타낸다. Numeric Date 형식으로 나타낸다. 이 값으로 토큰이 발급된지 얼마나 오래됐는지 확인할 수 있다.
  • jti: JWT ID, JWT의 식별자를 나타낸다.



공개 클레임(Public Claim)

공개 클레임은 JWT를 사용하는 사람들에 의해 정의되는 클레임으로, 충돌 방지를 위해 URI 형태로 이름을 짓거나, IANA JSON Web Token Claims Registry 라는 곳에 직접 클레임을 등록해야 한다.

 

사실 단순히 서버와 클라이언트 사이에서 사용자를 인증하는 용도로 사용한다면 크게 신경 쓰지 않아도 좋다. 서버-클라이언트 사이의 단순 통신을 넘어 제 3자도 JWT 토큰을 사용할 때 충돌이 일어나지 않도록 합의된 클레임이라고 생각하면 된다.

 

{
  "email": "sample@domain.com",
  "profile": "http://domain.com/image.png",
  "http://domain.com/xxx/yyy/is_admin": true
}

 

위 처럼 등록된 공개 클레임인 email, profile 등을 사용할 수도 있고, http://domain.com/xxx/yyy/is_admin 처럼 URI 형태로도 사용할 수 있다.



비공개 클레임(Private Claim)

서버와 클라이언트 사이에서만 협의된 클레임으로, 공개 클레임과 충돌이 일어나지 않게 사용하면 된다.

 

{
  "user_id": "123456790",
  "user_age": 25
}



서명(Signature)

 

특정 암호화 알고리즘을 사용하여, Base64 인코딩된 헤더와 Base64 인코딩된 Payload 그리고 비밀키를 이용하여 암호화한다. 서명을 통해 서버는 헤더 혹은 payload가 누군가에 의해 변조되었는지 그 무결성을 검증하고 보장할 수 있다.

 

HMAC SHA256을 사용할 서명 생성을 아래와 같은 수도코드(pseudo-code) 로 나타낼 수 있다.

 

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)



JWT 직접 만들어보기

이제 JWT의 원리를 알아보았으니, 직접 JWT를 만들어본다. 개발 환경은 작성 기준 node.js LTS 버전인 v16.13.2을 사용한다.

 

Header

{
  "alg": "HS256",
  "typ": "JWT"
}

 

Payload

{
  "email": "devhudi@gmail.com",
  "name": "Hudi",
  "isAdmin": true
}

헤더에 나와있듯 암호화 알고리즘은 HMAC SHA256 (HS256) 을 사용한다.



사전준비

app.js 파일을 생성하고, 아래와 같이 crypto 모듈을 불러온다. 이는 node.js에서 암호화 등의 작업을 할 때 사용되는 모듈이다.

 

const crypto = require("crypto")

 

또한 아래와 같이 json 형태의 객체를 Base64 로 인코딩 해주는 함수를 작성하자.

 

function base64(json) {
  const stringified = JSON.stringify(json)
  // JSON을 문자열화
  const base64Encoded = Buffer.from(stringified).toString("base64")
  // 문자열화 된 JSON 을 Base64 로 인코딩
  const paddingRemoved = base64Encoded.replaceAll("=", "")
  // Base 64 의 Padding(= or ==) 을 제거

  return paddingRemoved
}

 

Base64로 문자열을 인코딩 하면, 결과물 마지막에 = 혹은 ==가 가끔 같이 나오는 경우가 존재한다. 이를 Padding이라고 하는데, 이를 제거하지 않으면 URL Safe 하지 않게 되므로 반드시 제거하자. 제거해도 Decode를 정상적으로 할 수 있다.

 

혹시 replaceAll에서 replaceAll is not a fuction 오류가 발생하는가? node.js 버전이 v15보다 낮은지 확인해 보자. replaceAll는 ES2021(ES12)에서 공식 스펙으로 포함되었다.



Header 만들기

const header = {
  alg: "HS256",
  typ: "JWT",
}

const encodedHeader = base64(header)
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

 

단순히 header JSON을 base64 인코딩 해준다.



Payload 만들기

const payload = {
  email: "devhudi@gmail.com",
  name: "Hudi",
  isAdmin: true,
}

const encodedPayload = base64(payload)
// eyJlbWFpbCI6ImRldmh1ZGlAZ21haWwuY29tIiwibmFtZSI6Ikh1ZGkiLCJpc0FkbWluIjp0cnVlfQ



Signature 만들기

const signature = crypto
  .createHmac("sha256", "secret_key")
  .update(`${encodedHeader}.${encodedPayload}`)
  .digest("base64")
  .replaceAll("=", "")

// KeefPR1ixDwoNnBQ77YsBYQxXFkZR1VcAkah6yle5lk

 

인코딩된 Header와 Payload를 점(.)으로 이어 붙인 것은 SHA256 알고리즘을 사용하여 HMAC으로 암호화 한다. 이 또한 Base64 로 표현하도록 설정한다. 마찬가지로 Padding을 제거한다.

 

HMAC (Keyed-hash Message Authentication Code) 이란, 메시지 인증 코드 (MAC) 의 한 유형으로서 특정 Key 와 함께 특정 Message 를 Hash 값으로 만드는 암호화 방식이다. 공격자로 하여금 레인보우 테이블 기법의 해킹을 어렵게 하기 위해 원문과 함께 비밀키를 더하여 해싱하는 것 이다.



조합하기

const jwt = `${encodedHeader}.${encodedPayload}.${signature}`

최종 결과물은 다음과 같다.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImRldmh1ZGlAZ21haWwuY29tIiwibmFtZSI6Ikh1ZGkiLCJpc0FkbWluIjp0cnVlfQ.KeefPR1ixDwoNnBQ77YsBYQxXFkZR1VcAkah6yle5lk



검증하기

JWT 토큰을 가장 쉽게 검증하는 방법은 jwt.io에 접속하는 것이다. 웹 사이트에 접속한 후 먼저 우측 하단 'VERIFY SIGNATURE'의 your-256-bit-secret를 우리의 비밀키 secret-key로 변경하자. 그 다음 우리가 생성한 JWT를 좌측에 붙여넣는다.



JWT 사용 시 주의사항

 

JWT는 세션과 달리 무상태(Stateless) 한 특징을 가지고 있다. 서버에서는 아무런 정보도 가지고 있지 않으며, 토큰 자체의 만료일자까지 토큰 자신이 가지고 있다. 만약 이런 토큰을 누군가 탈취해간다면? 누구나 토큰을 탈취 당한 사람의 계정에 접근할 수 있게 될 것 이다.

 

하지만, 서버에서는 JWT가 만료될 때까지 아무런 조취도 취할 수 없으며, 그저 바라만보고 있어야 한다. 즉, 이미 발행된 토큰에 대해 서버는 아무런 제어도 할 수 없다.

 

수 년전 이런 취약점을 악용하여, 페이스북 유저들의 토큰을 피싱사이트를 이용해 탈취하여 악용하는 사례가 굉장히 많이 발생한 적이 있어 시끄러웠던 적이 있었다.

 

이런 취약점을 막기 위해 현재는 많은 서비스들이 Access Token과 Refresh Token을 함께 사용하는 방식을 채택한다. 이 방식은 Access Token으로 사용자를 인가하지만, 그 만료 기간이 매우 짧다.(5분, 1시간, 24시간 등 다양하지만, 1일을 넘지 않는게 보통)

 

Access Token이 만료되면, 클라이언트는 같이 발급된 Refresh Token을 이용하여 서버에 Access Token 재발급을 요청한다. Refresh Token은 약 2주가량 만료기간을 길게 잡는다.

 

하지만, Refresh Token까지 탈취된다면 공격자는 Access Token을 발급받아 탈취자인양 행세를 할 수 있으므로, 이에 대한 조치도 추가로 필요하다.



마치며

생각보다 JWT 자체를 생성하는 것은 그리 거창한 작업은 아니다. 하지만, 이런 단순 작업은 라이브러리에게 맡기는 것이 훨씬 편할 것 이다. node.js 진영에서는 jsonwebtoken이라는 가장 대중적으로 사용되는 JWT 라이브러리가 존재한다.

 

본 포스팅에서는 원리를 직접 알아보기 위해 일일히 인코딩, 암호화 하여 JWT를 생성하였지만, 실제 제품을 개발할 때에는 상용 라이브러리를 쓰는 것을 추천한다.

'인터넷 기본 지식' 카테고리의 다른 글

#11. CORS, preflight  (0) 2024.06.24
#9. RESTful 웹 API 디자인  (1) 2024.06.19
#8. LSP  (2) 2024.06.10
#7. 프로그래밍 패러다임  (1) 2024.06.09
#6. DNS(Domain Name System)  (0) 2024.05.31

대부분의 웹 어플리케이션은 client가 application과 상호 작용하는 데 사용할 수 있는 API를 표시한다. 잘 디자인된 API는 아래와 같은 특성을 지원해야 한다.

 

  1. 플랫폼 독립성: 모든 client는 내부의 API가 어떻게 구현되는지 몰라도 API를 호출할 수 있어야 한다. 그러기 위해서는 표준 프로토콜을 사용해야 하고, client 및 web service가 교환할 데이터 형식에 동의할 수 있는 메커니즘이 있어야 한다.
  2. 서비스 진화: Web API는 client 어플리케이션과 독립적으로 기능을 진화시키고 추가할 수 있어야 한다. API가 진화해도 기존 클라이언트 어플리케이션은 수정 없이 계속 작동할 수 있어야 한다. 모든 기능은 client application이 해당 기능을 완전히 이용할 수 있도록 검색이 가능해야 한다.

 

REST는 무엇인가?

2000년에 Roy Fielding이 제안했으며 웹 서비스 디자인을 위한 아키텍처 접근 방식이다. 다음과 같은 특징을 지니고 있다.

  • 하이퍼미디어 기반 분산 시스템을 구축하기 위한 아키텍처 스타일
    • 리소스를 URI로 식별하고. HTTP 메서드(POST, GET, PUT, DELETE)를 사용하여 상태를 전이
  • 기본 프로토콜과 독립적이며, HTTP에 종속되지 않는다.
    • REST는 특정 통신 프로토콜(e.g. HTTP, FTP, SMTP 등)에 의존하지 않는 아키텍처 스타일이다.
    • REST는 HTTP 프로토콜에만 제한되지 않는다. 허나 HTTP는 REST의 가장 일반적인 구현 방법이지만, 필수적인 요소는 아니다.
  • 대부분의 REST API 구현은 HTTP를 사용

HTTP와 REST의 차이점으로, REST는 개방형 표준을 사용하며, 특정 구현에 바인딩 되지 않는다.

  • 개방형 표준은 누구나 접근하고 사용할 수 있는 표준을 의미한다. 따라서 REST API는 다양한 클라이언트에서 호환성을 가질 수 있다.
  • REST는 특정 서버 기술이나 클라이언트 기술에 종속되지 않는다. 즉, RESTful 웹 서비스는 다양한 프로그래밍 언어와 프레임워크로 구현될 수 있다.

 

REST 웹 서비스는 ASP.NET 등 다양한 언어로 작성 가능
클라이언트 어플리케이션은 HTTP 요청 생성 및 응답 구문 분석을 위해 어떤 언어 또는 도구 집합도 사용 가능

 

다음은 HTTP를 사용하는 RESTful API의 몇 가지 기본 디자인 원칙이다.

  • REST API는 리소스를 중심으로 디자인된다. 클라이언트에서 액세스할 수 있는 모든 종류의 개체, 데이터 또는 서비스가 리소스에 포함된다.
  • 리소스마다 해당 리소스를 고유하게 식별하는 URI인 식별자가 있다. 예를 들어 특정 고객 주문 문의의 URI는 다음과 같다.
https://adventure-works.com/orders/1
  • client가 리소스의 표현을 교환하여 서비스와 상호작용한다. 많은 Web API가 교환 형식으로 JSON을 사용한다. 예를 들어 위에 나열된 URI에 대한 GET 요청은 이 응답 본문을 반환할 수 있다.
{"orderId":1,"orderValue":99.90,"productId":1,"quantity":1}
  • 균일한 인터페이스
    • REST API는 균일한 인터페이스를 사용하여 클라이언트와 서버 간의 상호작용을 표준화한다.
    • HTTP를 기반으로 하는 REST API에서는 GET, POST, PUT, PATCH, DELETE와 같은 표준 HTTP 동사를 사용하여 리소스에 작업을 수행한다.
      GET /users/1       -> 사용자 ID 1에 대한 정보 가져오기
      POST /users        -> 새로운 사용자 생성
      PUT /users/1       -> 사용자 ID 1의 전체 정보 업데이트
      PATCH /users/1     -> 사용자 ID 1의 일부 정보 업데이트
      DELETE /users/1    -> 사용자 ID 1 삭제
  • 상태 비저장 요청 모델
    • REST API는 상태 비저장(stateless) 요청 모델을 사용한다.
    • 각 HTTP 요청은 독립적이어야 하며, 요청 간에 일시적인 상태 정보를 유지하지 않는다.
    • 이는 모든 요청이 독립적으로 작동해야 하며, 서버는 클라이언트의 상태를 기억하지 않는다는 것을 의미한다.
    • # 첫 번째 요청 GET /orders/123 # 서버는 이 요청을 처리하며, 클라이언트의 이전 상태를 기억하지 않습니다. # 두 번째 요청 POST /orders # 서버는 이 요청도 독립적으로 처리합니다.
  • 확장성
    • 상태 비저장 모델 덕분에 REST API는 확장성이 뛰어난다.
    • 클라이언트와 특정 서버 간에 선호도를 유지할 필요가 없으므로, 모든 서버가 모든 클라이언트의 요청을 처리할 수 있다.
    • 그러나 백엔드 데이터 저장소의 쓰기 작업 등 다른 요소가 확장성을 제한할 수 있다.
  • 데이터 저장소의 확장 전략
    • 데이터 저장소를 확장하는 전략에는 가로, 세로, 및 기능 데이터 분할이 포함된다.
    • 이러한 전략을 통해 데이터 저장소의 확장성을 높일 수 있다.
    • 1. 가로 분할(Sharding): 데이터를 여러 서버에 분산 저장하여 읽기 및 쓰기 작업을 분산시킨다. 2. 세로 분할(Vertical Partitioning): 데이터 베이스의 테이블을 기능별로 분할하여 각 기능을 독립적으로 확장한다. 3. 기능 데이터 분할(Functional Partitioning): 애플리케이션의 기능별로 데이터를 분할하여 관리한다.
  • REST API는 표현에 포함된 하이퍼미디어 링크에 따라 구동된다. 예를 들어 다음은 order의 JSON 표현을 보여준다. order과 관련된 고객을 가져오거나 업데이트하는 링크를 포함하고 있다.
{
  "orderID":3,
  "productID":2,
  "quantity":4,
  "orderValue":16.60,
  "links": [
    // rel: 현재 리소스와 관련된 다른 리소스에 대한 링크 파일
    //        링크된 리소스와의 관계를 설명한다. 여기서는 product로 제품과 관련된 리소스임을 나타낸다.
    // href: 링크된 리소스의 URI이다. 
    // action: HTTP 메서드를 지정한다.
    {"rel":"product","href":"https://adventure-works.com/customers/3", "action":"GET" },
    {"rel":"product","href":"https://adventure-works.com/customers/3", "action":"PUT" }
  ]
}

 

Leonard Richardson은 Web API의 성숙도를 측정하기 위해 4단계 모델을 제안했다. 이 모델은 API의 RESTful 성격을 평가하는 데 사용된다.

  1. 수준 0: 단일 URI
    • 특징: 모든 작업이 단일 URI에 대한 POST 요청으로 이루어진다.
    • 예시: 모든 요청이 http://example.com/api 에 POST로 전송되어야 하는 경우
      POST /api
      {
      "action": "getUser",
      "userId": 1
      }
  2. 수준 1: 리소스별 URI
    • 특징: 개별 리소스에 대해 별도의 URI를 정의
    • 예시: 사용자. 제품 등 각각의 리소스에 대해 별도의 URI를 사용
      GET /users/1
      GET /products/2
  3. 수준 2: HTTP 메서드
    • 특징: HTTP 메서드(GET, POST, PUT, DELETE)를 사용하여 리소스에 대한 작업을 정의한다.
    • 예시: 각 URI에 대해 적절한 HTTP 메서드를 사용하여 작업을 수행한다.
      GET /users/1       // 사용자 정보 가져오기
      POST /users        // 새 사용자 생성
      PUT /users/1       // 사용자 정보 업데이트
      DELETE /users/1    // 사용자 삭제
  4. 수준 3: 하이퍼미디어(HATEOAS)
    • 특징: 응답에 하이퍼미디어 링크를 포함하여 클라이언트가 다음 작업을 탐색할 수 있게 한다.
    • 예시: 리소스에 대한 작업 링크를 응답에 포함한다.
      {
      "userId": 1,
      "name": "John Doe",
      "links": [
        {"rel": "self", "href": "/users/1"},
        {"rel": "orders", "href": "/users/1/orders"}
       ]
      }



리소스를 중심으로 API 디자인 구성

즉, 웹 API가 표시하는 entity에 집중해야 한다. 예를 들어 전자 상거래 시스템에서 기본 entity는 customer과 order이다.
주문 정보가 포함된 HTTP POST 요청을 전송하여 주문 만들기를 구현할 수 있다. HTTP 응답은 order이 성공적으로 수행되었는지 여부를 나타낸다. 가능하다면 리소스 URI는 동사(리소스에 대한 작업)가 아닌 명사(리소스)를 기반으로 해야 한다

 

https://adventure-works.com/orders // Good

https://adventure-works.com/create-order // Avoid

 

리소스가 단일 실제 데이터 항목을 기반으로 할 필요는 없다. 예를 들어 order 리소스는 내부적으로는 관계형 DB의 여러 table로 구현할 수 있지만, 클라이언트에 대해서는 단일 entity로 표시된다. 단순히 DB의 내부 구조를 반영하는 API를 만들면안된다. REST의 목적은 entity 및 해당 entity에서 애플리케이션이 수행할 수 있는 작업을 모델링하는 것이다. 클라이언트는 내부 구현에 노출되면 안된다.

// 잘못된 설계
GET /orders_table/123
GET /customers_table/456
GET /products_table/789

// 올바른 설계
GET /orders/123

위와 endpoint를 통해 필요한 모든 정보를 얻을 수 있다.

{
  "orderId": 123,
  "orderDate": "2023-06-01",
  "customer": {
    "customerId": 456,
    "name": "John Doe",
    "email": "john.doe@example.com"
  },
  "products": [
    {
      "productId": 789,
      "productName": "Widget",
      "quantity": 2,
      "price": 19.99
    }
  ],
  "total": 39.98
}

 

entitiy는 종종 collection(주문, 고객)으로 그룹화된다. collection은 collection 내 항목과는 별도의 리소스이며 고유한 URI가 있어야 한다.
예를 들어 다음 URI는 주문 컬랙션을 나타낼 수 있다.

 

https://adventure-works.com/orders

 

collection URI에 HTTP GET 요청을 보내면 collection에 있는 항목 목록을 검색한다. 또한 collection의 항목마다 고유의 URI가 있다.
항목의 URI에 대한 HTTP GET 요청은 해당 항목의 세부 정보를 반환한다.

 

URI에 일관적인 명명 규칙을 적용한다. 일반적으로 이렇게 하면 collection을 참조하는 URI에 대해 복수 명사를 사용할 수 있다.
collection 및 항모에 대한 URI를 계층 구조로 구성하는 것이 좋다. 예를 들어 /customers는 고객 collection의 경로이고, /customers/5는 ID가 5인 고객의 경로이다. 이 접근 방식을 사용하면 웹 API를 직관적으로 유지할 수 있다. 또한 많은 Web API 프레임워크는 매개 변수가 있는 URI 경로를 기반으로 요청을 routing할 수 있으므로 개발자는 경로 /customers/{id} 에 대한 경로를 정의할 수 있다.

 

서로 다른 리소스 형식과 이러한 연결을 표시하는 방법 사이의 관계도 고려해야 한다. 예를 들어 /customers/5/orders는 고객 5에 대한 모든 주문을 나타낼 수 있다. 반대 방향으로 이동하여 /orders/99/customer 같은 URI를 사용하여 주문에서 고객으로의 연결을 표시할 수도 있다.
그러나 이 모델을 너무 확장하면 구현이 어려울 수 있다. HTTP 응답 메시지의 본문에 연결된 리소스에 대한 탐색 가능한 링크를 제공하는 방법이 좋다.

 

좀 더 복잡한 시스템에서는 /customers/1/orders/99/products 처럼 클라이언트가 여러 관계 수준을 탐색할 수 있는 URI를 제공하고 싶을 수 있다. 그러나 이 수준의 복잡성은 유지하기 어려울 수 있으며 나중에 리소스 사이의 관계가 변하면 유연성이 떨어진다. 그 대신 URI를 비교적 간단하게 유지해보자. 애플리케이션이 리소스 참조를 지정한 후에는 이 참조를 사용하여 해당 리소스와 관련된 항목을 찾을 수 있어야 한다. 이전 쿼리를 /customers/1/orders URI로 바꿔서 고객 1의 모든 주문을 찾은 후 /orders/99/products로 바꿔서 이 주문의 제품을 찾을 수 있다.

 

팁!
리소스 URI를 컬렉션/항목/컬렉션 보다 더 복잡하게 요구하지 않는 것이 좋다.

'인터넷 기본 지식' 카테고리의 다른 글

#11. CORS, preflight  (0) 2024.06.24
#10. JWT  (0) 2024.06.24
#8. LSP  (2) 2024.06.10
#7. 프로그래밍 패러다임  (1) 2024.06.09
#6. DNS(Domain Name System)  (0) 2024.05.31

LSP(Liscov Substituion Principle)

 

S가 T의 하위 유형이면 프로그램에서 자료형 T의 객체는 해당 프로그램의 원하는 속성을 변경하지 않고 자료형 S의 객체로 교체(치환)할 수 있어야 한다.

 

리스코프 치환 원칙이란 부모 객체와 자식 객체가 있을 때 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체할 수 있다는 원칙이다.

 

OOP에서 상속이 일어나면, 하위 타입인 자식 객체는 상위 타입인 부모 객체의 특성을 가지며, 그 특성을 토대로 확장할 수 있다.

리스코프 치환 원칙은 올바른 상속을 위해, 자식 객체의 확장이 부모 객체의 방향을 온전히 따르도록 권고하는 원칙이다.



LSP를 위반한 예제 코드

직사각형, 정사각형 관계를 코드로 보자

public class Rectangle {

    public int width;
    public int height;

    // 너비 반환, Width Getter
    public int getWidth() {
        return width;
    }
    // 너비 할당, Width Setter
    public void setWidth(int width) {
        this.width = width;
    }

    // 높이 반환, Height Getter
    public int getHeight() {
        return height;
    }
    // 높이 할당, Height Setter
    public void setHeight(int height) {
        this.height = height;
    }

    //직사각형 넓이 반환 함수
    public int getArea() {
        return width * height;
    }
}

public class Square extends Rectangle{
    @Override
    public void setWidth(int Width) {
        super.setWidth(width);
        super.setHeight(getWidth());
    }

    @Override
    public void setHeight(int height) {
        super.setHeight(height);
        super.setWidth(getHeight());
    }
}

Rectangle 객체를 상속 받은 Square 클래스에서는 정사각형의 너비와 높이가 같다는 특징을 구현했다. 너비와 높이 둘 중 하나를 입력해도
나머지 값이 일치되도록 메서드를 override 해주었다.

아래는 Rectangle 클래스이다. 높이를 5, 너비를 10으로 설정했다.

public class Main {
    public static void main(String[] args) {

        Rectangle rectangle = new Rectangle();
        rectangle.setHeight(5);
        rectangle.setWidth(10);

        System.out.println(rectangle.getArea()); // 50
    }
}

다음은 Square 테스트이다.

리스코프 치환 원칙은 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체할 수 있다는 원칙이다.

 

그렇다면 Rectangle 클래스에서의 테스트와 같은 값을 할당했을 때, 당연히 완전히 같은 결과를 반환해야 한다.

public class Main {
    public static void main(String[] args) {

        Rectangle square = new Square();

        square.setWidth(10);
        square.setHeight(5);


        System.out.println(square.getArea()); // 25
    }
}

아래는 50이 아니라 25가 반환되었다. 가장 마지막에 수행된 setHeight(5)가 객체의 너비와 높이를 모두 5로 할당했기에
25가 나오는 것은 당연하다.



위의 코드 둘다 Rectangle 자료형으로 인스턴스를 받고 있다. 하지만 Rectangle과 Square 클래스의 동작이 전혀
다르다는 사실을 알 수 있다.

 

이는 바로 정사각형이 직사각형을 상속 받는 것이 올바른 상속 관계가 아니라는 것을 의미한다.
자식 객체가 부모 객체의 역할을 완전히 대체하지 못한다는 의미이다.

 

이렇게 잘못된 객체를 상속하거나 올바르게 확장하지 못할 경우, 겉으로 보기엔 정상적이지만 올바른 객체라고 할 수는 없다.

 

위 같은 코드가 리스코프 치환 원칙을 위배하는 코드이다.



LSP를 준수한 코드

올바르게 성립하는 상속 관계를 구현한 코드이다.

public class Shape {

    public int width;
    public int height;

    // 너비 반환, Width Getter
    public int getWidth() {
        return width;
    }
    // 너비 할당, Width Setter
    public void setWidth(int width) {
        this.width = width;
    }

    // 높이 반환, Height Getter
    public int getHeight() {
        return height;
    }
    // 높이 할당, Height Setter
    public void setHeight(int height) {
        this.height = height;
    }

    // 사각형 넓이 반환
    public int getArea() {
        return width * height;
    }
}

//직사각형 클래스
public class Rectangle extends Shape {

    public Rectangle(int width, int height) {
        setWidth(width);
        setHeight(height);
    }
}

//정사각형 클래스
public class Square extends Shape{

    public Square(int length) {
        setWidth(length);
        setHeight(length);
    }
}

public class Main {
    public static void main(String[] args) {

        Shape rectangle = new Rectangle(10, 5);
        Shape square = new Square(5);

        System.out.println(rectangle.getArea()); // 50
        System.out.println(square.getArea()); // 25
    }
}



정리

리스코프 치환 원칙은 상속되는 객체는 반드시 부모 객체를 완전히 대체할 수 있어야 한다고 권고한다.

 

첫 번째의 직사각형 클래스를 상속받은 정사각형 객체 예제처럼, 올바르지 못한 상속관계는 제거하고 부모 객체의 동작을 완벽히 대체할 수 있는 관계만 상속하도록 코드를 설계해야 한다.

출처

https://velog.io/@harinnnnn/OOP-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-5%EB%8C%80-%EC%9B%90%EC%B9%99SOLID-%EB%A6%AC%EC%8A%A4%EC%BD%94%ED%94%84-%EC%B9%98%ED%99%98-%EC%9B%90%EC%B9%99-LSP

'인터넷 기본 지식' 카테고리의 다른 글

#10. JWT  (0) 2024.06.24
#9. RESTful 웹 API 디자인  (1) 2024.06.19
#7. 프로그래밍 패러다임  (1) 2024.06.09
#6. DNS(Domain Name System)  (0) 2024.05.31
#5. 웹 브라우저는 어떻게 작동하는가?  (0) 2024.05.31

프로그래밍 패러다임이란?

간단하게 프로그래밍의 스타일을 뜻한다. 프로그램은 순차, 분기, 반복, 참조로 구성되는데 이를 어떤 관점(스타일)을 중심적으로 설계하느냐에 따라 패러다임의 종류가 나뉜다. 다시 말하면, 프로그래밍 패러다임은 개발자로 하여금 프로그래밍을 할 때 관점을 제시해주는 역할을 한다.

현재 프로그래밍 패러다임은 많은 종류가 있다.
크게 명령형과 선언형 프로그래밍으로 나뉜다.

명령형 프로그래밍은 컴퓨터에 문제를 어떻게 해결하는지 명령을 내리는 방식으로, 대표적으로 절차지향 프로그래밍객체지향 프로그래밍이 있다.

선언형(함수형) 프로그래밍은 컴퓨터에게 무엇을 해야하는지를 선언해주는 방식으로, 함수형에서 어떻게 해결하는지를 설명한 방법을 변수에 담을 수 있기 때문에 그 방법이 담긴 변수만 무엇인지만 명시해주면 된다.



절차지향 프로그래밍(Procedural Programming)

절차지향(절차적) 프로그래밍이란 프로그램을 함수나 절차의 집합으로 구성하는 패러다임이다. 일련의 명령문을 순차적으로 실행하는 방식으로 프로그램을 작성한다.


def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

def main():
    x = 10
    y = 5
    print("Add:", add(x, y))        # Add: 15
    print("Subtract:", subtract(x, y))  # Subtract: 5

if __name__ == "__main__":
    main()

함수 호출 중심이란 전체 로직에서 재사용성이 높은(재사용이 가능한) 코드들을 별도의 함수로 분리하여 필요할 때마다 해당 함수를 호출하는 방식의 프로그래밍을 뜻한다.

TOP-DOWN 방식의 프로그래밍이다. 이러한 방식은 추상적인 개념을 구체화해 나아가는 방식이다. 예를 들어 *"핸드폰으로 사진을 찍는다"* 라는 추상적인 상황을 *"카메라 앱을 선택한다" -> "원하는 모드를 선택한다." -> "초점을 맞춘다" -> "셔터와 버튼을 누른다"* 와 같이 구체화해 나아가는 방식을 말한다.

절차지향 프로그래밍에서는 Main 함수가 하나의 큰 개념이라고 보면, 그 안에서 함수를 호출하고, 조건문을 사용하는 등을 통해서 개념을 구체화해 나간다.

절차지향 프로그래밍은 순차적으로 함수를 불러오기 때문에 함수의 역할만 알면 전반적인 프로그램의 목적을 이해하기 쉽다. 반대로 프로그램이 복잡해질수록 함수가 많아지지만 데이터는 main에서 관리하므로 프로그램의 방향성을 이해하기 어려워지며 유지보수도 어려워진다.



객체 지향 프로그래밍(Object-Oriented Programming, OOP)

객체지향 프로그래밍이란 객체를 최소 단위로 두며 이 객체에 책임을 명확히 해 각각 객체끼리 상호작용을 하도록 설계하는 프로그래밍 패러다임이다. 절차지향에서 처럼 main에서 프로그램의 진행을 다루는 것이 아니라 모든 것을 객체라고 생각하고 객체끼리의 상호관계를 통해서 일을 처리하는 방식이다.

 class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        raise NotImplementedError("Subclass must implement this method")

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

dog = Dog("Buddy")
cat = Cat("Whiskers")

print(dog.speak())  # Buddy says Woof!
print(cat.speak())  # Whiskers says Meow!

절차지향과 다르게 객체지향 프로그래밍은 각각의 객체마다 각각의 데이터와 메소드(함수)를 가지고 있다. 각각의 데이터와 메소드(함수)는 경우에 따라 외부에서 접근할 수 있기도 하며 보호되기도 한다.

프로그래밍은 전체적으로 객체와 객체 간의 메서드 호출로 이루어진다. 그리고 각 객체는 자신의 기능을 수행하는데 필요한 데이터를 직접 가지고 있다.

객체지향 프로그래밍은 다형성, 의존성 주입 등의 특징으로 코드를 쉽게 확장시킬 수 있어 여럿이서 협력할 때 사용하기에 좋다.

객체지향 프로그래밍은 직관적이며, 유지보수가 쉽다. 반면에 객체에 대한 이해가 없다면 코드를 이해하기 어려울 수 있다. 왜냐하면 입력에 따라서 다양한 작업으로 진행되기 때문이다.

주요 특징

  • 클래스와 객체: 클래스는 객체의 청사진이며 객체는 클래스의 인스턴스이다.
  • 캡슐화: 객체의 상태를 보호하고, 객체의 내부 구현을 숨기는 것을 의미한다.
  • 상속: 새로운 클래스가 기존 클래스의 특성과 행동을 재사용할 수 있도록 한다.
  • 다형성: 동일한 인터페이스를 사용하여 서로 다른 데이터 타입을 처리할 수 있다.



함수형 프로그래밍(Functional Programming)

함수형 프로그래밍은 함수가 최소 단위이며 외부 상태를 갖지 않는 함수들을 통해 파이프 형태로 프로그래밍을 하는 패러다임이다. 또한 함수형 프로그래밍은 모든 데이터의 처리를 수학적 함수로 취급하여 결과 값을 구하는 방식의 프로그래밍이다.

여기서 외부 상태를 갖지 않는다는 의미는 같은 입력을 넣었을 때 언제나 같은 출력을 내보낸다는 것이다. 다시 말하면 어떤 외부 요인도 함수의 입출력에는 영향을 주지 않는 다는 것이다. 통제하지 못하는 외부 상태를 사용한다면 예측하지 못하는 부작용을 가질 수 있기에 외부 상태를 수정할 수 없도록 하는 것이 중요하다.

// 순수 함수: 동일한 입력에 대해 항상 동일한 출력을 반환
const add = (a, b) => a + b;

// 고차 함수: 함수를 인수로 받거나 함수를 반환하는 함수
const applyFunction = (f, x, y) => f(x, y);

// 불변성: 데이터가 변경되지 않으며, 데이터 변경 시 새로운 데이터를 생성한다.
const originalArray = [1, 2, 3];
const newArray = [...originalArray, 4];

// 합성: 작은 함수를 조합하여 복잡한 연산을 구성
const increment = x => x + 1;
const double = x => x * 2;

const incrementAndDouble = x => double(increment(x));

console.log(applyFunction(add, 10, 5));  // 15
console.log(originalArray);  // [1, 2, 3]
console.log(newArray);  // [1, 2, 3, 4]
console.log(incrementAndDouble(2));  // 6

함수형 프로그래밍 코드에서는 한 번 초기화한 객체 및 변수는 변하지 않는다. 이러한 특성을 불변성이라고 하는데, 이 불변성 때문에 프로그램의 동작을 예측하기 쉽고 프로그램의 데이터 흐름이 유지될 수 있다.

함수형 프로그래밍은 주어진 문제를 잘게 쪼개 그 문제를 해결하는 함수를 만들고 그 함수들을 결합하는 방식으로 문제를 해결해 나간다.

외부 상태로 인한 부작용이 없기에 안정적이다. 그러므로 동시성을 가지는 프로그램(대용량 데이터를 병렬적으로 처리하는 경우)에 사용하기 적합하다. 하지만 온전히 함수형 프로그래밍으로 구성하기 위해선 정말 다양한 함수들을 조합해 사용해야 한다는 단점이 있다.



반응형 프로그래밍(Reactive Programming)

비동기 데이터 흐름에 기반을 둔 프로그래밍 패러다임으로, 데이터 중심 사고 방식인 프로그래밍이다. 이벤트나 변화에 반응하기 위한 비동기적 데이터 처리 및 흐름 기반 프로그래밍 패러다임이다. 이것은 프로그래밍 언어로 정적 또는 동적인 데이터 흐름을 쉽게 표현할 수 있어야하며, 데이터 흐름을 통해 하부 실행 모델이 자동으로 변화를 전파할 수 있다는 것을 의미한다.

 

데이터와 데이터 스트림에 영향을 미치는 모든 변경 사항을 관련된 모든 당사자들에게 전파하는, 모든 프로그램을 reactive 프로그램이라고 할 수 있다. 다음과 같은 이점이 있다.

  • 간결해진 thread 사용
  • 간단한 비동기 연산
  • 콜백 지옥의 제거

 

반응형 프로그래밍을 가장 쉽게 이해할 수 있는 예1.

| - | A | B |C |D |
|---|---|---|
| 1열 | 1 | 2 |=A1+B1 |=A1+B1+C1 |

  • C와 D 열에는 수식을 선언적으로 작성해 둔다.
  • A1, B1의 값을 변경하면 즉시 변경사항이 전파 -> C1의 값이 자동으로 변경됨
  • C1이 변경되자, D1에도 수식이 반영되어 값이 변경됨
  • 어떤 순서로 데이터가 변경되어야 하는지 정해주지 않았음에도 하나의 데이터의 흐름이 만들어짐

 

예2.

// 명령형 프로그래밍 - 결과값이 저장
int a=10, b=20;
int c = a+b;
System.out.println(c)); // c = 30;
a = 20;
System.out.println(c)); // c = 30; c에 계산식의 결과값 30만 저장됨

// 반응형 프로그래밍 - 계산식이 저장
int a=10, b=20;
int c = a+b;
System.out.println(c)); // c = 30;
a = 20;
System.out.println(c)); // c = 40; c에 계산식 자체가 저장되어 사용하는 변수 a의 값이 바뀐 것이 반영됨

OOP와 다르게 데이터의 흐름에 더 집중한다. 이 데이터 흐름은 관찰할 수 있거나 필터링하거나 다룰 수 있으며 새로운 흐름을 만들기 위해 다른 흐름과 병합할 수도 있다.

 

직접 재계산명령이 필요없다. 변수 값을 바꾸면 해당 변수를 참조하는 모든 식들이 연쇄적으로 재평가되면서 스스로의 값을 갱신한다.

출처

https://ontheway.tistory.com/69
https://velog.io/@ehgus8621/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%ED%8C%A8%EB%9F%AC%EB%8B%A4%EC%9E%84

'인터넷 기본 지식' 카테고리의 다른 글

#9. RESTful 웹 API 디자인  (1) 2024.06.19
#8. LSP  (2) 2024.06.10
#6. DNS(Domain Name System)  (0) 2024.05.31
#5. 웹 브라우저는 어떻게 작동하는가?  (0) 2024.05.31
#4 WAS, 웹 서버  (0) 2024.05.31

DNS(Domain Name System) 란?

도메인 네임 시스템(Domain Name System, DNS)은 호스트의 도메인네임(www.example.com)을 네트워크주소(291.168.1.0)로 변환하거나, 그 반대의 역할을 수행하는 시스템이다.

서비스 도메인 주소 IP 주소
다음(Daum) daum.net 203.133.167.81
네이버(Naver) naver.com 223.130.200.104
구글(Google) google.com 142.250.207.14

아래에 자세히 설명하겟지만 도메인네임으로 IP주소를 찾는 과정은 다음과 같다.
1. 도메인 주소 exam.com을 브라우저에 입력하게 되면, 도메인 주소들을 가지고 있는 네임서버(DNS 서버)에 접속
2. 네임서버에 접속한 도메인(exam.com)과 연결된 IP 정보를 확인하고, IP를 사용자 PC에 전달
3. 사용자 PC는 전달받은 서버의 IP주소로 접속
4. 서버의 IP로 연결된 브라우저에 서버의 내용(홈페이지)을 출력

DNS 작동 원리

클라이언트가 도메인명을 브라우저에 검색하면, 먼저 도메인 정보가 저장된 네임 서버(DNS 서버)로 가서 도메인과 일치하는 IP주소로 가라고 지시하게 되고, 다시 그 IP주소로 접속하게 되면 홈페이지가 열린다.


이 때 도메인 & IP정보를 얻는 과정이 약간 복잡하다.
전세계에는 도메인 수가 너무나도 많기에 DNS 서버 종류를 계층화해서 단계적으로 처리한다.

DNS 동작 순서

  1. 웹 브라우저에 www.naver.com 을 입력하면 먼저 PC에 저장된 Local DNS(기지국 DNS 서버)에게 'www.naver.com'이라는 hostname에 대한 IP 주소를 요청한다. Local DNS에는 "www.naver.com의 IP 주소"가 있을 수도 없을 수도 있다.
    • 만일 과거 접속했었다면 Local DNS에 접속정보가 캐싱되어 있어, 바로 PC에 IP 주소를 주고 끝난다.
Local DNS(기지국 DNS 서버):
    기본적으로 인터넷을 사용하기 위해서 IP를 할당해주는 통신사(KT, SK, LG 등...)에 등록하게 된다.
    컴퓨터의 LAN선을 통해 인터넷이 연결되면, 가입했던 각 통신사의 기지국 DNS 서버가 등록되게 된다.
    그러니까 KT를 사용하는 집이면 KT DNS이 되고, SK통신사 사용하는 집이면 SK DNS가 자동으로 셋팅된다.
  1. Local DNS는 이제 'www.naver.com의 IP주소"를 찾아내기 위해 다른 DNS 서버들과 통신(DNS 쿼리)을 시작한다.
    먼저 Root DNS 서버에게 "www.naver.com"의 IP 주소를 요청한다.
Root DNS(루트 네임서버):
    Root DNS는 인터넷의 도메인 네임 시스템의 루트 존이다.
    ICANN이 직접 관리하는 서버로, TLD DNS 서버 IP들을 저장해두고 안내하는 역할을 한다.
    전세계에 961개의 루트 DNS가 운영되고 있다.
  1. Root DNS 서버는 "www.navercom"의 IP주소를 찾을 수 없어 Local DNS 서버에게 "www.naver.com의 IP 주소를 찾을 수 없다고 다른 DNS 서버(TLD DNS 서버)에게 물어봐"라고 응답을 한다.

  2. 이제 Local DNS 서버는 com 도메인을 관리하는 TLD DNS 서버(최상위 도메인 서버)에 다시 www.naver.com에 대한 IP주소를 요청한다.

TLD(Top-Level Domain, 최상위 도메인) DNS Server란?
    TLD는 도메인 등록 기관(Registry)이 관리하는 서버로, 도메인 네임의 가장 마지막 부분을 말한다.
    e.g. .com, .co.kr
    또한 Authoritative DNS 서버 주소를 저장해두고 안내하는 역할을 한다.
  1. com 도메인을 관리하는 DNS 서버에도 해당 정보가 없으면, Local DNS 서버에게 "www.naver.com의 IP주소를 찾을 수 없어. 다른 DNS 서버(Authoritative DNS 서버)에게 물어봐"라고 응답한다.

  2. Local DNS 서버는 naver.com DNS 서버(Authoritative DNS 서버)에게 다시 www.naver.com의 IP 주소를 요청한다.

Authoritative DNS Server
    실제 개인 도메인과 IP 주소의 관계가 기록/저장/변경되는 서버.
    그래서 권한의 의미인 Authoritative가 붙는다.
    일반적으로 도메인/호스팅 업체의 '네임서버'를 말하지만, 개인이나 회사 DNS 서버 구축을한 경우에도 여기에 해당하게 된다.
  1. naver.com DNS 서버에는 "www.naver.com의 IP주소"가 있다. 그래서 Local DNS 서버에게 IP주소 222.122.195.6의 응답을 한다.

  2. 이를 수신한 Local DNS는 www.naver.com의 IP주소를 캐싱을 하고 이후 다른 요청이 있을 시 응답할 수 있도록 IP주소 정보를 단말(PC)에 전달해준다.

이렇게 Root DNS -> TLD DNS -> Authoritative DNS의 과정 즉, Local DNS가 여러 DNS에 차례대로 
요청하여 그 답을 찾는 과정을 "재귀적 쿼리(Recursive Query)"라고 부른다.

DNS 서버 종류

1. 기지국 DNS 서버

인터넷을 설치시 각각 통신사가 있다. 그리고 각각의 통신사마다 DNS 서버가 존재한다.

2. Root DNS 서버

Root DNS는 최상위 DNS서버로 해당 DNS부터 시작해서 아래 딸린 node DNS 서버에게로 차례차례 물어보게 되는 구조로 짜여져 있다.

트리구조로 되어 있기에 모든 DNS 서버들은 이 Root DNS Server의 주소를 기본적으로 갖고 있다.
그래서 모르는 Domain Name이 온다면 가장 먼저 Root DNS에게 물어보게 되는 것이다.


Root DNS Server에서의 목록에도 해당 Domain Name의 IP 정보가 없다면 다음 DNS 서버의 IP값을 리턴하는데, 이를 TLD(최상위 도메인) 서버이다.
만일 google.com라면 ".com"을 관리하는 TLD 서버에게 물어보라고 정보를 주는 것이다.

3. TLD 서버(Top-Level Domain, 최상위 도메인 서버)

이 루트도메인 바로 아래단계에 있는 것을 1단계 도메인이라고 하며 이를 TLD(최상위 도메인)이라고 한다.
TLD(최상위 도메인은 국가명을 나타내는 국가 최상위 도메인과 일반적으로 사용되는 일반 최상위 도메인으로 구분된다.


도메인을 구입할 경우 1단계의 도메인 중에 하나를 선태갛고 원하는 도메인명을 지정하여 등록한다.

.biz : 사업 
.com : 영리 목적의 기업이나 단체 
.co.국가로 쓰기도 한다.(co.kr 등) 
.edu : 미국의 4년제 이상 교육기관
.info : 정보 관련 
.jobs : 취업 관련 사이트 
.name : 개인 사용자 
.net : 네트워크를 관리하는 기관 
.org : 비영리 기관

4. Second-level DNS 서버(2차 도메인)

Root DNS 서버에서 return한 TLD 서버주소기지국 DNS서버에서 받아서 다시 TLD 서버에 요청을 했었다.
그리고 TLD 서버에서는 Second-level DNS 서버를 return 해준다.


만일 naver.com이나 google.com을 요청했다면, TLD 서버에서 .com을 파악하고 그 앞에 달린 문자열을 보고 네이버나 구글 서버에 요청을 하는 것이다.


그렇게 요청 받은 Second DNS 서버는 자체적으로 sub 도메인 서버로 또 넘기게 된다.

5. Sub DNS 서버(최하위 서버)

서브 도메인 서버는 www. dev. mail. cafe. 등등을 구분하는 최하위 서버를 말한다.
naver 서버라도 그 안에서 네이버 홈, 메일, 블로그, 카페 등 여러 서비스가 있다. 이 서비스들을 구분하는 도메인 네임이라고 보면 된다.

정리

예를 들어 www.naver.com을 접속한다고 가정하자.

  1. 사용자 브라우저: www.naver.com에 접속 시도
  2. 로컬 DNS 캐시 확인: 캐시에 정보가 없으면 DNS 서버로 쿼리 전송
  3. Root DNS 서버: '.com' TLD 서버 주소 반환
  4. TLD DNS 서버: naver.com의 2차 DNS 서버 주소 반환
  5. 2차 DNS 서버: www.naver.com에 대한 IP주소 또는 서브 도메인 서버 주소 반환.
  6. 서브 도메인 서버: (필요한 경우)최종 IP주소 반환

DNS 서버는 다음과 같은 구조로 되어 있다.

DNS 문자열 구조

도메인 URI는 다음과 같이 구성되어 있다.
blog.naver.com. 에서 "blog"는 sub, "naver"은 Second-level, "com"은 Top-level, "."은 Root이다.


모든 Computer들은 Root domain DNS server의 IP 주소는 알고 있다.

  • Root Domain을 담당하는 "DNS서버"에서는 TLD(Top-Level Domain)을 담당하는 서버목록과 IP를,
  • TLD(Top-Level Domain)을 담당하는 DNS 서버는 Second-Level Domain을 담당하는 서버 목록과 IP를,
  • Second-Level Domain을 담당하는 DNS 서버는 Sub Domain을 담당하는 서버 목록과 IP를 알고 있는 것이고,

결국, blog.naver.com의 IP주소는 Sub domain을 전담하고 있는 DNS 서버가 알고 있는 것이다.

DNS Cache

만일 위의 과정을 거쳐 www.gigigi.com의 IP 주소를 성공적으로 받았다고 하자.
몇 분후 다시 "www.naver.com"에 방문하려고 한다면, 또 다시 위의 같은 복잡한 과정을 반복해서 IP 주소를 받아오지 않는다.


그렇기에, PC에는 DNS Cache라는 Cache를 활용해 Cache안에 자주쓰는 Domain Name 주소를 저장해 놓는다.

출처

https://inpa.tistory.com/entry/WEB-%F0%9F%8C%90-DNS-%EA%B0%9C%EB%85%90-%EB%8F%99%EC%9E%91-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4-%E2%98%85-%EC%95%8C%EA%B8%B0-%EC%89%BD%EA%B2%8C-%EC%A0%95%EB%A6%AC

'인터넷 기본 지식' 카테고리의 다른 글

#8. LSP  (2) 2024.06.10
#7. 프로그래밍 패러다임  (1) 2024.06.09
#5. 웹 브라우저는 어떻게 작동하는가?  (0) 2024.05.31
#4 WAS, 웹 서버  (0) 2024.05.31
#3 HTTP/HTTPS  (0) 2024.05.31

브라우저란?

웹 브라우저는 동기(Synchronous)적으로 (HTML + CSS), Javascript 언어를 해석하여 내용을 화면에 보여주는 응용 소프트웨어이다.

웹 브라우저가 웹 서버에 필요한 자원(웹 페이지)를 요청하면 서버는 응답하고 웹 브라우저는 이를 해석한 후 사용자(Client)에게 보여준다.
보통 자원은 HTML문서지만, PDF, 이미지 등 다양한 형태일 수 있다.

브라우저의 구조

1. 사용자 인터페이스(UserInterface)

사용자가 접근할 수 있는 영역. URI를 입력할 수 있는 주소 표시줄, 이전/다음 버튼, 북마크 메뉴, 새로 고침 버튼, 현재 문서의 로드를 중단할 수 있는 정지 버튼, 홈 버튼 등 요청한 페이지를 보여주는 창 등의 모든 부분

2. 브라우저 엔진

사용자 인터페이스와 렌더링 엔진 사이에서 중개자 역할을 한다.
사용자 인터페이스의 주소 입력창에 github.com 을 입력했다고 하자. 그 다음에는 브라우저 엔진이 작동하게 된다.
만약 자료 저장소(Data Storage)에 데이터가 없다면 입력받은 URI 값을 렌더링 엔진에 전달해준다.

  • 웹 브라우저마다 전용 브라우저 엔진을 사용한다.
    1. Gecko: 파이어 폭스
    2. Webkit: 사파리
    3. Blink: 크롬, 오페라
    4. Trident: 마이크로소프트
-moz-border-radius: 1em; // 파이어폭스 브라우저에 적용
-ms-border-radius: 2em; // 익스플로어에 적용, 보통 생략
-o-border-radius: 3em; // 오페라에 적용
-webkit-border-radius: 4em; // 구글, 사파리 브라우저에 적용

3. 자료 저장소(Data Storage)

자료 저장소를 거치지 않고 URI를 입력할 때마다 서버로 가서 데이터를 받아오면, 불필요한 메서드 통신이 발생한다.
따라서 자료 저장소로부터 자주 다운로드 받는 데이터를 저장해두고, 사용할 때마다 서버로부터 데이터를 요청(통신)할 필요 없이 바로 받아올 수 있다. 캐싱이라고도 한다.

4. 통신

서버에게 HTTP 요청을 하고, 서버로부터 응답받은 데이터(HTML, CSS, JS)를 렌더링 엔진에 전달해준다.

5. 자바스크립트 해석기

자료 저장소 또는 통신 단계를 거쳐 데이터를 응답 받은 렌더링 엔진은 javascript 해석기를 통해 javascript를 파싱하게 된다.
Chrome에서는 V8이라는 javascript 엔진을 사용한다. 이것을 통해 javascript를 파싱하게 되는 것이다.

6. UI 백엔드

select, input 등 기본적인 위젯을 그리는 인터페이스이다.

7. 렌더링 엔진

렌더링 엔진은 요청받은 내용을 브라우저 화면에 표시해주는 역할을 한다. 브라우저마다 사용되는 렌더링 엔진이 각각 다르기에 모든 브라우저가 동일한 소스를 화면에 동일하게 그려주지 아니한다. 또한 엔진마다 읽을 수 있는 코드의 버전도 다르기에 크로스 브라우징(cross browsing) 이슈가 발생한다.

크로스 브라우징(cross browsing): 웹 페이지 제작 시 모든 브라우저에서 깨지지 않고 의도한 대로 올바르게 나오게 하는 작업을 말한다. 

렌더링 엔진의 동작 과정

 

'HTML 파싱 단계'와 '렌더 트리 구축, 배치, 페인팅' 단계는 병렬적으로 일어난다. 즉 DOM 트리가 완성이 되는 순간 바로바로 렌더 트리가 구축이 되고 그려지게 된다. 따라서 사용자는 DOM 트리가 완벽하게 구축될 때까지 기다릴 필요 없이 렌더 트리가 그려짐으로 인해서 브라우저가 렌더링 될 때 위에서부터 조금씩 표시되는 것이다.

단계 설명
HTML 파싱 1. 렌더링 엔진은 HTML 문서의 구문 분석(파싱) 2. 파싱된 요소를 '콘텐츠 트리'라는 트리의 'DOM 노드'로 변환 3. 외부 CSS 파일, 스타일 요소도 같이 파싱 4. 스타일 정보, HTML 표시 규칙, javascript 파싱 결과물은 '렌더 트리'를 만드는데 사용된다.
렌더 트리 생성 HTML, CSS를 파싱해서 만들어진 '렌더 트리'에는 색상, 면적과 같은 시각적 속성을 포함하며, 이것을 정해진 순서대로 렌더링 한다.
렌더 트리 배치(레이아웃) DOM node는 배치 정보를 갖고 있다. 화면에 정해진 순서대로 배치하는 '레이아웃 단계'를 거치게 된다. 생성 과정이 끝났을 때 이루어지며 노드가 화면에 정확한 위치에 표시되는 것을 의미한다.
렌더 트리 페인팅 node 배치가 끝나면 UI 백엔드에서 렌더 트리의 각 node들을 순회하고, 페인팅 작업을 하여 그리기를 완료하게 된다.

1. DOM(Document Object Model), CSSOM(CSS Object Model) 생성 -> Parsing

  • HTML을 파싱하여 DOM 노드를 만든다. 이 DOM 노드들을 병합하여 DOM 트리를 만든다.
  • CSS를 파싱하여, CSSOM(CSS Object Model) 트리를 만들게 된다.

브라우저는 렌더링 할 문서를 HTML과 CSS로 나눠서 읽는데 각각 HTML Parser, CSS Parser가 담당한다. 이후 이들은 각각 Object Model을 만든다.

HTML 파싱과 DOM 생성 과정

  1. 서버는 브라우저로부터 요청받은 HTML 파일을 읽은 후 메모리에 저장, 그리고 메모리에 저장된 바이트(1101010101010...)를 응답한다.
  2. 브라우저는 응답받은 바이트 형태의 문서를 meta태그의 charset 속성에 지정된 인코딩 방식(UTF-8)에 따라 문자열로 반환
  3. 문자열로 변환된 HTML문서를 이번에는 문법적 의미를 갖는 코드의 최소 단위인 토큰(token)으로 분해한다.
  4. 토큰들의 내용에 따라 객체로 변환하여 각 노드들을 생성한다.(문서 노드, 요소 노드. 속성 노드, 텍스트 노드)
  5. HTML은 요소 간의 부자 관계인 중첩 관계를 갖는데, 이를 반영하여 모든 노드들을 트리 구조로 구성하여 DOM을 만든다.
CSS파싱과 CSSOM 생성 과정

렌더링 엔진은 HTML 문서를 한줄 한줄 순차적으로 파싱하여 DOM을 생성. 이때 CSS를 로드하는 link태그, 혹은 style태그를 만나면 DOM 생성을 중지한 후 CSS파싱의 결과물인 CSSOM을 생성하는 과정을 진행

<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8"> // 여기까지 해석 후, 
  <link rel="stylesheet" href="style.css"> //link를 만나면 DOM생성을 중지하고 CSS파일을 서버에 요청한 후 응답받아 CSS파싱을 시작한다. 
  ...

css파싱 과정은 바이트 > 문자 > 토큰 > 노드 > CSSOM 생성 순으로 HTML의 파싱과정과 동일하다.

자바스크립트 파싱 과정

렌더링 엔진은 HTML 문서를 한 줄씩 순차적으로 파싱하다가 자바스크립트 파일을 로드하는 script 태그를 만나면 DOM 생성을 일시 중단한다.


script 태그의 src에 정의된 자바스크립트 파일을 서버에 요청하여 응답받으면 자바스크립트 코드를 파싱하기 위해 자바스크립트 엔진에게 제어권을 넘긴다.


자바스크립트 파싱이 끝나면 렌더링 엔진으로 다시 제어권을 넘기고 DOM 생성을 이어나간다.


만약 생성되지 않은 DOM을 조작한다면 에러가 발생할 수 있다. 따라서 body 요소 아래에 자바스크립트를 위치시키거나 DOM 생성이 완료된 시점에 자바스크립트가 실행되도록 한다.

  1. 자바스크립트 코드를 토크나이저가 어휘 분석하여 문법적 의미를 갖는 코드의 최소 단위인 토큰들로 분해하는데 이것을 토큰나이징이라 한다.
  2. parser가 토큰들을 구문분석하여 AST(Abstract Syntax Tree: 추상 구문 트리)로 파싱한다.
  3. 바이트 코드 생성기가 AST를 바이트코드로 변환한다.
  4. 인터프리터에 의해 바이트코드를 실행한다.

2. 렌더 트리 구축(Attachment)

CSSOM 트리와 DOM 트리를 결합하여, 표시해야 할 순서로 내용을 그려낼 수 있도록 하기 위해 렌더 트리를 형성한다. 이 과정을 Attachment라고 한다.


렌더 트리는 화면에 표시되는 각 노드의 위치를 계산하여 레이아웃에 사용되고 픽셀을 화면에 그리는 페인트 과정에도 사용된다.

렌더 트리의 생성을 위해 브라우저는 다음과 같은 작업을 한다.

  1. DOM 트리의 루트부터 노드 각각을 모두 탐색한다.
    • 이 때 화면에 표시되지 않는 일부 노드들(script, meta 태그 등...)은 렌더 트리에서 제외된다.
    • CSS 속성 중 'display:none' 같이 화면에서 숨겨지는 속성도 렌더 트리에서 반영되지 않는다.
  2. 화면에 표시되는 각 노드에 대해 적절하게 일치하는 CSSOM 규칙을 찾아 적용한다.
  3. 화면에 표시되는 노드를 콘텐츠 및 계산된 스타일과 함께 렌더트리로 생성된다.

3. 렌더 트리 배치(Layout of Reflow)

렌더 트리가 생성되고, 기기의 뷰포트 내에서 렌더 트리의 노드가 정확한 위치와 크기를 계산한다.
이때 모든 상대적인 값이 픽셀값으로 변환된다. CSS에 상대적인 값 %, rem, vh으로 할당된 값들은 절대적인 값인 px 단위로 변환된다.
이러한 배치 과정을 Layout 또는 Reflow라고 한다.

4. 렌더 트리 그리기(Paint)

렌더 트리의 각 노드를 화면의 실제 픽셀로 나타날 때 Painting메서드가 호출된다. Painting 과정 후 브라우저 화면에 UI가 나타나게 된다.

'인터넷 기본 지식' 카테고리의 다른 글

#7. 프로그래밍 패러다임  (1) 2024.06.09
#6. DNS(Domain Name System)  (0) 2024.05.31
#4 WAS, 웹 서버  (0) 2024.05.31
#3 HTTP/HTTPS  (0) 2024.05.31
#2 웹 페이지, 웹 사이트, 웹 서버 및 검색 엔진의 차이점  (0) 2024.05.30

웹서버와 WAS 서버의 비교

항목 웹서버 WAS서버
정의 정적인 컨텐츠(HTML, CSS, 이미지 등)를 제공하는 서버 동적인 컨텐츠(웹 애플리케이션)를 처리하고 제공하는 서버
기능 HTTP 프로토콜을 이용해 클라이언트에게 웹 페이지 제공 웹 애플리케이션 실행 및 데이터 처리, 웹 서버와 클라이언트 간의 중계 역할
주요 소프트웨어 Apache, NginX, IIS Tomcat, JBoss, WebLogic, WebSphere
     
실제 웹 서비스에서는 웹 서버와 WAS 서버가 함께 사용되는 경우가 많다.    

웹 서버

웹 브라우저 클라이언트로부터 HTTP 요청을 받아들이고 HTML 문서와 같은 웹 페이지를 반환하는 컴퓨터 프로그램

웹 서버는 사용자가 웹 브라우저에서 어떠한 페이지 요청을 하면 웹 서버에서 그 요청을 받아 정적 컨텐츠를 제공하는 서버이다. 여기서 정적 컨텐츠란 단순 HTML 문서, CSS, Javascript, 이미지, 파일 등 즉시 응답가능한 컨텐츠이다. 또한 웹 서버가 동적 컨텐츠를 요청 받으면 WAS에게 해당 요청을 넘겨주고, WAS에서 처리한 결과를 클라이언트(사용자)에게 전달해주는 역할도 한다.

대표적인 웹 서버로는 Apache가 있다.

웹 서버의 역할

웹 서버는 클라이언트가 웹 브라우저를 통해 요청한 정적 컨텐츠를 제공하는 역할을 한다. 웹 서버는 주로 HTTP 프로토콜을 사용하여 작동하며, 클라이언트가 URL을 통해 요청한 웹 페이지를 찾아 전송해준다.

e.g. 회사 홈페이지, 블로그, 뉴스 사이트 등

WAS 서버

인터넷 상에서 HTTP 프로토콜을 통해 사용자 컴퓨터나 장치에 애플리케이션을 수행해주는 미들웨어로서, 주로 동적 서버 컨텐츠를 수행하는 것으로 웹 서버와 구별이 되며, 주로 DB 서버와 같이 수행한다.

WAS는 웹 서버와 웹 컨테이너가 합쳐진 형태이다. 웹 서버 단독으로는 처리할 수 없는 DB의 조회나 다양한 로직 처리가 필요한 동적 컨텐츠를 제공한다. WAS는 JSP, Servlet 구동환경을 제공해주기 때문에 웹 컨테이너 혹은 서블릿 컨테이너라고도 불린다.

대표적인 WAS 종류로는 Tomcat이 있다.

웹 컨테이너: 웹 서버가 보낸 JSP, PHP 등의 파일을 수행한 결과를 다시 웹 서버로 보내주는 역할을 함

WAS 서버의 역할

WAS 서버는 웹 애플리케이션을 실행하여 동적 컨텐츠를 생성하고, 웹 서버와 클라이언트 간의 데이터 처리를 담당하는 역할을 한다. WAS 서버는 클라이언트의 요청에 따라 DB에서 정보를 가져오거나, 웹 애플리케이션을 실행하여 동적인 웹 페이지를 생성한 후 결과를 웹 서버에 전달한다.
웹 서버는 이를 받아 클라이언트에게 전달한다.

e.g. 온라인 쇼핑몰, 은행 인터넷 뱅킹, SNS 등

Web Service Architecture

웹 어플리케이션은 요청 처리 방식에 따라 다양한 구조를 가질 수 있다.

  1. 클라이언트(사용자) -> 웹 서버 -> DB
  2. 클라이언트(사용자) -> WAS -> DB
  3. 클라이언트(사용자) -> 웹 서버 -> WAS -> DB

Web Server와 WAS를 구분하는 이유

  • Web Server가 필요한 이유: Web Server에서는 정적 컨텐츠만 처리하도록 기능을 분배하여 서버의 부담을 줄일 수 있다.
    • 클라이언트에 정적 컨텐츠(이미지, 문서...)를 보낸다고 하자. 이미지 파일과 같은 정적인 파일들은 웹 문서(HTML문서)가 클라이언트로 보내질 때 함께 가는 것이 아니다. 클라이언트는 HTML 문서를 먼저 받고 그에 맞게 필요한 이미지 파일들을 다시 서버로 요청하면 그때서야 이미지 파일을 받아온다.
    • Web Server를 통해 정적인 파일들을 Application Server까지 가지 않고 앞단에서 빠르게 보내줄 수 있다.
  • WAS가 필요한 이유: WAS를 통해 요청에 맞는 데이터를 DB에서 가져와서 비즈니스 로직에 맞게 그때 그때 결과를 만들어서 제공함으로써 자원을 효율적으로 사용할 수 있다.
    • Web Server만을 이용한다면 사용자가 원하는 요청에 대한 결과값을 모두 미리 만들어 놓고 서비스를 해야한다. 허나 이를 수행하기 위한 자원은 절대적으로 부족하다.
    • WAS가 Web Server의 기능도 모두 수행하면 되지 않는가?
      1. 기능을 분리하여 서버 부하 방지: 정적 컨텐츠 요청까지 WAS가 처리한다면 정적 데이터 처리로 인해 부하가 커지게 되고, 동적 컨텐츠의 처리가 지연됨에 따라 수행 속도가 느려진다.
        1. 물리적으로 분리하여 보안 강화: SSL에 대한 암복호화 처리에 Web Server를 사용
        2. 여러 대의 WAS를 연결 가능
        • Load Balancing을 위해서 Web Server를 사용
        • fail over(장애 극복), fail back 처리에 유리
        • 큰 서비스의 경우 Web Server와 WAS를 분리하여, 또 여러 대의 서버를 사용하여 장애에 쉽게 대응할 수 있다.
        • 앞 단의 Web Server에서 오류가 발생한 WAS를 이용하지 못하도록 한 후 WAS를 재시작함으로써 사용자는 오류를 느끼지 못하고 이용할 수 있다.
load balancing: 애플리케이션을 지원하는 리소스 풀 전체에 네트워크 트래픽을 균등하게 배포하는 방법 

HTTP(Hyper Text Transfer Protocol)

웹 상에서 브라우저와 서버가 데이터를 주고 받을 때 사용하는 프로토콜

이름에 Hypertext가 포함되어 있으나 텍스트, 미디어 등의 데이터도 전송할 수 있다.

웹 브라우저와 웹 서버의 소통을 위해 디자인되었으며, 전통적인 클라이언트-서버 아키텍처 모델에서 클라이언트가 HTTP 메시지 양식에 맞춰 요청을 보내면, 이에 서버는 HTTP 메서지 양식에 맞춰 응답을 한다.

프로토콜: 통신 규악이라는 뜻으로 데이터를 주고 받는 방식에 대한 교칙

HTTP 동작 방식

HTTP는 서버/클라이언트 모델을 따른다. 클라이언트가 서버에 요청을 보내면 서버는 요청에 맞는 응답을 클라이언트에게 보낸다.

  1. connect: 클라이언트가 원하는 서버에 접속
  2. request: 클라이언트가 서버에게 원하는 요청을 보냄
  3. response: 서버가 요청에 대한 결과를 클라이언트에게 보내고 응답
  4. close: 응답이 끝나면 서버와 클라이언트 연결 종료(Stateless)
  5. HTTP 특징
  • HTML 통신은 클라이언트의 요청(Request) 와 그에 대한 서버의 응답(Response)으로 이루어진다.
  • 어떤 종류의 데이터라도 전송이 가능하다. HTML문서 말고도 단순 텍스트나 이미지, 오디오 등의 미디어 데이터도 전송 가능
  • TCP/IP를 이용하는 응용 프로토콜로 80번 포트를 사용한다.
    • TCP에서는 바이트 스트림(Byte Stream) 서비스를 제공한다. 즉 큰 데이터를 잘게 쪼갠 뒤 전송하는 서비스다.
    • 정확히 연결되었는지 확인하기 위해서 3 hand-shaking을 활용한다. 그렇기에 TCP는 신뢰성을 담당한다.
    • IP에서는 데이터 패킷들을 목적지에 전달한다.
    • 이 때 전달 주소를 IP주소로 아는 것이 아닌, MAC 주소로 구분한다. 둘을 같이 쓰는데 계층형인 IP주소를 가고자 하는 주소의 "방향"을 알 수 있다. 여기서 ARP(Address Resolution Protocol)를 사용한다.
  • 과거: 비연결성(Connectionless) 프로토콜이다. Connectionless는 한 가지 요청에 대한 응답을 받으면 그 연결을 끊어버리는 것을 의미한다. 이는 연결에 대한 리소스를 줄일 수 있는 장점이 있지만 같은 클라이언트에서 오는 요청도 계속 연결/해제 해야 한다는 단점이 있다. 그렇기에 클라이언트의 이전 상태를 서버가 알 수 없다.(stateless)
    (이를 해결하기 위해 cookiesession이 등장하였다.)
  • 현재: 지속연결 프로토콜로 바뀌었다. 지속 연결을 통해 서버의 부하를 줄이고 통신속도를 높였다. 여기서 리스폰을 기다리지 않고 바로 다음을 리퀘스트를 보내는 파이프라인(pipeline)도 가능하다. 허나 이는 성능향상이 미미하기에 잘 사용되지 않는다.
Hand-shaking: 두 호스트가 서로 연결할 때에 필요한 정보들을 주고받는 일련의 과정들
    1. 요청(SYN) : 내 목소리 들려? 들려? 들려? 응답할 때 까지 지정횟수 만큼 요청
    2. 응답1(ACK) : 응 들려
    3. 응답 2(ACK): 나도 들려! 이후 요청

IP주소: 컴퓨터 주소가 아니라 LAN Card에 연결되어 있는 회선(랜선)의 주소를 말한다. 즉 고정되어 있는 것이 아니라 인터넷맘에 접속할 때마다 달라진다. (사람 이름)

MAC주소: MAC주소는 LAC Card의 주소를 말한다. 네트워크 상에서 서로를 구분하기 위해서 Device 마다 할당된 물리적인 주소이다. 인터넷이 가능한 장비(PC, 휴대폰)들이 가지고 있는 물리적인 주소라고 생각할 수 있다.(사람 주민번호)

ARP(Address Resolution Protocol, ARP): 네트워크 상에서 IP주소를 물리적 네트워크 주소로 대응(bind)시키기 위해 사용되는 프로토콜이다. 즉 IP주소를 MAC 주소와 매칭시키기 위한 프로토콜이다.

HTTP 메시지(HTTP Message)

HTTP 메시지의 요청과 응답은 유사한 구조를 가진다.

  • Start line: 요청의 상태를 나타내며, 항상 첫 번째 줄에 위치한다.
  • Status line: 응답의 상태를 나타내며, 항상 첫 번째 줄에 위치한다.
  • HTTP headers: 요청을 지정하거나 메시지에 포함된 본문을 설명하는 헤더의 집합이다.
  • empty line: 헤더와 본문을 구분하는 빈 줄이다.
  • body: 요청과 관련된 데이터나 응답과 관련된 데이터 또는 문서를 포함한다.

요청(Requests)

HTTP Requests는 클라이언트가 서버에 보내는 메시지다.

Start line

  • 수행할 작업(GET, PUT, POST 등)이나 방식(HEAD, OPTIONS)을 설명하는 HTTP 메서드를 나타낸다.
  • 요청 대상(URL 또는 URI) 또는 프로토콜, 포트, 도메인의 절대 경로 등은 요청 컨텍스트에 작성되며 HTTP 메서드마다 다르게 작성된다.
  • HTTP 버전에 따라 HTTP 메서드의 구조가 달라진다. 따라서 start line에 HTTP 버전이 함께 입력된다.
URI(Unifrom Resource Identifier): 자원의 위치 뿐만 아니라 자원에 대한 고유 식별자로서 URL 의미를  포함한다.
    e.g. https://naver.com/mail?page=12
URL(Uniform Resource Locator): Resource의 정확한 위치 정보(파일의 위치)를 나타낸다.
    e.g. https://naver.com/mail

Headers

HTTP Requests의 Headers는 기본 구조를 따른다.
헤더의 이름, 콜론, 값 형태로 입력되며 값은 헤더에 따라 다르다.

  • 일반 헤더(General headers): 메시지 전체에 적용되는 헤더로 body를 통해 전송되는 데이터와는 관련이 없는 헤더이다.
  • 요청 헤더(Request headers): fetch를 통해 가져올 리소스나 클라이언트 자체에 대한 자세한 정보를 포함하는 헤더를 의미한다.
  • 표현 헤더(Representation headers): body에 담긴 리소스의 정보(컨텐츠 길이, mime 타입 등)를 포함하는 헤더이다.

Body

Body는 요청의 본문으로 HTTP 메시지 구조의 마지막에 위치한다. 모든 요청에 body가 필요한 것은 아니다.

GET, HEAD, DELETE, OPTIONS처럼 서버에 리소스를 요청하는 경우에는 본문이 필요하지 않다.
단, POST나 PUT과 같은 일부 요청에 대해서는 데이터를 업데이트하기 위해 body를 사용한다.

응답(Responses)

HTTP Responses는 클라이언트의 요청을 서버가 응답하는 것이다.

Status line

현재 프로토콜의 버전, 요청의 결과를 나타내는 상태 코드, 그리고 상태 코드에 대한 설명을 나타내는 상태 텍스트가 담겨있다.

Headers

  • 일반 헤더(General headers): 메시지 전체에 적용되는 헤더로, body를 통해 전송되는 데이터와는 관련이 없는 헤더이다.
  • 응답 헤더(Response headers): 위치 또는 서버 자체에 대한 정보(이름, 버전 등)와 같이 응답에 대한 부가적인 정보를 갖는 헤더이다. Vary, Accept-Ranges와 같은 상태 줄에 넣기에는 공간이 부족한 추가 정보를 제공한다.
  • 표현 헤더(Representation headers): body에 담긴 리소스의 정보(컨텐츠 길이, MIME 타입 등)를 포함하는 헤더이다.

body

응답의 바디에는 HTTP 메시지 구조의 마지막에 위치한다. 모든 응답에 body가 필요하지는 않다. 201, 204와 같은 상태 코드를 가지는 응답에는 본문을 필요로 하지 않는다.

HTTP Method

HTTP를 이용하여 클라이언트에서 서버로 요청을 보낼 때 어떠한 목적으로 요청을 하는 것인지 정의를 내릴 수 있다.
예를 들어 '단순 조회'를 목적으로 하는지, '데이터 수정'을 목적으로 하는지 등을 미리 정의내려서 요청을 보낼 수 있는데 이 때 HTTP Method라는 것을 사용한다.

이름 역할
GET 서버에게 데이터를 달라는 요청을 할 때 사용
HEAD GET과 같지만 서버가 응답할 때 Body없이 Header만 리턴
POST 서버에게 데이터를 전송하는 요청할 때 사용
PUT 서버에서 요청 URI의 데이터를 수정하거나 새로 추가하도록 요청할 때 사용
PATCH 서버의 데이터를 일부 수정할 때 사용
DELETE 서버에서 요청 URI의 데이터를 삭제하도록 요청할 때 사용
TRACE 클라이언트로부터 수신한 요청을 응답에 포함시켜서 전달(디버깅용)
OPTIONS 서버에게 특정 데이터가 어떤 Method를 지원하는지 알아볼 때 사용

HTTP 상태 코드

HTTP Status 코드라고 불리며, 서버가 응답을 전송할 때 같이 전송하는 코드이다.
앞 자리는 1~5의 숫자 중 하나이며 이 중 4와 5는 비정상적인 상황, 즉 오류가 있음을 의미한다.

MDN의 상태코드
MDN의 상태코드-고양이

  1. 1xx - 정보 응답
    • 100 continue: 현재 요청이 진행중이며 문제 없다는 것을 의미한다.
  2. 2xx - 성공 응답
    • 200 OK: 요청이 성공적으로 완료되었음을 의미한다.
    • 201 Created: 요청이 성공적으로 완료되었고 새로운 리소스가 생성되었음을 의미한다. 보통 POST아니면 PUT요청이 뒤에 따라온다.
  3. 3xx - 리다이렉션 메시지
    • 300 Multiple Choice: 요청에 대해 하나 이상의 응답이 가능함을 의미한다.
    • 301 Moved Permanently: 요청한 리소스의 URI가 변경되었음을 의미한다.
  4. 4xx - 클라이언트 에러 응답
    • 400 Bad Request: 잘못된 문법으로 인해 서버가 요청을 이해하지 못했음을 의미한다.
    • 401 Unauthorized: 요청을 보낸 클라이언트가 인증되지 않았음을 의미한다.
    • 403 Forbidden: 요청을 보낸 클라이언트가 리소스에 접근할 권리가 없음을 의미한다.
    • 404 Not Found: 서버가 요청받은 리소스를 찾을 수 없음을 의미한다.
    • 408 Request Timeout: 요청 중 시간이 초과되었음을 의미한다.
    • 418 I'm a Teapot: 서버가 찻주전자이기 때문에 커피 내리기를 거절했다는 의미이다.
  5. 5xx - 서버 에러 응답
    • 500 Internal Server Error: 서버에 문제가 있지만 서버가 해당 문제를 처리할 줄 모름을 의미한다.
    • 502 Bad Gateway: 서버가 게이트웨이로부터 잘못된 응답을 받았음을 의미한다.
    • 503 Service Temporarily Unavailable: 일시적으로 서버를 이용할 수 없음을 의미한다. 보통 유지보수를 위해 서버를 잠시 중단시켰거나 과부하로 인한 다운이 원인이다.
    • 504 Gateway Timeout: 서버가 게이트웨이 역할을 하고 있으며 다른 서버로부터 적시에 응답을 받지 못했음을 의미한다.

HTTPS란?

HyperText Transfer Protocol over Secure Socket Layer의 약자인 HTTPS는 HTTP에 데이터 암호화가 추가된 프로토콜이다. HTTPS는 HTTP와 다르게 443번 포트를 사용하며, 네트워크 상에서 중간에 제 3자가 정보를 볼 수 없도록 암호화를 지원하고 있다.

대칭키 암호화와 비대칭키 암호화

HTTPS에서는 대칭키 암호화 방식과 비대칭키 암호화 방식을 모두 사용하고 있다.

  • 대칭키 암호화
    • 클라이언트와 서버가 동일한 키를 사용해 암호화/복호화를 진행함
    • 키가 노출되면 매우 위험하지만 연산 속도가 빠름
  • 비대칭키 암호화
    • 1개의 쌍으로 구성된 공개키와 개인키를 암호화/복호화 하는데 사용함
    • 키가 노출되어도 비교적 안전하지만 연산 속도가 느림

비대칭키 암호화

비대칭키 암호화는 공개키/개인키 암호화 방식을 이용해 데이터를 암호화하고 있다. 공개키와 개인키는 서로를 위한 1쌍의 키이다.

  • 공개키: 모두에게 공개가능한 키
  • 개인키: 나만 가지고 알고 있어야 하는 키
    암호화를 공개키로 하느냐 개인키로 하느냐에 따라 상황이 달라진다.
  • 공개키 암호화: 공개키로 암호화를 하면 개인키로만 복화화할 수 있다. -> 개인키는 나만 가지고 있으므로, 나만 볼 수 있다.
  • 개인키 암호화: 개인키로 암호화하면 공개키로만 복호화할 수 있다. -> 공개키는 모두에게 공개되어 있으므로, 내가 인증한 정보임을 알려 신뢰성을 보장할 수 있다.

HTTPS의 동작 과정

HTTPS는 대칭키 암호화와 비대칭키 암호화를 모두 사용하여 빠른 연산 속도와 안전성을 모두 얻고 있다.
HTTPS 연결 과정(Hand-Shaking)에서는 서버와 클라이언트 간에 세션키를 교환한다. 여기서 세션키는 주고 받는 데이터를 암호화하기 위해 사용되는 대칭키이며, 데이터 간의 교환에는 빠른 연산 속도가 필요하므로 세션키는 대칭키로 만들어진다.

이 때 대칭키, 즉 세션키를 서버, 클라이언트 사이에 공유하는 과정에서 비대칭키가 사용된다.
즉, 처음 연결을 성립하여 안전하게 세션키를 공유하는 과정에서 비대칭키가 사용되는 것이고, 이후에 데이터를 교환하는 과정에서 빠른 연산 속도를 위해 대칭키가 사용되는 것이다.

실제 HTTPS 연결 과정이 성립되는 흐름은 다음과 같다.

  1. 클라이언트가 서버로 최초 연결 시도를 함
  2. 서버는 공개키(엄밀하게는 인증서)를 브라우저에게 넘겨줌
  3. 브라우저는 인증서의 유효성을 검사하고 세션키를 발급함
  4. 브라우저는 세션키를 보관하며 추가로 서버의 공개키로 세션키를 암호화하여 전송함
  5. 서버는 개인키로 암호화된 세션키를 복호화하여 세션키를 얻음
  6. 클라이언트와 서버는 동일한 세션키를 공유하므로 데이터를 전달할 때 세션키로 암호화/복호화를 진행함

HTTPS의 발급 과정

서버는 클라이언트와 세션키를 공유하기 위한 공개키를 생성해야 하는데, 일반적으로는 인증된 기관(Certificate Authority)에 공개키를 전송하여 인증서를 발급받는다.

  1. A기업은 HTTP 기반의 애플리케이션에 HTTPS를 적용하기 위해 공개키/개인키를 발급함
  2. CA 기업에게 돈을 지불하고, 공개키를 저장하는 인증서의 발급을 요청함
  3. CA 기업은 CA기업의 이름, 서버의 공개키, 서버의 정보 등을 기반으로 인증서를 생성하고, CA 기업의 개인키로 암호화하여 A기업에게 이를 제공함
  4. A기업은 클라이언트에게 암호화된 인증서를 제공함.
  5. 브라우저는 CA기업의 공개키를 미리 다운받아 갖고 있어, 암호화된 인증서를 복호화함
  6. 암호화된 인증서를 복호화하여 A기업의 공개키로 세션키를 공유함

인증서는 CA의 개인키로 암호화되었기에 신뢰성을 확보할 수 있다.
클라이언트는 A 기업의 공개키로 데이터를 암호화하였기 때문에 A기업만 복호화하여 원본의 데이터를 얻을 수 있다.
여기서 인증서에는 A 기업의 공개키가 포함되어 있으므로, A 기업의 공개키라고 봐도 무방하다.
또한 브라우저에는 인증된 CA 기관의 정보들이 사전에 등록되어 있어 인증된 CA기관의 인증서가 아닐 경우 다음과 같은 형태로 브라우저에서 보여지게 된다.

HTTP/HTTPS

HTTP는 암호화가 추가되지 않았기에 보안이 취약하고 HTTPS는 안전하게 데이터를 주고받을 수 있다. 허나 HTTPS의 경우 암호화/복호화 과정이 필요하기에 HTTPS보다 속도가 느리다. 또한 HTTPS는 인증서를 발급하고 유지하기 위한 추가 비용이 발생한다.

개인정보와 같은 민감한 데이터를 주고 받아야 한다면 HTTPS를, 노출이 되어도 괜찮은 단순한 정보 조회 등 만을 처리하고 있다면 HTTP를 이용하면 된다.

ref

https://jin-network.tistory.com/46
https://mangkyu.tistory.com/98
https://seing.tistory.com/183?category=552898
https://tibetsandfox.tistory.com/18
https://ittrue.tistory.com/192
https://jaehoney.tistory.com/290

웹 페이지

웹페이지는 브라우저에 표시 될 수 있는 간단한 문서이다. 이러한 문서는 HTML언어로 작성된다. 웹페이지에는 다음과 같은 유형의 리소스가 포함된다.

  • 스타일(css)
  • 스크립트(js,ts)
  • 미디어: 이미지, 사운드, 비디오

웹 사이트

웹 사이트는 고유한 도메인 이름을 공유하는 웹페이지의 모음이다. 특정 웹사이트의 각 웹페이지는 사용자가 그 사이트의 다른 페이지로 이동할 수 있는 (클릭가능한)링크를 제공한다.

웹 서버

웹 서버는 하나 이상의 웹사이트를 호스팅하는 컴퓨터이다. "호스팅"은 모든 웹 페이지와 해당 지원을 해당 컴퓨터에서 사용할 수 있음을 의미한다. 웹 서버는 사용자 요청에 따라 호스팅하는 웹 사이트의 모든 웹 페이지를 모든 사용자의 브라우저로 보낸다.
만일 "웹 사이트가 응답하지 않아요!" 라고 말하는 경우는 "웹 서버"가 응답하지 않아 "웹 사이트"를 이용할 수 없다는 의미이다.

검색 엔진

검색 엔진은 사람들이 다른 웹사이트를 찾는데 도움을 주는 웹사이트이다. 참고로 브라우저는 웹 페이지를 표시하는 소프트웨어다.

'인터넷 기본 지식' 카테고리의 다른 글

#6. DNS(Domain Name System)  (0) 2024.05.31
#5. 웹 브라우저는 어떻게 작동하는가?  (0) 2024.05.31
#4 WAS, 웹 서버  (0) 2024.05.31
#3 HTTP/HTTPS  (0) 2024.05.31
#1. 인터넷은 어떻게 동작하는가?  (0) 2024.05.30

+ Recent posts