백엔드 개발자로 일하다 보면 코드 외에도 알아야 할 게 많다. 데이터베이스 설계와 튜닝, 서버 인프라 구성, 클라우드 서비스 활용까지 세 영역이 맞물려 돌아가야 서비스가 안정적으로 운영된다. 이 글에서는 각 영역의 핵심 개념과 실무에서 어떻게 적용하는지를 요약 형태로 정리한다.
1. 데이터베이스(DB)
인덱스 전략
인덱스는 조회 성능을 좌우한다. 잘 설계하면 수십 배 빠르지만, 잘못 쓰면 쓰기 성능을 오히려 떨어뜨린다.
-- 자주 조회하는 컬럼에 단일 인덱스
CREATE INDEX idx_user_email ON users(email);
-- WHERE 조건 + ORDER BY 함께 쓰는 패턴엔 복합 인덱스
CREATE INDEX idx_order_user_created ON orders(user_id, created_at DESC);
-- 인덱스가 무력화되는 패턴 (함수 사용 금지)
SELECT * FROM users WHERE LOWER(email) = 'test@email.com'; -- 인덱스 안 탐
SELECT * FROM users WHERE email = 'test@email.com'; -- 인덱스 탐
SQL
복사
실무 체크리스트:
•
자주 조회하는 컬럼, JOIN 키, WHERE 조건 컬럼에 인덱스 추가
•
복합 인덱스는 선택도(카디널리티)가 높은 컬럼을 앞에
•
인덱스가 너무 많으면 INSERT/UPDATE 성능 저하
•
EXPLAIN / EXPLAIN ANALYZE로 실행 계획 반드시 확인
트랜잭션과 격리 수준
동시 요청이 많을수록 트랜잭션 처리가 중요해진다.
격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read |
READ UNCOMMITTED | 발생 | 발생 | 발생 |
READ COMMITTED | 방지 | 발생 | 발생 |
REPEATABLE READ | 방지 | 방지 | 발생 |
SERIALIZABLE | 방지 | 방지 | 방지 |
실무에서는 대부분 READ COMMITTED 또는 REPEATABLE READ를 기본으로 쓴다. MySQL InnoDB의 기본값은 REPEATABLE READ다.
-- 트랜잭션 처리 기본
BEGIN;
UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
UPDATE accounts SET balance = balance + 10000 WHERE id = 2;
COMMIT;
-- 격리 수준 변경
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SQL
복사
커넥션 풀(Connection Pool)
DB 커넥션을 매번 새로 맺는 건 비용이 크다. 커넥션 풀을 써서 미리 맺어둔 커넥션을 재사용한다.
# Spring Boot - HikariCP 설정 예시
spring:
datasource:
hikari:
maximum-pool-size: 10 # 최대 커넥션 수
minimum-idle: 5 # 최소 유지 커넥션
connection-timeout: 30000 # 커넥션 대기 타임아웃 (ms)
idle-timeout: 600000 # 유휴 커넥션 유지 시간 (ms)
max-lifetime: 1800000 # 커넥션 최대 수명 (ms)
YAML
복사
실무 팁: maximum-pool-size는 CPU 코어 수 × 2 + 유효 디스크 수를 기준으로 잡는다. 무조건 크게 설정하면 오히려 컨텍스트 스위칭 비용이 늘어난다.
캐시 전략 (Redis 활용)
자주 읽히고 잘 안 바뀌는 데이터는 Redis에 캐싱해서 DB 부하를 줄인다.
// Spring Cache + Redis 예시
@Cacheable(value = "users", key = "#id")
public User getUser(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("사용자 없음"));
}
@CacheEvict(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
Java
복사
2. 인프라
서버 구성 기본 패턴
•
로드 밸런서: 트래픽을 여러 서버로 분산. L4(TCP)는 빠르고, L7(HTTP)는 경로 기반 라우팅 가능
•
DB 이중화: Primary에 쓰고, Replica에서 읽음. 읽기 트래픽 분산 + 장애 대비
•
무중단 배포: Blue-Green, Rolling, Canary 배포로 서비스 중단 없이 업데이트
Docker와 컨테이너
# 멀티 스테이지 빌드 예시 (이미지 용량 최소화)
FROM gradle:8-jdk17 AS builder
WORKDIR /app
COPY . .
RUN gradle build -x test
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Docker
복사
# docker-compose.yml 기본 구조
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- db
- redis
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: mydb
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
mysql_data:
YAML
복사
실무 팁:
•
애플리케이션 설정값(비밀번호, API 키)은 절대 이미지에 넣지 않는다. 환경 변수나 Secret Manager로 주입한다.
•
멀티 스테이지 빌드를 쓰면 최종 이미지에 빌드 도구가 포함되지 않아 용량이 줄어든다.
Nginx 리버스 프록시
Nginx는 리버스 프록시, 로드 밸런서, 정적 파일 서빙을 모두 처리할 수 있어 실무에서 자주 쓰인다.
3. 클라우드 (AWS 기준)
핵심 서비스 요약
서비스 | 역할 | 실무 사용 예 |
EC2 | 가상 서버 | 앱 서버, 배치 서버 |
RDS | 관리형 DB | MySQL, PostgreSQL 운영 |
ElastiCache | 관리형 캐시 | Redis 클러스터 |
S3 | 객체 스토리지 | 이미지, 파일 업로드 |
CloudFront | CDN | 정적 자원 빠른 전달 |
ECS/EKS | 컨테이너 오케스트레이션 | Docker 컨테이너 운영 |
ALB | L7 로드 밸런서 | 경로 기반 라우팅 |
Route 53 | DNS | 도메인 관리, 헬스체크 |
IAM | 권한 관리 | 서비스 간 권한 제어 |
CloudWatch | 모니터링 | 로그, 메트릭, 알람 |
VPC 구성 기본 패턴
•
Public Subnet: 인터넷 직접 접근 가능. ALB, Bastion 호스트만 둔다.
•
Private Subnet: 인터넷 직접 접근 불가. 앱 서버, DB는 여기에 위치시켜 보안을 강화한다.
•
NAT Gateway: Private Subnet의 서버가 외부 API를 호출할 때 필요 (단방향 아웃바운드)
S3 파일 업로드 패턴
클라이언트가 파일을 직접 서버로 올리면 서버 부하가 커진다. Presigned URL을 쓰면 클라이언트가 S3에 직접 업로드하게 할 수 있다.
// AWS SDK v2 - Presigned URL 발급 예시
S3Presigner presigner = S3Presigner.create();
PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(10))
.putObjectRequest(r -> r
.bucket("my-bucket")
.key("uploads/" + fileName)
.contentType("image/jpeg"))
.build();
PresignedPutObjectRequest presignedRequest = presigner.presignPutObject(presignRequest);
String uploadUrl = presignedRequest.url().toString();
Java
복사
CI/CD 파이프라인 기본 구조
# .github/workflows/deploy.yml 핵심 구조
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-2
- name: Build and push Docker image
run: |
docker build -t my-app .
docker tag my-app:latest $ECR_REGISTRY/my-app:latest
docker push $ECR_REGISTRY/my-app:latest
- name: Deploy to ECS
run: |
aws ecs update-service \
--cluster my-cluster \
--service my-service \
--force-new-deployment
YAML
복사
모니터링과 알람
서비스가 죽었을 때 내가 먼저 알아야 한다. 사용자가 먼저 알면 이미 늦다.
마치며
DB, 인프라, 클라우드는 각각 독립된 영역처럼 보이지만 실무에서는 항상 함께 움직인다. 쿼리가 느린 건 인덱스 문제일 수도 있고, 커넥션 풀 설정 문제일 수도 있고, 인프라 스펙 문제일 수도 있다. 각 영역의 기본 원리를 이해하고 있어야 장애 상황에서 빠르게 원인을 좁혀나갈 수 있다. 이 글에서 다룬 내용을 뼈대로 삼고, 필요한 부분을 깊게 파고드는 방식으로 학습하는 걸 추천한다.

