백엔드 학습 정리

지금까지 공부한 백엔드 개념을 정리한 문서입니다.

핵심 흐름: DB 안정성 → 동시성 제어 → 비즈니스 로직 → 캐시/Redis → Docker → CI/CD 배포


목차

  1. 트랜잭션
  2. ACID
  3. 동시성
  4. 트랜잭션 격리 수준
  5. 비즈니스 로직
  6. 멱등성
  7. Redis
  8. 인메모리 DB
  9. Docker
  10. Docker Image와 Container
  11. Dockerfile
  12. Docker Volume
  13. Docker Compose
  14. Docker Network
  15. Docker 환경 변수
  16. Spring과 Node.js 서버 실행 차이
  17. 멀티 모듈
  18. OS와 CPU 아키텍처
  19. CI/CD
  20. GitHub Actions와 Jenkins
  21. Docker Hub를 이용한 백엔드 배포 흐름
  22. 배포할 때 주의할 점
  23. 전체 개념 연결
  24. 한 줄 요약
  25. 다음에 공부하면 좋은 주제

1. 트랜잭션

트랜잭션은 여러 DB 작업을 하나의 작업 단위로 묶는 것이다.

핵심은 다음과 같다.

전부 성공하거나
전부 실패해야 한다.

예를 들어 사용자의 닉네임과 전화번호를 동시에 변경한다고 해보자.

SELECT user;
UPDATE nickname;
UPDATE phone_number;

사용자는 닉네임과 전화번호가 둘 다 바뀌는 것을 기대한다.

그런데 닉네임만 바뀌고 전화번호 변경이 실패하면 데이터가 이상해진다.

그래서 이런 작업은 트랜잭션으로 묶어야 한다.

BEGIN;

UPDATE users
SET nickname = 'newName'
WHERE id = 1;

UPDATE user_profiles
SET phone_number = '010-1234-5678'
WHERE user_id = 1;

COMMIT;

중간에 실패하면 다음처럼 처리한다.

ROLLBACK;

즉,

성공 → COMMIT
실패 → ROLLBACK

트랜잭션에서 중요한 점

트랜잭션은 보통 같은 DB 커넥션 안에서 실행되어야 한다.

DB 입장에서 트랜잭션은 특정 커넥션, 즉 특정 세션 안에서 시작되고 끝나기 때문이다.

커넥션 A
BEGIN
UPDATE 닉네임
UPDATE 전화번호
COMMIT

이렇게 하나의 커넥션 안에서 처리되어야 하나의 트랜잭션으로 묶인다.

반대로 작업마다 다른 커넥션을 쓰면 DB 입장에서는 각각 별개의 작업으로 볼 수 있다.

Spring에서는 보통 @Transactional을 사용하면 해당 메서드 안에서 필요한 DB 작업들이 같은 트랜잭션으로 묶인다.

@Transactional
public void updateUserInfo(Long userId, String nickname, String phoneNumber) {
    userRepository.updateNickname(userId, nickname);
    userProfileRepository.updatePhoneNumber(userId, phoneNumber);
}

트랜잭션을 어디에 걸어야 하나?

보통은 Service 계층에 건다.

Controller → 요청/응답 처리
Service → 비즈니스 로직 + 트랜잭션
Repository → DB 접근

예를 들어 회원 정보 수정은 단순히 DB update 하나가 아니라 다음 흐름 전체가 하나의 비즈니스 작업이다.

1. 사용자 조회
2. 닉네임 중복 검사
3. 닉네임 변경
4. 전화번호 변경
5. 변경 이력 저장

그래서 트랜잭션은 보통 이 흐름을 담당하는 Service에 건다.


2. ACID

ACID는 DB 트랜잭션이 안전하게 동작하기 위한 4가지 성질이다.

쉽게 말하면 다음 질문에 대한 답이 될 수 있다.

DB를 왜 쓰냐?
그냥 파일이나 문서에 저장하면 안 되냐?

→ DB는 ACID를 통해 데이터 안정성을 보장하기 때문이다.

A - Atomicity, 원자성

트랜잭션 안의 작업은 전부 성공하거나 전부 실패해야 한다.

닉네임 변경 성공
전화번호 변경 실패

이런 상태가 되면 안 된다.

둘 다 성공하거나, 둘 다 실패해야 한다.

성공 → COMMIT
실패 → ROLLBACK

C - Consistency, 일관성

트랜잭션 전후로 DB의 규칙이 깨지면 안 된다.

예를 들어 이런 규칙이 있다고 해보자.

잔액은 음수가 되면 안 된다.
이메일은 중복되면 안 된다.
주문은 반드시 회원과 연결되어야 한다.

트랜잭션이 실행된 후에도 이런 규칙이 유지되어야 한다.

즉, 트랜잭션 전에도 정상 상태, 트랜잭션 후에도 정상 상태여야 한다.


I - Isolation, 격리성

동시에 여러 트랜잭션이 실행되어도 서로의 중간 상태에 영향을 받으면 안 된다.

예를 들어 재고가 1개 남아 있는데 두 명이 동시에 구매한다고 해보자.

A 사용자: 재고 1개 확인
B 사용자: 재고 1개 확인
A 사용자: 구매 성공
B 사용자: 구매 성공

이렇게 되면 재고가 1개인데 2명이 구매한 이상한 상황이 된다.

격리성은 이런 동시성 문제를 막기 위한 성질이다.


D - Durability, 지속성

한 번 COMMIT된 데이터는 장애가 나도 사라지면 안 된다.

예를 들어 주문이 완료되었다면,

주문 완료
COMMIT
서버 장애 발생

서버가 죽더라도 주문 기록은 DB에 남아 있어야 한다.


3. 동시성

동시성은 여러 요청이 동시에 같은 데이터에 접근하거나 수정할 때 생기는 문제를 다룬다.

