[Docker] 스프링 도커 컴포즈 개발

도커 컴포즈

도커에 대해 아무것도 몰랐던 나는 정말 2일 밤 내내 머리를 싸매고 Springboot 컨테이너를 만들기 위해 애를 썼다.(현재 시각 오전 3시 47분)

현재 진행중인 프로젝트 Springboot 이미지 파일을 만들어서 컨테이너로 실행도 시켜보고 했지만 결과는 참혹스러운 에러들만 나왔다(정말 길게)

대충 보니 MySQL을 DB로 사용중인데 그게 연결이 안되는 것 같았다.

그래서 뭐 하는 수 없지 MySQL도 올려야하나봥…하고 MySQL을 도커 실행시켜 디비도 만들고 테이블도 만들고 MySQL 워크벤치에서도 확인하고 음 잘되는군!!! 싶어서 이제 다시 SpringBoot 컨테이너를 실행시켰지만 여전히 에러

정말 많은 일과 에러가 있었지만 거의 혼이 나간 상태로 작업해서 다 잊기도 했고… 성공할꺼란 생각도 없이 막막하게 해서 캡쳐도 하기 싫었다…(예전에 Heroku에 Spring 배포 실패 후 배포는 처다보기 싫었음..)


그치만 이 글은 성공기다 성공기!!!!

뭐 이 글을 보시는 시점에서는 대부분 실패해서 찾아온 것이라고 믿고… 도커의 설치와 그런건 딱히 얘기하지 않겠다.

나는 그냥 기본적으로 최신버전 Docker 데스크톱을 깔았고, 이외의 설정은 건드린게 없는 것 같다(아마도)

프로젝트 설정

  • JAVA 11
  • Gradle
  • MySQL

이외의 정보는 여기서는 필요없을 것 같다.

일단 해결했던 방법은 도커 컴포즈를 사용하는 것이였다.

설명 제쳐놓고 방법부터!

빌드 파일인 Jar 파일을 만드는 법은 나중에 다 끝나고 해야할 방법도 있지만 이런 방법도 있다.

docker1

여기 클릭해놓은 bootJar을 클릭하면

docker2

build/libs 에 jar파일이 생기게 된다. 이 루트 설정이 도커파일에 들어가서 아래와 같은 코드가 나온다.

FROM openjdk:11
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

간단히 설명하자면 자바는 11버전 그리고 jar파일 위치 ${} 안에 매개변수로 들어가는 거라고 생각하면 된다.

왠진 모르지만 이거 그대로 하는게 정신 건강에 편할것이다.. 다른 방식으로 했는데 루트는 같았는데 흠… jar파일을 못찾고 막 그랬다..

그리고 꼭 도커파일은 프젝 최상위루트에 넣어놓도록…

그 후에는 우리는 도커 컴포즈를 이용할 것이므로 파일을 만들어줘야한다. 이도 마찬가지로 도커파일과 같은 루트에 만들어줘야한다.

docker3

보이는 것처럼 docker-compose.yml 파일을 만들어준다.

version: '3'

services:
  database:
    container_name: mysql_db
    image: mysql/mysql-server:5.7
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: eungae
      MYSQL_ROOT_HOST: '%'
      MYSQL_ROOT_PASSWORD: 1234
      TZ: 'Asia/Seoul'
    ports:
      - "3306:3306"
    volumes:
      - ./mysql/conf.d:/etc/mysql/conf.d # MySQL 설정 파일 위치
    command:
      - "mysqld"
      - "--character-set-server=utf8mb4"
      - "--collation-server=utf8mb4_unicode_ci"
    networks:
      - test_network

  application:
    container_name: docker-compose-test
    restart: on-failure
    build:
      context: ./
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://mysql_db:3306/eungae?useSSL=false&allowPublicKeyRetrieval=true
      SPRING_DATASOURCE_USERNAME: "root"
      SPRING_DATASOURCE_PASSWORD: "1234"
    depends_on:
      - database
    networks:
      - test_network

networks:
  test_network:

여기 설정과 properties에 설정이 달라서 나는 오류로 좀 1시간정도 헤맸는데 어쩌피 application.properties에 MySQL 설정은 잘못되어있어도 컴포즈가 우선순위라서 상관 없지만 상관 있는 것이 properties에 하나 있었다…

그것은 바로 port 번호였다. 위에보면 나는 yml에 포트번호를 8080으로 해놨는데 프젝 포트번호는 9099여서 컴포즈를 다 올리고 실행했는데도 계속 localhost:8080, 9090 모두 화이트라벨조차 뜨지 않고 그냥 안됐다.

그래서 꼭 포트번호를 따로 지정해뒀다면 둘이 통일하던지…컴포즈 파일에만 하든지 했으면 좋겠다.(그리고 빌드 다시하고 컴포즈 올려야한다…코드에만 고친다고 되는게 아니다…)

application.properties는 뭐 처음에 mysql로 잘 로컬에서 돌리시던 분이면 딱히 고칠 필요는 없지만 나중에 에러가 생겨서 고생하지 않으려면 추가해주는 게 좋은 한 줄을 위해 공유하겠다. ⇒ Swagger 사용자는 맨 마지막 줄을 추가해주세요…오류납니다…

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/eungae?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul

