배경

 

다음과 같은 관계를 갖는 테이블이 있다고 하자.

 

member는 회원들의 회원번호와 가입일을,

sales는 회원별 물품을 주문한 정보를,

menu는 각 상품 정보를 가지는 테이블이다.

 

이때, 현재 members, sales, menu에 다음과 같이 데이터들이 적재돼있다고 하자.

 

1. members

 

2. sales

 

3. menu

 

 

주어진 문제

 

각 고객들이 구입한 첫 물건은?

 

sales 테이블에는 각 회원들이 특정 물품을 구입한 날에 대한 데이터들이 적재돼있으니, 이 테이블에서 적절한 쿼리를 활용해볼 수 있을 것이다. 이 때 처음 물품을 구매한 날짜에 여러 물품을 구매했다면, 해당 물품들의 정보를 모두 출력해야 한다.

 

 

풀이1 : GROUP BY를 활용한 접근

 

sales테이블에 있는 구매한 물품들에 대한 상세정보는 sales와 menu의 product_id들을 INNER JOIN을 걸어 알아낼 수 있다. 그러나 회원별로 가장 처음 구매한 물품들에 대한 정보만 필요하다. sales에서 customer_id로 GROUP BY를 걸고, 그 안에서 MIN함수를 통해 가장 낮은 order_date를 뽑아내면 다음과 같이 회원별로 물품을 처음 구매한 날짜를 추출할 수 있다.

 

SELECT customer_id, min(order_date)
FROM sales
GROUP BY customer_id;

 

 

위 결과를 WHERE절에서 활용, sales테이블에서 다음과 같이 회원별로 해당 날짜에 대한 데이터들을 추출 가능하다.

 

SELECT *
FROM sales
WHERE (customer_id, order_date) in (
    SELECT customer_id, min(order_date)
    FROM sales
    GROUP BY customer_id
);

 

 

그러나 위 결과에서 4, 5번 레코드를 보면 중복되는 레코드들임을 알 수 있다. C는 2021년 1월 1일에 3번 물품을 2번 구매했기 때문에 이런 결과가 생기는 것이다. 따라서 다음과 같이 DISTINCT를 걸어줘서 중복을 제거할 수 있다.

 

✅ DISTINCT

: SQL에서 중복된 값을 제거하여 결과 집합에서 고유한 값을 반환하는 데 사용되는 키워드. DISTINCT 뒤에 2개 이상의 컬럼을 사용하면, 해당 컬럼들로 구성되는 조합들에 대한 중복을 제거하는 식으로 동작한다.

(즉, 아래 예제에서는 customer_id, order_date, product_id로 이뤄지는 조합에서 중복이 없어진다)

 

SELECT DISTINCT customer_id, order_date, product_id
FROM sales
WHERE (customer_id, order_date) in (
    SELECT customer_id, min(order_date)
    FROM sales
    GROUP BY customer_id
);

 

 

이제 menu와 INNER JOIN을 걸어 다음과 같이 처음 구매한 물품의 상세정보를 뽑아낼 수 있다.

 

SELECT DISTINCT customer_id, order_date, sales.product_id, product_name, price
FROM sales
INNER JOIN menu ON sales.product_id = menu.product_id 
WHERE (customer_id, order_date) in (
    SELECT customer_id, min(order_date)
    FROM sales
    GROUP BY customer_id
);

 

 

쿼리를 좀만 다듬으면(사실 SELECT절에서 필요한 정보만 뽑도록 하면), 다음과 같이 회원 아이디와 상품명만 추출 가능하다.

 

SELECT DISTINCT customer_id, product_name
FROM sales
INNER JOIN menu ON sales.product_id = menu.product_id 
WHERE (customer_id, order_date) in (
    SELECT customer_id, min(order_date)
    FROM sales
    GROUP BY customer_id
);

 

 

 

풀이 2 : PARTITION BY를 이용한 접근

 

그러나 이 문제는 PARTITION BY를 이용해서도 풀 수 있다.

 

✅ PARTITION BY?

: 특정 그룹 내에서의 집계 또는 순위를 매길 때 사용하는 함수.

(집계함수 : MIN, MAX, SUM 등을 말함)

(순위함수 : ROW_NUMBER, RANK 등을 말함)

 

🧐 GROUP BY와의 차이점?