백엔드에서 동시성이 중요한 이유는 여러 사용자가 동시에 요청을 보내기 때문이다.

예를 들어 다음 상황이 있다.

재고 1개 남음

A 사용자 구매 요청
B 사용자 구매 요청

둘 다 동시에 재고를 읽으면 둘 다 구매 성공할 수 있다.

이런 문제를 막기 위해 트랜잭션, 락, 제약조건, 멱등성 같은 개념이 필요하다.


대표적인 동시성 문제

1. Lost Update

두 요청이 동시에 같은 데이터를 수정해서 한쪽 변경이 사라지는 문제다.

기존 닉네임: manwook

A 요청: minsu로 변경
B 요청: jiwon으로 변경

최종 결과: jiwon

A의 변경이 B에 의해 덮어씌워질 수 있다.


2. Race Condition

여러 요청의 실행 순서에 따라 결과가 달라지는 문제다.

재고 확인
재고 차감
주문 생성

이 과정에서 동시에 요청이 들어오면 예상하지 못한 결과가 생길 수 있다.


4. 락

락은 동시에 같은 데이터를 수정할 때 데이터 정합성을 지키기 위한 방법이다.

크게 두 가지가 있다.

1. 낙관적 락
2. 비관적 락

4-1. 낙관적 락

낙관적 락은 처음부터 DB row를 잠그지 않는다.

대신 데이터를 수정할 때 내가 처음 읽은 버전과 DB의 현재 버전이 같은지 확인한다.

보통 version 컬럼을 사용한다.

id: 1
stock: 10
version: 3

수정할 때 다음처럼 처리한다.

UPDATE products
SET stock = stock - 1,
    version = version + 1
WHERE id = 1
  AND version = 3;

만약 다른 트랜잭션이 먼저 수정해서 version이 4가 되었다면 이 update는 실패한다.

affectedRows = 0

즉, 내가 읽은 이후 누군가 먼저 수정했다는 뜻이다.


낙관적 락 장점

성능이 좋다.

처음부터 DB row를 잠그지 않기 때문에 대기 시간이 적다.

동시 요청이 많아도 락 때문에 줄 서는 일이 적다.

그래서 충돌이 자주 안 나는 서비스에 적합하다.

예시는 다음과 같다.

게시글 수정
프로필 수정
설정 변경
상품 설명 수정

낙관적 락 단점

충돌이 나면 실패한다.

affectedRows = 0

이 나오면 다시 조회하거나, 사용자에게 실패를 알려주거나, 재시도해야 한다.

즉, 재시도 로직이 필요하다.


4-2. 비관적 락

비관적 락은 데이터를 읽을 때부터 다른 트랜잭션이 수정하지 못하게 row를 잠그는 방식이다.

SELECT *
FROM products
WHERE id = 1
FOR UPDATE;

이렇게 하면 해당 row에 락이 걸린다.

다른 트랜잭션은 이 row를 수정하려고 하면 락이 풀릴 때까지 기다려야 한다.

트랜잭션 A: row 잠금
트랜잭션 B: 같은 row 수정 시도
트랜잭션 B: 대기
트랜잭션 A: COMMIT
트랜잭션 B: 실행

비관적 락 장점

데이터 정합성을 강하게 지킬 수 있다.

한 트랜잭션이 row를 잡고 있으면 다른 트랜잭션은 기다려야 한다.

그래서 동시에 수정되면 위험한 곳에 적합하다.

예시는 다음과 같다.

재고 차감
결제
선착순 쿠폰
좌석 예약
포인트 차감
계좌 이체

비관적 락 단점

성능이 떨어질 수 있다.

다른 요청들이 락이 풀릴 때까지 기다려야 하기 때문이다.

트랜잭션이 길어지면 대기 시간이 길어지고, 심하면 데드락이 발생할 수도 있다.


낙관적 락 vs 비관적 락

구분낙관적 락비관적 락
기본 생각충돌이 자주 안 날 것이다충돌이 날 수 있으니 미리 잠근다
방식version 체크row lock
성능좋음상대적으로 낮음
충돌 시실패 후 재시도 필요다른 요청이 대기
적합한 곳게시글, 프로필, 설정재고, 결제, 쿠폰, 예약
단점재시도 로직 필요대기, 데드락 가능

정리하면 다음과 같다.

충돌이 드물다 → 낙관적 락
충돌이 자주 난다 → 비관적 락
재시도 비용이 더 비싸다 → 비관적 락

5. 트랜잭션 격리 수준

트랜잭션 격리 수준은 동시에 여러 트랜잭션이 실행될 때 어느 정도까지 서로를 격리할지 정하는 설정이다.

대표적으로 4가지가 있다.


Read Uncommitted

커밋되지 않은 데이터도 읽을 수 있다.

문제가 많아서 거의 사용하지 않는다.

다른 트랜잭션이 아직 COMMIT 안 한 데이터도 읽음

이때 생길 수 있는 문제가 Dirty Read다.


Read Committed

커밋된 데이터만 읽는다.

하지만 같은 트랜잭션 안에서 같은 데이터를 다시 읽었을 때 값이 달라질 수 있다.

1번 조회: 재고 10
다른 트랜잭션이 재고 5로 변경 후 COMMIT
2번 조회: 재고 5

이런 문제를 Non-repeatable Read라고 한다.


Repeatable Read

같은 트랜잭션 안에서는 같은 데이터를 반복 조회해도 같은 결과를 보장한다.

하지만 조건에 맞는 새로운 row가 추가되는 문제는 생길 수 있다.

이런 문제를 Phantom Read라고 한다.


Serializable

가장 강한 격리 수준이다.

트랜잭션을 거의 순서대로 실행하는 것처럼 처리한다.

데이터 정합성은 강하지만 성능은 가장 낮다.


격리 수준 정리

