발단
해커톤에 참여하게 되면서 스프링을 사용하고 싶지만 개발 시간이 느려질까 우려되었다.
미리 틀정도 만들어가면 좋을 것 같아서 시작하게 된 보일러 플레이트 만들기 및 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파일이 정상적으로 생성이 되지않았거나 두개 이상일 가능성이 높다.
- 도커허브에서 레포지토리 생성
여기 들어가서 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 |
---|