: GROUP BY, PARTITION BY 모두 특정 컬럼을 기준으로 레코드들을 그룹핑한다는 공통점이 있다. 그러나 GROUP BY는 그룹핑되는 과정에서 레코드들의 상세 정보들이 없어지는 반면(레코드들이 집약되기 때문), PARTITION BY는 그룹핑돼도 레코드들의 상세 정보가 남아있게 된다.

 

sales테이블에 대해 회원별로 구매 횟수를 알고 싶다고 할 때, GROUP BY와 PARTITION BY에 대한 다음 결과를 비교해보면 더욱 잘 이해할 수 있다.

 

 

1) GROUP BY로 그룹핑해서 계산하는 경우

 

SELECT customer_id, count(*) as purchased_count
FROM sales
GROUP BY customer_id;

 

 

sales 테이블이 갖고 있던 레코드들이 나오는게 아니라, 집계 결과만 나오는 모습을 확인 가능하다. 만약 order_date나 product_id를 SELECT에서 찍어서 보려고 하면, 1055 ERROR가 난다 (GROUP BY가 사용된 경우, GROUP BY로 그룹핑할 때 사용된 컬럼만 SELECT에서 명시가 가능하기 때문)

 

 

2) PARTITION BY로 그룹핑해서 계산하는 경우

 

SELECT customer_id, count(*) OVER (PARTITION BY customer_id) as purchased_count
FROM sales;

 

 

sales테이블이 갖고 있는 레코드들이 살아있음을 확인할 수 있다. SELECT절에서도 order_date, product_id컬럼값을 뽑아낼 수 있으며 에러가 나지 않는다.

 

SELECT customer_id, order_date, product_id, count(*) OVER (PARTITION BY customer_id) as purchased_count
FROM sales;

 

 

 

 

RANK순위 함수와 ORDER BY를 사용하면 그룹핑된 섹션 내에서 order_date를 기준으로 다음과 같이 순위를 매길 수 있다.

 

SELECT
    customer_id,
    order_date,
    product_name,
    RANK() OVER (PARTITION BY customer_id ORDER BY order_date) as order_date_rank
FROM sales
INNER JOIN menu ON sales.product_id = menu.product_id;

 

 

그룹핑된 각 섹션 내에서 order_date값이 같으면 같은 rank가 매겨진다. RANK와는 달리 DENSE_RANK를 사용하면 동일한 등수가 발생해도 다음번 등수는 이전 등수 + 1이 된다.

 

위 결과를 WHERE절에서 활용, order_date_rank가 1인 레코드만 추출하면 원하는 결과를 가져올 수 있다. 

 

WITH ranked_date as (
    SELECT
        customer_id,
        order_date,
        product_name,
        RANK() OVER (PARTITION BY customer_id ORDER BY order_date) as order_date_rank
    FROM sales
    INNER JOIN menu ON sales.product_id = menu.product_id
)

SELECT DISTINCT customer_id, product_name
FROM ranked_date
WHERE order_date_rank = 1;

 

 

 

더 적합한 방식은?

 

위에서 소개한 GROUP BY와 PARTITION BY 모두 같은 결과를 뱉는다. 그러면 둘 중 어느 것이 좀 더 "적합"할까? 어느 것이 더 효율적으로 동작하는가? 지금은 sales테이블에 데이터가 끽해야 15개 남짓이지만, x억개 있는 상황이라면 뭐가 더 나을까?

 

쿼리 실행 계획을 살펴보며 어떤 것이 더 나을지 살펴보자.

 

 

문제 출처 : 8 week SQL Challenge

https://8weeksqlchallenge.com/

 

8 Week SQL Challenge

Start your SQL learning journey today!

8weeksqlchallenge.com

 

 

mysqldump?

쉽게 말해 mysql db의 상태를 덤프를 떠서 그대로 백업뜨는 걸 말한다.

sql파일이 만들어지며, dump를 뜬 시점에서 db가 가지고 있던 테이블들과 내용들에 대한 create, insert문들이 들어가있다.

이를 활용해 로컬에서 다루던 db를 그래도 배포 환경에 옮기는 작업, 배포 환경에 있던 db를 로컬로 옮기는 작업 등을 수행할 수 있다.

 

본 포스트에선 AWS RDS에 있는 mysql 데이터를 덤프떠서 로컬로 가져와본다.


 