격리 수준설명성능정합성
Read Uncommitted커밋 안 된 데이터도 읽음높음낮음
Read Committed커밋된 데이터만 읽음좋음보통
Repeatable Read같은 트랜잭션 내 반복 조회 보장보통높음
Serializable가장 강한 격리낮음매우 높음

6. 비즈니스 로직

비즈니스 로직은 서비스가 해결하려는 문제와 규칙을 코드로 표현한 것이다.

단순히 코드를 많이 쓰는 게 비즈니스 로직이 아니다.

예를 들어 쇼핑몰이라면 이런 것들이 비즈니스 로직이다.

재고가 있어야 주문할 수 있다.
결제 금액은 주문 금액과 일치해야 한다.
이미 사용한 쿠폰은 다시 사용할 수 없다.
탈퇴한 회원은 주문할 수 없다.
포인트는 보유 금액보다 많이 사용할 수 없다.

비즈니스 로직에 포함될 수 있는 것

validation
DB 조회
DB 저장
외부 API 호출
상태 변경
권한 검사
도메인 규칙 적용

다만 핵심은 DB 통신 자체가 비즈니스 로직이라기보다는, 어떤 규칙에 따라 데이터를 읽고 바꾸는 흐름이 비즈니스 로직이라는 점이다.


예시

@Transactional
public void createOrder(Long userId, Long productId, int quantity) {
    User user = userRepository.findById(userId);
    Product product = productRepository.findById(productId);

    if (user.isDeleted()) {
        throw new IllegalStateException("탈퇴한 회원은 주문할 수 없습니다.");
    }

    if (product.getStock() < quantity) {
        throw new IllegalStateException("재고가 부족합니다.");
    }

    product.decreaseStock(quantity);

    Order order = Order.create(user, product, quantity);

    orderRepository.save(order);
}

여기서 중요한 비즈니스 로직은 다음과 같다.

탈퇴한 회원은 주문할 수 없다.
재고가 부족하면 주문할 수 없다.
주문하면 재고가 차감된다.

7. 멱등성

멱등성이란 같은 요청을 여러 번 보내도 결과가 한 번 보낸 것과 동일한 성질이다.

1번 요청한 결과
10번 요청한 결과

최종 상태가 같으면 멱등하다.

멱등성이 중요한 이유

네트워크는 항상 안정적이지 않다.

예를 들어 주문 요청을 보냈는데 응답이 늦거나 끊길 수 있다.

프론트 → 주문 생성 요청
백엔드 → 주문 생성 성공
응답 전송 중 네트워크 오류
프론트 → 실패로 판단하고 재시도
백엔드 → 주문 또 생성

이러면 주문이 중복 생성될 수 있다.

그래서 결제, 주문, 쿠폰 발급 같은 요청은 멱등성을 신경 써야 한다.


HTTP 메서드 기준

일반적으로 이렇게 이해하면 된다.

메서드멱등성설명
GET멱등여러 번 조회해도 상태 변화 없음
PUT멱등같은 상태로 여러 번 덮어씀
DELETE멱등여러 번 삭제해도 최종 상태는 삭제됨
POST보통 멱등 아님요청마다 새 리소스가 생성될 수 있음
PATCH경우에 따라 다름구현 방식에 따라 다름

예를 들어 좋아요 API를 이렇게 만들면 멱등하다.

PUT /posts/1/like

의미는 다음과 같다.

이 게시글을 좋아요 상태로 만들어라.

여러 번 보내도 최종 상태는 좋아요다.

반대로 다음 방식은 위험하다.

POST /posts/1/like-toggle

의미는 다음과 같다.

현재 상태를 반대로 바꿔라.

여러 번 보내면 상태가 계속 바뀐다.

1번 요청 → 좋아요
2번 요청 → 좋아요 취소
3번 요청 → 좋아요

그래서 toggle 방식은 멱등하지 않다.


프론트에서 하는 멱등성 보조 처리

프론트는 중복 요청을 줄일 수 있다.

예를 들어 버튼 중복 클릭을 막는다.

const [isSubmitting, setIsSubmitting] = useState(false);

const handleSubmit = async () => {
  if (isSubmitting) return;

  setIsSubmitting(true);

  try {
    await createOrder();
  } finally {
    setIsSubmitting(false);
  }
};

버튼도 비활성화한다.

<button disabled={isSubmitting} onClick={handleSubmit}>
  주문하기
</button>

하지만 프론트만으로는 완벽하지 않다.

사용자가 새로고침하거나, 네트워크 재시도가 발생하거나, 악의적으로 직접 API를 호출할 수도 있기 때문이다.

진짜 멱등성 보장은 백엔드에서 해야 한다.


백엔드에서 Idempotency-Key 사용

프론트가 요청마다 고유한 키를 보내고, 백엔드는 그 키로 중복 요청을 막는다.

const idempotencyKey = crypto.randomUUID();

await fetch("/api/orders", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": idempotencyKey,
  },
  body: JSON.stringify({
    productId: 1,
    quantity: 1,
  }),
});

백엔드는 이렇게 처리한다.

처음 들어온 Idempotency-Key
→ 주문 생성
→ 결과 저장

같은 Idempotency-Key로 다시 요청
→ 새 주문 생성 X
→ 기존 결과 반환

멱등성이 중요한 곳

결제
주문 생성
쿠폰 발급
포인트 적립/차감
회원가입
폼 제출
좋아요/팔로우
페이지 진입 로그
mutation 재시도
React useEffect 안의 POST 요청

정리하면 다음과 같다.

프론트는 중복 요청을 줄인다.
백엔드는 중복 요청이 와도 결과가 한 번만 반영되게 보장한다.

8. Redis

Redis는 대표적인 인메모리 Key-Value DB다.

쉽게 말하면 데이터를 디스크보다 훨씬 빠른 메모리, 즉 RAM에 저장하는 저장소다.

