웹 개발 개념 정리

주니어 개발자를 위한 웹 개발 기초:

I. 핵심 CS 지식

개발자로서 탄탄한 기본기를 쌓기 위해 반드시 알아야 할 핵심 컴퓨터 과학(CS) 지식을 정리해본다. 복잡한 개념을 명확하고 직접적인 설명으로 풀어내고자 한다.

1. 프로그램, 프로세스, 프로세서, 그리고 스레드

컴퓨터 작동의 기본 단위인 프로그램, 프로세스, 프로세서, 스레드의 개념을 명확히 정의하고 그 관계를 설명한다.

  • 프로그램 (Program): 정적인 명령어 집합

    프로그램이란 특정 작업을 수행하기 위해 작성된 코드와 명령어의 집합이다. 컴파일 과정을 거쳐 실행 가능한 파일 형태로 하드 디스크나 SSD 같은 보조기억장치에 저장되어 있는 ‘정적인(static)’ 상태다. 즉, 아직 실행되지 않고 저장만 되어 있는 코드 덩어리 그 자체를 의미한다.

  • 프로세스 (Process): 실행 중인 프로그램

    프로세스란 보조기억장치에 있던 프로그램이 실행을 위해 메모리(RAM)에 적재(loading)되어, 운영체제(OS)로부터 시스템 자원(CPU 시간, 메모리 등)을 할당받은 ‘동적인(dynamic)’ 상태를 말한다. 간단히 말해, ‘실행 중인 프로그램’이 바로 프로세스다. 하나의 프로그램으로 여러 개의 프로세스를 생성할 수 있다. 예를 들어, 크롬 브라우저 프로그램을 여러 개 실행하면 각 창이 독립적인 프로세스로 동작한다.

    프로세스는 운영체제가 관리하는 작업의 최소 단위이며, 고유한 생명주기(Process State)를 가진다. 프로세스는 생성(Create)되어 CPU를 할당받기 위해 대기하는 ‘준비(Ready)’ 상태, CPU를 점유하여 코드를 실행하는 ‘실행(Running)’ 상태, I/O 작업 등을 기다리며 잠시 실행을 멈춘 ‘대기(Waiting)’ 상태를 거치며, 실행이 완료되면 ‘종료(Terminated)’ 상태가 되어 모든 자원을 시스템에 반납한다. 운영체제는 이러한 프로세스들의 상태를 관리하며 시스템 자원을 효율적으로 스케줄링한다.

  • 프로세서 (Processor/CPU): 명령어 실행 주체

    프로세서는 중앙 처리 장치(CPU)를 의미하며, 메모리에서 프로세스의 명령어를 가져와 해석하고 연산을 수행하는 핵심 하드웨어 장치다. CPU 내부에는 실질적인 연산을 담당하는 ‘코어(Core)’가 있으며, 코어의 개수가 많을수록 여러 작업을 동시에 처리하는 병렬 처리(Parallelism) 능력이 향상된다.

  • 스레드 (Thread): 프로세스 내의 실행 흐름 단위

    스레드란 하나의 프로세스 내에서 실행되는 더 작은 실행 흐름의 단위다. 한 프로세스는 하나 이상의 스레드를 가질 수 있으며, 이를 멀티스레딩(Multi-threading)이라고 한다.

    프로세스 내의 스레드들은 해당 프로세스의 메모리 자원(코드, 데이터, 힙 영역)을 공유한다. 이 덕분에 스레드 간 데이터 교환이 용이하고 자원을 효율적으로 사용할 수 있다. 하지만 각 스레드는 독립적인 실행 흐름을 유지해야 하므로, 자신만의 프로그램 카운터(PC), 레지스터 집합, 그리고 스택(Stack) 공간은 별도로 할당받는다. 스레드를 활용하면 하나의 프로세스 내에서 여러 작업을 동시에 수행하는 것처럼 보이는 동시성(Concurrency)을 구현하거나, 멀티코어 환경에서 실제로 여러 작업을 병렬로 처리하여 성능을 높일 수 있다.