AWS RDS에 넣어뒀던 data들을 dump를 떠서 로컬로 가져오려고 한다.

DB 관리를 위해 띄워뒀던 Bastion에 접속해 다음과 같은 커맨드를 날리면 된다

 

mysql -h {RDS 엔드포인트} -u {유저명} -p {dump할 DB명} > {경로와 생성할 sql파일의 이름}.sql

 

 

이때 permission denied 에러가 날 수 있는데, 이는 생성할 sql파일의 경로를 ~와 같이 자신에게 권한이 부여된 경로를 작성하면 된다. 내 경우 다음과 같이 커맨드를 날렸다.

 

mysqldump -h somsatangdb.c7jz8i0idkiy.ap-northeast-2.rds.amazonaws.com -u swtAdmin -p wooyoungsoo > ~/petdori.sql

 

 

그 다음 로컬에서 scp 커맨드를 통해 EC2에 위치한 dump파일을 로컬로 가져온다

 

scp -i {pem파일 경로} "{EC2 사용자명}@{EC2 ip}:{다운할 dump파일의 경로와 이름}" {다운받을 파일의 로컬에서의 경로 및 이름}

 

 

별도 설정을 하지 않았다면, EC2 사용자명은 ubuntu는 ubuntu, amazon linux는 ec2-user다.

또한, 로컬에서 커맨드를 날리는 것이므로 EC2에 적절한 보안그룹을 설정해줘야 한다는 사실을 잊으면 안 된다.

내 경우 다음과 같이 커맨드를 날렸다

 

scp -i ./sst.pem "ubuntu@13.124.138.97:~/petdori.sql" ./petdori.sql

 

 

다음과 같이 dump파일이 잘 다운받아진 걸 확인 가능하다.

 

 

다음과 같이 rds에서 가지고 있던 테이블과 내용물들에 대한 쿼리가 작성돼있는 걸 볼 수 있다.

 

 

그 다음, 나는 로컬에서 도커 컨테이너로 mysql을 사용했던만큼, 컨테이너 내부로 dump파일을 옮겨야 했다. 다음 형태로 커맨드를 입력하면 된다.

 

docker cp {로컬에서의 dump경로 및 이름} {컨테이너명}:{dump파일을 둘 컨테이너 내 경로}

 

 

내 경우 다음과 같이 입력했다.

 

docker cp ./Downloads/petdori.sql mysqlDB:/

 

 

이제 docker exec을 가미해 다음과 같은 형태의 커맨드를 날리면 된다.

 

mysql -u {사용자명} -p {DB명} < {덤프파일}

 

 

이렇게 하면 지정한 DB에 덤프파일에 있던 쿼리들이 수행된다. 내 경우 다음과 같이 커맨드를 날렸다.

 

mysql -u root -p wooyoungsoo < petdori.sql

 

 

다음과 같이 RDS상에 있던 테이블과 내용물들이 잘 들어가있는 걸 볼 수 있었다!

 

 

/etc/passwd  파일

사용자들의 계정 정보가 저장된 파일이다. 다음과 같은 구조이다.

유저명:x:UID:GID:설명:홈 디렉토리:로그인 쉘

 

  1. 유저명 : 말 그대로 유저 이름
  2. x : 원래 초기 유닉스 시스템에선 사용자 암호가 이쪽에 저장됐으나, 현재 사용자 암호는 /etc/shadow에 있다(해쉬된 형태로)
  3. UID : 사용자들에게 할당되는 고유한 ID 번호
  4. GID : 그룹 ID로, 리눅스에서 사용자는 무조권 한 개 이상의 그룹에 소속돼있다.
  5. 설명 : 사용자의 실명, 부서명 같은 것들
  6. 홈 디렉토리 : 해당 사용자 계정에 할당된 홈 디렉토리 경로 (ex: /home/jofe 등)
  7. 로그인 쉘 : 사용자의 로그인 쉘을 지정(로그인 후 켜지는 쉘)

 

/etc/shadow 파일

사용자 암호에 대한 정보를 별도로 관리하는 파일이다. 전지전능한 root께서만 읽고 쓸 수 있으며 shadow그룹은 읽기만 가능하다. 다음과 같은 구조다.