일반 DB → 디스크 중심
Redis → 메모리 중심

그래서 Redis는 매우 빠르다.


Redis를 왜 쓰나?

DB는 안정적이지만 모든 요청을 DB가 직접 처리하면 부담이 커진다.

예를 들어 매번 인기 게시글을 DB에서 조회한다고 해보자.

사용자 1 → 인기 게시글 조회 → DB 조회
사용자 2 → 인기 게시글 조회 → DB 조회
사용자 3 → 인기 게시글 조회 → DB 조회

요청이 많아지면 DB에 부하가 생긴다.

이때 Redis에 자주 조회되는 데이터를 저장해두면 빠르게 응답할 수 있다.

사용자 → Redis 조회
Redis에 있음 → 바로 응답
Redis에 없음 → DB 조회 후 Redis 저장

Redis 대표 사용처

캐시
세션 저장
로그인 토큰 블랙리스트
인증번호 저장
조회수 카운트
랭킹
Rate Limiting
Idempotency-Key 저장
분산 락
간단한 큐
Pub/Sub

Redis 자료구조

Redis는 단순히 문자열만 저장하는 게 아니라 여러 자료구조를 제공한다.

자료구조설명사용 예시
String단순 key-value인증번호, 토큰, 캐시
Hash객체 형태 저장유저 정보, 설정값
List순서 있는 목록
Set중복 없는 집합좋아요 유저 목록
Sorted Set점수 기반 정렬랭킹
Pub/Sub메시지 발행/구독실시간 알림

TTL

Redis는 데이터에 만료 시간을 줄 수 있다.

인증번호 3분 후 만료
로그인 세션 1시간 후 만료
Idempotency-Key 24시간 후 만료

예시:

SET verification:phone:01012345678 123456 EX 180

뜻은 다음과 같다.

01012345678 번호의 인증번호를 123456으로 저장
180초 후 자동 삭제

Redis 캐시 패턴

가장 흔한 방식은 Cache Aside 패턴이다.

1. Redis에서 먼저 조회
2. Redis에 있으면 바로 반환
3. Redis에 없으면 DB 조회
4. DB 결과를 Redis에 저장
5. 응답 반환

예시 흐름:

GET /products/1

Redis 조회
→ cache miss
→ DB 조회
→ Redis 저장
→ 응답

다음 요청

Redis 조회
→ cache hit
→ 바로 응답

Redis 주의점

Redis는 빠르지만 모든 데이터를 Redis에만 저장하면 위험할 수 있다.

기본적으로 메모리 기반이기 때문이다.

Redis도 RDB, AOF 같은 영속화 기능이 있지만, 보통 핵심 데이터의 원본 저장소는 MySQL, PostgreSQL 같은 RDBMS가 맡는다.

정리하면 다음과 같다.

RDBMS → 원본 데이터 저장소
Redis → 빠른 조회, 임시 데이터, 캐시, 락, 세션 등에 사용

9. 인메모리 DB

인메모리 DB는 데이터를 주로 메모리 RAM에 저장하는 DB다.

대표적인 예시가 Redis다.


인메모리 DB 특징

매우 빠르다.
디스크 I/O가 적다.
휘발성 데이터에 적합하다.
서버가 죽으면 데이터 손실 위험이 있다.
메모리 용량 제한을 받는다.

일반 DB와 인메모리 DB 비교

구분일반 RDBMS인메모리 DB
저장 위치디스크 중심메모리 중심
속도상대적으로 느림매우 빠름
안정성높음설정에 따라 다름
용도핵심 원본 데이터캐시, 세션, 임시 데이터
예시MySQL, PostgreSQLRedis

쉽게 비유하면

MySQL = 금고
Redis = 책상 위 메모장

금고는 안전하지만 꺼내는 데 시간이 걸린다.

책상 위 메모장은 빠르게 볼 수 있지만, 정리하지 않으면 사라지거나 덮어쓸 수 있다.

그래서 보통 이렇게 쓴다.

중요한 원본 데이터 → MySQL/PostgreSQL
빠르게 접근할 데이터 → Redis

10. Docker

Docker는 애플리케이션을 실행하는 데 필요한 것들을 컨테이너 이미지로 묶어서 실행할 수 있게 해주는 도구다.

즉, 내 서버 코드만 들어가는 게 아니라 실행 환경까지 같이 들어간다.

서버 코드
런타임
의존성
빌드 결과물
실행 명령어
포트 정보

Docker를 쓰는 이유

1. 환경 충돌 방지

서버 A는 Node 18이 필요하고, 서버 B는 Node 20이 필요할 수 있다.

Docker 없이 한 서버에 직접 설치하면 버전 충돌이 날 수 있다.

Docker는 컨테이너마다 환경을 분리한다.

컨테이너 A → Node 18
컨테이너 B → Node 20

2. 배포 안정성 증가

내 컴퓨터에서는 잘 되는데 서버에서는 안 되는 문제가 줄어든다.

왜냐하면 Docker 이미지는 코드뿐만 아니라 실행 환경까지 같이 들고 가기 때문이다.

내 컴퓨터에서 만든 이미지
→ 서버에서도 같은 환경으로 실행

3. 장애 전파 방지

하나의 컨테이너가 죽어도 다른 컨테이너에는 영향이 적다.

backend 컨테이너 장애
mysql 컨테이너는 계속 실행
redis 컨테이너도 계속 실행

4. 보안과 격리

컨테이너는 기본적으로 파일 시스템, 프로세스, 네트워크가 분리된다.

물론 Docker가 완전한 보안 장벽은 아니지만, 서버에 직접 모든 프로그램을 설치하는 것보다 관리하기 좋다.


5. 여러 서비스를 깔끔하게 운영 가능

하나의 서버에서 여러 서비스를 분리해서 띄울 수 있다.

