이 글은 지난 번 올린 게시글로 발생한 오류에 대해 겪은 트러블슈팅과 회고에 대한 글이다.
Spring Boot 서버가 메모리 문제인지 계속해서 멈추는 문제가 있었고, 회원 가입은 성공해도 레디스 연동이 되지 않아 로그인에서 500에러가 발생했다. 이 오류를 해결하는 과정에서 많은 걸 시도해보고 경험해봤기 때문에 똑같은 실수를 반복하지 않기 위해 기록해두려 한다.
1. 현재 배포 환경
현재 프로젝트의 배포 환경은 다음 이미지와 같다.
AWS의 EC2 인스턴스로 서버 배포를 진행 중이고, DB 서버와 SpringBoot 서버를 각각 나누어 도커 환경에서 실행 중이다.
시도해본 내용이 정말 많은데...
2. 트러블 슈팅
1. SpringBoot 서버 멈춤 이슈
Batch 작업으로 openAPI의 데이터를 호출하고 저장하는 과정이 있었는데 이 과정에서 CPU 사용량이 계속 100을 찍으면서 서버가 멈추는 문제가 발생했다.
오류 로그를 확인해볼 수도 없었기에 해당 서버를 t2.medium으로 스케일링을 확장하여 진행했더니 순조롭게 해결되었다. (나중에 확인한 거지만 t2.medium은 너무 과하고 메모리 사용량을 확인하니 최소 1GiB를 사용했기에 t2.small 정도가 적당했다.)
2. Redis 연동 문제 발생
시도해본 것들
2-1. 분리한 서버를 하나로 통합
t2.medium 으로 스케일링업하였기에 하나의 서버에서 다 실행 가능하다고 판단하였다.
docker-compose.yml에 SpringBoot app과 DB 설정을 모두 작성한 뒤 실행하였으나 문제가 해결되지 않았다.
2-2. 동일한 도커 환경의 네트워크에서 실행되도록 설정 추가
Java 서버의 docker-compose.yml
version: "3.9"
services:
app:
image: 0304ohyeryung/tasty_track:latest
container_name: tt
ports:
- "8080:8080"
env_file:
- ./env.list
networks:
- shared_network # 공유 네트워크 사용
networks:
shared_network:
driver: bridge
# 이름을 명시적으로 지정
name: my_shared_network
DB 서버의 docker-compose.yml
version: "3.9"
services:
mysql:
image: mysql:8.0
container_name: tt-mysql
ports:
- "3306:3306"
environment:
MYSQL_DATABASE: ${DB_NAME} # 데이터베이스 이름
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} # 데이터베이스 루트 비밀번호
volumes:
- mysql-vl:/var/lib/mysql
redis:
image: redis:latest
container_name: tt-redis
ports:
- "6379:6379"
volumes:
- redis-vl:/data
networks:
- shared_network # 자바 서버와 동일한 네트워크에 연결
volumes:
mysql-vl:
driver: local
redis-vl:
driver: local
networks:
shared_network:
driver: bridge
# 이름을 명시적으로 지정
name: my_shared_network
도커에서 실행 중인 네트워크 내용
서로 다른 네트워크 ID와 Subnet을 사용하고 있어 두 컨테이너가 통신하지 못하고 있었다. 각 서버에서 docker network inspect 결과를 확인해보니, 네트워크 설정이 달랐다.
- 첫 번째 서버 (ip-172-31-5-78)의 네트워크는
Subnet: 172.19.0.0/16
네트워크 ID: 8c9fe6b8c034f13207d34abe176b2f7c50f4f66fb1f835f8d384e73943f7efeb - 두 번째 서버 (ip-172-31-4-148)의 네트워크는
Subnet: 172.18.0.0/16
네트워크 ID: 69c2baea39b383dbd46a59cd70737d5f62fbdc536d607f90bb1dd0be7c76c9c5
서로 다른 네트워크에 속한 컨테이너들은 기본적으로 통신할 수 없으며, 통신하려면 같은 네트워크에 속해야 한다.
그래서 다시 하나의 서버에서 실행하도록 수정해보았는데도 Redis 연동에 실패하였다.
2-3. Redis 자체에서 외부 접속 허용 설정 추가
Redis에 접속하여 아래와 같은 설정도 추가하였는데 무용지물이었다.
이 외에도 EC2의 보안 규칙 설정, Redis에 접속하여 로그 확인, ping 명령어로 네트워크 응답 확인, 환경 변수 설정 확인 등 수도 없이 확인하고, 빌드, Docker 재실행, 서버 재실행을 무한 반복했다.
그러다 구글링을 통해 Spring Boot의 RedisConfig 설정에서 문제가 발생했다는 글을 발견했다. 설마 하는 마음으로 RedisConfig 파일을 확인해보니, password 설정만 되어 있었고, host와 port 설정이 빠져 있었다. 이 때문에 배포 환경에서 Redis 연결 정보를 제대로 읽어오지 못했던 것이다.
3. 해결한 코드
3-1. 로컬 환경 세팅
> local RedisConfig.java
@Configuration
public class RedisConfig {
@Value("${REDIS_HOST}")
private String redisHost;
@Value("${REDIS_PORT}")
private int redisPort;
@Value(("${REDIS_PASSWORD}"))
private String password;
@Bean
public RedisConnectionFactory connectionFactory() {
RedisStandaloneConfiguration redisConfiguration = new RedisStandaloneConfiguration();
redisConfiguration.setHostName(redisHost);
redisConfiguration.setPort(redisPort);
redisConfiguration.setPassword(password);
return new LettuceConnectionFactory(redisConfiguration);
}
> local application.yml
# 4. 배포용
---
jwt:
secretkey: ${JWT_SECRET_KEY}
spring:
config:
activate:
on-profile: prod # 프로파일명
datasource:
url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}
username: ${DB_USER}
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update # 배포 환경에서는 update 사용
naming:
implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true
properties:
hibernate:
format_sql: true
show_sql: true
dialect: org.hibernate.dialect.MySQLDialect
data:
redis:
host: ${REDIS_HOST}
port: ${REDIS_PORT}
password: ${REDIS_PASSWORD}
discord:
name: TastyTrack 점심 추천!
webhook-url: ${DISCORD_WEBHOOK_URL}
sql:
init:
mode: always # 배포 환경에서는 SQL 파일 자동 실행을 방지
data-locations:
- classpath:db/region.sql
- classpath:db/schema-mysql.sql
batch:
job:
enabled: false # 배포 환경에서 배치 작업 자동 실행 방지
Entity 클래스에 @Table 설정으로 명시한 이름으로 테이블이 생성되도록 하기 위해 naming 전략을 수정하였다.
또한, ddl-auto 설정을 update로 했기 때문에, Spring Batch에서 필요한 테이블들은 자동으로 생성되지 않았고, 이를 SQL 스크립트로 수동 생성하여 추가해야 했다.
수정한 뒤에 도커 이미지를 다시 생성하여 push 하는 것 잊지 않기!!
3-2. 배포 환경 설정
(EC2 서버에 접속하고 도커 및 도커 컴포즈를 설치하는 내용은 여기에서 확인)
> DB 서버의 docker-compose.yml
version: "3.9"
services:
mysql:
image: mysql:8.0
container_name: tt-mysql
ports:
- "3306:3306"
environment:
MYSQL_DATABASE: ${DB_NAME} # 데이터베이스 이름
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} # 데이터베이스 루트 비밀번호
volumes:
- mysql-vl:/var/lib/mysql
redis:
image: redis:latest
container_name: tt-redis
ports:
- "6379:6379"
env_file:
- ./.env # .env 파일에서 환경 변수 불러오기
volumes:
- redis-vl:/data # Redis 데이터 저장 볼륨
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"] # 비밀번호를 .env 파일에서 가져오기
volumes:
mysql-vl:
driver: local
redis-vl:
driver: local
> DB 서버의 .env
DB_HOST=mysql
DB_PORT=3306
DB_NAME=[설정할 DB이름]
DB_USER=[DB접속 사용자 이름]
DB_PASSWORD=[DB 비밀번호]
REDIS_PORT=6379
REDIS_PASSWORD=[REDIS 비밀번호]
> Java 서버의 docker-compose.yml
version: "3.9"
services:
app:
image: 0304ohyeryung/tasty_track:latest
container_name: tt
ports:
- "8080:8080"
env_file:
- ./env.list
> Java 서버의 env.list
SPRING_PROFILES_ACTIVE=prod
DB_HOST=[DB 서버의 EC2 public IP]
DB_PORT=3306
DB_NAME=tasty_track_mysql
DB_USER=root
DB_PASSWORD=allclear
REDIS_HOST=[DB 서버의 EC2 public IP]
REDIS_PORT=6379
REDIS_PASSWORD=[REDIS 비밀번호]
# JWT키 값, openAPI 키, DISCORD URL 등 그 외 필요한 환경설정
SPRING_PROFILES_ACTIVE : 배포 환경에서 실행되는 application.yml 을 읽을 수 있도록 설정하였다.
* 여기서 HOST는 DB 서버의 EC2 public IP 주소 값을 입력해야 한다!
이제 각각 서버를 실행하고 로그를 확인해본다!
docker-compose up -d
-d : 백그라운드에서 실행하겠다는 뜻
docker ps
실행 중인 컨테이너 확인
docker logs [컨테이너명]
실행 중인 컨테이너의 로그를 확인할 수 있다.
docker logs -f [컨테이너명]
실행 중인 컨테이너의 로그를 계속해서 확인할 수 있다. (보통 이걸 주로 사용하고 명령어 입력창으로 나올 때에는 Control(^) + C 키로 나온다)
docker exec -it [mysql 컨테이너명] mysql -u [사용자명] -p
해당 명령어로 도커에 띄운 mysql에 접속 가능하다.
[예시]
docker exec -it tt-mysql mysql -u root -p
+ 필자의 경우 DB 접속 후 한글이 깨져서 보여서 아래 명령어로 접속하였다.
docker exec -it tt-mysql mysql -u root -p --default-character-set=utf8mb4
비밀번호를 입력하라는 게 뜨면 설정해 둔 비밀번호를 입력한 뒤 접속하면 된다.
MySQL DB 관련 명령어
DB 전체 조회
SHOW DATABASES;
사용하려는 DB 선택
USE [데이터베이스명];
해당 DB의 테이블 조회
SHOW TABLES;
테이블의 데이터 전체 조회
SELECT * FROM [테이블명];
REDIS 관련 명령어
해당 명령어로 Redis에 접속한다.
docker exec -it [REDIS 컨테이너명] redis-cli
[예시]
docker exec -it tt-redis redis-cli
auth [설정한 비밀번호]
비밀번호 인증이 완료되어야 명령어 입력이 가능하다.
ping 명령어를 입력해서 pong이 나오면 잘 연결이 되었다는 의미다!
4. 소감 및 회고
문제 원인을 찾고 해결하는 데는 30분도 채 걸리지 않았다. 문제를 해결했을 때는 정말 기뻤지만, 한편으로는 허무함도 있었다. 가장 기본적인 부분도 확인하지 않은 채 배포 환경 문제라고만 단정 짓고 시간을 낭비했기 때문이다.
이번 경험을 통해 Docker와 Redis 설정 명령어에 대해 많은 것을 배울 수 있었다. 또한, 기존 코드에서 무심코 지나쳤던 작은 부분들이 큰 문제를 일으킬 수 있다는 교훈도 얻었다. 앞으로는 다른 사람이 작성한 코드라도 꼼꼼하게 확인하는 습관을 들여야겠다고 다짐했고, 문제 해결의 핵심은 원인을 정확히 찾는 것이라는 사실을 다시 한 번 깨달았다.
Keep (계속할 점)
- 기록하기
코드를 수정할 때마다 버전(ver1, ver2 등)을 기록해 두어, 나중에 원래 상태로 복구하거나 수정할 때 시간을 절약할 수 있었다. - ChatGPT 사용하기
ChatGPT를 통해 오류를 검색하고 다양한 가능성을 탐색할 수 있었다.
Problem (문제점)
- ChatGPT에 지나치게 의존한 것
빠르게 결과를 얻으려고 ChatGPT를 많이 활용했지만, 이로 인해 사고가 고착화되는 경향이 있었다. 또한, ChatGPT의 잘못된 답변을 너무 깊이 파고들어 문제 해결이 지연되기도 했다. - 로컬의 기본 코드를 확인하지 않은 것
문제를 배포 환경에서만 발생한다고 생각해, 로컬 코드를 다시 확인하지 않아 시간을 낭비했다.
Try (시도해볼 점)
- 다양한 정보 소스 활용하기
앞으로는 ChatGPT뿐만 아니라 구글링을 통해 다른 개발자들이 겪은 문제를 참고하고, 필요한 경우 원서나 공식 문서를 찾아보는 습관을 기르겠다. 다양한 정보 소스를 활용해 문제를 다각도로 해결할 방법을 모색하겠다. - 기존 코드 다시 보기
내 기억에만 의존하지 않고, 항상 코드를 꼼꼼하게 살펴보는 습관을 들이겠다. 문제를 지나치게 빠르게 해결하려 하지 않고, 기본적인 부분부터 차근차근 확인하는 자세를 유지하겠다. - 배포 환경 설정 개선
현재 Spring Batch 작업을 위해 수동으로 DB를 주입하고 있는데, 이를 Flyway와 같은 DB 형상 관리 툴로 자동화하고 버전 관리를 체계적으로 진행하는 방안을 고려하겠다.
참고자료
'Server > docker' 카테고리의 다른 글
[Docker] 도커로 EC2에 서버 배포하기 (1) | 2024.09.03 |
---|---|
Docker를 활용한 Redis 기본 세팅 및 Springboot 연결 확인 (0) | 2024.08.26 |
[원티드 프리온보딩 백엔드 챌린지] TIL 도커란?_실전편 (0) | 2023.08.05 |
[원티드 프리온보딩 백엔드 챌린지] TIL 도커란?_이론편 (0) | 2023.08.03 |
[Docker] M1에 Docker Desktop 설치하는 법 (0) | 2023.04.11 |