2. 홈페이지 vs. 웹 서비스: 결정적 차이는 ‘CRUD’에 있다!

‘홈페이지’와 ‘웹 서비스’는 종종 혼용되지만, 데이터 처리 관점에서 명확한 차이가 있다. 그 핵심적인 구분 기준은 데이터 조작 기능, 즉 ‘CRUD’의 유무다.

  • 홈페이지 (정적 콘텐츠): 단방향 정보 제공

    전통적인 의미의 홈페이지는 주로 정보를 일방적으로 제공하는 데 목적이 있다. 회사 소개, 제품 설명, 연락처 등 정적인 콘텐츠를 사용자에게 보여주는 것이 주된 기능이다. 사용자는 주어진 정보를 ‘읽기(Read)’만 할 수 있을 뿐, 데이터를 직접 생성, 수정, 삭제하는 상호작용은 거의 불가능하다.

  • 웹 서비스 (동적 상호작용): 양방향 데이터 처리

    웹 서비스는 사용자가 데이터의 주체가 되어 적극적으로 상호작용하는 양방향 플랫폼이다. 사용자는 단순히 정보를 소비하는 것을 넘어, 새로운 데이터를 생성하고, 기존 데이터를 수정하며, 불필요한 데이터를 삭제할 수 있다. 이러한 동적 상호작용의 핵심에 CRUD가 있다.

  • CRUD란 무엇인가? 데이터 처리의 네 가지 기본 연산

    CRUD는 대부분의 소프트웨어가 갖는 기본적인 데이터 처리 기능인 생성(Create), 읽기(Read), 갱신(Update), 삭제(Delete)를 의미하는 약어다.

    • Create (생성): 새로운 데이터를 데이터베이스에 저장하는 연산이다.

      • 예시: 블로그에 새 글 작성, SNS에 사진 업로드, 쇼핑몰 회원가입 등


    • Read (읽기): 데이터베이스에 저장된 데이터를 조회하여 사용자에게 보여주는 연산이다.

      • 예시: 게시판 글 목록 보기, 친구 프로필 조회, 상품 상세 정보 확인 등


    • Update (수정): 이미 저장된 기존 데이터를 변경하는 연산이다.

      • 예시: 작성한 글의 오타 수정, 프로필 사진 교체, 배송지 주소 변경 등


    • Delete (삭제): 데이터베이스에서 특정 데이터를 영구적으로 제거하는 연산이다.

      • 예시: 작성한 댓글 삭제, 장바구니 상품 제거, 회원 탈퇴 등


    결론적으로, 사용자가 데이터에 대해 C(생성), U(수정), D(삭제) 연산을 수행할 수 있다면 이는 ‘웹 서비스’의 특징을 갖춘 것이다. 반면 R(읽기) 기능만 주로 제공한다면 ‘홈페이지’에 가깝다고 볼 수 있다.


3. 대칭키와 비대칭키: 암호화의 핵심 원리