backend 컨테이너
mysql 컨테이너
redis 컨테이너
nginx 컨테이너

Docker 핵심 개념

개념설명
Image실행에 필요한 파일을 묶은 템플릿
Container이미지를 실행한 실제 프로세스
Dockerfile이미지를 만드는 설명서
Docker Hub이미지를 올리고 받을 수 있는 저장소
Volume컨테이너 밖에 데이터를 저장하는 공간
Network컨테이너끼리 통신하기 위한 네트워크
Docker Compose여러 컨테이너를 한 번에 관리하는 도구

11. Docker Image와 Container

Image

이미지는 실행 파일 묶음이다.

서버 코드
Node.js/JDK/Python 같은 런타임
라이브러리
빌드 결과물
실행 명령어

이미지는 직접 실행되는 것이 아니라, 컨테이너를 만들기 위한 템플릿이다.


Container

컨테이너는 이미지를 실제로 실행한 것이다.

Image = 클래스
Container = 객체

또는 이렇게 이해해도 된다.

Image = 설치 파일
Container = 실행 중인 프로그램

12. Dockerfile

Dockerfile은 Docker 이미지를 만들기 위한 설명서다.

Node.js 서버 예시는 다음과 같다.

FROM node:20

WORKDIR /app

COPY package.json pnpm-lock.yaml ./

RUN corepack enable
RUN pnpm install --frozen-lockfile

COPY . .

RUN pnpm build

EXPOSE 3000

CMD ["pnpm", "start"]

Dockerfile 명령어 정리

명령어설명
FROM어떤 기본 이미지에서 시작할지 정함
WORKDIR컨테이너 안의 작업 디렉토리
COPY로컬 파일을 이미지 안으로 복사
RUN이미지 빌드 중 실행할 명령어
EXPOSE컨테이너가 사용하는 포트를 문서화
CMD컨테이너가 시작될 때 실행할 명령어

주의할 점

EXPOSE 3000은 실제로 서버 포트를 외부에 여는 명령이 아니다.

단지 이 컨테이너가 3000번 포트를 사용한다는 정보를 적어두는 것이다.

실제로 포트를 연결하려면 docker run -p나 Docker Compose의 ports를 사용해야 한다.

docker run -p 8080:3000 my-server

뜻은 다음과 같다.

호스트 서버의 8080번 포트
→ 컨테이너 내부의 3000번 포트

즉 포트 매핑은 다음 순서다.

HOST_PORT:CONTAINER_PORT

예를 들어 다음 설정은,

ports:
-"3000:3310"

다음 의미다.

내 서버 또는 내 컴퓨터의 3000번 포트
→ 컨테이너 내부의 3310번 포트

13. Docker Volume

Docker에서 Volume은 컨테이너가 삭제되어도 데이터를 유지하기 위한 저장 공간이다.

컨테이너는 기본적으로 일회성이다.

컨테이너 삭제
→ 컨테이너 안에 저장된 파일도 삭제될 수 있음

그래서 DB 데이터, 업로드 파일, 로그처럼 사라지면 안 되는 데이터는 컨테이너 안에만 두면 위험하다.

이때 사용하는 것이 Volume이다.


MySQL 예시

MySQL을 Docker로 실행한다고 해보자.

docker run mysql

그러면 MySQL 프로그램과 DB 데이터 파일이 컨테이너 안에 생긴다.

MySQL 컨테이너
├─ MySQL 프로그램
└─ DB 데이터 파일

여기서 회원가입을 하면 DB에 데이터가 저장된다.

users 테이블
- id: 1
- name: manwook

그런데 MySQL 컨테이너를 삭제하면 컨테이너 안의 데이터도 사라질 수 있다.

docker rm mysql-container

그래서 DB 데이터 저장 위치를 컨테이너 바깥에 연결한다.

MySQL 컨테이너
└─ /var/lib/mysql


Docker Volume mysql-data

이렇게 하면 MySQL은 여전히 /var/lib/mysql에 저장한다고 생각하지만, 실제 데이터는 Docker Volume에 저장된다.


Volume 사용 예시

docker run \
  -v mysql-data:/var/lib/mysql \
  mysql

핵심은 이 부분이다.

-v mysql-data:/var/lib/mysql

뜻은 다음과 같다.

mysql-data 볼륨을
컨테이너 안의 /var/lib/mysql 경로에 연결해라.

쉽게 비유하면

컨테이너 = 임시 노트북
볼륨 = 외장하드

컨테이너를 삭제하는 것은 노트북을 폐기하는 것과 비슷하다.

하지만 외장하드인 볼륨을 유지하면 데이터는 남아 있다.


Volume이 필요한 대표 예시

DB 데이터
Redis 영속화 파일
사용자 업로드 이미지
로그 파일
빌드 캐시

특히 Docker로 DB를 띄울 때는 거의 항상 Volume을 붙인다고 보면 된다.


14. Docker Compose

Docker Compose는 여러 컨테이너를 하나의 설정 파일로 관리하는 도구다.

예를 들어 백엔드, MySQL, Redis를 한 번에 띄울 수 있다.

services:
backend:
image: my-backend:latest
ports:
-"3000:3000"
env_file:
- .env
depends_on:
- mysql
- redis

mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: mydb
volumes:
- mysql-data:/var/lib/mysql
ports:
-"3306:3306"

redis:
image: redis:7
ports:
-"6379:6379"

volumes:
mysql-data:

Compose에서 중요한 점

같은 Docker Compose 안의 서비스들은 기본적으로 같은 네트워크에 묶인다.

그래서 백엔드에서 MySQL에 접속할 때 localhost가 아니라 서비스 이름인 mysql을 사용할 수 있다.

DATABASE_HOST=mysql
REDIS_HOST=redis

컨테이너 안에서 localhost는 자기 자신을 의미한다.

