본문 바로가기

Backend/Spring

스프링 보일러 플레이트 Docker EC2배포하기 A to Z

발단

해커톤에 참여하게 되면서 스프링을 사용하고 싶지만 개발 시간이 느려질까 우려되었다. 

미리 틀정도 만들어가면 좋을 것 같아서 시작하게 된 보일러 플레이트 만들기 및 EC2 배포하기!

 


기본 코드

CRUD는 어디에도 기본적으로 들어갈 것 같아서 post 라는 엔티티를 두어 간단한 crud를 구현하였다. 

이후에 해커톤에서도 도메인별로 나눠서 구현할 예정이다.

    @PostMapping("")
    @ApiOperation("글을 생성합니다.")
    public void createPost(@Valid @RequestBody PostCreateRequest request) {
        postService.createPost(request);
    }

    @GetMapping("/{postId}")
    @ApiOperation("글을 아이디별로 조회합니다.")
    public PostResponse getPostById(@PathVariable Long postId) {
        return postService.getPostById(postId);
    }

    @GetMapping("")
    @ApiOperation("글 목록을 조회합니다.")
    public PostListResponse getAllPosts() {
        return postService.getAllPosts();
    }

    @PutMapping("/{postId}")
    @ApiOperation("글 내용을 수정합니다.")
    public void updatePost(
            @PathVariable Long postId,
            @Valid @RequestBody PostUpdateRequest request) {
        postService.updatePost(postId, request);
    }

세부 코드는 깃허브 링크 에 가면 볼 수 있다.

 

로컬 DB연동을 위한 Docker-compose.yml

(로컬 도커 설치는 생략한다.)

 

  •  프로젝트 가장 바깥 경로에 docker-compose.yml 파일 생성

  •  docker-compose.yml에 mysql db 컨테이너 설정하기
version: '3'
services:
  mysql:
    image: mysql:8-oracle
    container_name: demo_db_container
    ports:
      - "3307:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: demo

 

  •  파일이 존재하는 경로에서 docker-compose up -d 또는 /usr/local/bin/docker-compose -f ${파일경로}/dockercompose.yml up -d 입력 (IntelliJ에서는 초록색 화살표를 누르면 알아서 해준다 ^_^)
/usr/local/bin/docker-compose -f /Users/pine_lee/Desktop/PROJECT/spring_boilerplate/demo/src/docker-compose.yml up -d
  • 성공시 컨테이너가 돌아간다.

DB 툴에 연결해놓으면 데이터 확인에 용이하다.


에러 처리

에러 및 리스폰스 처리는 오픈소스를 이용해서 간단하게 처리해보았다.

Runtime Error로 던지고 application.yml 파일에서 설정을 하면 쉽게 오류처리를 할 수 있다.

 

  • 디펜던시 추가
repositories {
    maven { url 'https://jitpack.io' }
}

dependencies {
    implementation 'com.github:zkdlu:api-response-spring-boot-starter:Tag'
}
  • 서비스코드에서 이렇게 에러를 처리하고
    @Transactional(readOnly = true)
    public PostResponse getPostById(final Long postId) {
        final Post post = postRepository.findById(postId)
                .orElseThrow(NotExistsPostException::new);
        return PostResponse.of(post);
    }
  • 런타임 에러를 상속받아서 클래스를 생성하여
package com.spring_boilerplate.demo.post;

public class NotExistsPostException extends RuntimeException {
}
  • application.yml에서 이를 정의한다.
spring:
  response:
    success:
      code : 200
      msg : SUCCESS
    exceptions:
      postNotExists:
        code: 401
        msg: '글이 존재하지 않습니다. '
        type : com.spring_boilerplate.demo.post.NotExistsPostException

 

오픈소스 링크 << 

 

GitHub - zkdlu/api-response-spring-boot-starter: Spring api response provides a standard structure to response data and exceptio

Spring api response provides a standard structure to response data and exception handling - GitHub - zkdlu/api-response-spring-boot-starter: Spring api response provides a standard structure to res...

github.com


API 명세서- Swagger
  • 디펜던시 추가
  implementation group: 'io.springfox', name: 'springfox-boot-starter', version: '3.0.0'
  • 스웨거 설정 파일 생성

config.swagger.SwaggerConfig.java

package com.spring_boilerplate.demo.config.swagger;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RestController;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.ResponseBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Response;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger.web.DocExpansion;
import springfox.documentation.swagger.web.UiConfiguration;
import springfox.documentation.swagger.web.UiConfigurationBuilder;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static springfox.documentation.builders.RequestHandlerSelectors.withClassAnnotation;

@EnableSwagger2
@Configuration
@Import(BeanValidatorPluginsConfiguration.class)
public class SwaggerConfig {
    private static final String API_NAME = "Demo Project API";
    private static final String API_VERSION = "0.0.1";
    private static final String API_DESCRIPTION = "demo project";