인터넷 통신 중 데이터를 가로채도 내용을 알 수 없도록 보호하는 ‘암호화’ 기술의 핵심 원리인 대칭키와 비대칭키 방식을 알아본다.

  • 대칭키 (Symmetric Key): 단일 키 암호화

    • 원리: 데이터를 암호화(encryption)할 때와 복호화(decryption)할 때 ‘동일한 하나의 키’를 사용하는 방식이다. 송신자와 수신자가 같은 키를 공유해야만 통신이 가능하다.
    • 장점: 암호화 및 복호화 연산 과정이 단순하여 속도가 매우 빠르다. 따라서 대용량 데이터를 암호화하는 데 효율적이다.
    • 단점 (키 배송 문제): 통신을 시작하기 전에 송신자와 수신자가 어떻게든 안전하게 동일한 키를 공유해야 한다는 치명적인 문제가 있다. 만약 이 키를 전달하는 과정에서 키가 유출되면, 이후의 모든 암호화된 통신 내용은 쉽게 해독될 수 있다. 또한, 통신 상대방이 늘어날수록 관리해야 할 키의 개수가 기하급수적으로 증가한다.
  • 비대칭키 (Asymmetric Key): 공개키 암호화

    • 원리: 대칭키의 키 배송 문제를 해결하기 위해 고안된 방식으로, 암호화와 복호화에 서로 다른 키를 사용한다. 이 두 키는 수학적으로 한 쌍을 이루며, 각각 ‘공개키(Public Key)’와 ‘개인키(Private Key)’라고 불린다.
    • 작동 방식:
      1. 데이터를 수신할 주체(B)가 자신만의 ‘개인키’와 그에 맞는 ‘공개키’ 한 쌍을 생성한다.
      2. ‘개인키’는 절대로 외부에 노출하지 않고 안전하게 보관하며, ‘공개키’는 데이터를 보낼 주체(A)를 포함한 누구에게나 공개적으로 배포한다.
      3. 데이터를 보내는 A는 B로부터 받은 ‘공개키’를 사용하여 데이터를 암호화한 후 전송한다.
      4. 암호화된 데이터는 오직 그 공개키와 쌍을 이루는 B의 ‘개인키’로만 복호화할 수 있다. 따라서 중간에 데이터가 탈취되더라도 개인키가 없는 공격자는 내용을 볼 수 없다.
    • 장점: 공개키는 이름 그대로 외부에 공개되어도 되므로, 키를 안전하게 전달해야 하는 문제가 근본적으로 해결된다. 또한, 개인키 소유자만이 데이터를 복호화할 수 있다는 특성을 이용해 ‘전자 서명’을 구현함으로써 메시지 출처를 증명하고 부인 방지 기능을 제공할 수 있다.
    • 단점: 암호화 및 복호화 과정에 복잡한 수학적 연산이 필요하여 대칭키 방식에 비해 속도가 현저히 느리다.
  • 실전 적용: 하이브리드 방식 (HTTPS의 원리)

    대칭키는 빠르지만 키 교환이 위험하고, 비대칭키는 안전하지만 느리다. 따라서 실제 웹 통신 보안 표준인 SSL/TLS (HTTPS)에서는 두 방식의 장점을 결합한 ‘하이브리드 방식’을 사용한다.

    1. (비대칭키 활용) 통신 초기 단계(Handshake)에서는 안전하지만 느린 ‘비대칭키’ 방식을 사용하여, 실제 데이터를 암호화하는 데 사용할 ‘대칭키’를 안전하게 교환한다.
    2. (대칭키 활용) 양측이 안전하게 ‘대칭키’를 공유한 후에는, 실제 대용량 데이터(로그인 정보, 메시지 등)는 속도가 빠른 ‘대칭키’로 암호화하여 통신한다.

      이처럼 비대칭키는 대칭키를 안전하게 교환하는 목적만으로 사용하고, 실제 데이터 통신은 대칭키로 처리하여 보안과 성능을 모두 확보한다.
  • 대칭키 vs. 비대칭키 핵심 비교


항목대칭키비대칭키
키 (Key)암호화/복호화에 동일한 키 1개 사용암호화/복호화에 서로 다른 키 1쌍(공개키/개인키) 사용
속도 (Speed)빠름느림
키 관리키 배송/교환 문제 발생, 관리할 키가 많음키 배포가 용이하며 관리가 수월함
보안성키가 노출되면 전체 통신이 취약해짐개인키만 안전하게 보관하면 보안성이 높음
주요 용도대용량 데이터 암호화 (예: AES)키 교환, 디지털 서명 (예: RSA)

II. 인증 파헤치기: Firebase 로그인 따라가 보기