backend 컨테이너 안의 localhost
= backend 컨테이너 자기 자신

따라서 backend 컨테이너에서 mysql 컨테이너에 접속하려면 보통 다음처럼 한다.

mysql:3306
redis:6379

15. Docker Network

Docker Network는 컨테이너끼리 통신하게 해주는 네트워크다.

예를 들어 다음처럼 구성할 수 있다.

backend 컨테이너
mysql 컨테이너
redis 컨테이너

이 컨테이너들이 같은 네트워크 안에 있으면 서로 통신할 수 있다.

backend → mysql
backend → redis

반대로 네트워크를 분리하면 특정 컨테이너끼리만 통신하게 만들 수 있다.


네트워크 분리가 필요한 이유

보안 강화
서비스 간 통신 범위 제한
외부에 DB를 직접 노출하지 않기
운영 환경 구조 관리

예를 들어 MySQL은 외부 인터넷에 직접 열 필요가 없다.

외부 사용자 → backend
backend → mysql

이 구조가 더 안전하다.


16. Docker 환경 변수

DB 주소, Redis 주소, API Key, JWT Secret 같은 값은 코드에 직접 넣으면 안 된다.

이런 값은 환경 변수로 주입한다.

DATABASE_URL=mysql://user:password@mysql:3306/mydb
REDIS_URL=redis://redis:6379
JWT_SECRET=secret

Docker 실행 시 넣을 수도 있다.

docker run -e DATABASE_URL=... my-server

Docker Compose에서는 보통 .env 파일이나 env_file을 사용한다.

services:
backend:
env_file:
- .env

주의할 점은 비밀번호, API Key 같은 값은 Docker 이미지 안에 넣으면 안 된다는 것이다.

나쁜 예:
Dockerfile 안에 JWT_SECRET 작성

좋은 예:
서버 실행 시 환경 변수로 주입

17. Spring과 Node.js 서버 실행 차이

Spring

Spring 서버는 보통 하나의 프로세스 안에서 멀티스레드로 여러 요청을 처리한다.

Spring 애플리케이션 1개
→ 여러 스레드로 요청 처리

그래서 일반적인 상황에서는 서버 하나에 Spring 앱 프로세스 하나만 띄워도 여러 요청을 동시에 처리할 수 있다.


Node.js

Node.js는 기본적으로 하나의 프로세스가 하나의 이벤트 루프를 가진다.

Node.js 프로세스 1개
→ 이벤트 루프 1개

Node.js도 비동기 I/O 덕분에 많은 요청을 처리할 수 있지만, CPU 코어를 여러 개 제대로 쓰려면 프로세스를 여러 개 띄우는 방식이 유리하다.

Node.js 프로세스 여러 개
→ 여러 CPU 코어 활용

그래서 Node.js에서는 PM2 cluster mode, Docker replica, Kubernetes replica 같은 방식으로 여러 프로세스를 띄우기도 한다.


18. 멀티 모듈

멀티 모듈은 하나의 프로젝트를 역할별 모듈로 나누는 구조다.

예시는 다음과 같다.

app-api
→ API 서버

app-batch
→ 배치 서버

domain
→ 핵심 비즈니스 로직

infrastructure
→ DB, Redis, 외부 API 구현

common
→ 공통 유틸, 예외, 응답

멀티 모듈을 쓰는 이유

코드 역할이 명확해짐
의존성 관리가 쉬워짐
공통 코드 재사용 가능
API 서버와 배치 서버를 분리 가능
큰 프로젝트에서 유지보수성이 좋아짐

예시 구조

backend-project
├─ app-api
│  └─ Controller, API 실행 진입점

├─ app-batch
│  └─ 배치 작업 실행

├─ domain
│  └─ Entity, 도메인 규칙, 핵심 비즈니스 로직

├─ infrastructure
│  └─ DB Repository 구현, Redis, 외부 API Client

└─ common
   └─ 공통 예외, 응답, 유틸

의존성 방향

멀티 모듈에서 중요한 것은 의존성 방향이다.

좋은 구조는 핵심 로직이 외부 기술에 덜 의존하는 구조다.

app-api → domain
app-api → infrastructure
infrastructure → domain
domain → 다른 외부 기술에 의존하지 않음

특히 domain 모듈은 가능하면 깨끗하게 유지하는 것이 좋다.

domain 안에 Controller 넣지 않기
domain 안에 Redis 코드 넣지 않기
domain 안에 외부 API 호출 코드 넣지 않기

주의점

작은 프로젝트에서 너무 잘게 나누면 오히려 복잡해질 수 있다.

처음부터 과하게 나누기보다는, 프로젝트가 커지고 역할이 명확해질 때 나누는 것이 좋다.

정리하면 다음과 같다.

멀티 모듈은 하나의 백엔드 프로젝트를 기능과 역할별로 쪼개서 관리하는 방식이다.
핵심은 역할 분리와 의존성 방향이다.

19. OS와 CPU 아키텍처

운영체제와 CPU 아키텍처는 서로 다른 개념이다.

Windows / macOS / Linux = 운영체제 OS

x86 / amd64 / arm64 = CPU 아키텍처

즉, x86이 윈도우용이고 amd64가 맥용인 게 아니다.


CPU 아키텍처 정리

x86    = 32비트 Intel/AMD 계열
amd64  = 64비트 Intel/AMD 계열, x86_64라고도 부름
arm64  = 64비트 ARM 계열

맥 기준

예전 Intel Mac
→ amd64 / x86_64

요즘 M1/M2/M3/M4 Mac
→ arm64

윈도우 기준

일반 윈도우 PC 대부분
→ amd64

ARM 윈도우 노트북
→ arm64

서버 기준

일반적인 클라우드 서버, 예를 들어 EC2의 많은 인스턴스는 amd64 기반이다.

