저는 docker container에서 spring api 서버를 배포하는 것이 목표였습니다.
이에 대한 과정으로 dockerfile과 docker compose를 활용해 간단하게 배포하는 것이 있었습니다.
backend | 2024-08-22T19:54:07.402Z INFO 1 --- [spring-wallet] [ main] k.springwallet.SpringWalletApplication : Starting SpringWalletApplication v0.0.1-SNAPSHOT using Java 17.0.2 with PID 1 (/app.jar started by root in /)
backend | 2024-08-22T19:54:07.419Z INFO 1 --- [spring-wallet] [ main] k.springwallet.SpringWalletApplication : No active profile set, falling back to 1 default profile: "default"
backend | 2024-08-22T19:54:13.247Z INFO 1 --- [spring-wallet] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
backend | 2024-08-22T19:54:13.256Z INFO 1 --- [spring-wallet] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
backend | 2024-08-22T19:54:13.383Z INFO 1 --- [spring-wallet] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 82 ms. Found 0 JPA repository interfaces.
backend | 2024-08-22T19:54:13.463Z INFO 1 --- [spring-wallet] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
backend | 2024-08-22T19:54:13.469Z INFO 1 --- [spring-wallet] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode.
backend | 2024-08-22T19:54:13.551Z INFO 1 --- [spring-wallet] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 33 ms. Found 0 Redis repository interfaces.
backend | 2024-08-22T19:54:17.050Z INFO 1 --- [spring-wallet] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
backend | 2024-08-22T19:54:17.106Z INFO 1 --- [spring-wallet] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
backend | 2024-08-22T19:54:17.107Z INFO 1 --- [spring-wallet] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.19]
backend | 2024-08-22T19:54:17.276Z INFO 1 --- [spring-wallet] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
backend | 2024-08-22T19:54:17.284Z INFO 1 --- [spring-wallet] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 9145 ms
backend | 2024-08-22T19:54:18.088Z INFO 1 --- [spring-wallet] [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
backend | 2024-08-22T19:54:18.514Z INFO 1 --- [spring-wallet] [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.4.4.Final
backend | 2024-08-22T19:54:18.712Z INFO 1 --- [spring-wallet] [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled
backend | 2024-08-22T19:54:20.176Z INFO 1 --- [spring-wallet] [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
backend | 2024-08-22T19:54:20.316Z INFO 1 --- [spring-wallet] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
backend | 2024-08-22T19:54:21.627Z ERROR 1 --- [spring-wallet] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Exception during pool initialization.
backend |
backend | com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
backend |
backend | The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
backend | at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:174) ~[mysql-connector-j-8.3.0.jar!/:8.3.0]
backend | at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64) ~[mysql-connector-j-8.3.0.jar!/:8.3.0]
backend | at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:815) ~[mysql-connector-j-8.3.0.jar!/:8.3.0]
backend | at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:438) ~[mysql-connector-j-8.3.0.jar!/:8.3.0]
backend | at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:241) ~[mysql-connector-j-8.3.0.jar!/:8.3.0]
backend | at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:189) ~[mysql-connector-j-8.3.0.jar!/:8.3.0]
backend | at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:138) ~
... 중략
뭔가 하여튼 매우 긴 에러 문장이 출력이 되기 시작합니다.
대학생 4년을 보냈지만 이런 에러는 뇌정지를 유발하기 좋은 상황이죠.
정신을 붙잡고 긴 에러 문장 중 핵심을 뽑아봅시다.
com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure
Caused by: java.net.ConnectException: Connection refused
제가 생각한 에러의 주요 문장은 위와 같습니다.
즉 MySQL과의 연결이 제대로 수행되지 않은 것이 이 에러의 문제라고 봅니다.
사실 간단한 문제였습니다.
아래는 제 프로젝트의 application.properties입니다.
spring.application.name=spring-wallet
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/${DB_NAME}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
oauth.google.client_id=${GOOGLE_CLIENT_ID}
oauth.google.client_secret=${GOOGLE_CLIENT_SECRET}
oauth.google.redirect_url=${GOOGLE_REDIRECT_URL}
spring.jwt.secret.access=${ACCESS_TOKEN}
spring.jwt.secret.refresh=${REFRESH_TOKEN}
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
spring.security.oauth2.client.registration.google.scope=profile, email
spring.data.redis.host=${REDIS_HOST}
spring.data.redis.port=${REDIS_PORT}
결론은 MySQL 연결하기 위한 부분인
spring.datasource.url=jdbc:mysql://localhost:3306/${DB_NAME}
이 부분에서 문제가 생겼습니다.
docker container 내부에서 spring 프로젝트를 생성하고 이를 container의 외부 즉 서버 자체의 mysql에 접속하여 실행하는 것이 저의 목표였습니다.
다만 docker container 내부에서의 localhost는 서버가 아닌 컨테이너 내부의 ip가 지정됩니다.
여기서 문제가 생겼습니다.
그럼 해결은 간단합니다. localhost가 아닌 호스트의 ip로 변환해주기만 하면 됩니다.
spring.application.name=spring-wallet
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://{DB_HOST}:{DB_PORT}/${DB_NAME}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
oauth.google.client_id=${GOOGLE_CLIENT_ID}
oauth.google.client_secret=${GOOGLE_CLIENT_SECRET}
oauth.google.redirect_url=${GOOGLE_REDIRECT_URL}
spring.jwt.secret.access=${ACCESS_TOKEN}
spring.jwt.secret.refresh=${REFRESH_TOKEN}
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
spring.security.oauth2.client.registration.google.scope=profile, email
spring.data.redis.host=${REDIS_HOST}
spring.data.redis.port=${REDIS_PORT}
위와 같이 수정하고 DB_HOST에는 서버의 ip를 넣어줬습니다. 하지만... 결과는 똑같았습니다.
위에서 언급한 에러와 똑같은 에러가 발생했습니다. 뭔가 달라지기라도 하면 찾겠는데 아무런 문제가 해결되지 않았습니다.
한창 구글링을 하던 중 mysql 접속을 검색하니 자연스럽게 상위에 있던 것이 MySQL 외부 접속이었습니다.
우선 root 계정으로 MySQL에 접속합니다.
sudo mysql -u root -p
그리곤 아래와 같은 명령어를 통해 계정을 확인합니다.
use mysql; // 1
select user, host from user; // 2
1 : 유저 정보를 확인하기 위해 MySQL db를 선택합니다.
2 : 유저와 호스트 정보를 확인합니다.