spring.datasource.username=root
spring.datasource.password=1234

spring.jpa.show-sql = true
spring.jpa.hibernate.ddl-auto = create
spring.jpa.properties.hibernate.format_sql=true

spring.jpa.open-in-view=false

**spring.mvc.pathmatch.matching-strategy=ant_path_matcher**

그리고 build.gradle에 추가해주어야 할 것이 있다.

jar {
    enabled = false
}

이제 다 했다면 ./gradlew clean build 로 jar을 생성해야 오류가 안 날 가능성이 높다. 다 지우고 하는거라고 생각하면 된다.


docker-compose 파일 실행

하기전에 정신건강에 피해가 가지 않고 싶다면 3306 포트의 PID 값을 찾아 미리 Kill 해놓길 바란다

이제 도커 컴포즈 파일을 실행할 것이다.

docker-compose up

만약에 변경사항이 생겨 빌드 파일을 새로 빌드했고 다시 도커파일을 빌드하고 싶다면..!

docker-compose up --build -d

이를 터미널에 치고 가만히 도커 컨테이너에 올라간 우리의 도커 컴포즈를 보고 있으면…잘 되었다면 오류가 없이 잘 실행될 것이다.

서운할 정도로 글이 짧다..내가 한 건 엄청 많은데 기록 안해둬서 아쉽다…


환경변수 세팅

환경변수 세팅은 따로 해놓을 수 있는데 이것도 마찬가지로 프로젝트 최상위 루트에 .env 파일을 만들어주고

docker4

MYSQL_ROOT_HOST=%
MYSQL_ROOT_PASSWORD=1234
MYSQL_DATABASE=eungae

SPRING_DATASOURCE_USERNAME=root
version: '3'

services:
  database:
    container_name: mysql_db
    image: mysql
    restart: unless-stopped
    env_file:
      - .env
    ports:
      - "3306:3306"
    volumes:
      - ./mysql/conf.d:/etc/mysql/conf.d # MySQL 설정 파일 위치
    command:
      - "mysqld"
      - "--character-set-server=utf8mb4"
      - "--collation-server=utf8mb4_unicode_ci"
    networks:
      - test_network

  application:
    container_name: waffle-eungae-backend
    restart: on-failure
    build:
      context: ./
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    env_file:
      - .env
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://mysql_db:3306/${MYSQL_DATABASE}?useSSL=false&allowPublicKeyRetrieval=true
      SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
      SPRING_DATASOURCE_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    depends_on:
      - database
    networks:
      - test_network

networks:
  test_network:

이런식으로 사용해주면 따로 관리할 수 있다. MemberService로 이동하자.

먼저 MemberService에 new 하고 있는 MemberRepository를 고쳐줘야한다.

그리고 나서 Alt + Insert를 통해 생성자를 만들어준다.

private final MemberRepository memberRepository;

public MemberService(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}

이렇게 바뀌면 MemberService에서 직접 new 하지 않고 외부에서 memberRepository를 넣어주는 형식이 된다.

이런것을 우리는 의존성 주입(Dependency Injection)이라고 한다.

이제 다시 MemberServiceTest로 이동하자.

MemberService와 MemoryMemberRepository의 new를 제거해준다.

다음 @BeforeEach를 이용해 시작하기 전마다 실행하는 코드를 짜준다.

그 안에서 memberRepository의 new를 하고, memberService 안에 new 했던 memberRepository를 넣어준다.

MemberService memberService;
MemoryMemberRepository memberRepository;

@BeforeEach
public void beforeEach(){
    memberRepository = new MemoryMemberRepository();
    memberService = new MemberService(memberRepository);
}

최종코드

MemberServiceTest.java

import mess.hellospring.domain.Member;
import mess.hellospring.domain.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach(){
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }

    @AfterEach
    public void afterEach(){
        memberRepository.clearStore();
    }

    @Test
    void 회원가입() {
        //given
        Member member = new Member();
        member.setName("spring");
        //when
        Long saveId = memberService.join(member);
        //then
        Member findMember = memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMember.getName());
    }

    @Test
    public void 중복회원예외(){
        //given
        Member member1 = new Member();
        member1.setName("spring");
        Member member2 = new Member();
        member2.setName("spring");
        //when
        memberService.join(member1);

        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));

        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

        /*try{
            memberService.join(member2);
            fail();
        }catch (IllegalStateException e){
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        }*/
        //then
    }

    @Test
    void 회원찾기() {

    }

    @Test
    void findOne() {

    }
}

MemberService.java

import mess.hellospring.domain.Member;
import mess.hellospring.repository.MemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    /**
     * 회원 가입
     */
    public Long join(Member member){
        validateDuplicateMember(member); //중복 회원 검증
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

    /**
     * 전체 회원 조회
     */
    public List<Member> findMembers(){
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId){
        return memberRepository.findById(memberId);
    }
}

Leave a comment