유저명:암호:최종변경일:MIN:MAX:WARNING:INACTIVE:EXPIRE:Flag

 

  1. 유저명 : 말 그대로 유저 이름
  2. 암호 : 해쉬된 형태의 암호가 있다
  3. 최종 변경일 : 1970년 1월 1일을 기준으로 최종 변경된 날을 표기
  4. MIN : 비밀번호 변경 주기를 의미(암호 변경 후 사용해야 하는 최소 기간)
  5. MAX : 비밀번호를 사용가능한 최대 기간. 즉 유효 기간이라 생각하면 됨
  6. WARNING : 암호가 만료되기 x일 전부터 알려주겠다 할 때 x에 해당
  7. INACTICE : 암호가 만료된 후에도 이 값으로 지정된 날수 동안은 로그인 가능(마지막으로 삶을 돌아보는 시간..)
  8. EXPIRE : 사용자 계정이 만려되는 날짜(1970년 1월 1일 기준)
  9. Flag : 향후 사용할 목적으로 비워둔 항목

 

참고로 암호의 경우 단순히 해쉬된 암호만 띡하니 있지 않고, 사용된 해쉬 알고리즘의 타입(MD5, SHA)와 salt값도 동봉돼있다. 비번을 해쉬할 때 단순히 비밀번호만 해쉬하는게 아니라 랜덤하게 만들어낸 salt값을 함께 해싱하는데, 이를 통해 유저 A, B가 똑같은 평문 비밀번호를 사용해도 salt값이 달라서 전체적인 해쉬 값은 다른 걸 가지게 된다. 보안에도 용이한 셈.

 

 

/etc/group 파일

각 그룹들의 정보가 저장된 파일. 참고로 각 사용자마다 기본 그룹이라는게 있는데, /etc/passwd에 사용자별로 가지는 gid가 바로 그것이다. 

그룹명:x:GID:그룹 멤버

 

  1. 그룹명 : 말 그대로 그룹 이름
  2. x : 그룹의 암호 저장(그룹 암호는 newgrp 커맨드로 자신이 속하지 않은 그룹으로 전환할 때 필요함)
  3. GID : 그룹을 식별하는 고유 번호
  4. 그룹 멤버 : 해당 그룹에 속하는 멤버들의 을 쉼표로 구분해서 이 영역에 표기

 

/etc/gshadow 파일

그룹 암호가 저장된 파일

 

그룹명:그룹 암호:관리자:그룹 멤버

 

  1. 그룹명 : 말 그대로 그룹 이름
  2. 그룹 암호 : 암호화된 그룹 암호
  3. 관리자 : 그룹의 암호나 멤버를 바꿀 수 있는 사용자 계정. 여러 개면 쉼표로 구분
  4. 그룹 멤버 : 그룹에 속한 멤버들의 계정명. 쉼표로 구분함

 

리눅스에서 사용자 계정 추가하기

1. useradd 커맨드 활용

기본적으론 "useradd 유저명"을 통해 사용자 계정 생성 가능

-u, -g 등의 옵션을 붙여 UID나 GID 등을 직접 지정하는 것이 가능하다.

정말 아무것도 없이 계정만 생성해주므로, 비밀번호 등은 별도로 설정해줘야 한다

 

2. adduser 커맨드 활용

기본적으론 "adduser 유저명"을 통해 사용자 계정 생성 가능

-uid, -gid 등의 옵션을 붙여 UID, GID 등을 직접 지정하는 것이 가능하다.

생성 시 비밀번호 등도 입력받게끔 만들어져있다.

 

비슷하게, groupadd커맨드를 통해 기존에 없던 그룹 생성도 가능하며, -g 등의 옵션을 곁들여 GID지정도 가능하다.

 

 

리눅스에서 사용자 계정 수정하기 - usermod 커맨드 활용

usermod [옵션] [유저명]

-u, -g 등의 옵션으로 UID, 기본 그룹 등의 수정이 가능하다. -aG옵션을 통해 해당 유저를 sudo등의 그룹에 넣을 수도 있다.

비슷하게, groupmod커맨드를 통해 그룹의 GID등을 수정가능하다.

 

 

리눅스에서 사용자 계정 삭제 - userdel 커맨드 활용

userdel [옵션] [유저명]

-r 옵션을 줘서 홈 디렉토리도 삭제할 수 있고, -f옵션을 통해 해당 사용자가 로그인 중이어도 강제 삭제가 가능하다.