위와 같이 유저 정보와 그에 따른 호스트 정보가 나옵니다.
유저에 대응되는 호스트는 유저에 접근할 수 있는 사용자의 출처를 의미합니다.
즉 localhost가 지정되어 있는 유저에 접근할 수 있는 출처는 localhost 즉 나 자신만이 가능합니다.
%는 어디에서나 접근할 수 있도록 설정하는 것 입니다.
제가 사용하는 db는 springwalletdb고 이를 외부 접속(docker container)을 할 수 있도록 하기 위해 %로 지정합니다.
수정하기 위해 아래와 같은 명령어로 진행했지만
update user set host='%' where user='springwalletdb'
다음과 같은 에러가 발생합니다.
ERROR 1356 (HY000): View 'mysql.user' references invalid table(s) or column(s) or function(s) or definer/invoker of view lack rights to use them
원인은 버전이 업그레이드됨에 따라 mysql.user가 테이블이 아닌 view로 변경되었기 때문에 update가 불가능한 것입니다.
그래서 아래와 같이 입력해서 수정했습니다.
update global_priv set host='%' where user='springwalletdb'

제대로 수정된 모습입니다.
이 뿐만이 아닙니다. mysql 설정을 건드려야하는 작업을 추가로 해줘야 합니다.

보시면 /ect/mysql에 들어가면 여러 설정 파일 및 디렉토리가 존재합니다.
다른 분들을 보면 my.cnf에 바인딩 정보가 있어 이를 수정하면 되는데 저는 바인딩 정보가 아닌 다른 것이 있었습니다.

바인딩 정보 대신 includedir이 있습니다. 자세하게는 몰라도 명시된 디렉토리를 include한다는 뜻이겠죠?
그래서 저기 있는 디렉토리를 뒤진 결과
/etc/mysql/mariadb.conf.d/50-server.cnf에 바인딩 정보가 있는 것을 확인했습니다.

위와 같이 bind-address = 127.0.0.1 을 0.0.0.0으로 수정합니다.
127.0.0.1로 하면 외부 접속이 전부 차단되는 상황이고 이를 0.0.0.0으로 수정해서 외부 접속을 허용하는 것 입니다.
위와 같은 과정을 거친 경우에 저는 문제 없이 프로젝트가 실행되는 것을 확인했습니다.
다만 환경변수를 통해 ip 정보를 넣는 것이 아닌 다른 방법도 있어 이를 좀 더 작성하고 글을 마치겠습니다.
우리는 흔히 자신의 컴퓨터를 가르키는 루프백 주소로 localhost를 사용합니다. 그리고 이는 127.0.0.1을 가르킨다는 것도 물론 다들 아실겁니다.
근데 어떻게 localhost가 127.0.0.1 을 가르키는 것일까요?
이는 etc/hosts를 확인하면 알 수 있습니다.
etc/hosts 파일을 열어보면 아래와 같이 작성되어 있습니다.

위의 이미지를 보면
127.0.0.1 localhost
라고 작성되어 있는 것을 확인할 수 있습니다.
그렇습니다. /etc/hosts는 ip를 특정 단어로 맵핑해주는 파일이라고 생각하시면 됩니다. 그렇다면 docker container의 etc/hosts 에 서버 ip를 맵핑한다면 별도로 환경변수는 필요없을 것입니다.
그렇다면 이를 어떻게 적용할까요? docker container 내부에서 직접 변경하고 프로젝트를 실행하면 될까요?
그게 귀찮아서 우리는 dockerfile과 docker compose를 통해 자동화를 합니다. 그래서 저는 docker compose에서 ect/hosts에 추가할 수 있는 방법을 찾기로 합니다.
방법은 extra_hosts를 docker-compose.yml에 추가하는 것 입니다.
services:
spring:
extra_hosts:
- "host.docker.internal:192.168.200.179"
host.docker.internal 라고 명시하면 원래 extra_hosts에 별도로 추가하지 않아도 된다고 하던데 알고보니 mac에서만 가능하다고 하네요...
그래서 host.docker.internal을 꼭 지킬필요는 없고 자신이 원하는 이름으로 설정하면 되겠습니다.
우선 container에 대한 개념이 부족해 해맸던 경우였던 것 같네요... 같은 환경에서 프로젝트와 db가 있는게 아닌데 localhost라고 명시한 것이며 외부접근이 차단된 db에 계속 접근한 것도 말이죠..
사실 글로 적으면 간단한 부분이지만 실제로 거의 일주일을 가까이 붙잡고 있던 문제기도 했습니다.
(ip를 잘못 적어서 접근이 안되는 상황도 있었는데 차마 이건 위에서 문제 해결이라고 적긴 부끄러워서 패스...)
간단요약을 하자면
가 되겠네요.