하지만 ARM 기반 인스턴스도 있다.

그래서 Docker 이미지를 만들 때 운영 서버의 CPU 아키텍처를 신경 써야 한다.

예를 들어 M시리즈 맥북에서 Docker 이미지를 빌드하면 기본적으로 arm64 이미지가 만들어질 수 있다.

그 이미지를 amd64 서버에서 실행하려고 하면 문제가 생길 수 있다.

이럴 때는 다음처럼 플랫폼을 지정해서 빌드할 수 있다.

docker buildx build --platform linux/amd64 -t my-server .

또는 멀티 아키텍처 이미지를 만들 수도 있다.

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t my-server .

정리하면 다음과 같다.

OS와 CPU 아키텍처는 별개다.
Docker 이미지는 실행될 서버의 OS/아키텍처와 맞아야 한다.
요즘 맥북 M시리즈는 arm64,
일반 PC/서버는 대부분 amd64다.

20. CI/CD

CI/CD는 코드를 자동으로 검사하고 배포하는 흐름이다.

CI = Continuous Integration
CD = Continuous Delivery 또는 Continuous Deployment

CI

CI는 코드를 합치기 전에 자동으로 검사하는 과정이다.

보통 다음 일을 한다.

코드 checkout
의존성 설치
lint 검사
test 실행
build 실행
Docker image build

즉, CI의 목적은 다음과 같다.

문제 있는 코드가 main 브랜치에 들어가지 않도록 막기

CD

CD는 빌드된 결과물을 서버에 배포하는 과정이다.

보통 다음 일을 한다.

Docker image push
서버 접속
새 이미지 pull
기존 컨테이너 종료
새 컨테이너 실행
health check

즉, CD의 목적은 다음과 같다.

사람이 직접 서버에 접속해서 배포하지 않도록 자동화하기

21. GitHub Actions와 Jenkins

둘 다 CI/CD 도구다.


GitHub Actions

GitHub Actions는 GitHub에 내장된 자동화 도구다.

GitHub 저장소와 연결이 쉽다.

push 발생
pull request 발생
main branch merge
tag 생성

이런 이벤트에 맞춰 자동으로 workflow를 실행할 수 있다.

장점은 다음과 같다.

GitHub와 연동이 쉽다.
설정이 비교적 간단하다.
별도 서버 운영이 필요 없다.
개인/소규모 프로젝트에 쓰기 좋다.

Jenkins

Jenkins는 직접 설치해서 운영하는 CI/CD 서버다.

장점은 자유도가 높다는 것이다.

플러그인이 많다.
복잡한 배포 파이프라인 구성 가능
회사 내부망 환경에 적합
여러 저장소/서버/권한 구조와 연결 가능

단점은 직접 관리해야 한다는 점이다.

Jenkins 서버 관리 필요
플러그인 관리 필요
보안 관리 필요
credential 관리 필요

정리하면 다음과 같다.

개인/소규모 프로젝트 → GitHub Actions가 편함
회사/복잡한 인프라/내부망 → Jenkins를 많이 씀

22. Docker Hub를 이용한 백엔드 배포 흐름

전체 흐름은 다음과 같다.

1. 개발자가 GitHub에 코드 push
2. GitHub Actions 실행
3. 테스트 실행
4. Docker 이미지 빌드
5. Docker Hub에 이미지 push
6. EC2 서버에 SSH 접속
7. EC2에서 새 이미지 pull
8. 기존 컨테이너 종료
9. 새 컨테이너 실행
10. 배포 완료

구체적인 배포 흐름

GitHub
↓ push

GitHub Actions
↓ docker build

Docker Image
↓ docker push

Docker Hub
↓ docker pull

EC2
↓ docker compose up -d

Backend Container 실행

EC2 서버에서 하는 일

EC2에는 보통 Docker와 Docker Compose가 설치되어 있어야 한다.

서버 안에는 docker-compose.yml.env 파일이 있다.

EC2
├─ docker-compose.yml
└─ .env

배포할 때는 보통 다음 명령을 실행한다.

docker compose pull
docker compose up -d

뜻은 다음과 같다.

docker compose pull
→ Docker Hub에서 최신 이미지 가져오기

docker compose up -d
→ 새 이미지로 컨테이너 실행

이미지 태그 관리

실무에서는 latest만 쓰는 것보다 버전 태그를 같이 쓰는 게 좋다.

예를 들어 Git commit SHA를 태그로 사용할 수 있다.

my-backend:latest
my-backend:a1b2c3d

이렇게 하면 문제가 생겼을 때 이전 버전으로 롤백하기 쉽다.

docker run my-backend:a1b2c3d

GitHub Actions 예시

name: Deploy Backend

on:
push:
branches:
- main

jobs:
deploy:
runs-on: ubuntu-latest

steps:
-name: Checkout source code
uses: actions/checkout@v4

-name: Login to Docker Hub
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin

-name: Build Docker image
run: docker build -t ${{ secrets.DOCKER_USERNAME }}/my-backend:latest .

-name: Push Docker image
run: docker push ${{ secrets.DOCKER_USERNAME }}/my-backend:latest

-name: Deploy to EC2
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_SSH_KEY }}
          script:|
            cd /home/ubuntu/app
            docker compose pull
            docker compose up -d

여기서 중요한 값들은 GitHub Secrets에 저장한다.

DOCKER_USERNAME
DOCKER_PASSWORD
EC2_HOST
EC2_USERNAME
EC2_SSH_KEY

비밀번호나 SSH Key를 코드에 직접 넣으면 안 된다.


23. 배포할 때 주의할 점

1. 환경 변수는 서버에서 관리

DB 비밀번호, JWT Secret, API Key는 GitHub 코드나 Docker 이미지에 넣으면 안 된다.

코드 저장소 X
Dockerfile X
GitHub Secrets 또는 EC2 .env O

