[실습] API 문서화 실습
Spring Rest Docs를 이용한 API 문서화 실습 개요
이번 실습은 Spring Rest Docs를 이용해 API 문서를 만들어 보는 실습입니다.
- 지난 챕터까지 학습했던 구현 코드들이 기본적으로 포함되어 있으며, 이를 기반으로 요구 사항에 맞게 API 문서화를 위한 테스트 케이스를 작성한 후, API 문서를 생성하면 됩니다.
- 실습용 프로젝트 패키지는 ‘com.springboot’ 패키지 하위에 아래와 같이 구성되어 있습니다.
- advice
- audit
- coffee
- config
- dto
- exception
- member
- order
- response
- stamp
- validator
- 테스트 케이스는 일반적으로 Gradle 기반 프로젝트에서 ‘src/test/java/**’ 경로에 작성합니다.
- 여러분들이 작성할 API 문서화를 위한 비어있는 테스트 케이스 역시 ‘src/test/java/com/springboot/homework’의 실습 과제용 테스트 클래스 (MemberControllerDocumentationHomeworkTest) 내부에 포함되어 있습니다.
실습 사전 준비
- 실습용 샘플 프로젝트 복제
- 아래 github 링크에서 실습용 repository를 fork합니다.
- fork한 repository를 여러분의 PC에서 git clone 명령으로 local repository에 복제합니다.
- IntelliJ IDE로 clone 받은 forked local repository 디렉토리의 프로젝트를 Open합니다.
- 아래 실습 요구 사항에 따라 실습을 진행합니다.
- 작성한 코드는 main branch에 작성해 주세요.
- main branch가 아닌 별도의 branch를 생성해서 작업을 했다면 작업이 끝난 후, 반드시 main branch로 merge 해야 합니다.
실습 과제 내용
실습 1: MemberController 클래스에 대한 API 문서 생성용 테스트 케이스 작성
- API 문서용 테스트 케이스 작성을 위한 설명
- MemberController의 각 핸들러 메서드의 API 스펙 정보에 맞게 API 문서를 잘 생성하는지 확인하세요.
- 현재 여러분들이 완성해야 할 MemberControllerDocumentationHomeworkTest테스트 클래스의 테스트 케이스가 아래 경로에 비어 있는 채로 작성되어 있습니다.
- src/test/java/com/springboot/homework/MemberControllerDocumentationTest.java
- MemberControllerDocumentationHomeworkTest테스트 클래스에서 비어 있는 아래의 테스트 케이스를 작성해 주세요.
- getMemberTest()
- MemberController 클래스의 getMember() 핸들러 메서드 API 스펙 정보를 문서화하기 위한 테스트 케이스입니다.
- getMembersTest()
- MemberController 클래스의 getMembers() 핸들러 메서드 API 스펙 정보를 문서화하기 위한 테스트 케이스입니다.
- deleteMemberTest()
- MemberController 클래스의 deleteMember() 핸들러 메서드 API 스펙 정보를 문서화하기 위한 테스트 케이스입니다.
- getMemberTest()
- 작성한 테스트 클래스의 테스트 케이스를 실행해서 API 문서 스니펫을 생성해 주세요.
- 제한 사항
- 테스트 케이스 작성에 대한 별도의 제한 사항은 없습니다.
- 테스트 입력 값에 별도의 제한 사항은 없습니다.
실습 2: 실습 1에서 생성한 문서 스니펫의 내용을 웹 브라우저로 확인 가능하도록 하기
- 실습 2 수행을 위한 설명
- 실습 1에서 생성한 문서 스니펫을 포함할 템플릿 문서(index.adoc)는 아래 경로에 이미 생성되어 있습니다.
- index.adoc 경로: ‘src/docs/asciidoc/index.adoc’
- 실습 1에서 생성한 문서 스니펫을 index.adoc 템플릿 파일에 포함시켜서 템플릿 문서를 구성하세요.
- 실습 1에서 생성한 스니펫으로 구성된 템플릿 문서를 빌드 타임에 index.html로 변환하세요.
- 변환된 index.html이 아래 경로에 포함되어야 합니다.
- index.html 경로: ‘src/main/resources/static/docs/index.html’
- 애플리케이션을 실행시킨 후, 웹 브라우저에서 아래 URL로 접속하세요.
- 웹 브라우저에서 여러분이 만든 API 문서를 확인할 수 있어야 합니다.
- 실습 1에서 생성한 문서 스니펫을 포함할 템플릿 문서(index.adoc)는 아래 경로에 이미 생성되어 있습니다.
여러분이 만든 API 문서를 웹 브라우저에서 확인할 수 있다면 여러분은 Spring Rest Docs를 이용해 API 문서를 만들 수 있는 기본 능력을 갖추게 된 것입니다.
만약 웹 브라우저에서 API 문서를 확인할 수 없더라도 실망하지 마세요.
실습 과제용 solution으로 제공한 solution 프로젝트의 코드를 잘 분석하고, 다시 한번 차근차근 따라 해 보세요.
웹 브라우저에 표시된 API 문서를 확인하고 미소 짓는 여러분들의 모습을 볼 수 있을 거라 믿습니다.
많은 시행착오를 거칠 수 있지만 시행착오를 겪으면서 더 성장한 여러분 자신의 모습을 볼 수 있길 기대합니다!
MemberControllerDocumentationTest
package com.springboot.homework;
import com.google.gson.Gson;
import com.jayway.jsonpath.JsonPath;
import com.springboot.member.controller.MemberController;
import com.springboot.member.dto.MemberDto;
import com.springboot.member.entity.Member;
import com.springboot.member.mapper.MemberMapper;
import com.springboot.member.service.MemberService;
import com.springboot.stamp.Stamp;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.*;
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext;
import org.springframework.http.MediaType;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.doNothing;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.request.RequestDocumentation.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
public class MemberControllerDocumentationTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private MemberService memberService;
@MockBean
private MemberMapper mapper;
@Autowired
private Gson gson;
@Test
public void getMemberTest() throws Exception {
// TODO 여기에 MemberController의 getMember() 핸들러 메서드 API 스펙 정보를 포함하는 테스트 케이스를 작성 하세요.
// given
long memberId = 1;
MemberDto.Response response = new MemberDto.Response(1L,
"lucky@google.com",
"김러키",
"010-1234-5678",
Member.MemberStatus.MEMBER_ACTIVE,
new Stamp());
given(memberService.findMember(Mockito.anyLong())).willReturn(new Member());
given(mapper.memberToMemberResponse(Mockito.any(Member.class))).willReturn(response);
// when
ResultActions actions = mockMvc.perform(
get("/v11/members/{memberId}", memberId)
.accept(MediaType.APPLICATION_JSON)
);
// then
actions.andExpect(status().isOk())
.andExpect(jsonPath("$.data.memberId").value(memberId))
.andExpect(jsonPath("$.data.email").value(response.getEmail()))
.andDo(
document(
"get-member",
preprocessRequest(),
preprocessResponse(),
pathParameters(
List.of(parameterWithName("memberId").description("회원 식별자 ID"))
),
responseFields(
List.of(
fieldWithPath("data").type(JsonFieldType.OBJECT).description("결과 데이터").optional(),
fieldWithPath("data.memberId").type(JsonFieldType.NUMBER).description("회원 식별자"),
fieldWithPath("data.email").type(JsonFieldType.STRING).description("이메일"),
fieldWithPath("data.name").type(JsonFieldType.STRING).description("이름"),
fieldWithPath("data.phone").type(JsonFieldType.STRING).description("휴대폰 번호"),
fieldWithPath("data.memberStatus").type(JsonFieldType.STRING).description("회원 상태: MEMBER_ACTIVE(활동중), MEMBER_SLEEP(휴먼 계정), MEMBER_QUIT(회원 탈퇴)"),
fieldWithPath("data.stamp").type(JsonFieldType.NUMBER).description("스탬프 갯수")
)
)
));
}
@Test
public void getMembersTest() throws Exception {
// TODO 여기에 MemberController의 getMembers() 핸들러 메서드 API 스펙 정보를 포함하는 테스트 케이스를 작성 하세요.
// given
String page = "1";
String size = "10";
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("page", page);
queryParams.add("size", size);
Member member1 = new Member("lucky@google.com", "김러키", "010-1234-1234");
member1.setStamp(new Stamp());
Member member2 = new Member("lucky@google.com", "김러키", "010-1234-4342");
member2.setStamp(new Stamp());
Page<Member> members = new PageImpl<>(List.of(member1, member2),
PageRequest.of(0, 10, Sort.by("memberId").descending()), 2);
MemberDto.Response responseMember1 = new MemberDto.Response(1L,
"lucky@google.com",
"김러키", "010-1234-1234",
Member.MemberStatus.MEMBER_ACTIVE,
new Stamp());
MemberDto.Response responseMember2 = new MemberDto.Response(2L,
"latte@google.com",
"김라떼", "010-1234-4342",
Member.MemberStatus.MEMBER_ACTIVE,
new Stamp());
List<MemberDto.Response> responses = new ArrayList<>(List.of(responseMember1, responseMember2));
given(memberService.findMembers(Mockito.anyInt(), Mockito.anyInt())).willReturn(members);
given(mapper.membersToMemberResponses(Mockito.anyList())).willReturn(responses);
// when
ResultActions actions = mockMvc.perform(
get("/v11/members")
.params(queryParams)
.accept(MediaType.APPLICATION_JSON)
);
// then
MvcResult result = actions.andExpect(status().isOk())
.andDo(
document(
"get-members",
preprocessRequest(),
preprocessResponse(),
requestParameters(
List.of(
parameterWithName("page").description("Page 번호"),
parameterWithName("size").description("Page Size 번호")
)
),
responseFields(
List.of(
fieldWithPath("data").type(JsonFieldType.ARRAY).description("결과 데이터").optional(),
fieldWithPath("data[].memberId").type(JsonFieldType.NUMBER).description("회원 식별자"),
fieldWithPath("data[].email").type(JsonFieldType.STRING).description("이메일"),
fieldWithPath("data[].name").type(JsonFieldType.STRING).description("이름"),
fieldWithPath("data[].phone").type(JsonFieldType.STRING).description("휴대폰 번호"),
fieldWithPath("data[].memberStatus").type(JsonFieldType.STRING).description("회원 상태: MEMBER_ACTIVE(활동중), MEMBER_SLEEP(휴먼 계정), MEMBER_QUIT(회원 탈퇴)"),
fieldWithPath("data[].stamp").type(JsonFieldType.NUMBER).description("스탬프 갯수"),
fieldWithPath("pageInfo").type(JsonFieldType.OBJECT).description("페이지 정보"),
fieldWithPath("pageInfo.page").type(JsonFieldType.NUMBER).description("페이지 번호"),
fieldWithPath("pageInfo.size").type(JsonFieldType.NUMBER).description("페이지 사이즈"),
fieldWithPath("pageInfo.totalElements").type(JsonFieldType.NUMBER).description("전체 요소 수"),
fieldWithPath("pageInfo.totalPages").type(JsonFieldType.NUMBER).description("전체 페이지 수")
)
)
)
).andReturn();
List list = JsonPath.parse(result.getResponse().getContentAsString()).read("$.data");
assertThat(list.size(), is(2));
}
@Test
public void deleteMemberTest() throws Exception {
// TODO 여기에 MemberController의 deleteMember() 핸들러 메서드 API 스펙 정보를 포함하는 테스트 케이스를 작성 하세요.
// given
long memberId = 1L;
doNothing().when(memberService).deleteMember(Mockito.anyLong());
// when
ResultActions actions = mockMvc.perform(
delete("/v11/members/{member-id}", memberId)
);
// then
actions.andExpect(status().isNoContent())
.andDo(document(
"delete-member",
preprocessRequest(),
preprocessResponse(),
pathParameters(
List.of(
parameterWithName("member-id").description("회원 식별자 ID")
)
)
));
}
}
[Spring MVC] 애플리케이션 빌드 / 실행 / 배포
API 계층부터 데이터 액세스 계층, 그리고 테스트를 넘어 API 문서화라는 여정까지 쉼 없이 달려온 여러분들께 진심 어린 박수를 보내고 싶습니다.
이제 이번 섹션의 마지막 관문입니다.
여러분이 지금까지 만든 애플리케이션은 IntelliJ IDE 안에서만 실행시켜 보았을 텐데 실제 외부의 클라이언트가 여러분이 만든 애플리케이션의 API를 사용할 수 있으려면 애플리케이션의 빌드 과정을 거친 후에 생성되는 실행 파일을 서버에 배포해야 합니다.
이번 시간에는 여러분이 만든 커피 주문 샘플 애플리케이션을 최종 빌드 후, 실행 가능한 파일을 생성하는 방법을 알아보도록 하겠습니다.
그리고 생성된 실행 파일을 여러분의 로컬 PC에서 실행해 보도록 하겠습니다.
[Spring MVC] 애플리케이션 빌드/실행/배포 학습을 위한 사전 준비 사항
이번 유닛의 학습을 원활하게 진행하기 위해 지금까지 여러분들이 구현해 본 Controller 및 Service , Repository 등의 클래스들이 포함된 템플릿 프로젝트를 사용하도록 하겠습니다.
데이터 액세스 계층은 Spring Data JPA의 리포지토리로 구성이 되어 있습니다.
- 템플릿 프로젝트 복제
- 아래 github 링크에서 실습용 repository를 clone합니다.
- IntelliJ IDE로 clone 받은 local repository 디렉토리의 프로젝트를 Open합니다.
- 학습을 진행하며 학습 내용에 따라 예제 코드를 타이핑해봅니다.
[Spring MVC] 애플리케이션 빌드/실행/배포 학습 참고용 레퍼런스 코드
이번 유닛에서 학습한 예제 코드는 아래 github에서 확인할 수 있습니다.
챕터에서 사용한 예제 코드는 챕터에 있는 코드들을 직접 타이핑해본 후, 학습 내용을 조금 더 구체적으로 이해하기 위한 용도로만 활용해 주세요.
- 애플리케이션 빌드/실행/배포 유닛에 사용한 예제 코드
학습 목표
- Gradle 기반의 애플리케이션 실행 파일을 빌드할 수 있다.
- 빌드로 생성된 애플리케이션 실행 파일을 로컬 PC에서 실행할 수 있다.
- 애플리케이션 빌드 시, 프로파일(Profile)을 적용할 수 있다.
- Spring Boot 기반 애플리케이션의 기본적인 배포 방법을 이해할 수 있다.
[기본] 애플리케이션 빌드/실행/배포
이번 시간에는 여러분들이 지금껏 학습을 진행하며 만들어 본 커피 주문 샘플 애플리케이션을 빌드해서 실행 가능한 애플리케이션 실행 파일(Executable Jar)을 만든 후, IntelliJ IDE 환경이 아닌 실제 서버에서 애플리케이션을 실행시키듯이 여러분의 PC에서 애플리케이션을 실행시켜 보도록 하겠습니다.
애플리케이션 빌드
여러분들은 지금까지 IntelliJ IDE의 도움을 받아 몇 번의 마우스 클릭만으로 여러분들이 만든 커피 주문 샘플 애플리케이션을 실행시켰습니다.
IntelliJ IDE 같은 통합 개발 환경 도구의 힘을 빌리지 않고, Spring Boot 기반 애플리케이션을 실행하는 몇 가지 방법들이 존재하지만 그 방법들을 지금 당장 모두 다 알 필요는 없습니다.
어디까지나 개발 진행 중에 그때그때 상황에 맞는 편한 방법을 사용하면 되는 것입니다.
그런데 여러분들이 만든 애플리케이션을 로컬 환경이 아닌 서버 환경에서 실행 가능하게 하려면 Gradle이나 Maven 같은 빌드 툴을 이용해서 Spring Boot 기반의 애플리케이션 소스 코드를 빌드하는 기본적인 방법은 알고 있어야 합니다.
우리는 Gradle 기반 프로젝트에서 샘플 애플리케이션을 만들고 있으므로 Gradle을 이용해 애플리케이션 소스 코드를 빌드하는 방법을 알아보도록 하겠습니다.
IntelliJ IDE 이외에 Spring Boot 기반의 애플리케이션을 실행하는 방법에 대해서 더 알아보고 싶다면 아래 \[심화 학습]을 참고하세요.
✔ IntelliJ IDE를 이용한 빌드
IntelliJ IDE를 이용해 애플리케이션을 빌드하는 방법은 어렵지 않습니다.
[그림 3-94] IntelliJ IDE를 이용한 빌드
[그림 3-94]는 IntelliJ IDE에서 사용할 수 있는 애플리케이션 빌드를 위한 태스크(task)입니다.
Spring Boot은 Gradle 빌드 툴을 이용해 애플리케이션을 빌드할 수 있는 플러그인을 지원하기 때문에 Gradle task 명령을 통해 애플리케이션을 손쉽게 빌드할 수 있습니다.
빌드 순서는 다음과 같습니다.
- 우측 상단의 [Gradle] 윈도우 탭을 클릭합니다.
- 프로젝트 이름 > Tasks > build에서 :bootJar 또는 :build task를 더블 클릭합니다.
[그림 3-95] IntelliJ IDE를 이용한 빌드 후, 생성된 실행 파일
빌드가 정상적으로 종료되면 [그림 3-95]와 같이 build/libs 디렉토리에 Jar 파일 하나가 생성됩니다.
생성된 Jar 파일은 여러분의 로컬 PC에서 실행 가능한 애플리케이션 실행 파일인데, 애플리케이션 실행에 대해서는 뒤에서 살펴보도록 하겠습니다.
:build와 :bootJar 중 어떤 task를 실행해서 애플리케이션 실행 파일을 만들어야 될까?
:build 태스크를 실행하면 :assemble, :check 같이 Gradle에서 빌드와 관련된 모든 task들을 실행시킵니다. 그리고 실행 가능한 Jar 파일 이외에 plain Jar 파일 하나를 더 생성합니다.
반면에 :bootJar는 빌드와 관련된 모든 task들을 실행하는 것이 아니라 애플리케이션의 실행 가능한 Jar(Executable Jar) 파일을 생성하기 위한 task만 실행합니다.
단순히 Executable Jar 파일만 필요하다면 :bootJar task를 실행하면 됩니다.
✔ Gradle Task를 이용한 빌드
IntelliJ IDE를 이용하면 클릭 몇 번 만으로 손쉽게 빌드할 수 있습니다.
하지만 때로는 IntelliJ IDE가 설치되어 있지 않은 상황에서 빌드를 해야 될 경우도 생길 수 있을 것입니다.
이럴 땐 Gradle task 명령어를 콘솔에서 바로 입력하여 빌드를 진행할 수 있습니다.
Gradle task 명령어를 직접 입력하는 방법은 다음과 같습니다.(Windows 기준)
Mac의 경우, Mac에서 지원하는 터미널을 오픈한 후, 아래의 작업을 모두 진행할 수 있습니다.
- 먼저 여러분의 템플릿 프로젝트(또는 여러분이 직접 생성한 프로젝트)가 위치해 있는 디렉토리 경로로 이동합니다.
[그림 3-96] Spring Boot 프로젝트 root 경로
2. Gradle task를 CLI 명령으로 입력할 수 있는 콘솔창을 템플릿 프로젝트 root 경로에서 오픈합니다.
콘솔은 Windows의 cmd나 Git Bash, Windows Power Shell이나 터미널 등 모두 가능합니다.
- Git Bash가 설치되어 있지 않고, 사용해 보고 싶은 분은 아래 링크를 클릭하세요. https://git-scm.com/downloads
- Windows 터미널이 설치되어 있지 않고, 사용해 보고 싶은 분은 아래 링크를 클릭하세요. https://docs.microsoft.com/ko-kr/windows/terminal/install
3. 아래의 명령을 입력해서 애플리케이션 빌드를 진행합니다.
- Windows 터미널의 경우,
PS D:\\\\springboot\\\\project\\\\springboot-build> .\\\\gradlew bootJar
- Git Bash의 경우
MINGW64 /d/springboot/project/kdt/for-ese/springboot-build (main)
$ ./gradlew build
빌드가 정상적으로 종료되면 IntelliJ에서 빌드를 진행할 때와 마찬가지로 build/libs 디렉토리에 Jar 파일 하나가 생성됩니다.
애플리케이션 실행
빌드가 성공적으로 완료되었다면 이제 생성된 Jar(Executable Jar) 파일을 이용해서 애플리케이션을 실행할 수 있습니다.
애플리케이션 실행 순서는 다음과 같습니다.
- 빌드를 통해 생성된 Jar 파일이 있는 디렉토리 경로로 이동합니다.
- 터미널 창을 오픈한 후에 아래의 [그림 3-97]과 같이 입력합니다.
[그림 3-97] 빌드된 애플리케이션 실행 예
애플리케이션을 실행하는 방법은 놀랍도록 간단합니다.
java -jar Jar 파일명.jar 만 입력하면 우리가 만든 애플리케이션을 서버 환경에서 실행시킬 수 있습니다.
✅ 프로파일(Profile) 적용
그런데 우리가 지금까지 만든 애플리케이션에는 한 가지 문제점이 있는데 그것은 바로 데이터베이스입니다.
우리는 현재 로컬 환경에서 인메모리 DB인 H2를 사용하고 있습니다.
우리가 만약 빌드된 애플리케이션 실행 파일을 서버 환경에 배포해서 운영한다면 인메모리 DB를 사용하면 안 됩니다.
어떤 이유로 인해 서버가 다운된다거나 그로 인해 애플리케이션이 한 번이라도 재시작해야 되는 경우가 생긴다면 인메모리 DB에 저장된 데이터는 모두 초기화될 테니까요.
그렇다면 어떻게 해야 될까요?
로컬 환경에서 개발을 진행할 때는 기존 application.yml 파일에 이미 설정되어 있는 H2를 사용하고, 서버용 jar 파일을 빌드할 경우에는 빌드 전에 기존에 application.yml 파일에 설정되어 있는 H2 정보 대신에 서버에서 사용하는 DB 정보로 수정한 뒤에 빌드하면 될 것입니다.
그런데 매번 그렇게 하는 건 너무 불편하고 비효율적이며 개발자스럽지 못하다는 생각이 듭니다.
이런 불편함을 줄이기 위해서 Spring에서는 프로파일(Profile)이라는 아주 편리한 기능을 제공합니다.
지금부터 우리가 만든 애플리케이션에 프로파일(Profile)을 적용해서 애플리케이션이 빌드될 때, 로컬 환경에서는 로컬 환경의 DB 설정 정보를 실행 파일에 포함하고, 서버 환경일 경우에는 서버 환경의 DB 설정 정보를 실행 파일에 포함시켜 보겠습니다.
[그림 3-98] 프로파일 설정을 위한 yml 파일 생성
먼저 \[그림 3-98]과 같이 기존의 application.yml 파일 외에 application-local.yml 파일과 application-server.yml 파일을 추가합니다.
그러고 나서 세 개의 yml 파일의 내용을 각각 아래와 같이 구성합니다.
application.yml
# 일반적으로 애플리케이션 실행 환경에 상관없는 공통 정보들은 application.yml에 설정할 수 있습니다.
# 현재는 비어있는 상태입니다.
application.yml 파일은 주로 애플리케이션의 실행 환경에 상관없이 공통적으로 적용할 수 있는 프로퍼티를 설정할 수 있습니다.
현재 우리가 만들고 있는 샘플 애플리케이션에는 특별히 필요한 공통 정보가 없으므로 비워둡니다.
application-local.yml
# 로컬 환경에서 사용하는 정보들은 application-local.yml 파일에 설정합니다.
spring:
h2:
console:
enabled: true
path: /h2
datasource:
url: jdbc:h2:mem:test
jpa:
hibernate:
ddl-auto: create # (1) 스키마 자동 생성
show-sql: true # (2) SQL 쿼리 출력
properties:
hibernate:
format_sql: true # (3) SQL pretty print
sql:
init:
data-locations: classpath*:db/h2/data.sql
logging:
level:
org:
springframework:
orm:
jpa: DEBUG
server:
servlet:
encoding:
force-response: true
기존에 application.yml 파일에 설정해 둔 프로퍼티들을 모두 application-local.yml 파일로 옮겼습니다.
우리가 지금껏 사용해 왔던 프로퍼티 정보들은 모두 로컬 환경에서 사용되던 정보들이니까요.
application-server.yml
# 서버 환경에서 사용하는 정보들은 application-server.yml 파일에 설정합니다.
# 현재는 비어있는 상태입니다.
서버 환경에서 필요한 정보들은 application-server.yml 파일에 설정할 수 있습니다.
우리가 만약 샘플 애플리케이션을 실제 서버나 AWS 같은 클라우드에서 실행시켜야 한다면 application-server.yml 파일에 설정하면 됩니다.
대표적인 서버 환경의 설정 정보는 DB 접속 정보입니다.
애플리케이션 빌드 시, 프로파일 기능을 이용해서 서버 환경의 DB 접속 정보를 포함시키는 작업은 실습 과제에서 여러분이 직접 해 보도록 하겠습니다.
자, 그럼 이제 이 상태에서 우리가 항상 애플리케이션을 실행하던 것처럼 IntelliJ에서 애플리케이션을 실행해 보겠습니다.
애플리케이션이 실행이 되었다면 H2 웹 콘솔(http://localhost:8080/h2)에 접속해 보세요.
아마도 접속이 안될 겁니다.
왜 안될까요?
우리가 프로파일 기능을 이용하기 위해 두 개의 yml 파일을 더 추가한 후, 기존에 application.yml 파일에 있던 H2 웹 콘솔 설정까지 application-local.yml 파일로 옮겼습니다.
그런데 우리가 애플리케이션을 실행시킬 때, 아직 프로파일을 적용하지 않았습니다.
애플리케이션을 실행시키면, 프로파일을 적용하든 그렇지 않든 application.yml 파일에 설정된 정보는 항상 읽어옵니다.
그런데 현재 application.yml 파일에 H2 관련 설정들이 존재하지 않기 때문에 H2 웹 콘솔이 정상적으로 접속되지 않는 것입니다.
H2 웹 콘솔에 정상적으로 접속하기 위해서는 로컬 환경의 프로파일을 적용하면 됩니다.
✔ IntelliJ IDE에서 프로파일 적용
[그림 3-99] IntelliJ 프로파일 적용 예 1
[그림 3-99]와 같이 [Edit Run/Debug Configuration] 다이얼로그를 오픈하기 위해 애플리케이션 실행 파일이 위치한 셀렉트 박스를 클릭한 후, [Edit Configurations]를 클릭합니다.
[그림 3-100] IntelliJ 프로파일 적용 예 2
[Program arguments] 필드에 [그림 3-100]과 같이 --spring.profiles.active=local을 입력해서 활성화할 프로파일을 ‘local’로 지정합니다. 여기서 ‘local’은 application-local.yml 파일명에서의 ‘local’을 가리킵니다.
이제 애플리케이션을 다시 실행하면 H2 웹 콘솔에 정상적으로 접속이 되는 걸 확인할 수 있습니다.
Spring에서 프로파일을 지정하는 가장 손쉬운 방법은 앞에서 보았다시피 application-local.yml처럼 ‘-(대시)’를 기준으로 프로파일명을 yml 파일 이름 안에 포함하는 것이란 걸 기억하세요.
프로파일 명은 여러분 마음대로 정해도 상관없습니다.
만약 프로파일이 정상적으로 적용되었는지 확인하고 싶다면 코드 3-218과 같이 애플리케이션이 실행될 때 출력되는 로그를 확인하면 됩니다.
. ____ _ __ _ _
/\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\
( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\
\\\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.1)
[main] c.c.Section3Week4BuildApplication : Starting Section3Week4BuildApplication using Java 11.0.1 on hjs6877 with PID 2756
[main] c.c.Section3Week4BuildApplication : The following 1 profile is active: local
...
...
[코드 3-218] 프로파일이 적용된 애플리케이션 로그 출력 예
애플리케이션 실행 파일에 프로파일을 적용하는 방법은 간단합니다.
[그림 3-97]에서 실행 파일을 실행시키는 java -jar springboot-build-0.0.1-SNAPSHOT.jar에 IntelliJ IDE에서 Program arguments에 추가했던 것과 동일하게 --spring.profiles.active=local 설정을 추가해 주면 됩니다.
Spring에서 지원하는 프로파일 기능은 빌드 후 생성되는 애플리케이션 실행 파일에 대한 실행 환경을 간단한 명령어 한 줄 만으로 설정할 수 있는 편리한 기능이면서 굉장히 중요한 기능이므로 사용법을 반드시 기억하기 바랍니다.
애플리케이션 배포
이제 마지막 단계입니다.
우리가 이번 섹션에서 학습한 모든 내용들은 결국 외부 클라이언트에게 REST API 서비스를 제공하기 위해서 최종적으로 애플리케이션을 서버에 배포하기 위함입니다.
하지만 이번 섹션에서 우리가 만들어 본 샘플 애플리케이션을 서버에 배포하지는 않습니다.
구체적인 서버 배포 방법은 다음 섹션에서 여러분들이 직접 실습을 통해 익힐 예정이니 조금만 기다려 주세요.
Spring Boot 기반의 실행 가능한 Jar 파일(Executable Jar)을 서버에 배포하는 방법에는 어떤 것들이 있는지 대략적인 내용만 간단히 살펴보고 이번 섹션을 마무리하도록 하겠습니다.
✅ 전통적인 배포 방법
Spring Boot 기반의 Executable Jar 파일을 서버에 배포하는 가장 일반적인 방법은 scp나 sftp 같은 표준 유닉스 툴을 이용해서 서버로 간단히 전송하는 것입니다.
서버로 전송된 Jar 파일은 JVM이 설치된 환경이라면 어디서든 손쉽게 실행할 수 있습니다.
✅ 클라우드 서비스를 위한 배포 방법
Executable Jar 파일은 특히 클라우드 환경에 손쉽게 배포할 수 있습니다.
- PaaS(Platform as a Service)
- Cloud Foundry, Heroku
대표적인 PaaS 제공 기업인 Cloud Foundry에서 제공하는 cf command line 툴을 사용하면 Executable Jar 파일을 손쉽게 배포할 수 있습니다.
cf command line 툴 사용 예.
$ cf push acloudyspringtime -p target/app-0.0.1-SNAPSHOT.jar
- IaaS(Infrastructure as a Service)
- Executable Jar는 AWS Elastic Beanstalk, AWS Container Registry, AWS Code Deploy 같은 서비스를 이용해서 손쉽게 배포가 가능합니다.
- Microsoft의 클라우드 서비스인 Azure 역시 Azure Spring Cloud, Azure App Service에서 Spring Boot 기반의 Executable Jar 파일 배포 기능을 제공합니다.
- Google Cloud 역시 Executable Jar 파일 배포를 위한 여러 가지 옵션을 제공하고 있습니다.
- CI / CD 플랫폼을 사용한 배포
- 여러분들이 실무에서 Executable Jar 파일에 대한 배포 자동화를 이루고 싶다면 Github Actions나 Circle CI 같은 CI / CD 플랫폼을 이용해 AWS나 Azure 같은 클라우드 서비스에 Executable Jar 파일을 자동 배포하도록 구성할 수 있습니다.
CI / CD 플랫폼을 이용한 배포 자동화는 배포 시간 단축을 통해 여러분의 퇴근 시간을 앞당겨 줄 수 있는 중요한 기술이라고 생각하며, 실제 실무에서도 굉장히 많이 쓰이는 기술이므로 이어지는 섹션에서 꼭 잘 배워두길 바라겠습니다.
핵심 포인트
- Spring Boot 기반 애플리케이션을 가장 손쉽게 빌드할 수 있는 방법은 IntelliJ 같은 IDE의 기능을 활용하는 것이다.
- IDE 등의 기능을 활용하기 어려운 상황에서는 Gradle 명령어를 사용해 손쉽게 빌드할 수 있다.
- Spring에서는 프로파일 기능을 이용해서 빌드 후 생성되는 애플리케이션 실행 파일에 대한 실행 환경을 손쉽게 지정할 수 있다.
- Spring Boot 기반의 Executable Jar 파일은 전통적인 서버, 클라우드 환경, 가상화 환경에 손쉽게 배포할 수 있다.
심화 학습
- IntelliJ IDE 이외의 방법으로 Spring Boot 기반의 애플리케이션을 실행하는 방법을 더 알아보고 싶다면 아래 링크를 참고하세요.
- Spring의 Profile 기능에 대해서 더 알아보고 싶다면 아래 링크를 참고하세요.
- scp에 대해서 더 알아보고 싶다면 아래 링크를 참고하세요.