비슷하게, groupdel커맨드를 통해 그룹을 삭제할 수 있다.

 

 

리눅스에서 소속 그룹 변경하기 - newgrp 커맨드 활용

newgrp [그룹명]

말 그대로 사용자가 작업시 현재 소속 그룹을 기본 그룹이 아닌 다른 그룹으로 바꿀 때 사용하는 커맨드다.

 

 

UID vs EUID

UID : 실제 유저의 ID다.

EUID : 유효한 사용자 ID로, 현재 명령을 누구 권한으로 수행하고 있는가에 해당하는 개념이다. 즉, UID가 a인 상태에서 커맨드를 실행했을 때 EUID는 a가 아니라 b가 될 수도 있다.

 

 

파일/디렉토리의 소유자 변경하기

chown [옵션] [사용자계정] [파일 또는 디렉토리명]

ex) chown root file1 : file1의 소유자를 root로 바꾼다

 

 

 

 

 

 

 

 

 

 

'Linux' 카테고리의 다른 글

리눅스 awk 커맨드 사용법 및 동작 원리  (0) 2024.04.21

레디스(Redis)의 개념

Remote Dictionary Server의 줄임말로, key-value 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈 소스 기반의 NoSQL DB다. 타 DB처럼 디스크에 데이터를 저장하는게 아닌 메모리에 데이터를 저장하는 인-메모리 DB이며, 메모리를 통한 빠른 속도 때문에 Cache 용도로 많이 활용되지만 그 외에도 Message Broker나 Streaming engine으로서의 역할도 맡을 수 있다.

 

Message broker로 쓰인다는 것의 의미 : 분산 시스템에서 메시지 전달 및 이벤트 처리를 관리하기 위한 중요한 역할을 하는 소프트웨어 컴포넌트로 활용된다는 것

Streaming engine로 쓰인다는 것의 의미 : 스트림(Streams)라고 불리는 자료 구조를 활용해 실시간 데이터 처리를 위한 목적으로 활용된다는 것

 

 

레디스에서 고가용성을 확보하는 방법

1. Stand alone (No HA)

Redis 서버 1대로 아키텍처를 구성하는 방법을 말하며, 해당 서버가 다운되면 인생 끝나는 것이므로 고가용성이 보장되지 않는다. 서버가 다운될 시 AOF 또는 snapshot을 사용해 재시작한다.

 

AOF : Append Only File의 줄임말로, Redis의 변경 사항을 기록하는 파일을 말한다. Redis 서버에 새로운 명령(조회는 제외)이나 데이터 변경이 발생하면, 해당 명령 또는 변경 내용을 AOF 파일에 연속적으로 추가한다. 이는 변경 사항을 로그로 남기는 방식이다.

snapshot : 특정 시점에 메인 메모리에 있는 모든 레디스 데이터를 디스크에 쓴 것, 즉 일종의 백업을 말한다.

 

 

2. 이중화 (a.k.a Master & Slave, Half HA)

Redis 서버 2대로 아키텍처를 구성하는 것을 말하며, Slave는 Master의 데이터를 실시간으로 전달받아 보관한다. Master가 다운될 시 Slave를 FailOver시킬 수 있으나 수동으로 직접 해줘야 한다. Slave를 하나가 아닌 여러 개를 둘 수도 있다.

 

 

3. 이중화 + 센티널 (HA, 무중단 서비스 가능)

Master & Slave 구성에 센티널(Sentinal)을 추가해서 각 서버를 감시하도록 하도록 하는 아키텍처를 구성하는 것을 말한다. 센티널은 Master를 감시하고 있다가 Master가 다운되면 Slave를 Master로 승격시킨다. Redis Client(즉 Application)은 새로운 Master로 접속해서 서비스를 계속한다. 센티널은 데이터 처리는 담당하지 않으며, 센티널 자체가 다운되는 상황을 고려해 일반적으로 3대의 센티널을 운용한다.

 

센티널이란 : Master와 Slave들을 감시하고 있다가 Master가 다운되면 이를 감지해서 관리자의 개입없이 자동으로 Slave를 Master로 올려주는 감시자(보초)를 말한다. 즉 센티널은 감시, 자동 장애조치(Automatic FailOver)의 역할을 하며 알림(FailOver될 때 관리자한테 메일 보내던가 하는..)의 역할도 맡을 수 있다.

 

 

