HTTP 헤더란?
- HTTP 요청과 응답 메시지에 포함되는 메타 데이터
- 키-값 쌍 형태로 구성되어 있다.
- 클라이언트와 서버 간에 추가 정보를 전달하는 데 사용된다.
HTTP 헤더의 역할
- 요청 및 응답 정보 제공
- 요청 메시지의 헤더는 클라이언트가 요청하는 리소스에 대한 정보를 제공한다.
- 응답 메시지의 헤더는 서버가 전송하는 리소스에 대한 정보를 제공한다.
- 통신 제어
- 헤더는 HTTP 통신을 제어하는 데 사용된다.
- 캐싱, 인증, 압축 등을 설정하는 데 사용된다.
- 확장성
- HTTP 헤더는 새로운 기능을 추가하기 위해 사용될 수 있다.
HTTP 헤더의 종류
- 일반 헤더
- 요청과 응답 모두에 적용되는 헤더
- Host, Connection, Cache-Control 등이 해당한다.
- 요청 헤더
- 클라이언트가 서버로 보내는 헤더
- User-Agent, Accept, Content-Type 등이 해당한다.
- 응답 헤더
- 서버가 클라이언트로 보내는 헤더
- Content-Length, Content-Type, Location 등이 해당한다.
표현 헤더
- 요청과 응답 모두에 적용되는 헤더
Content-Type
- 표현 데이터의 형식을 나타낸다.
- 미디어 타입이나 문자 인코딩을 나타낸다.
- 예시
text/html; charset=uft-8
application/json
image/png
Content-Encoding
- 표현 데이터의 압축 방식을 나타낸다.
- 데이터를 전달하는 곳에서 압축 후 인코딩 헤더를 추가한다.
- 데이터를 읽는 쪽에서 인코딩 헤더의 정보로 압축을 해제한다.
- 예시
gzip
deflate
identity
Content-Language
- 표현 데이터의 자연 언어를 나타낸다.
- 예시
ko
en
en-US
Content-Length
- 표현 데이터의 길이를 나타낸다.
- 바이트 단위로 나타낸다.
- Transfer-Encoding(전송 코딩)을 사용하면 Content-Length를 사용하면 안 된다.
협상 헤더 (= 컨텐츠 네고시에이션)
- 협상 헤더는 요청시에만 사용한다.
- 클라이언트가 선호하는 표현 요청을 나타낸다.
Accept
- 클라이언트가 선호하는 미디어 타입을 나타낸다.
Accept-Charset
- 클라이언트가 선호하는 문자 인코딩을 나타낸다.
Accept-Encoding
- 클라이언트가 선호하는 압축 인코딩을 나타낸다.
Accept-Language
- 클라이언트가 선호하는 자연 언어를 나타낸다.
협상과 우선순위
- Quality Values(q)를 통해 우선순위를 나타낸다.
- 0 ~ 1 사이의 값을 사용한다.
- 값이 클수록 높은 우선순위를 가진다.
- 생략하면 1과 같은 의미를 가진다.
- 구체적인 것이 우선순위를 가진다.
- 예시 :
*/*
와tex/plain
이 있다면text/plain
이 우선순위를 가진다.
- 예시 :
전송 방식 헤더
- 클라이언트와 서버 간의 데이터 전송 방식을 제어하고 최적화하는 데 사용된다.
Content-Length (단순 전송)
- 가장 기본적인 전송 방식
- HTTP 헤더에서 요청 또는 응답 본문의 길이(바이트)를 나타낸다.
- 서버는 Content-Length를 사용하여 클라이언트가 받아야 할 데이터의 양을 알려준다.
- 클라이언트는 Content-Length를 사용하여 데이터 수신 완료 여부를 판단한다.
Content-Encoding (압축 전송)
- 서버가 응답 본문을 압축하는 데 사용한 알고리즘을 나타낸다.
- 압축은 데이터 전송량을 줄여 네트워크 속도를 향상시키는 데 도움이 된다.
- 예시
gzip
- 가장 일반적으로 사용되는 압축 알고리즘
deflate
- gzip보다 약간 더 효율적이지만 CPU 사용량이 더 높음
br
- Brotli 알고리즘
- gzip 및 deflate보다 더 높은 압축률 제공
Transfer-Encoding (분할 전송)
- 서버가 응답 본문을 어떻게 전송하는지 나타낸다.
- 예시
chunked
- 본문을 여러 개의 청크로 나누어 전송
identity
- 본문을 압축하지 않고 그대로 전송
Range, Content-Range (범위 전송)
Range
- 클라이언트가 서버에게 요청하는 특정 범위의 데이터를 나타낸다.
- 대규모 파일을 다운로드할 때 특정 부분만 선택적으로 다운로드하는 데 유용하게 사용된다.
- 예시 :
Range: bytes=0-1023
=> 파일의 처음 1024 바이트만 요청
Content-Range
- 서버가 지원하는 데이터 범위 요청 방식을 나타낸다.
- 예시
bytes
일반 정보 헤더
- HTTP 일반 정보 헤더는 요청 및 응답 메시지 모두에 적용되는 헤더
- 메시지 전체에 대한 정보를 제공하며, 통신 제어, 캐싱, 연결 관리 등에 사용된다.
Connection
- HTTP 연결 유형을 나타낸다.
- 예시
keep-alive
- 서버와 클라이언트 간의 연결을 유지하여 여러 요청을 처리한다.
close
- 각 요청 후 연결을 닫는다.
Cache-Control
- 캐싱 정책을 설정한다.
- 예시
max-age
- 캐시 유효기간 설정
- 단위 : 초
no-cache
- 캐싱 금지
no-store
- 서버에서 응답을 저장하지 않도록 설정
Pragma
- 캐싱 및 기타 프록시 지침을 나타낸다.
- 예시
no-cache
- 캐시 금지
Date
- 메시지 생성 시간을 나타낸다.
- 응답에서 사용한다.
- 예시
Date: Tue, 15 Nov 2023 12:00:00 GMT
Via
- 프록시 정보를 나타낸다.
- 예시
Via: 1.1 proxy.example.com (squid/3.1.23)
Warning
- 경고 메시지를 나타낸다.
- 예시
Warning: 199 Miscellaneous warning
Upgrade
- 프로토콜 업그레이드를 요청하거나 승인한다.
- 예시
Upgrade: HTTP/2.0
Upgrade: HTTP/3.0
Keep-Alive
- 지속적인 연결을 설정한다.
- 예시
Keep-Alive: timeout=5, max=100
Referer
- 현재 요청된 페이지의 이전 웹 페이지의 주소를 나타낸다.
- A에서 B로 이동하는 경우 B를 요청할 떄
Referer: A
를 포함해서 요청한다. - Referer를 통해서 유입 경로를 분석할 수 있다.
- 요청에서 사용한다.
User-Agent
- 유저 에이전트 애플리케이션 정보를 나타낸다.
- 클라이언트의 애플리케이션 정보를 나타낸다.
- 웹 브라우저 정보 등
- 어떤 브라우저에서 장애가 발생하는지 파악할 수 있다.
- 요청에서 사용한다.
- 예시
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
Server
- 요청을 처리하는 오리진 서버의 소프트웨어 정보를 나타낸다.
- 응답에서 사용한다.
- 예시
Server: Apache/2.2.22 (Debian)
server: nginx
특별한 정보 헤더
- 요청 또는 응답 메시지에 대한 추가 정보를 제공하는 헤더
- 클라이언트와 서버 간의 통신을 제어하고 특정 기능을 구현하는 데 사용된다.
Host
- 요청한 호스트 정보 (도메인)
- 요청에서 사용한다.
- 필수
- 하나의 서버가 여러 도메안을 처리해야할 때 필요하다.
- 하나의 IP 주소에 여러 도메인이 적용되어 있을 때 필요하다.
- 예시
Host: www.google.com
Location
- 리다이렉션될 페이지를 나타낸다.
- 웹 브라우저는 3XX 응답 결과에 Location 헤더가 있으면 Location의 위치로 자동 이동한다.
Allow
- 허용 가능한 HTTP 메서드를 나타낸다.
- 405 Method Not Allowed에서 응답에 포함해야 한다.
- 예시
Allow: GET, HEAD, PUT
Retry-After
- 유저 에이전트가 다음 요청을 하기까지 기다려야 하는 시간을 나타낸다.
- 503 Service Unavailable에서 응답에 포함해야 한다.
- 예시
Retry-After: Fri, 31 Dec 1999 23:59:59 GMT
=> 날짜 표기Retry-After: 120
=> 초단위 표기
인증 헤더
Authorization
- 서버에 전달할 클라이언트 인증 정보를 나타낸다.
- 클라이언트가 서버에 자격 증명을 제공하는 데 사용된다.
- 인증 방식
- Basic:
- 사용자 이름과 비밀번호를 Base64로 인코딩하여 전송한다.
- Digest
- MD5 해시를 사용하여 사용자 이름과 비밀번호를 안전하게 전송한다.
- Bearer
- OAuth 토큰을 사용하여 인증한다.
- Basic:
- 예시
Authorization: <인증 방식> <자격 증명>
Authorization: Basic xxxxxxxxxxxxxxxx
WWW-Authenticate
- 리소스 접근시 필요한 인증 방법을 나타낸다.
- 401 Unauthorized 응답과 함께 사용한다.
- 인증 방식
- Basic
- 서버가 Basic 인증을 요청한다.
- Digest
- 서버가 Digest 인증을 요청한다.
- Basic
- 추가 정보
- realm
- 인증 영역을 나타낸다.
- nonce
- 서버에서 생성한 임의 문자열을 나타낸다.
- qop
- 질문-응답 인증을 사용할지 여부를 나타낸다.
- realm
- 예시
WWW-Authenticate: <인증 방식> <추가 정보>
WWW-Authenticate: Newauth realm="apps", type=1, title="Login to \"apps\"", Basic realm="simple"
Proxy-Authorization
- 프록시 서버에 전달할 클라이언트 인증 정보를 나타낸다.
- 인증 방식
- Basic
- 프록시 서버 인증에 Basic 인증을 사용한다.
- Digest
- 프록시 서버 인증에 Digest 인증을 사용한다.
- Basic
- 예시
Proxy-Authorization: <인증 방식> <자격 증명>
Proxy-Authorization: Basic xxxxxxxxxxxxxxxx
쿠키 헤더
- HTTP 쿠키 헤더는 클라이언트와 서버 간의 상태 정보를 유지하는 데 사용된다.
쿠키
: 서버에서 클라이언트로 전송된 작은 데이터 조각- 클라이언트는 이후 요청에 쿠키를 포함하여 서버에 다시 전송한다.
- 활용 방법
- 사용자 인증 및 세션 관리
- 사용자 설정 저장
- 웹사이트 분석
- 추적 및 타겟팅 광고
- 주의사항
- 쿠키는 개인 정보를 포함할 수 있으므로 보안에 유의해야 한다.
- 쿠키를 사용하면 사용자 추적이 가능하므로 개인정보 보호 측면에서 주의해야 한다.
동작 방식
- 서버는 Set-Cookie 헤더를 사용하여 클라이언트로 쿠키를 전송한다.
- 클라이언트는 브라우저에 쿠키를 저장한다.
- 클라이언트가 동일한 서버에 요청을 보낼 때마다 브라우저는 쿠키를 요청 헤더에 포함하여 서버에 다시 전송한다.
- 서버는 쿠키를 사용하여 클라이언트를 식별하고 이전 요청과 현재 요청을 연결한다.
Set-Cookie
- 서버에서 클라이언트로 쿠키를 전송할 때 사용한다.
- 예시
Set-Cookie: nickname=honggildong; expires=date; path=/
Cookie
- 클라이언트에서 서버로 쿠키를 전송할 때 사용한다.
- 예시
Cookie: nickname=honggildong
쿠키 헤더 주요 속성
- Name
- 쿠키의 이름
- Value
- 쿠키의 값
- Domain
- 쿠키가 적용되는 도메인
- 생략 시 쿠키를 생성한 웹 서버에서만 사용할 수 있다.
example.org
에서 생성한 쿠키에domain=example.org
지정 시 해당 쿠키는example.org
와dev.example.org
에서 사용할 수 있다.example.org
에서 생성한 쿠키에 domain 지정을 생략하면 해당 쿠키는example.org
에서만 사용할 수 있다.
- Path
- 쿠키가 적용되는 경로
- 명시된 경로를 포함한 하위 경로 페이지에서만 접근할 수 있게 한다.
- 일반적으로는
path=/
로 지정한다. path=/home
지정 시/home
이나/home/room
에서는 접근할 수 있지만/hello
에서는 접근할 수 없다.
- Expires
- 쿠키의 유효기간
- Secure
- 기본적으로 쿠키는 http와 https를 구분하지 않고 전송한다.
Secure=true
일 경우 쿠키가 HTTPS 연결에서만 전송될 수 있도록 설정한다.
- HttpOnly
- 기본적으로 쿠키는 JavaScript에서 접근할 수 있다.
HttpOnly=true
일 경우 쿠키가 JavaScript에서 접근할 수 없도록 설정한다.- XSS 공격 방지
- SameSite
- 기본적으로 쿠키는 요청 도메인과 쿠키에 설정된 도메인이 달라도 전송된다.
SameSite=true
일 경우 요청 도메인과 쿠키에 설정된 도메인이 같은 경우에만 쿠키가 전송된다.
HTTP 캐시 (Cache)
- 웹 또는 앱의 클라이언트가 이용하는 서비스 과정에서 재사용할 수 있는 HTTP 리소스들을 임시로 저장하는 보관 공간
- 개발자 도구의 네트워크 탭에서 확인했을 때 회색 글씨로 나오는 것은 캐시로 불러들인 HTTP 리소스다.
HTTP 캐시의 특징
- 투명성
- 사용자는 캐시가 작동하는 방식을 인지하지 못한다.
- 효율성
- 캐시에 저장된 콘텐츠는 다시 요청할 필요 없이 바로 제공된다.
- 확장성
- 캐시는 다양한 웹 콘텐츠에 적용될 수 있다.
- 단순성
- 캐시 사용은 비교적 간단하다.
HTTP 캐시의 장점
- 네트워크 트래픽 감소
- 캐시에 저장된 콘텐츠는 다시 요청할 필요 없기 때문에 네트워크 트래픽을 줄일 수 있다.
- 응답 속도 향상
- 캐시에 저장된 콘텐츠는 서버로부터 받는 것보다 훨씬 빠르게 제공될 수 있기 때문에 응답 속도를 향상시킬 수 있다.
- 서버 부하 감소
- 캐시에 저장된 콘텐츠는 서버에서 직접 제공하지 않아도 되기 때문에 서버 부하를 줄일 수 있다.
- 사용자 경험 개선
- 네트워크 트래픽 감소와 응답 속도 향상은 사용자 경험을 개선하는 데 도움이 된다.
HTTP 캐시의 단점
- 콘텐츠 변경 시 문제
- 콘텐츠가 변경되었을 때 사용자가 최신 버전을 보지 못할 수 있다.
- 그래서 실무에서는 만약
common.js
라는 파일의 내용이 변경되었다면common.js?v=20240324
처럼 강제로 캐시를 타지 않도록 처리한다.
- 유효성 검증 과정
- 캐시에 저장된 콘텐츠가 유효한지 확인하기 위한 과정이 필요하다.
- 검증하는 과정은 추가적인 오버헤드를 발생시킬 수 있다.
- 보안 문제
- 캐시에 저장된 콘텐츠가 악성 코드에 감염될 수 있다.
HTTP 캐시의 동작 원리
- 요청
- 사용자가 웹 브라우저를 통해 URL을 요청하면 브라우저는 먼저 캐시에 해당 URL의 콘텐츠가 있는지 확인한다.
- 캐시 적중
- 캐시에 요청한 콘텐츠가 있다면 브라우저는 서버에 요청하지 않고 바로 캐시에서 콘텐츠를 가져와 사용자에게 제공한다.
- 캐시 미적중
- 캐시에 요청한 콘텐츠가 없다면 브라우저는 웹 서버에 요청을 보낸다.
- 응답
- 웹 서버는 요청한 콘텐츠를 브라우저에 전송한다.
- 캐시 저장
- 웹 서버로부터 받은 콘텐츠는 브라우저 캐시에 저장된다.
캐시 시간 초과
- 캐시 유효 시간이 초과하면, 서버를 통해 데이터를 다시 조회하고, 캐시를 갱신한다.
- 이때 다시 네트워크 다운로드가 발생한다.
검증 헤더와 조건부 요청
캐시 시간 초과
- 캐시 유효 시간이 초과해서 서버에 다시 요청했을 때는 두 종류의 결과가 발생한다.
- 서버에서 기존 데이터를 변경한다.
- 서버에서 기존 데이터를 변경하지 않는다.
- 캐시는 만료되었는데 서버에서 데이터를 변경하지 않으면 데이터를 새로 보내는 것보다 캐시를 재사용하는 것이 리소스의 활용도가 높다.
- 단, 클라이언트의 데이터와 서버의 데이터가 같다는 사실을 확인할 수 있는 방법이 필요하다.
- 따라서 HTTP 요청 시 검증 헤더를 추가하여 서버 측 데이터의 마지막 수정 시간과 비교해서 데이터 전송 여부를 결정한다.
- 검증 헤더와 조건부 요청을 통해서 웹 브라우저가 캐시된 리소스가 최신 버전인지 확인하는 과정을
캐시 신선도 검사
라고 부른다.
검증 헤더
- 서버에서 클라이언트 측에 전달하는 헤더
Last-Modified
와ETag
가 해당한다.
조건부 요청 헤더
- 클라이언트에서 서버 측에 전달하는 헤더
If-Modified-Since
와If-None-Match
가 해당한다.
Last-Modified & If-Modified-Since
- 서버는
Last-Modified
헤더를 통해서 데이터가 마지막에 수정된 시각을 클라이언트에 전달한다.- 예시 :
Last-Modified: Mon Mar 25 2024 00:25:17 GMT+0900
- 예시 :
- 클라이언트는 캐시가 만료되면
If-Modified-Since
헤더를 통해서 캐시가 가지고 있는 데이터의 마지막 수정 시각을 서버에 전달한다.- 해석하면
이 때 이후로 데이터가 변경되었나요?
같은 의미를 가진다. - 예시 :
If-Modified-Since: Mon Mar 25 2024 00:25:17 GMT+0900
- 해석하면
- 서버가 클라이언트 측에서 받은
If-Modified-Since
헤더의 값을 서버 측 데이터의 마지막 수정 시각과 비교한다. - 만약 두 시각이 일치하는 경우 서버는 응답 메시지에서 HTTP BODY 부분을 제거해서
304 Modified
로 결과를 반환한다. - 클라이언트는 자신의 요청 결과가
304 Modified
일 경우 캐시에 있는 리소스를 재사용한다. - 1초 미만의 단위로 캐시를 조정할 수 없다.
- 날짜 기반의 로직을 사용한다.
- 데이터를 일부만 수정한 경우에도 전체를 반환한다.
- 예시 : {A, B, C}에서 실제 반환되는 건 {A, B}이지만 {C}가 수정된 경우
- 스페이스나 주석처럼 크게 영향이 없는 수정도 전체를 반환한다.
ETag & If-None-Match
ETag
: Entity Tag- 캐시용 데이터에 임의의 고유한 버전명을 지정하는 방식
- 예시 :
ETag: "v1.0"
,ETag: "modifywhat"
- 예시 :
- 데이터가 변경되면 버전명을 바꾸어서 저장한다.
- Hash를 다시 생성한다.
- ETag를 서버에 전달해서 해당 버전명이 같으면 데이터를 유지하고, 다르면 데이터를 다시 받는다.
- 만약 두 버전명이 일치하는 경우 서버는 응답 메시지에서 HTTP BODY 부분을 제거해서
304 Modified
로 결과를 반환한다. - 서버는
ETag
헤더를 클라이언트에 전달한다.- 클라이언트는 응답 결과를 캐시에 저장한다.
- 클라이언트는
If-None-Match
헤더를 통해 저장했던ETag
헤더의 값을 서버에 전달한다.- 예시 :
If-None-Match: "v1.0"
- 예시 :
- 캐시 제어 로직을 완전히 서버에서 관리하는 방식
- 클라이언트는 단순히 버전명을 서버에 전달한다.
- 클라이언트는 서버의 캐시 메커니즘을 알 수 없다.
캐시 제어 헤더
Cache-Control
- 캐시 동작을 제어한다.
- 종류
- max-age
- 캐시에 저장된 리소스가 유효한 시간을 지정한다.
- 단위 : 초
- no-cache
- 캐시를 사용하지 않도록 지시한다.
- 데이터는 캐시를 해도 되지만, 데이터가 변경됬는지 항상 원(Origin)서버에 검증을 받고 사용해야 한다.
- no-store
- 리소스를 캐시에 저장하지 않도록 지시한다.
- 데이터에 민감한 정보가 있으므로 저장하면 안 된다는 것을 의미한다.
- 메모리에서 사용하고 최대한 빨리 삭제해야 한다라는 것을 의미한다.
- max-age
Pragma
- 캐시 동작을 제어한다.
Pragma: no-cache
라고 하면Cache-Control: no-cache
처럼 동작한다.- HTTP 1.0의 하위 호환
- 요즘에는 그닥 사용되지 않는 방식
- 하위 호환때문에 필요한 경우에만 사용한다.
Expires
- 캐시 만료일을 정확한 시간으로 지정하는 방법
Expires: Mon, 01 Jan 1990 00:00:00 GMT
처럼 사용한다.- HTTP 1.0 부터 사용한다.
- 지금은 더 유연한
Cache-Control: max-age
를 권장한다. Cache-Control: max-age
와 함께 사용하면Expires
는 무시된다.
프록시 캐시
- 웹 브라우저가 물리적으로 멀리 있는 원(Origin)서버에 컨텐츠를 요청하면 응답받는 속도도 느릴 수 밖에 없다.
- 웹 브라우저와 원 서버 사이에
프록시 캐시 서버
를 추가해서 느린 응답 속도를 보완한다. - 웹 브라우저는 특정 서버에 요청을 보냈을 때 우선 DNS같은 조건들을 확인해서 프록시 캐시 서버를 거치는 지 확인한다.
- 만약 프록시 캐시 서버를 거쳐야 하는 경우 웹 브라우저는 프록시 캐시 서버에 요청을 보낸다.
- 프록시 캐시 서버는 웹 브라우저가 요청한 컨텐츠를 자신이 보유하고 있는지에 대한 여부에 따라 2가지 결과를 낸다.
- 만약 보유하고 있다면 프록시 캐시 서버는 해당 컨텐츠를 웹 브라우저로 보낸다.
- 만약 보유하고 있지 않다면 프록시 캐시 서버는 해당 컨텐츠를 원 서버에 호출하여 컨텐츠를 받은 뒤 그 컨텐츠를 웹 브라우저에 반환한다.
- 프록시 캐시 서버는 웹 브라우저가 요청한 적이 있는 컨텐츠를 보유하고 있다.
- 그래서 첫 조회 요청을 받은 컨텐츠는 응답 속도가 비교적 느린 편이다.
- 프록시 캐시 서버가 가지고 있는 캐시는
public cache
라고 부른다. - 웹 브라우저같은 클라이언트가 가지고 있는 캐시는
private cache
라고 부른다.
Cache-Control
Cache-Control
헤더를 통해 클라이언트와 서버가 캐싱 정책을 제어한다.- 종류
public
- 응답이 public 캐시에 저장되도록 제어한다.
private
- 응답이 private 캐시에만 저장되도록 제어한다.
- 응답이 해당 클라이언트만을 위한 것임을 의미한다.
- 기본값
no-store
- 리소스를 저장하지 않도록 제어한다.
no-cache
- 리소스를 캐시하지 않도록 제어한다.
must-revalidate
- 리소스를 사용하기 전에 항상 서버에 유효성을 검증하도록 제어한다.
proxy-revalidate
- 프록시 서버가 리소스를 사용하기 전에 항상 서버에 유효성을 검증하도록 제어한다.
- 예시
Cache-Control: max-age=3600, public
Age
- 오리진 서버에서 응답 후 프록시 캐시 내에 머문 시간을 제어한다.
- 단위 : 초
캐시 무효화
- 실시간 거래같은 경우에는 데이터를 캐시하면 안 된다.
- 확실하게 캐시를 무효화하지 않게 하려면 아래의 두 헤더를 적용해야 한다.
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
MIME Type
- 인터넷에서 컨텐츠 형식을 식별하는 데 사용되는 표준
type/subtype
형식으로 구성된다.- 예시
text/html
- HTML 문서
text/plain
- 일반 텍스트
image/jpeg
- JPEG 이미지
image/png
- PNG 이미지
application/pdf
- PDF 문서