앞서 다룬 이론적 개념들이 실제 서비스에서 어떻게 적용되는지 BaaS(Backend as a Service)인 Firebase 인증 시스템을 통해 구체적으로 살펴본다.

1. Firebase 로그인 전체 흐름

Firebase 소셜 로그인 과정은 네 주체 간의 상호작용으로 이루어진다.

  • 참여자

    • 사용자: 서비스를 이용하려는 주체.
    • 프론트엔드 (클라이언트 앱): 사용자와 직접 상호작용하는 웹/앱.
    • Firebase 인증 서버: 구글이 운영하는 인증 전문 서버.
    • 백엔드 서버: 서비스의 핵심 비즈니스 로직을 처리하는 자체 개발 서버.
  • 로그인 흐름 (Google 로그인 예시)

    1. 사용자 → 프론트엔드: 사용자가 프론트엔드 화면의 ‘Google로 로그인’ 버튼을 클릭하여 로그인 절차를 시작한다.


    2. 프론트엔드 → Firebase 인증 서버: 프론트엔드는 Firebase SDK의 signInWithPopup()과 같은 함수를 호출하여 Firebase 인증 서버에 인증을 요청한다. 이 요청에는 사전에 Firebase 콘솔에서 설정한 제공자(Provider) 정보가 포함된다.


    3. Firebase 인증 서버 ↔ 사용자: Firebase는 구글 로그인 팝업창을 띄운다. 사용자는 이 팝업창에서 자신의 구글 계정 정보를 입력하여 인증을 완료한다. 이 과정에서 사용자의 비밀번호는 프론트엔드나 백엔드 서버에 전달되지 않는다.


    4. Firebase 인증 서버 → 프론트엔드: 인증이 성공하면, Firebase 인증 서버는 해당 사용자를 식별하는 정보가 담긴 ID 토큰과 토큰 갱신에 사용될 리프레시 토큰을 프론트엔드에 발급한다.


    5. 프론트엔드 → 백엔드 서버: 프론트엔드는 백엔드 서버의 보호된 API(예: 마이페이지 정보 요청)를 호출할 때, HTTP 요청의 Authorization 헤더에 Bearer <ID_TOKEN> 형식으로 ID 토큰을 첨부하여 전송한다.


    6. 백엔드 서버 → Firebase 인증 서버: 백엔드 서버는 클라이언트가 보낸 ID 토큰을 맹목적으로 신뢰하지 않는다. 대신 Firebase Admin SDK의 verifyIdToken() 함수를 사용하여 해당 토큰이 유효한지 Firebase 인증 서버에 직접 검증을 요청한다.


    7. Firebase 인증 서버 → 백엔드 서버: 토큰 검증이 성공하면, Firebase는 해당 토큰이 유효함을 확인하고 토큰에 포함된 사용자의 고유 식별자(UID) 등의 정보를 백엔드 서버에 반환한다.


    8. 백엔드 서버 → 프론트엔드: 백엔드 서버는 검증된 UID를 기반으로 자체 데이터베이스에서 사용자를 식별하고, 요청된 비즈니스 로직을 처리한 후 그 결과를 프론트엔드에 응답한다.


이 흐름의 핵심은 인증 과정을 신뢰할 수 있는 제3자(Firebase)에게 위임하고, 그 결과로 발급된 암호학적으로 검증 가능한 증명서(ID 토큰)를 통해 사용자의 신원을 확인하는 것이다.

2. 백엔드 서버의 토큰 검증