4. 레디스 클러스터 (HA)

샤딩(sharding)을 사용하여 복수의 Redis노드에 데이터를 분할하는 방식으로 아키텍처를 구성하는 것을 말한다. Master가 3대라면, 전체 데이터를 3대에 나누어 저장하는 것이다. (100개가 있다면 1번에 33, 2번에 33, 3번에 34개 이런 식으로). 데이터들의 key에 hash함수를 멕인 값에 따라 어느 Master 서버로 데이터를 둘지 결정하게 된다. 각 Master 서버가 데이터 처리 뿐만 아니라 센티널 역할도 같이 수행하며, 최소 3대의 Master 서버가 필요하다.

 

샤딩을 통해 데이터들을 분할해서 각 Master 서버에 저장하기 때문에 하나의 서버라도 다운되면 데이터 유실이 생기지 않을까라는 생각을 할 수 있으나, 위 그럼처럼 클러스터를 구성하면 아무리 Master들이 다운되도 하나의 서버만 살아있다면 정상적인 운용이 가능하다.

 

클러스터 방식을 통해 여러 대의 서버가 하나로 묶여 마치 1개의 시스템처럼 동작하게 되며, 여러 서버에 데이터를 분산하여 저장하기 때문에 부하를 여러 대의 서버로 분산시키므로 더 빠른 속도로 사용자에게 서비스를 제공할 수 있게 된다. 

 

샤딩(Sharding)이란 : 대량의 데이터를 처리하기 위해 여러 개의 데이터베이스에 분할하는 기술 즉 DBMS안에서 데이터를 나누는 것이 아니라 DBMS 밖에서 데이터를 나누는 방식임에 유의하자.

 

 

클러스터 vs 센티널

우선 둘 다 고가용성을 챙길 수 있다. 그러나 클러스터는 확장성이 있는 반면, 센티널은 확장성이 없는 아키텍처다. 또한 클러스터는 센티널에 비해 빠른 액세스 속도를 보일 수 있다. 반면 센티널은 클러스터에 비해 배포 및 관리가 용이하다. 따라서, 일반적으로 중소 정도의 규모라면 센티널을, 수평적 확장이 필요한 대규모의 경우는 클러스터를 추천한다고 한다.

 

 

참고한 자료

http://redisgate.kr/redis/configuration/redis_overview.php

 

Redis Architecture Overview

 

redisgate.kr

 

저작자의 허락을 받고 아래 영상의 내용을 요약한 글입니다.

 

https://www.youtube.com/watch?v=H6lpFRpyl14&t=268s 

 


HTTPS : Http + Secure. 기존의 HTTP에 "안전"을 더한 형태. 그럼 뭘 안전하게 하는 것인가? 크게 2가지로 볼 수 있다

 

  1. 내가 사이트에 보내는 정보들을 제 3자가 못 보게 한다
  2. 내가 접속하는 사이트가 신뢰할 수 있는 사이트인지 알려준다

 

그러면 이 두 가지 내용이 어떤 원리로 구현되는가?

 

1은 비대칭키를 사용한 암호화로 가능하다. 서버가 가진 "공개키"로 암호화해서 보내면, 해당 암호문은 "개인키"를 가진 서버만이 해독 가능하다. 따라서 제 3자가 내가 보내는 트래픽을 엿봐도 해독(복호화)할 수 없다. (제 3자도 서버의 공개키를 알 수는 있겠지만, 비대칭키의 특성상 공개키로 암호화한 내용을 공개키로 복호화할 수 없기 때문)

 

2도 마찬가지로 비대칭키 특성을 통해 가능하다. 서버에서 보내는 정보는 서버의 "개인키"로 일부가 암호화돼있다. 그러나 내가 가진 서버의 "공개키"로는 오직 해당 서버의 "개인키"로 암호화한 것만 복호화가 가능하다. 즉 해당 서버가 아닌 다른 서버에서 온 내용은 내가 가진 공개키로는 복호화가 안 된다는 거다. 이 때 신뢰가능한, 즉 공인된 특정 기관에서 내가 가진 공개키가 해당 서버의 공개키가 맞다는 인증만 받는다면 해당 서버를 신뢰할 수 있게 되는 것.

 

 

