| Cloudflare | AWS | |
|---|---|---|
| 정체 | 네트워크 / 보안 플랫폼 | 클라우드 인프라 플랫폼 |
| 핵심 역할 | 트래픽 최적화 + 보안 | 서버 / DB / 스토리지 등 인프라 제공 |
| 비유 | 건물 앞 경비원 + 도로 | 건물 자체 |
| 기능 | Cloudflare | AWS |
|---|---|---|
| 서버 (EC2) | ❌ | ✅ |
| 데이터베이스 (RDS) | ❌ | ✅ |
| 스토리지 (S3) | ❌ | ✅ (S3) |
| CDN | ✅ (핵심) | ✅ (CloudFront) |
| DDoS 방어 | ✅ (무료 포함) | ✅ (Shield, 유료) |
| DNS 관리 | ✅ (무료) | ✅ (Route 53, 유료) |
| SSL 인증서 | ✅ (무료 자동) | ✅ (ACM, 무료) |
| 서버리스 함수 | ✅ (Workers) | ✅ (Lambda) |
| 정적 사이트 배포 | ✅ (Pages) | ✅ (S3 + CloudFront) |
| 가격 | 무료 플랜 강력 | 종량제, 복잡함 |
실제로 많은 서비스가 AWS + Cloudflare를 함께 사용해:
사용자
↓
Cloudflare (DNS + CDN + DDoS 방어 + SSL)
↓
AWS EC2 (Spring Boot 서버)
↓
AWS RDS (MariaDB)
↓
AWS S3 (이미지 저장)
Cloudflare = 네트워크 레이어 (앞단)
AWS = 인프라 레이어 (뒷단)
경쟁 관계가 아니라 상호 보완 관계!
개인 프로젝트라면:
모범 답안
Cloudflare는 네트워크 레이어 플랫폼으로, 사용자와 서버 사이에 위치해 CDN, DDoS 방어, DNS, SSL 등을 담당합니다. AWS는 클라우드 인프라 플랫폼으로 EC2, RDS, S3 등 실제 서버와 데이터베이스, 스토리지를 제공합니다. 두 서비스는 경쟁 관계가 아니라 상호 보완 관계로, 실무에서는 Cloudflare를 앞단 보안/속도 최적화에, AWS를 뒷단 인프라 운영에 함께 사용하는 경우가 많습니다.
모범 답안
CDN(Content Delivery Network)은 전 세계 여러 곳에 분산된 서버에 콘텐츠를 캐싱해두고, 사용자와 가장 가까운 서버에서 데이터를 전달하는 기술입니다. 예를 들어 한국 사용자가 미국 서버의 이미지를 요청할 때 CDN 없이는 직접 미국 서버까지 요청이 가지만, CDN을 쓰면 한국에 캐싱된 서버에서 바로 응답하므로 응답 속도가 크게 빨라집니다. Cloudflare는 이 CDN을 무료로 제공하고, AWS는 CloudFront라는 유료 CDN 서비스를 제공합니다.
모범 답안
DDoS(Distributed Denial of Service)는 수많은 공격자가 동시에 서버에 요청을 보내 서버를 마비시키는 공격입니다. Cloudflare는 이 트래픽을 서버 앞단에서 차단해주는데, 무료 플랜에서도 기본적인 DDoS 방어가 포함됩니다. AWS는 Shield Standard를 무료로 제공하고, 고급 방어는 Shield Advanced(유료)를 사용합니다. 실무에서는 Cloudflare를 앞단에 두는 것만으로도 대부분의 DDoS 공격을 효과적으로 막을 수 있습니다.
모범 답안
DNS(Domain Name System)는 사람이 읽기 쉬운 도메인 주소(예: google.com)를 컴퓨터가 이해할 수 있는 IP 주소(예: 142.250.196.110)로 변환해주는 시스템입니다. 전화번호부에 비유할 수 있습니다. Cloudflare는 무료로 DNS 서비스를 제공하며 응답 속도가 매우 빠릅니다. AWS는 Route 53이라는 DNS 서비스를 유료로 제공합니다.
모범 답안 (프로젝트 기반)
스키장 정보 플랫폼(snowbs.life) 프로젝트에서 두 서비스를 함께 사용했습니다. 도메인 DNS 관리와 SSL 인증서는 Cloudflare에서 무료로 처리하고, Next.js 프론트엔드는 Cloudflare Pages에 배포했습니다. 백엔드 API 서버는 AWS EC2(Ubuntu)에 올리고, 데이터베이스는 Neon DB(PostgreSQL)를 사용했습니다. 이 구조 덕분에 프론트엔드 배포 비용은 0원으로 유지하면서 백엔드 인프라만 AWS에서 관리할 수 있었습니다.
모범 답안
둘 다 서버 없이 함수 단위로 코드를 실행하는 서버리스 서비스입니다. 가장 큰 차이는 실행 위치입니다. Cloudflare Workers는 전 세계 CDN 엣지 서버에서 실행되어 사용자와 가장 가까운 곳에서 즉시 응답하므로 응답 속도가 매우 빠릅니다. AWS Lambda는 특정 리전의 서버에서 실행되며 콜드 스타트(처음 실행 시 지연)가 발생할 수 있습니다. 단순 API 처리나 미들웨어는 Workers가 유리하고, 복잡한 비즈니스 로직이나 다른 AWS 서비스와 연동이 필요한 경우 Lambda가 적합합니다.
모범 답안
SSL/TLS는 인터넷 통신을 암호화하는 프로토콜입니다. 브라우저 주소창의
https://와 자물쇠 아이콘이 SSL이 적용됐다는 표시입니다. SSL이 없으면 사용자가 입력한 비밀번호나 개인정보가 네트워크에서 평문으로 전송돼 탈취될 수 있습니다. Cloudflare는 도메인을 연결하는 것만으로 SSL 인증서를 자동으로 무료 발급해줍니다. AWS는 ACM(Certificate Manager)을 통해 무료로 발급할 수 있지만 EC2에 직접 적용하려면 추가 설정이 필요합니다.
| 키워드 | 한 줄 설명 |
|---|---|
| CDN | 가까운 서버에서 빠르게 콘텐츠 전달 |
| DDoS | 대량 트래픽으로 서버 마비시키는 공격 |
| DNS | 도메인 → IP 주소 변환 시스템 |
| SSL/TLS | 통신 암호화 프로토콜 (https) |
| 엣지 컴퓨팅 | 사용자 가까운 곳에서 연산 처리 |
| 서버리스 | 서버 관리 없이 함수 단위 실행 |
| 리전 (Region) | AWS 데이터센터 지역 단위 |
| 오리진 서버 | CDN 뒤에 있는 실제 서버 |
| 캐싱 | 자주 쓰는 데이터를 가까운 곳에 임시 저장 |
| 프록시 | 클라이언트와 서버 사이 중계 역할 |
다음 요구사항을 만족하는 게시판을 설계합니다.
요구사항 문장에서 명사를 뽑아보면 테이블의 후보가 보입니다.
| 요구사항 속 명사 | 테이블 후보 |
|---|---|
| 회원 | member |
| 게시글 | board |
| 카테고리 | category |
| 댓글 | comment |
| 좋아요 | board_like |
각 테이블이 서로 어떤 관계인지 정리합니다.
| 관계 | 설명 |
|---|---|
| member : board = 1 : N | 한 회원은 여러 게시글을 쓸 수 있음 |
| category : board = 1 : N | 한 카테고리는 여러 게시글을 가질 수 있음 |
| board : comment = 1 : N | 한 게시글에 여러 댓글이 달릴 수 있음 |
| member : comment = 1 : N | 한 회원은 여러 댓글을 쓸 수 있음 |
| member : board (좋아요) = N : N | 한 회원은 여러 게시글에 좋아요를 누르고, 한 게시글은 여러 회원에게 좋아요를 받음 |
💡 N:M(다대다) 관계는 테이블 하나로 표현할 수 없습니다. 중간에 연결 테이블(
board_like)을 두어 1:N + N:1 구조로 풀어내야 합니다. 이번 게시판에서는board_like가 바로 그 연결 테이블입니다.
member (1) ----< (N) board
member (1) ----< (N) comment
category (1) ----< (N) board
board (1) ----< (N) comment
member (N) ----< board_like >---- (N) board
코드를 작성하기 전에 이렇게 관계를 먼저 그려보는 습관이 중요합니다. 손으로 그리든, 종이에 쓰든, 표로 정리하든 상관없습니다. 설계를 먼저 끝내고 SQL은 그 다음입니다.
CREATE TABLE MEMBER (
member_id BIGINT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password VARCHAR(255) NOT NULL,
email VARCHAR(100) NOT NULL,
created_at DATETIME NOT NULL
);
CREATE TABLE CATEGORY (
category_id BIGINT PRIMARY KEY,
category_name VARCHAR(50) NOT NULL
);
CREATE TABLE BOARD (
board_id BIGINT PRIMARY KEY,
member_id BIGINT NOT NULL,
category_id BIGINT NOT NULL,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
view_count INT DEFAULT 0,
created_at DATETIME NOT NULL,
FOREIGN KEY (member_id) REFERENCES MEMBER (member_id),
FOREIGN KEY (category_id) REFERENCES CATEGORY (category_id)
);
CREATE TABLE COMMENT (
comment_id BIGINT PRIMARY KEY,
board_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME NOT NULL,
FOREIGN KEY (board_id) REFERENCES BOARD (board_id),
FOREIGN KEY (member_id) REFERENCES MEMBER (member_id)
);
CREATE TABLE BOARD_LIKE (
board_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (board_id, member_id),
FOREIGN KEY (board_id) REFERENCES BOARD (board_id),
FOREIGN KEY (member_id) REFERENCES MEMBER (member_id)
);
| 컬럼명 | 타입 | 제약조건 |
|---|---|---|
| id | BIGINT | PK, AUTO_INCREMENT |
| name | VARCHAR(50) | NOT NULL |
| VARCHAR(100) | NOT NULL, UNIQUE | |
| password | VARCHAR(100) | NOT NULL |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
| 컬럼명 | 타입 | 제약조건 |
|---|---|---|
| id | BIGINT | PK, AUTO_INCREMENT |
| name | VARCHAR(50) | NOT NULL, UNIQUE |
| 컬럼명 | 타입 | 제약조건 |
|---|---|---|
| id | BIGINT | PK, AUTO_INCREMENT |
| title | VARCHAR(200) | NOT NULL |
| content | TEXT | NOT NULL |
| view_count | INT | DEFAULT 0 |
| member_id | BIGINT | NOT NULL, FK → member(id) |
| category_id | BIGINT | NOT NULL, FK → category(id) |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
| 컬럼명 | 타입 | 제약조건 |
|---|---|---|
| id | BIGINT | PK, AUTO_INCREMENT |
| content | VARCHAR(500) | NOT NULL |
| board_id | BIGINT | NOT NULL, FK → board(id) |
| member_id | BIGINT | NOT NULL, FK → member(id) |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
| 컬럼명 | 타입 | 제약조건 |
|---|---|---|
| board_id | BIGINT | PK(복합), FK → board(id) |
| member_id | BIGINT | PK(복합), FK → member(id) |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
테이블을 만들 때는 참조하는 테이블보다 참조되는 테이블을 먼저 만들어야 합니다. (FK가 가리킬 대상이 먼저 존재해야 하기 때문)
생성 순서: member → category → board → comment → board_like
직접 H2 Console에서 위 순서대로 CREATE TABLE을 작성해봅니다. (정답은 연습 문제에서 확인합니다.)
샘플 데이터도 넣어봅니다.
INSERT INTO category (name) VALUES ('자유게시판');
INSERT INTO category (name) VALUES ('공지사항');
INSERT INTO member (name, email, password) VALUES ('홍길동', 'hong@test.com', '1234');
INSERT INTO member (name, email, password) VALUES ('김철수', 'kim@test.com', '1234');
INSERT INTO board (title, content, member_id, category_id) VALUES ('첫 게시글', '안녕하세요', 1, 1);
INSERT INTO comment (content, board_id, member_id) VALUES ('좋은 글이네요', 1, 2);
INSERT INTO board_like (board_id, member_id) VALUES (1, 2);
같은 회원이 같은 게시글에 좋아요를 한 번 더 누르면 어떻게 될까요? 직접 실행해서 에러를 확인해봅니다.
INSERT INTO board_like (board_id, member_id) VALUES (1, 2); -- 에러 발생 확인
DML(Data Manipulation Language)은 테이블 안에 들어있는 데이터의 내용을 다루는 명령어입니다. SECTION03에서 배운 DDL(테이블 구조)과 구분됩니다.
| 명령어 | 역할 |
|---|---|
| INSERT | 새로운 데이터(행) 추가 |
| UPDATE | 기존 데이터 수정 |
| DELETE | 기존 데이터 삭제 |
이번 SECTION에서는
SECTION04에서 설계한member,category,board,comment,board_like테이블을 계속 사용합니다.
INSERT INTO 테이블명 (컬럼1, 컬럼2, ...) VALUES (값1, 값2, ...);
INSERT INTO member (name, email, password) VALUES ('이영희', 'lee@test.com', '1234');
id, created_at처럼 AUTO_INCREMENT나 DEFAULT가 걸려있는 컬럼은 값을 직접 넣지 않아도 자동으로 채워집니다.
INSERT INTO member (name, email, password) VALUES
('박민수', 'park@test.com', '1234'),
('정수진', 'jung@test.com', '1234'),
('최동혁', 'choi@test.com', '1234');
INSERT INTO category VALUES (3, '질문');
테이블의 모든 컬럼 순서에 맞춰 값을 적어야 합니다. 컬럼이 추가되거나 순서가 바뀌면 바로 에러가 나기 때문에, 실무에서는 컬럼명을 명시하는 방식을 사용하는 것이 안전합니다.
UPDATE 테이블명 SET 컬럼1 = 값1, 컬럼2 = 값2 WHERE 조건;
UPDATE member SET name = '이영희2' WHERE id = 3;
UPDATE board SET title = '수정된 제목', content = '수정된 내용' WHERE id = 1;
UPDATE board SET view_count = view_count + 1 WHERE id = 1;
-- 위험! 모든 행의 view_count가 0으로 바뀜
UPDATE board SET view_count = 0;
WHERE 조건을 빼면 테이블의 모든 행이 수정됩니다. 실무에서 매우 자주 발생하는 실수이자 사고이므로, UPDATE 문을 작성할 때는 항상 WHERE 조건부터 먼저 떠올리는 습관을 들여야 합니다.
💡 안전하게 작업하는 습관: UPDATE 전에 같은 조건으로 SELECT를 먼저 실행해서 “내가 수정하려는 행이 정확히 이거 맞다”를 확인한 다음 UPDATE로 바꿔서 실행합니다.
SELECT * FROM board WHERE id = 1; -- 먼저 확인 UPDATE board SET title = '수정된 제목' WHERE id = 1; -- 확인 후 실행
DELETE FROM 테이블명 WHERE 조건;
DELETE FROM comment WHERE id = 2;
-- 위험! 테이블의 모든 행이 삭제됨 (테이블 구조는 남음)
DELETE FROM comment;
UPDATE와 마찬가지로 WHERE를 빠뜨리면 전체 데이터가 삭제됩니다. SECTION03에서 배운 TRUNCATE TABLE과 결과가 비슷하지만, DELETE는 조건을 걸 수 있다는 점이 가장 큰 차이입니다.
-- board의 id=1을 참조하는 comment, board_like가 있는 상태에서 board를 삭제하면?
DELETE FROM board WHERE id = 1; -- 에러 발생 (참조 무결성 위반)
board를 참조하고 있는 comment, board_like가 남아있는 상태로는 board를 삭제할 수 없습니다. 삭제하려면 참조하는 데이터(comment, board_like)를 먼저 지우거나, 비즈니스 로직에서 연관 데이터를 함께 처리해야 합니다.
-- 올바른 순서
DELETE FROM board_like WHERE board_id = 1;
DELETE FROM comment WHERE board_id = 1;
DELETE FROM board WHERE id = 1;
추후 JPA를 다시 학습하는 단계에서 연관 관계 설정(
CascadeType.REMOVE등)으로 이 과정을 자동화할 수 있습니다. 지금은 SQL로 직접 순서를 맞춰보는 연습이 중요합니다.
-- 1. 새 회원 등록
INSERT INTO member (name, email, password) VALUES ('강지훈', 'kang@test.com', '1234');
-- 2. 등록한 회원이 게시글 작성 (member_id는 방금 등록된 회원의 id로 가정, 예: 6)
INSERT INTO board (title, content, member_id, category_id) VALUES ('가입 인사', '안녕하세요!', 6, 1);
-- 3. 다른 회원이 댓글 작성
INSERT INTO comment (content, board_id, member_id) VALUES ('환영합니다', 1, 2);
-- 4. 게시글 내용 수정
UPDATE board SET content = '안녕하세요! 잘 부탁드립니다.' WHERE member_id = 6;
-- 5. 댓글 삭제
DELETE FROM comment WHERE board_id = 1 AND member_id = 2;
view_count = view_count + 1)