We’re rolling out custom instructions to give you more control over how ChatGPT responds. Set your preferences, and ChatGPT will keep them in mind for all future conversations.
프로젝트 기반과 UI 최적화 개발 서버 실행 https://nextjs.org/learn/dashboard-app 위 교재를 바탕으로 학습을 진행하였다. 1주차 학습 범위는 Chapter 1 ~ Chapter 3이다. pnpm 설치 pnpm은 npm의 디스크 용량 낭비와 속도 저하를 보완한 패키지 매니저이다. npm install -g pnpm 을 사용하여 pnpm을 설치하였다. pnpm을 통해 위 사이트에서 제공하는 starter-example 예제를 복사하였다. 개발 서버 시작 예제를 따라 세팅을 마친 후, pnpm dev 명령어로 개발 서버를 시작했다. localhost:3000 에 개발 서버가 열린다. pnpm dev 는 개발 중 실시간으로 코드를 수정하고 결과를 확인할 수 있는 로컬 개발 서버를 실행하는 명령어이다. pnpm build 는 배포용으로, 실제 서비스 환경에서 배포할 수 있도록 최적화된 결과물을 생성하는 명령어이다. Next.js 구조 프로젝트 폴더의 구조는 대략적으로 아래와 같이 구분할 수 있다. /app /app/ui /app/lib /public /app 애플리케이션의 페이지, UI 구성 요소, 로직 등이 포함되는 폴더이다. Next.js의 라우팅 구조를 구성하는 폴더이므로 주로 해당 폴더에서 작업하게 된다. app/ui 애플리케이션에서 사용되는 UI 구성 요소를 관리하는 폴더이다. 현재 학습용 프로젝트에서 구성 요소들은 미리 스타일링되어 있다. app/lib 애플리케이션에서 사용되는 공통 로직을 관리하는 폴더이다. 데이터 처리 함수, 유틸리티 함수 등 재사용할 수 있는 기능을 관리할 수 있다. /public 이미지 등과 같이 애플리케이션에서 사용하는 정적 데이터를 관하는 폴더이다. 글자 스타일 CSS CSS는 웹 페이지에서 글자색, 크기, 여백 등의 스타일을 지정해주는 언어이다. 클래스를 직접 작성하여 원하는 디자인을 지정할 수 있다. Global CSS를 사용하면 CSS 파일에 규칙을 작성하여 애플리케이션 전체에 스타일을 적용할 수 있다. Tailwind Tailwind는 클래스 이름을 추가하여 요소의 스타일을 지정하는 CSS 프레임워크이다. Tailwind에서 미리 정의한 클래스를 HTML 요소에 조합하여 스타일을 적용할 수 있다. CSS 모듈 CSS 모듈은 고유한 클래스 이름을 자동으로 생성하여 클래스 이름끼리 충돌하는 것을 방지해주는 모듈이다. 작성 방식은 일반 CSS와 거의 동일하며, 파일 이름을 .module.css로 작성한 뒤 컴포넌트에서 불러와 사용할 수 있다. Clsx를 이용한 조건부 클래스 Clsx는 조건에 따라 다른 CSS 클래스를 적용하기 위해 사용하는 라이브러리이다. if문으로 처리해도 전혀 문제되지 않으나, 코드의 간결성을 위해 사용한다. 'Tailwind A': status === 'pending', 'Tailwind B': status === 'paid', 위 코드처럼 작성하면, status가 pending 일 때 Tailwind A, paid 일 때 Tailwind B가 적용되도록 할 수 있다. 웹 폰트 레이아웃 이동 웹사이트에서 특정 폰트를 사용할 때, 웹에서 폰트 파일을 받아와야 한다. 하지만 웹 페이지 로딩과 폰트 로딩은 동시에 끝나지 않는다. 그래서 폰트 적용 전, 대체 폰트로 우선 사이트를 로딩한 뒤 폰트가 적용된다. 그런데 폰트마다 크기가 다르므로 폰트 로딩 후 이미지나 버튼 등이 밀리는 경우가 발생한다. 이렇게 폰트에 의해 주변 요소의 위치가 변하게 되는 것을 레이아웃 이동이라고 한다. CLS (Cumulative Layout Shift) 웹페이지가 로딩되는 동안 발생하는 예상하지 못한 레이아웃 이동의 정도를 나타내는 지표이다. CLS가 낮을수록 예상하지 못한 이동이 낮았다는 뜻이므로, 안정적인 웹페이지라고 볼 수 있다. next/font next/font는 Next.js에서 웹 폰트를 최적화하여 사용할 수 있도록 하는 기능이다. next/font 빌드 시점에 글꼴 파일을 다운로드하여 정적 파일로 불러온다. 글꼴 관련 네트워크 요청이 발생하지 않아 성능 저하가 발생하지 않는다. import { Inter } from 'next/font/google'; 위 코드는 google 폰트 중 Inter을 불러오는 코드이다. const inter = Inter( {subsets: ['latin'], }); 위와 같이 변수에 폰트 스타일을 저장한 뒤, 클래스로 불러와서 쓸 수 있다. 이미지 최적화 일반 이미지 HTML에서는 다음과 같이 이미지를 넣는다. <img src="/hero-desktop.png" alt="Dashboard" /> 이같은 경우는 프로그램에서 이미지의 가로 세로를 모르기 때문에 이미지가 차지할 공간을 미리 알지 못한다. 따라서 사진이 미리 저장되어있지 않다면 CLS가 증가한다. 이외에도 개발자가 이미지 로딩과 최적화를 직접 신경 써야 한다. next/image Next.js 에서는 다음과 같이 이미지를 넣을 수 있다. <Image src="/hero-desktop.png" width={1000} height={760} alt="Dashboard" /> 위 경우는 사진의 너비와 높이를 직접 지정했으므로 예상하지 못한 레이아웃 이동이 발생하지 않는다. 이외에도 WebP/AVIF 같은 효율적인 포맷을 사용할 수 있는 등, 이미지 최적화 기능을 Next.js에서 제공한다. 반응형 이미지 화면의 크기에 따라 이미지를 나타내는 방식이다. 이 방식을 이용해서 모바일과 데스크톱에 표시되는 이미지를 다르게 할 수 있다. className="hidden md:block" 위 코드는 기본적으로 이미지를 숨기고, 화면 크기가 md 이상인 경우에 표시한다. className="block md:hidden" 위 코드는 기본적으로 이미지를 표시하고, 화면 크기가 md 이상인 경우에 숨긴다. 공부하면서 어려웠던 점 CSS 규칙에 대한 부분이 이해가 가장 어려웠다. CSS로 스타일을 직접 지정할 수 있다는 점과, Tailwind는 정해진 틀에 맞춰서 디자인 할 수 있다는 점도 개념적으로는 이해했지만, 실제로 사용하기까지는 더 익숙해질 필요가 있을 것 같다. next/font 또는 next/image 처럼 내부에서 지원하는 기능들도 전부 처음 접해서 어색하긴 했다. 전체적인 코드 구조는 이해하기 쉬웠으나, 디자인에 관한 부분은 전부 처음 접해서 이가 어려운 것 같다. 1주차 학습 후기 처음에는 아예 폴더 구조만 봐도 하나도 모르겠었는데, 지금은 기본적인 틀은 보인다. clsx나 반응형 이미지 구현하는 부분 등은 일반 코딩과 거의 다름없어서 이해하기 쉬웠다. CSS로 디자인을 수정하는 부분은 알아야 할 내용이 아직 많은 것 같으므로 반복해서 사용해보는것이 중요할 것 같다.
개발 서버에서만 열린 API, 로컬에서 호출하려면 알아야 할 것들 0. 들어가며 회사에서 외부 API 연동을 테스트해야 하는 일이 생겼습니. 그런데 해당 API는 내 로컬 PC에서 호출할 수 없고, 개발 서버에서만 접속이 가능한 상태였습니다. 개발 서버에 코드를 배포해서 테스트하는 방법도 있었지만, 수정할 때마다 배포하고 확인하는 건 꽤 번거로운 일이었기 때문에, 로컬에서 바로 테스트를 진행하기 위해 SSH 포트 포워딩 을 사용했습니다. 실제 구현은 JSch 라이브러리를 사용해서 세부 동작을 직접 다룰 일은 없었지만, 적용하면서 공부한 동작 원리를 정리해 보려고 합니다. 포트 포워딩이 트래픽을 어떻게 넘겨주는지, 터널링은 정확히 무슨 의미인지, SSH 연결은 어떤 과정을 거쳐 맺어지는지를 차례로 따라가보겠습니다. 이 글은 그 시리즈의 1편으로, 원리를 이해하는 데 집중할 예정입니다. 1. 문제 상황 1-1. 네트워크 구성 현재 네트워크 구성은 아래와 같습니다. +-------+ +---------+ +------------+ ALLOW +-----------------+ | Local |---->| bastion |---->| dev-server |---------->| api.example.com | +-------+ +---------+ +------------+ +-----------------+ | ^ | DENY | +---------------------------- X ----------------------------+ 로컬에서 개발 서버까지는 bastion 서버를 거쳐 들어갑니다. 외부 API로 나가는 방화벽은 개발 서버에서만 열려있는 상태입니다. 1-2. 해결은 SSH 포트 포워딩. 그런데 이게 정확히 어떻게 동작하는 걸까? 외부 API는 개발 서버에서만 호출할 수 있습니다. 해결을 위해 SSH 포트포워딩 설정을 적용했고, 로컬에서 외부 API를 호출할 수 있게 됐습니다. 어떤 원리로 동작하는 걸까요? 포트 포워딩은 트래픽을 어떻게 넘겨주는 걸까? 흔히 "SSH 터널링"이라고 하던데, 포트 포워딩과 같은 말일까? 중간 서버를 거치는데, 어떻게 안전하게 통신할 수 있을까? 첫 번째 질문은 2장, 두 번째는 3장, 세 번째는 4~6장에서 답해보겠습니다. 2. 포트 포워딩이란 2-1. 정의 포트 포워딩(Port Forwarding) 은 특정 포트로 들어온 트래픽을 다른 주소(IP)와 포트로 넘겨주는 것 입니다. 전달하는 방법에는 두 가지가 있습니다. NAT 방식 : 봉투(패킷)의 주소만 고쳐 쓰고 그대로 보내는 방법 Proxy 방식 : 봉투(패킷)를 뜯어 내용물을 새 봉투(패킷)에 옮겨 담는 방법 2-2. NAT 방식: 패킷 헤더만 바꿔서 통과 NAT(Network Address Translation) 방식은 커널이 패킷 헤더의 목적지 IP와 Port만 바꿔서 넘깁니다. 공유기의 포트 포워딩이 대표적인 예입니다. Router 의 역할을 공유기가 수행합니다. [A] [Router] [B] | dst = Router:8080 | dst = 10.0.0.5:80 | |------------------------>|-------------------------->| | (rewrite dst only) | | | |<=============== TCP connection x 1 ================>| 데이터에는 손대지 않고, TCP 연결은 A와 B 사이에 1개뿐 입니다. 공유기는 연결의 끝점(Endpoint)이 아니라 지나가는 길목일 뿐이죠. 2-3. 프록시 방식: 헤더를 떼고 데이터를 꺼내 새 연결로 전송 프록시(Proxy) 방식은 중간 서버가 포트를 Listen 합니다. A의 연결을 중간 서버가 직접 받고, B와는 본인이 새 TCP 연결을 맺습니다. +-----+ TCP conn #1 +-----------------+ TCP conn #2 +-----+ | A |---------------->| Proxy (Listen) |---------------->| B | +-----+ +-----------------+ +-----+ TCP 연결이 구간별로 따로 있기 때문에, A 입장에서 통신 상대는 B가 아니라 프록시 서버가 됩니다. "헤더를 떼고 데이터를 꺼낸다"는 표현에는 주의할 점이 있는데요. Proxy 방식에서 전달하는 건 TCP의 바이트 스트림(Byte Stream) 입니다. 즉, 아래와 같이 들어온 패킷과 나가는 패킷이 1:1로 대응되지 않습니다. incoming packets [ 300B ][ 1400B ][ 200B ] | v kernel strips headers socket (stream) [ 1900 bytes .............. ] | v kernel builds new packets outgoing packets [ 1000B ][ 900B ] 헤더를 떼고 붙이는 건 커널이 수행합니다. 포트포워딩을 수행하는 프로그램은 소켓에서 바이트를 읽고, 다른 소켓에 쓸 뿐입니다. 구간별 TCP 상태도 독립적입니다. 시퀀스 번호, ACK, 재전송이 구간마다 따로 관리하죠. 예를 들어 A와 프록시 사이에서 패킷이 유실되면 재전송은 그 구간에서만 일어나기 때문에 B는 그 사실을 알지 못합니다. 통신의 중간에 별도의 프로그램이 끼어들었다고 생각하시면 됩니다. 별도의 프로그램이 수행 중이기 때문에 소켓에서 데이터를 read() 하고 다른 소켓에 write() 하는 과정이 발생합니다. 2-4. 방식은 장비가 아니라 동작으로 정해진다 주의할 점은 "공유기는 NAT, 서버는 프록시"처럼 장비에 따라 방식이 나누어지지는 않습니다. 같은 리눅스 서버라도 어떻게 동작시키느냐에 따라 방식이 달라집니다. # NAT 방식: 커널이 목적지 헤더만 바꾼다 iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 10.0.0.5:80 # 프록시 방식: socat이 연결을 받고, 목적지로 새 연결을 맺는다 socat TCP-LISTEN:8080,fork TCP:10.0.0.5:80 iptables의 DNAT는 NAT 방식이고, socat은 프록시 방식입니다. 그리고 SSH 포트 포워딩은 항상 프록시 방식입니다. 그 이유는 6장에서 설명하겠습니다. 2-5. 비교 비교 항목 NAT 방식 프록시 방식 동작 계층 커널의 네트워크 스택 (헤더 수정) 애플리케이션 (소켓 읽기/쓰기) TCP 연결 수 양 끝단 사이 1개 구간별로 따로 (2개 이상) 처리 방식 목적지 IP/Port만 바꿔서 통과 데이터를 받고, 다른 Connection 으로 데이터를 옮김 패킷 대응 1:1 1:1 아님 (바이트 스트림) 암호화 없음 없음 암호화는 포트포워딩 과 관련이 없습니다. 다만 포트포워딩으로 중계하는 데이터 자체를 암호화해서 전달할 수 있습니다. (SSH 는 암호화된 데이터를 전달합니다.) 3. 터널링이란 3-1. 정의: 어떤 프로토콜을 다른 프로토콜 안에 감싸서 운반하는 것 터널링(Tunneling) 은 어떤 프로토콜을 다른 프로토콜 안에 감싸서 운반하는 것 입니다. 이미 포장이 완료된 택배 상자를 배송하는 경우입니다. 물건을 상자에 넣으면 택배 기사는 상자에 붙은 송장만 보고 배달합니다. 상자 안에 무엇이 들었는지는 신경 쓰지 않죠. 상자 안에 또 다른 상자가 있더라도 그런 건 신경쓰지 않습니다. 입구에서 감싸는 것을 캡슐화(Encapsulation) , 출구에서 벗겨내는 것을 역캡슐화(Decapsulation) 라고 합니다. 중간 장비는 바깥쪽 프로토콜만 보고 전달합니다. [Source] [Middle devices] [Target] +------+ +--------+------+ +------+ | DATA | -wrap-> | OUTER | DATA | -unwrap->| DATA | +------+ +--------+------+ +------+ ^ middle devices read OUTER only 💡 재택근무 때 많이 쓰는 IPsec VPN이 대표적인 터널링입니다. 다만 특정 포트가 아닌 네트워크 전체를 감싸는 등 방식이 조금 다릅니다. 3-2. SSH 포트 포워딩은 왜 터널링인가 로컬 PC에서 bastion을 통해 MySQL에 접속한다고 해보겠습니다. [로컬] ==== SSH (TCP 22, 암호화) ====> [bastion] ---- MySQL 프로토콜 (TCP 3306) ----> [DB] 구간 1: SSH 터널 구간 2: 그냥 MySQL 통신 실제로 구간 1번을 지나가는 패킷은 이런 모양이 될겁니다. +------+-----------+------------------------------------+ | IP | TCP(22) | SSH 패킷 (암호화됨) | MySQL data | +------+-----------+------------------------------------+ 3-1의 조건에 하나씩 대입해 보겠습니다. 감싸기: 로컬의 ssh 클라이언트가 MySQL 데이터를 SSH 메시지로 감싸고 암호화합니다. 벗기기: bastion의 sshd(ssh용 프로그램)가 SSH를 벗겨내고 원래 MySQL 데이터를 꺼냅니다. 중간 장비: 22번 포트로 오가는 SSH 트래픽만 봅니다. 벗겨진 MySQL data 는 그대로 DB 로 전달만 됩니다. 중간 장비는 안에 들어있는 Data 가 MySQL 요청인지 신경쓰지 않습니다. 그래서 SSH만 허용된 환경에서도 다른 프로토콜을 통과시킬 수 있죠. 1장의 문제상황도 마찬가지였습니다. 로컬에서 쓸 수 있는 통로는 SSH뿐이었지만 그 통로를 통해 HTTP API 요청을 실어 보낼 수 있었습니다. 3-3. bastion 쉘에서 다시 ssh로 접속하는 건 터널링일까? 여기서 이런 의문이 생겼습니다. 평소 업무 시, 서버에 접속할 때는 PuTTY 같은 터미널 프로그램으로 bastion에 먼저 접속하고, 그 Shell에서 다시 개발 서버로 접속하게 됩니다. 이건 터널링일까요? +-------+ SSH #1 +---------+ SSH #2 +------------+ | My PC |=============>| bastion |=============>| dev-server | +-------+ +---------+ +------------+ ^ #1 ends here, #2 starts here 결론은 터널링이 아닙니다. 두 연결이 이어져 있을 뿐, 하나가 다른 하나를 감싸진 않습니다. 각각 별개의 Connection 일 뿐입니다. 첫 번째 SSH는 키 입력과 화면 출력만 운반합니다. 두 번째 SSH의 패킷을 감싸지 않습니다.. 두 번째 SSH는 bastion이 따로 맺은 별개의 연결입니다.. 데이터가 bastion에서 한 번 복호화 됩니다. 즉, bastion은 평문을 볼 수 있습니다. 반면 문제 해결에 사용한 ProxyJump ( ssh -J )는 두 번째 SSH가 첫 번째 SSH 안에 감싸져서 지나가기 때문에 터널링이라고 할 수 있습니다. 자세한 구조는 2편에서 다루겠습니다. 비교 항목 bastion 쉘에서 다시 ssh ProxyJump ( ssh -J ) 두 번째 SSH를 맺는 쪽 bastion 내 PC 감싸는 구조 없음 (두 연결이 이어짐) 두 번째 SSH가 첫 번째 SSH 안에 감싸짐 터널링 여부 X O bastion이 볼 수 있는 것 평문 (키 입력, 화면 출력) 암호화된 두 번째 SSH 트래픽 3-4. 터널링과 포트 포워딩은 같은 말일까? 같은 말이 아닙니다. 두 개념은 겹칠 수도 있고, 따로 존재할 수도 있습니다. 터널링 O 터널링 X 포트 포워딩 O SSH 포트 포워딩 공유기 NAT 포트 포워딩 X VPN 일반 통신 NAT 포트 포워딩은 헤더만 바꿀 뿐 감싸지 않기 때문에 터널링이 아닙니다. VPN은 감싸지만 특정 포트에 넘겨주는 게 아니기 때문에 포트 포워딩이 아닙니다. SSH 포트 포워딩은 둘 다에 해당합니다. 그래서 "SSH 터널링"은 정확히 말하면 "SSH 터널 위에서 동작하는 포트 포워딩" 입니다. 그리고 2장의 분류로는 프록시 방식에 해당합니다. 4. SSH란 4-1. TCP 위의 애플리케이션 계층 프로토콜 SSH(Secure Shell) 는 TCP 위에서 동작하는 애플리케이션 계층 프로토콜입니다. 기본 포트는 22번. SSH는 먼저 암호화된 통로를 하나 깔고, 그 위에서 쉘 접속, 파일 전송, 포트 포워딩 같은 일을 수행합니다. 4-2. SSH 내부 3계층 SSH는 내부적으로 세 개의 계층으로 나뉩니다. 아래 계층이 준비되어야 위 계층이 동작합니다. +----------------------+---------------------------------------+ | Connection | channels (shell, exec, port forward) | +----------------------+---------------------------------------+ | User Authentication | user auth (password, public key) | +----------------------+---------------------------------------+ | Transport | key exchange, encryption, integrity, | | | server authentication | +----------------------+---------------------------------------+ | TCP (port 22) | reliable byte stream | +----------------------+---------------------------------------+ Transport: 키를 교환하고, 암호화와 무결성을 보장하고, 서버를 인증합니다. User Authentication: 접속하려는 사용자가 누구인지 확인합니다. Connection: 인증된 연결 위에 실제 작업용 채널을 Open 합니다. 5장은 Transport와 User Authentication을, 6장은 Connection을 알아보겠습니다. 5. SSH 연결은 어떻게 맺어질까 이 장의 예시 접속 대상은 모두 bastion입니다. 5-1. 연결 순서 한눈에 보기 ssh user@bastion 을 실행하면 아래 순서로 연결이 맺어집니다. My PC bastion Layer | | | (1) TCP 3-way handshake (port 22) | TCP |<------------------------------------->| | (2) version / algorithm negotiation | Transport |<------------------------------------->| | (3) key exchange (ECDH) | Transport | + host key verification | |<------------------------------------->| ============ encrypted from here ================== | (4) user authentication | User Auth |<------------------------------------->| | (5) open channel | Connection |<------------------------------------->| | (6) shell / port forwarding data | Connection (실제 사용 부분) |<------------------------------------->| Transport 계층에서 수행하는 일은 암호화 알고리즘 결정과 호스트 키 확인, 키 교환입니다. 5-2. 키 교환과 세션 키 (ECDH) 암호화하려면 양쪽이 같은 키를 가져야 합니다. 그런데 그 키를 네트워크로 그냥 보내면 중간에서 훔쳐볼 수 있는 위험이 존재합니다. 그렇다면 키를 어떻게 나눠 가질 수 있을까요? 최신 방식에서는 ECDH(Elliptic Curve Diffie-Hellman) 키 교환을 사용합니다. 쉽게 비유하자면 물감을 섞는 느낌이라고 보시면 됩니다. (실제로는 수학 계산으로 동일한 값을 얻어냅니다.) 두 사람이 공개된 공통 색 하나를 정한다. 각자 남에게 보여주지 않는 비밀 색을 하나씩 고른다. 공통 색에 자기 비밀 색을 섞어서 상대에게 보낸다. 받은 색에 다시 자기 비밀 색을 섞는다. 결과적으로 둘은 같은 색을 얻게 되고, 중간에서 데이터를 엿본 사람이 있더라도 섞인 색만 봤기 때문에 그 색을 만들 수는 없다는 원리입니다. 이렇게 만든 값이 세션 키(Session Key) 의 재료가 됩니다. 이후 데이터는 이 키로 AES-GCM 같은 대칭키 암호화 방식으로 암호화합니다. ECDH 는 최신의 키 교환 방식입니다. SSH 에서는 고전적인 Diffie-Hellman 방식 이 오래 쓰였고, TLS 에서는 RSA 방식 이 자주 쓰였지만 둘 다 보안 취약점이 발견되어 최근은 ECDH 를 사용합니다. 5-3. TLS와 비교: 같은 암호 기술, 다른 프로토콜 먼저 짚고 가자면, SSH는 TLS를 쓰지 않습니다. SSH는 자체 암호화 프로토콜을 사용합니다. 둘은 같은 암호 알고리즘(ECDH, AES-GCM, ChaCha20-Poly1305, Ed25519 등)을 쓸 뿐 별개의 서로 다른 프로토콜입니다. 비교 항목 TLS SSH 키 교환 (EC)DHE ECDH 등 데이터 암호화 대칭키 (AES-GCM, ChaCha20-Poly1305 등) 대칭키 (AES-GCM, ChaCha20-Poly1305 등) 서버 신원 확인 X.509 인증서 + CA 호스트 키 + known_hosts "HTTPS는 인증서로 암호화한다"는 말을 자주 듣는데, 암호화 자체는 인증서가 하는 게 아닙니다. 키 교환으로 세션 키를 만들고, 만들어진 대칭키로 암호화합니다. 과거에는 인증서의 공개키가 암호화 과정에 사용됐지만 RSA 방식이 사양되면서 인증서는 서명에만 사용되게 되었습니다. 즉 인증서로 서버 신원만 수행합니다. 5-4. 호스트 키와 known_hosts (TOFU) SSH는 기본적으로 인증서가 아니라 서버의 공개키 자체를 받는데, 이 키를 호스트 키(Host Key) 라고 합니다. 업무 중에 서버에 처음 접속하는 경우, 아래와 비슷한 메시지를 본 적이 있을 겁니다. The authenticity of host 'bastion (10.0.0.10)' can't be established. ED25519 key fingerprint is SHA256:fAkEf1ngErPr1nTf0rExAmPlE0nLyAbCdEfGh1234. This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 를 입력하면 서버의 호스트 키는 클라이언트의 ~/.ssh/known_hosts 에 자동으로 저장됩니다. 이후 접속부터는 저장된 키와 비교만 수행하고, 키가 바뀌면 경고를 띄우고 접속을 막게 됩니다. @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 처음 본 키를 믿고, 이후로는 그 키만 믿는 방식입니다. 이를 TOFU(Trust On First Use) 라고 합니다. TOFU는 처음 접속하는 순간에 키가 진짜인지 확인할 수 없다는 약점을 가집니다. 일반적인 HTTP 요청처럼 다수의 모르는 유저가 쓰는 환경에선 사용이 불가능한 방법이겠지만 SSH는 정해진 소수의 서버에 반복해서 접속할 때 사용됩니다. 공격자가 노리려면 하필 처음 접속하는 그 순간에 내 경로 중간에 있어야 하기 때문에 공격 기회가 좁기 때문에 업무 환경에서는 많이 사용됩니다. 약점을 보완하는 방법도 있습니다. 처음 접속할 때 지문을 서버 관리자에게 받은 값과 비교하거나 known_hosts를 미리 배포해 두고 쓰면 첫 접속의 불확실성 자체가 사라집니다 5-5. SSH 인증서 서버가 많아지면 서버마다 키를 확인하고 등록하는 일이 번거롭습니다. 그래서 OpenSSH에는 자체 인증서(Certificate) 기능이 있습니다. X.509가 아닌 Ope
논문 링크: https://arxiv.org/abs/2012.14913 Introduction Transformer 기반 language model들은 self-attention을 통해 많을 성공을 이뤘다. 하지만 self-attention layer의 실제 파라미터는 전체 파라미터의 1/3 비율만 차지하며, 2/3는 feed-forward layer가 차지하고 있다. 본 논문에서는 가장 많은 비율을 차지하는 feed-forward layer가 무슨 역할을 하는지에 대해 연구하였다. 저자들은 feed-forward layer가 neural memory를 모방함을 보였다. 첫 번째 parameter matrix는 key에 해당하여 input의 입력 패턴을 감지하고, 두 번째 parameter matrix는 value에 해당하여 output 단어에 대한 확률 분포를 유도한다. 이를 통해 feed-forward layer는 여러 memory를 결합하여 output을 만들고, 각 layer의 output은 residual connection을 통해 최종 예측을 형성한다. Feed-forward layers as unnormalized key-value memories Feed-forward layers Transformer의 각 feed-forward layer(FFN)는 각 input vector를 독립적으로 처리하는 position-wise function 이다. $$FF(x)=f(xK^\top)V$$ 즉, 먼저 input $x$와 첫 번째 matrix $k$를 연산하고 비선형 함수를 적용한 뒤, 그 결과를 두 번째 matrix $C$와 곱해서 FFN의 output을 만든다. Neural memory Neural memory는 여러 개의 key-value pair 로 구성되고, 논문에서는 각각의 pair를 하나의 memory라고 부른다. $$MN(x)=\text{softmax}(xK^\top)V$$ 즉, 어떤 key가 들어오면 input과 관련 있는지 계산하고, 선택된 정도에 따라 value들을 결합한다. Feed-forward layers emulate neural memory 위와 같이 FFN은 neural memory와 거의 동일한 구조를 가지고 있으며, 차이는 어떤 activation function을 사용하느냐이다. key가 입력 패턴을 탐지하면 그 활성화 정도(memory coefficient)에 따라 대응하는 value들이 결합되는 unnormalized key-value memory로 해석할 수 있다. Keys capture input patterns 주장: key vector $K$가 sequence의 패턴 탐지기 역할을 할 것이다. Experiment 각 key가 실제로 특정 입력 패턴을 감지하는지 확인하기 위한 실험을 진행하였다. 16-layer Transformer에서 각 layer 당 10개의 key, 총 160개의 key를 random sampling 하였다. 각 key에 대해 training data의 모든 접두사의 memory coefficient(메모리계수)를 계산하고, 계수가 가장 높은 top-25 trigger examples를 추출하였다. NLP 전문가가 trigger examples에서 반복되는 패턴을 찾아 shallow pattern(recurring n-grams 등)과 semantic pattern(recurring topic 등)으로 분류하였다. 각 key의 top-25 example는 한 명의 expert가 annotation하였다. results 모든 key에서 최소 하나 이상의 패턴을 발견했으며, 각 key당 평균 3.6개의 패턴이 확인되었다. 전체 trigger examples의 65~80%가 하나 이상의 패턴과 연관되어 있었다. Lower layers(1~9) 에서는 n-gram이나 마지막 단어와 같은 shallow pattern 이 주로 나타났다. Upper layers(10~16) 에서는 표면적인 형태보다 문맥,주제와 관련된 semantic pattern 이 더 많이 나타났다. 마지막 token을 제거하는 실험에서 lower layer의 key는 크게 영향을 받은 반면, upper layer의 key는 상대적으로 영향을 덜 받았다. 이는 upper layer로 갈수록 shallow pattern에 대한 의존도가 감소함을 보여준다. Values represent distributions 각 key와 연결된 value가 어떤 정보를 가지고 있는가? 각 value가 가장 높은 확률을 주는 token과, 해당 key를 가장 강하게 활성화한 trigger example의 실제 next token을 비교한 그래프이다. Lower layers(1~10)에서는 둘의 agreement가 거의 0이지만, layer 11부터 빠르게 증가하여 최대 약 3.5%까지 올라갔다. 이는 random prediction의 0.0004%보다 훨씬 높은 수치이다. 따라서 특히 upper layer에서 value는 대응하는 key가 감지한 패턴 다음에 올 가능성이 높은 token에 대한 정보를 담고 있음을 보여준다. 그렇다면 value가 내놓는 확률 분포가 얼마나 확신을 가지고 있는가? value가 특정 token에 높은 확률을 줄수록, 그 token이 key의 trigger example의 실제 next token과 일치할 가능성도 증가했다. Aggregating Memories 여러 계층, 여러 셀에서 나온 정보가 어떻게 통합되는가? Intra-layer memory composition 하나의 feed-forward layer에는 여러 memory가 있고, FFN의 output은 활성화된 여러 value를 메모리 계수에 따라 가중합(weighted sum)해서 만들어진다. 하나의 input이 각 layer에서 수백 개의 memory를 활성화한다. layer 10 부근에서 활성 메모리의 수가 감소하는데, 이 시점은 figure 2에서 semantic pattern이 shallow pattern보다 많아지는 시점과 비슷하다. 최종 예측이 특정 memory 하나의 예측으로 결정되는지 확인하였다. 결과적으로 모든 layer에서 대부분의 개별 메모리셀은 정답과 일치하지 않았다. 이는 하나의 강한 memory가 output을 결정하는 것이 아니라 여러 memory가 결합되어 새로운 예측을 만드는 것을 의미한다. Inter-layer predictrion refinement 각 layer에서 만들어진 prediction들은 residual connection을 통해 점차 refine된다. residual의 prediction과 최종 prediction이 언제 같아지는지 확인한 그래프이다. 이미 lower layer에서도 약 1/3의 최종 prediction이 결정되어 있고, layer 10 이후 일치율이 빠르게 증가한다. FFN이 residual의 기존 prediction을 바꿀 때, 단순히 FFN 자신의 prediction으로 교체하는 경우는 드물다. 오히려 residual과 FFN의 정보를 조합해 둘과 다른 새로운 prediction을 만드는 경우가 많았다. Discussion and Conclusion 저자들은 Transformer의 feed-forward layer가 key-value memory처럼 작동한다는 것을 여러 실험을 통해 보였다. Key → 사람이 해석할 수 있는 특정 input pattern과 연관됨 Value → 특히 upper layer에서 대응하는 key의 pattern 다음에 올 token의 output distribution과 연관됨 Model output → 여러 memory의 distribution을 먼저 각 layer에서 결합하고, 이후 residual connection을 통해 layer를 거치면서 refine되어 최종 output을 만든다. future work layer embedding space 왜 upper layer에서 key-value 관계가 더 강해지는가 beyond language model BERT, translation model 등 다른 Transformer에서도 동일한 현상이 나타나는가 pratical implications FFN memory 분석을 모델 해석, privacy 분석, 더 나은 architecture 개발 등에 활용