그럼 프로그래머의 영역에서, HTTPS는 실제로 어떻게 구현될까?

 

우선 jofe라는 사이트가 공개키들을 뿌렸다고 가정하자. 난 이 공개키가 jofe란 사이트의 공개키인지(즉 정품인지)를 확인할 수 있어야 한다. 이를 인증해주는(보장해주는) 공인된 기관들이 존재하며, 이들을 CA(Certificate Authority)라고 부른다. 우리가 사용중인 브라우저들엔 이 CA들의 목록이 저장돼있다.

 

이런 상황에서, 내(client)가 jofe(server)에 접속할려고 한다. 랜덤한 데이터나 만들어서 서버에게 주면, 서버는 그에 대한 답변으로 랜덤한 데이터와 "CA에서 발급해준 해당 서버의 인증서"를 만들어서 내게 준다. (클라이언트 서버가 악수했다고 표현)

 

그럼 클라이언트는 해당 인증서가 진품인지를 판단하는데, 이 때 브라우저에 내장된 CA들의 정보를 통해 확인한다. CA의 인증을 받은 인증서들은 해당 CA의 개인키로 암호화돼있기 때문에 그 CA의 공개키로 복호화가 가능하다. 즉 받은 인증서가 진품이라면 브라우저에 있는 C의 공개키로 해당 인증서를 복호화 가능한 것이다.

 

성공적으로 복호화된 인증서에는 인증서를 보낸 서버(jofe)의 "공개키"가 들어있다. 하지만 그렇다고 이제부터 바로 내가 서버로 보낼 메시지들을 공개키로 암호화해서 보낼 수 있는 건 아니다. 왜냐하면 비대칭키는 대칭키 방식보다 컴퓨터에 주는 부담이 크기 때문. (주고받을 다량의 데이터를 하나하나 비대칭키로 암호화하는 건 부담이 된다는 것)

 

따라서 주고받을 데이터를 "대칭키"로 암호화한다. 대칭키를 사용하는 경우의 가장 큰 단점은 양쪽이 똑같은 키를 가져야 하므로 어쨌거나 한 번은 서로 통신을 해서 키를 주고받아야 하는데, 이 때 탈취당할 위험이 있다는 거다. 이 때 클라이언트는 아까 악수할 때 만들어진 데이터들을 이용해 임의의 대칭키를 만든 다음, 서버로 이를 공유할 때 "비대칭키"를 사용한다. 즉 서버의 "공개키"로 임의로 만든 대칭키를 암호화하여 보낸다는 말. 서버는 이를 자신의 "개인키"로 복호화하여 클라이언트가 만든 대칭키를 그대로 쓸 수 있게 되는 거고, 이제 양쪽이 서로 대칭키를 갖게 됐으니 이를 통해 서로 데이터들을 암호화하여 통신할 수 있게 된다.

 

그림으로 나타내면 다음과 같다

우선 클라이언트가 랜덤한 데이터를 만들어 서버로 보낸다

 

그러면 서버도 랜덤한 데이터와 함께 CA에서 발급해준 인증서를 보낸다(이 인증서는 해당 CA의 개인키로 암호화돼있다)

클라이언트는 브라우저에 내장된 CA목록들을 통해 해당 CA의 공개키로 인증서를 복호화한다. 어떤 공개키로도 복호화가 안되면 당연히 서버 쪽을 신뢰할 수 없는 거고, 복호화됐다면 인증서를 보낸 서버를 공인된 서버라고 신뢰할 수 있게 된다.

또한 해당 인증서는 서버의 공개키를 가지고 있어서, 복호화에 성공했다면 요로코롬 서버의 공개키를 얻게 되는 거다.

 

이제 클라이언트는 아까 만들었던 클라이언트쪽의 랜덤데이터와 서버로부터 받은 서버쪽 랜덤데이터를 활용해 대칭키를 만들고, 이를 인증서에 들어있던 서버의 공개키로 암호화하여 서버에게 보낸다.

서버는 이를 자신의 개인키로 복호화할 수 있고, 이를 통해 클라이언트와 서버가 동일한 대칭키를 갖게 되는 것이며 이를 통해 데이터를 서로 암호화하고 복호화하며 통신할 수 있게 된다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

+ Recent posts