2. DB 마이그레이션

배포할 때 DB 스키마 변경이 필요할 수 있다.

예를 들어 새로운 컬럼이 추가되었다면 서버 코드만 바꿔서는 안 된다.

코드 배포
DB 마이그레이션

이 순서를 잘 관리해야 한다.


3. Health Check

배포 후 서버가 정상적으로 떴는지 확인해야 한다.

예를 들어 이런 API를 만들 수 있다.

GET /health

응답:

{
  "status": "ok"
}

배포 후 이 API가 정상 응답하는지 확인하면 된다.


4. 로그 확인

배포 후 문제가 생기면 로그를 봐야 한다.

docker compose logs -f backend

5. 롤백

새 버전에 문제가 있으면 이전 이미지로 되돌릴 수 있어야 한다.

그래서 이미지 태그를 잘 관리하는 것이 중요하다.

현재 버전: my-backend:2026-06-29-a1b2c3d
이전 버전: my-backend:2026-06-28-z9y8x7

24. 전체 개념 연결

지금까지 배운 개념들은 따로 떨어진 게 아니라 서로 연결되어 있다.

예를 들어 주문 API를 만든다고 해보자.

POST /orders

이 API 하나에도 많은 개념이 들어간다.


주문 API 안에서 필요한 것

트랜잭션

주문 생성
재고 차감
결제 기록 저장

이 작업들은 전부 성공하거나 전부 실패해야 한다.


ACID

재고는 음수가 되면 안 됨
주문은 결제와 일치해야 함
COMMIT된 주문은 장애가 나도 남아 있어야 함

동시성 제어

재고 1개에 여러 명이 동시에 주문하면 안 된다.

그래서 비관적 락이나 낙관적 락을 고려해야 한다.


멱등성

주문 요청이 네트워크 문제로 재시도되어도 주문이 중복 생성되면 안 된다.

그래서 Idempotency-Key를 사용할 수 있다.


Redis

주문 중복 방지 키, 재고 캐시, 쿠폰 발급 제한, Rate Limiting 등에 Redis를 사용할 수 있다.


Docker

백엔드 서버, MySQL, Redis를 컨테이너로 띄울 수 있다.


CI/CD

코드를 push하면 자동으로 Docker 이미지를 만들고 EC2에 배포할 수 있다.


25. 한 줄 요약

트랜잭션
→ 여러 DB 작업을 하나로 묶어 전부 성공하거나 전부 실패하게 하는 것

ACID
→ DB 트랜잭션이 안전하게 동작하기 위한 4가지 성질

동시성
→ 여러 요청이 동시에 같은 데이터를 건드릴 때 생기는 문제

낙관적 락
→ 충돌이 적다고 보고 version으로 수정 충돌을 감지하는 방식

비관적 락
→ 충돌이 날 것 같으니 미리 row를 잠그는 방식

비즈니스 로직
→ 서비스의 핵심 규칙과 흐름을 코드로 표현한 것

멱등성
→ 같은 요청을 여러 번 보내도 최종 결과가 같은 성질

Redis
→ 빠른 처리를 위한 인메모리 Key-Value DB

인메모리 DB
→ 데이터를 주로 메모리에 저장하는 빠른 DB

Docker
→ 애플리케이션과 실행 환경을 컨테이너로 묶는 도구

Volume
→ 컨테이너가 삭제되어도 데이터를 유지하기 위한 저장 공간

Docker Network
→ 컨테이너끼리 통신하게 해주는 네트워크

Docker Compose
→ 여러 컨테이너를 한 번에 관리하는 도구

멀티 모듈
→ 프로젝트를 역할별 모듈로 나눠 관리하는 구조

OS/아키텍처
→ 운영체제와 CPU 명령어 체계는 별개의 개념

CI/CD
→ 테스트, 빌드, 배포를 자동화하는 흐름

GitHub Actions
→ GitHub 기반 CI/CD 자동화 도구

Jenkins
→ 직접 운영하는 강력한 CI/CD 서버

26. 다음에 공부하면 좋은 주제

지금 흐름이면 다음 주제들을 이어서 공부하면 좋다.

DB 인덱스
N+1 문제
Connection Pool
JPA 영속성 컨텍스트
JWT와 Session
Refresh Token
Redis 분산 락
Message Queue
Kafka/RabbitMQ
로그와 모니터링
Nginx
무중단 배포
Blue-Green 배포
테스트 코드
DB Migration
Kubernetes

특히 지금 배운 내용과 바로 연결되는 우선순위는 다음과 같다.

1. DB 인덱스
2. Connection Pool
3. N+1 문제
4. Redis 캐시 전략
5. 무중단 배포
6. 로그/모니터링

최종 정리

백엔드는 단순히 API를 만드는 게 아니라,

데이터를 안전하게 저장하고
동시에 들어오는 요청을 정확히 처리하고
서비스 규칙을 코드로 표현하고
장애가 나도 안정적으로 운영하고
배포를 자동화하는 것

까지 포함한다.

지금까지 공부한 내용은 백엔드의 큰 흐름으로 보면 이렇게 연결된다.

DB 안정성
→ 트랜잭션, ACID, 락, 격리 수준

서비스 규칙
→ 비즈니스 로직, validation, 멱등성

성능과 확장
→ Redis, 인메모리 DB, 캐시, 동시성 제어

실행 환경
→ Docker, Volume, Network, Compose

프로젝트 구조
→ 멀티 모듈, 의존성 방향

배포 자동화
→ GitHub Actions, Jenkins, Docker Hub, EC2, CI/CD

한 문장으로 정리하면 다음과 같다.

백엔드는 데이터를 안전하게 다루고,
동시에 들어오는 요청을 정확히 처리하며,
안정적으로 배포하고 운영하는 서버 시스템을 만드는 일이다.