    @Bean
    public UiConfiguration uiConfig() {
        return UiConfigurationBuilder.builder()
                .docExpansion(DocExpansion.LIST)
                .build();
    }

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
//                .securitySchemes(authorization())
                .ignoredParameterTypes()
                .select()
                .apis(withClassAnnotation(RestController.class))
                .paths(PathSelectors.ant("/**"))
                .build()
                .useDefaultResponseMessages(false)
                .globalResponses(HttpMethod.GET, this.createGlobalResponseMessages())
                .globalResponses(HttpMethod.POST, this.createGlobalResponseMessages())
                .globalResponses(HttpMethod.PUT, this.createGlobalResponseMessages())
                .globalResponses(HttpMethod.DELETE, this.createGlobalResponseMessages());
    }

    private List<Response> createGlobalResponseMessages() {
        return Stream.of(
                        HttpStatus.BAD_REQUEST,
                        HttpStatus.UNAUTHORIZED,
                        HttpStatus.CONFLICT,
                        HttpStatus.FORBIDDEN,
                        HttpStatus.NOT_FOUND,
                        HttpStatus.INTERNAL_SERVER_ERROR,
                        HttpStatus.BAD_GATEWAY,
                        HttpStatus.SERVICE_UNAVAILABLE
                )
                .map(this::createResponseMessage)
                .collect(Collectors.toList());
    }

    private Response createResponseMessage(HttpStatus httpStatus) {
        return new ResponseBuilder()
                .code(String.valueOf(httpStatus.value()))
                .description(httpStatus.getReasonPhrase())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title(API_NAME)
                .version(API_VERSION)
                .description(API_DESCRIPTION)
                .contact(new Contact("Sol Lee", "https://www.github.com/soleu", "dlthf555@gmail.com"))
                .build();
    }
}
  • controller에서 api 설명 추가
@PutMapping("/{postId}")
@ApiOperation("글 내용을 수정합니다.") // 이렇게
public void updatePost(
        @PathVariable Long postId,
        @Valid @RequestBody PostUpdateRequest request) {
    postService.updatePost(postId, request);
}

스웨거 설정을 이용해서 더 자세히 설명을 넣을 수도 있지만 생략했다.

 

localhost:8080/swagger-ui/index.html#/

경로로 들어가서 확인해볼 수 있다.


실서버 배포

사실 beanstalk을 사용하면 빠르게 배포할 수 있다고 들은 것 같은데, 사용해본적이 없으므로 docker를 이용하여 EC2에 수동으로 올릴 것이다.

EC2 및 RDS 생성 과정은 생략한다.

 

  • 프로젝트 경로에서 터미널에 한 줄씩 입력한다.
./gradlew clean  //기존 빌드 파일 초기화
./gradlew build //빌드 
./gradlew jar //jar파일 생성

** build>libs에 가면 생성된 jar 파일을 확인할 수 있다.

여기서 jar파일이 plain과 일반파일 두개로 생성될 때가 있는데, 이걸 그대로 도커에 올리게 되면 .jar파일이 모호하다는 에러 메세지가 나온다. 아래 설정을 build.gradle에 추가해서 이를 해결해주자.

//build.gradle
jar {
    enabled = false
}

 

  • 이제 만든 .jar파일을 도커 허브에 올려줘야한다.

프로젝트 가장 바깥경로에 Dockerfile을 생성한다.

FROM openjdk:11-jre-slim
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENV	PROFILE prod //application-prod.yml에 DB 등의 배포 환경 설정 필요
ENTRYPOINT ["java", "-Dspring.profiles.active=${PROFILE}", "-jar","/app.jar"]

그리고 이를 실행하여 빌드한다. 

docker build <Dockerfile 경로> -t <생성할 이미지 이름>

여기서 제대로 실행이 안된다면 .jar파일이 정상적으로 생성이 되지않았거나 두개 이상일 가능성이 높다.

 

  • 도커허브에서 레포지토리 생성

https://hub.docker.com/

여기 들어가서 create repository로 레포지토리를 생성한다.

 

  • 이제 빌드된 도커 컨테이너를 도커허브에 푸쉬해아하는데 필자는 M1 Pro 환경이므로 도커 빌드 명령어가 상이할 수 있다.
//m1 pro 기준 도커 빌드
docker build --platform amd64 --build-arg DEPENDENCY=build/dependency -t <도커허브아이디>/<repository이름> .
//도커 푸시 명령어
docker push <도커허브아이디>/<repository이름>
  • EC2에 접속해서 빌드된 컨테이너를 pull 받아주자. (EC2에 도커 설치 필수)
// 기존에 돌아가고 있는지 확인 (있다면 stop으로 멈추고 다시 실행해야 pull받은 내용으로 작동됨)
docker ps 

// docker pull
docker pull <도커아이디>/<레포지토리이름>

//컨테이너 실행
docker run -d -p 80:8080 <도커아이디>/<레포지토리이름>
//80포트에 접근하면 8080포트로 연결될 수 있게끔 설정했다. (연결이 안된다면 EC2 보안그룹 설정을 확인하자.)

마무리

실제 만들고 나서 좀 나중에 작성하는 글이라 디펜던시나 빠진 부분이 있을 수도 있다.

https://github.com/soleu/spring_boilerplate

 

GitHub - soleu/spring_boilerplate

Contribute to soleu/spring_boilerplate development by creating an account on GitHub.

github.com

깃헙 확인해주시고, 질문 편하게 해주세요 :) 

'Backend > Spring' 카테고리의 다른 글

Spring JPA - N+1 문제 정리  (0) 2022.04.26