백엔드에서 ID 토큰을 검증하는 과정은 API 보안의 핵심이다. 이 과정이 없다면 누구나 위조된 토큰으로 다른 사용자를 사칭할 수 있다.

  • 토큰의 종류: ID 토큰 vs. 리프레시 토큰

    • ID 토큰 (Access Token): 사용자의 신원과 권한을 증명하는 JWT(JSON Web Token) 형식의 인증서다. 유효 기간이 1시간으로 짧아 탈취 시 피해를 최소화할 수 있다. 백엔드 API를 호출할 때마다 자격 증명을 위해 사용된다.
    • 리프레시 토큰: ID 토큰이 만료되었을 때, 사용자가 재로그인 없이 새로운 ID 토큰을 발급받기 위해 사용되는 토큰이다. 유효 기간이 길며, Firebase 클라이언트 SDK가 이 토큰의 관리와 갱신을 자동으로 처리하므로 개발자가 직접 다룰 일은 거의 없다.
  • 백엔드 설정: Firebase Admin SDK 초기화

    백엔드 서버가 Firebase 인증 서버와 통신하려면 Admin SDK 설정이 필요하다.

    1. Firebase 프로젝트 콘솔의 ‘서비스 계정’ 탭에서 비공개 키 파일(.json)을 다운로드한다. 이 파일은 서버의 모든 권한을 가진 마스터키이므로 외부에 노출되지 않도록 안전하게 관리해야 한다.
    2. 다운로드한 키 파일을 사용하여 백엔드 애플리케이션에서 Admin SDK를 초기화한다. 파일 경로를 코드에 직접 작성하는 대신, GOOGLE_APPLICATION_CREDENTIALS 환경 변수로 경로를 지정하는 방식이 권장된다.
  • 검증 프로세스: 코드 레벨 분석

    Node.js(Express) 환경을 예로 들어 실제 검증 과정을 단계별로 설명한다.

    1. 토큰 추출: 백엔드 서버는 API 요청을 받으면, Authorization 헤더에서 Bearer 접두사를 제거하고 ID 토큰 문자열을 추출한다. 이 로직은 보통 미들웨어 함수로 구현된다.


      const authHeader = req.headers.authorization;
      if (!authHeader ||!authHeader.startsWith('Bearer ')) {
      return res.status(401).send('Unauthorized');
      }
      const idToken = authHeader.split(' ');

    2. 토큰 검증 (verifyIdToken): 추출한 ID 토큰을 admin.auth().verifyIdToken() 함수에 전달하여 검증을 수행한다.

      try {
      const decodedToken = await admin.auth().verifyIdToken(idToken);
      req.user = decodedToken; // 검증 성공 시, 요청 객체에 디코딩된 사용자 정보를 추가
      next(); // 다음 로직으로 제어 전달
      } catch (error) {
      return res.status(401).send('Unauthorized'); // 검증 실패
      }

      verifyIdToken 함수는 내부적으로 다음의 중요한 검증을 자동으로 수행한다 :

      • 서명(Signature) 검증: 토큰이 Firebase의 개인키로 올바르게 서명되었는지 확인하여 위조 여부를 판단한다.
      • 만료 시간(Expiration) 검증: 토큰의 유효 기간(1시간)이 지나지 않았는지 확인한다.
      • 발급자(Issuer) 및 대상(Audience) 검증: 토큰이 해당 Firebase 프로젝트에서 발급된 것이 맞는지 확인한다.
    3. UID 확보 및 활용: 검증이 성공하면 verifyIdToken 함수는 디코딩된 토큰 객체를 반환한다. 이 객체에 포함된 uid는 해당 사용자의 고유 식별자다.


      // API 라우터 핸들러
      app.get('/api/mypage', (req, res) => {
      const uid = req.user.uid; // 미들웨어에서 추가한 사용자 정보에서 uid를 추출

      // 이 uid를 사용하여 데이터베이스에서 사용자 정보를 조회하고 로직을 처리
      db.users.find({ id: uid }).then(userData => {
      res.json(userData);
      });
      });

      토큰이 유효하지 않으면 함수는 에러를 발생시키고, catch 블록에서 ‘401 Unauthorized’ 응답을 반환하여 접근을 차단한다. 이처럼 백엔드는 클라이언트의 주장을 신뢰하지 않고, 오직 암호학적으로 검증된 uid만을 신뢰하여 로직을 수행해야 한다.


코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다