Now Loading ...
-
-
패킷 단편화와 Multi Send 상황을 고려한 Netty Decoder 적용 (1)
패킷 단편화와 Multi Send 상황을 고려한 Netty Decoder 적용 (1)
Netty 는 비동기 이벤트 기반 프레임워크다. 이런 Netty 를 활용한 TCP 서버 개발 당시, 클라이언트의 다중 send() 요청을 하나의 Transaction 으로 간주하여 처리해야 했다.
또한, 다중 send()가 아니더라도 TCP 통신 특성 상, 여러 개로 단편화된 패킷이 수신될 경우를 대비하여 Netty Decoder 를 적용한 사례를 정리한 글이다.
클라이언트는 서버에서 메시지의 총 길이와 헤더 정보가 담긴 구조체를 send 하고, 바로 이어서 메시지의 Body 내용이 담긴 구조체를 send 한다.
클라이언트가 보내는 두 번의 send 과정 중, 데이터의 크기나 프로토콜 특성으로 인해 패킷이 분할되어 수신될 수 있다.
위의 2가지 상황을 고려하여 아래와 같이 보완 처리했다.
메시지의 총 길이와 헤더 정보 구조체에서 메시지의 총 길이를 추출하여 그 길이만큼 패킷을 계속 누적하여 수집
패킷이 유실될 가능성을 염두하여 특정 Timeout 만큼 대기하고 이후에는 패킷을 Drop 처리
1. 메시지의 총 길이만큼 패킷 누적
public class MultiSendDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
// 최소 4바이트 수신 여부 확인
if (4 > in.readableBytes()) {
return;
}
// 프로토콜 코드 peek
int code = in.getIntLE(in.readerIndex());
switch (code) {
case MultiSendConstants.OLD_PROTOCOL -> { // 구 프로토콜은 패킷 사이즈가 고정 (100 바이트 고정)
// 전체 데이터 수신시까지 누적
if ( 100 == in.readableBytes() ) {
out.add(in.readBytes(100));
} else {
log.debug("[RECEIVED SIZE]{}", in.readableBytes());
}
}
case MultiSendConstants.NEW_PROTOCOL_CODE -> out.add(in.readBytes(4)); // Multi Send 첫 번째 (프로토콜 코드값만 read)
case MultiSendConstants.NEW_PROTOCOL_DATA -> { // Multi Send 두 번째
// 패킷 구조 (message code:4 + message length:4 + message:N)
// message length 까지 수신된 경우
if ( 8 <= in.readableBytes() ) {
// message length peek (message code 4바이트만큼 건너뛴 offset 부터 읽기)
int messageSize = in.getIntLE(4 + in.readerIndex());
// message length + message 까지 전부 수신된 경우
if ((4 + messageSize) == in.readableBytes()) {
out.add(in.readBytes(4 + messageSize)); // message length:4 + message:N 만큼 데이터를 Netty Handler 에 전달
} else {
log.debug("[RECEIVED SIZE]{}, [MESSAGE SIZE]{}", in.readableBytes(), messageSize);
}
}
}
default -> {
log.warn("Unsupported protocol version. [CODE]{}", code);
// 미지원 프로토콜 요청 패킷 SKIP 처리
in.skipBytes(in.readableBytes());
ctx.close().addListener(ChannelFutureListener.CLOSE);
}
}
}
}
-
이력서
김진용
📧 이메일: yong9976@naver.com
🎨 GitHub: github.com/readra
프로필
Java & Spring 기반의 5년차 백엔드 개발자로 국내 5,000개 이상의 고객사 보유한 솔루션 회사에서 백엔드 플랫폼 개발 및 클라우드 기반 ABAC 모델 개발 등을 해왔습니다.
여러 동료들의 다양한 코드 리뷰에 참석하거나, 본인의 코드 리뷰에 참여를 권하여 코드 리뷰를 통해 지식을 공유하고, 배우는 것을 영양분으로 삼아서 확장성과 사용성을 신경 쓰는 개발자로 거듭나기 위해 노력하고 있습니다.
테스트를 중요하게 생각하여 테스트 코드 작성이나 제한적인 환경에서는 자체 테스트 드라이버&스텁을 만들어서 테스트 환경을 구축 및 수행합니다.
현재는 팀 내 백엔드 파트 리더로 백엔드와 클라우드 기반의 주요 프로젝트 수행, 파트원들과의 업무 분장, 파트원 매니징 등을 주요 업무로 하고 있습니다.
기술 스택
현재 업무에 사용 중 혹은 사용했던 기술들입니다.
Backend
Java
Spring Boot, Spring Batch, Spring Security, Spring MVC, Spring Data JPA
MyBatis, JPA, Hibernate
Junit5, Mockito
Maven, Gradle
IntelliJ
DevOps
MySQL, MariaDB, Oracle, Sybase
AWS EC2, CodeDeploy, RDS, DynamoDB
Tomcat
Linux Ubuntu, Linux CentOS
경력
피앤피시큐어 – 연매출 580억, 순이익 270억의 중소 보안 솔루션 기업
백엔드 개발자 | 2021년 5월 ~ 현재
ABAC(Attribute-based access control) 모델 개발
DBSAFER 백엔드 개발
Cloud(AWS/GCP/Azure/NCP) 자원 자동 탐지 및 자사 솔루션과 연계 기능 개발
휴머스온 – 통합메시징 솔루션 기업
웹 어플리케이션 개발자 | 2020년 1월 ~ 2021년 5월
Mail/SMS/Push/알림톡/Fax 발송 엔진 개발
B2B 프로젝트 참여 및 개발 수행
증권사 체결 전용 발송 엔진 개발 (대규모 트래픽, 데이터 환경 경험)
프로젝트
속성 기반 접근 제어(ABAC) 모델 개발
피앤피시큐어 (2024년 10월 ~ )
On-Premise 와 Cloud 시스템의 다양한 요소를 반영할 수 있는 표현력과 유연성이 높은 제어 모델 개발
리소스 속성(On-premise/Cloud) 수집 모듈 개발
온프레미스 시스템의 인사/서버/계정 등의 속성 정보를 수집하는 표준 모듈 개발
클라우드 시스템을 구성하는 속성 정보(VPC, AMI, SG, Tag 등)를 수집하는 표준 모듈 개발
기술 스택: Java, Spring Boot, WebClient, gRPC, AWS/GCP/Azure SDK
속성 자원 관리 서버 개발
수집한 속성 정보를 중앙에서 관리하는 API 서버 고도화 작업
복잡한 속성 조건 계산을 위한 속성 정보 구조화 및 캐싱 처리
관리자 페이지에서 ABAC 수립에 필요한 API 개발
레거시 접근 제어(RuBAC, RBAC) 모델 호환 지원 작업
기술 스택: Java, Spring Boot, Ehcache, gRPC, MySQL, MariaDB, MyBatis, HikariCP, JWT
접근제어 백엔드 고도화 개발
피앤피시큐어 (2024년 7월 ~ 2024년 12월)
기존에 제품 버전마다 형상을 분리해서 관리하고 있었기에, 개발 생산성과 일부 중복 개발로 인한 리소스 낭비를 줄이기 위한 여러 개의 접근제어 제품 버전을 통합으로 지원할 수 있는 아키텍쳐로 전환
기존의 특정 고객사 전용 요구사항 개발 기능을 별도의 형상으로 관리하고 있었기에, 히스토리 관리의 어려움과 개발 생산성이 낮았기에, 이러한 문제를 해결하고자 master 형상에서 특정 고객사 전용 요구사항 개발이 가능한 구조로 고도화했으며, 빌드 옵션 처리를 통해 master 형상 또는 특정 고객사 전용 형상으로 테스트 → 빌드 → Docs 생성까지 자동화 처리
대외 시스템 대상으로 API 오픈을 위해 강력한 인증 보안이 요구되었고, 이를 충족하기 위해 Refresh Token Rotation 과 Refresh Token 탈취 및 재사용 감지 기능 개발
기술 스택: Java, Spring Boot, Spring Security, MySQL, MariaDB, MyBatis, HikariCP, JWT
고객사 프로젝트 수행 (카카오페이/한국산업은행/카카오 등)
피앤피시큐어 (2021년 5월 ~ )
주요 성과
서버 자동 접속 관리 백엔드 개발 (기술 스택: Netty, Spring Boot)
각종 고객사 시스템(인사/결재/클라우드) 연동 (기술 스택: Spring Boot, AWS SDK)
고객사 프로젝트 수행 (라인뱅크/삼성증권 등)
휴머스온 (2020년 1월 ~ 2021년 5월)
주요 성과
입/출금, 체결 전용 메시지 우선 발송 엔진 개발 (기술 스택: Spring Batch)
솔루션 RDBMS 컨버팅 (Sybase → Oracle)
기간계 시스템 연동
자격증
정보처리기사
2019년 11월
기타 활동
블로그: 개발 관련 블로그 운영 (소스 보관함)
브런치스토리: 일상 이야기와 생각 정리 (토마)
교육
한세대학교 – 경기
정보통신공학과 학사 | 2014년 3월 – 2020년 2월 (졸업)
한영고등학교 – 서울
인문계 | 2010년 3월 – 2013년 2월 (졸업)
-
Spring Security 다중 인증 방식 주의사항 (2)
Spring Security 다중 인증 방식 주의사항 (2)
유효하지 않은 인증 정보로 JWT 발급이 가능한 현상
원인#1. UsernamePasswordAuthenticationToken 을 사용한 2가지의 인증 방식이 존재 (API KEY, JWT 방식)
해결 방안#1. 각 인증 방식에서 사용하는 Token 클래스를 분리 (JWT = UsernamePasswordAuthenticationToken, API KEY = AbstractAuthenticationToken 상속 사용자 정의 클래스)
원인#2. Provider 루프에 의해 JWT, API KEY Provider 의 authenticate 함수가 호출되는데, JWT Provider 처리 시, Password 불일치하더라도 다음 API KEY Provider 처리 시, ID는 일치하여 인증이 성공하는 현상
해결 방안#2. 각 인증 방식 Provider 에서 취급하는 Token 클래스가 아닌 경우, authenticate 함수 return 처리하여 인증 로직을 수행하지 않고 Pass 처리
잘못된 예외 처리로 인해 다중 인증 방식이 정상적으로 동작하지 않은 현상
원인#1. ProviderManager 에서 throw 처리하는 Exception 을 발생시키고 있어, 다음 Provider 로 넘어가지 않고 즉시 인증이 실패하는 현상
원인#2. 커스텀 인증 방식 중에 실제로 존재하지 않는 ID를 기반으로 한 익명 계정으로 인증할 수 있도록 기능 제공한 사례가 있다. 이 때, 존재하지 않는 익명의 계정 정보로 인증 요청이 온 경우, InternalAuthenticationServiceException 이 발생하여 익명 계정을 기반으로 인증을 수행하는 Provider 까지 넘어가지 않고 인증이 실패하는 현상
해결 방안#1. NPE 처럼 크리티컬한 오류 상태가 아닌 경우에는 InternalAuthenticationServiceException 혹은 AccountStatusException 사용을 지양
-
Spring Security 다중 인증 방식 주의사항 (1)
Spring Security 다중 인증 방식 주의사항 (1)
Spring Security 기반 API 인증 방식을 2가지 이상 지원 시, 발생했던 이슈를 정리한 글이다.
JWT 기반 인증 방식 (내/외부 사용자)
API KEY 기반 인증 방식 (내부 사용자 전용)
유효하지 않은 인증 정보로 JWT 발급이 가능한 이슈가 발생했다.
이슈 해결하는 과정에서 잘못된 예외 처리로 인해 다중 인증 방식이 정상적으로 동작하지 않는 현상도 발생했다.
ProviderManager Class 해체 분석
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
/* 중략 */
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
Iterator var9 = this.getProviders().iterator();
while(var9.hasNext()) { // 1번
AuthenticationProvider provider = (AuthenticationProvider)var9.next();
if (provider.supports(toTest)) {
if (logger.isTraceEnabled()) {
Log var10000 = logger;
String var10002 = provider.getClass().getSimpleName();
++currentPosition;
var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));
}
try { // 2번
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException var14) { // 3번
this.prepareException(var14, authentication);
throw var14;
} catch (AuthenticationException var15) { // 4번
lastException = var15;
}
}
}
if (result == null && this.parent != null) {
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
} catch (ProviderNotFoundException var12) {
} catch (AuthenticationException var13) {
parentException = var13;
lastException = var13;
}
}
if (result != null) { // 5번
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
} else {
if (lastException == null) { // 6번
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
this.prepareException((AuthenticationException)lastException, authentication);
}
throw lastException;
}
}
/* 중략 */
}
등록된 Providers 하나씩 루핑하면서 while 문 내부의 동작을 수행한다.
등록된 Provider 의 authenticate 함수의 결과를 result 에 저장한다.
InternalAuthenticationServiceException 혹은 AccountStatusException 발생 시, 예외가 throw 되며 인증에 실패한다.
AuthenticationException 발생 시, 마지막 예외 상태(lastException)에 예외를 저장하고 다음 Provider 로 넘어간다.
인증 결과가 존재할 경우, if 조건문 처리 후, 결과를 return 한다.
인증 결과가 없을 경우, 마지막 Exception 을 throw 한다.
대략 이런 구조로 인증 절차가 수행된다. 다음 포스팅에서는 아래의 원인에 대해 작성하겠다.
유효하지 않은 인증 정보로 JWT 발급이 가능했던 원인
잘못된 예외 처리로 인해 다중 인증 방식이 정상적으로 동작하지 않은 원인
-
-
Spring Boot + Maven 환경에서 커스텀 빌드하기 (3)
Spring Boot + Maven 환경에서 커스텀 빌드하기 (3)
CUSTOM REST API 예제 작성
고객사 전용 커스텀 예제를 작성한다.
마찬가지로 최대한 간단하게 작성한다. (이게 중요한 게 아니니까2)
Test Code
package com.toy.springbootmavencustombuild.site.customer;
import com.toy.springbootmavencustombuild.site.customer.rest.CustomerController;
import com.toy.springbootmavencustombuild.site.customer.service.CustomerService;
import org.junit.Before;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
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.http.MediaType;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.BDDMockito.given;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* CustomerController 테스트 코드
*
* @author readra
*/
@AutoConfigureRestDocs
@WebMvcTest(CustomerController.class)
public class CustomerControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private WebApplicationContext webApplicationContext;
@MockBean
private CustomerService customerService;
@Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
.apply(documentationConfiguration(new JUnitRestDocumentation()))
.build();
}
@Test
@DisplayName("Customer Hello World")
void helloWorldTest() throws Exception {
final int code = 100;
final String result = String.format(CustomerService.HELLO_WORLD, code);
// given
given(customerService.helloWorld(anyInt())).willReturn(result);
// when
ResultActions resultActions = mockMvc.perform(
RestDocumentationRequestBuilders.get("/v1/customer/hello-world/{code}", code)
.contentType(MediaType.APPLICATION_JSON)
);
// then
resultActions.andExpect(status().isOk())
.andDo(
document(
"Customer Hello World",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
pathParameters(
parameterWithName("code").description("코드")
),
responseFields(
fieldWithPath("message").type(JsonFieldType.STRING).description("메시지"),
fieldWithPath("code").type(JsonFieldType.NUMBER).description("코드")
)
)
)
.andDo(print());
}
}
Controller
package com.toy.springbootmavencustombuild.site.customer.rest;
import com.toy.springbootmavencustombuild.site.customer.service.CustomerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 고객사 전용 Controller Layer
*
* @author readra
*/
@RestController
@RequestMapping("/v1/customer/")
public class CustomerController {
private final CustomerService customerService;
@Autowired
public CustomerController(final CustomerService customerService) {
this.customerService = customerService;
}
/**
* 안녕하세요!
*
* @param code
* 코드
* @return
* 안녕하세요!
*/
@GetMapping("/hello-world/{code}")
public String helloWorld(@PathVariable int code) {
return customerService.helloWorld(code);
}
}
Service
package com.toy.springbootmavencustombuild.site.customer.service;
import org.springframework.stereotype.Service;
/**
* 고객사 전용 Service Layer
*
* @author readra
*/
@Service
public class CustomerService {
public static final String HELLO_WORLD = "{ \"message\" : \"Hello World My Customer\", \"code\" : %d }";
/**
* 안녕하세요!
*
* @param code
* 코드
* @return
* 안녕하세요!
*/
public String helloWorld(int code) {
return String.format(HELLO_WORLD, code);
}
}
REST Docs
= Rest Docs Customer Sample API Document
readra.github.io
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
[[introduction]]
== 소개
readra Spring Rest Docs API
---
=== 고객사 전용 안녕하세요 API
고객사 전용 안녕하세요 API를 소개합니다.
operation::Customer Hello World[snippets='http-request,path-parameters,http-response,response-fields,curl-request']
---
-
Spring Boot + Maven 환경에서 커스텀 빌드하기 (2)
Spring Boot + Maven 환경에서 커스텀 빌드하기 (2)
REST API 예제 작성
예제는 최대한 간단하게 작성한다. (이게 중요한 게 아니니까)
Test Code
package com.toy.springbootmavencustombuild.rest;
import com.toy.springbootmavencustombuild.service.HelloWorldService;
import org.junit.Before;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
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.http.MediaType;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.BDDMockito.given;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* HelloWorldController 테스트 코드
*
* @author readra
*/
@AutoConfigureRestDocs
@WebMvcTest(HelloWorldController.class)
public class HelloWorldControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private WebApplicationContext webApplicationContext;
@MockBean
private HelloWorldService helloWorldService;
@Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
.apply(documentationConfiguration(new JUnitRestDocumentation()))
.build();
}
@Test
@DisplayName("Hello World")
void helloWorldTest() throws Exception {
final int code = 100;
final String result = String.format(HelloWorldService.HELLO_WORLD, code);
// given
given(helloWorldService.helloWorld(anyInt())).willReturn(result);
// when
ResultActions resultActions = mockMvc.perform(
RestDocumentationRequestBuilders.get("/v1/hello-world/{code}", code)
.contentType(MediaType.APPLICATION_JSON)
);
// then
resultActions.andExpect(status().isOk())
.andDo(
document(
"Hello World",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
pathParameters(
parameterWithName("code").description("코드")
),
responseFields(
fieldWithPath("message").type(JsonFieldType.STRING).description("메시지"),
fieldWithPath("code").type(JsonFieldType.NUMBER).description("코드")
)
)
)
.andDo(print());
}
}
Controller
package com.toy.springbootmavencustombuild.rest;
import com.toy.springbootmavencustombuild.service.HelloWorldService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 예제 Controller Layer
*
* @author readra
*/
@RestController
@RequestMapping("/v1")
public class HelloWorldController {
private final HelloWorldService helloWorldService;
@Autowired
public HelloWorldController(final HelloWorldService helloWorldService) {
this.helloWorldService = helloWorldService;
}
/**
* 안녕하세요!
*
* @param code
* 코드
* @return
* 안녕하세요!
*/
@GetMapping("/hello-world/{code}")
public String helloWorld(@PathVariable int code) {
return helloWorldService.helloWorld(code);
}
}
Service
package com.toy.springbootmavencustombuild.service;
import org.springframework.stereotype.Service;
/**
* 예제 Service Layer
*
* @author readra
*/
@Service
public class HelloWorldService {
public static final String HELLO_WORLD = "{ \"message\" : \"Hello World\", \"code\" : %d }";
/**
* 안녕하세요!
*
* @param code
* 코드
* @return
* 안녕하세요!
*/
public String helloWorld(int code) {
return String.format(HELLO_WORLD, code);
}
}
REST Docs
= Rest Docs Sample API Document
readra.github.io
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
[[introduction]]
== 소개
readra Spring Rest Docs API
---
=== 안녕하세요 API
안녕하세요 API를 소개합니다.
operation::Hello World[snippets='http-request,path-parameters,http-response,response-fields,curl-request']
---
-
Spring Boot + Maven 환경에서 커스텀 빌드하기 (1)
Spring Boot + Maven 환경에서 커스텀 빌드하기 (1)
현재 나는 솔루션 회사에 근무하고 있다. 여러 업무 중 하나로 API 서버를 개발/유지보수하고 있다.
솔루션에는 대부분 기능의 표준이 존재하지만, 고객의 강력한 요청에 의해 표준의 경계가 흐려지거나 망가지는 케이스가 많다. (경험상)
API 서버라고 예외는 없다. 고객의 강력한 커스터마이징 요청이 있었고, 이를 대응하기 위해 하나의 프로젝트에서 고객사별로 커스텀 빌드가 가능하도록 처리한 내용을 정리하려고 한다.
개인적으로 하는 개발과 달리 다소 제약적인 회사 내, 개발 환경이라는 점 참고 바랍니다.
[AS-IS]
* 공통 코드는 하나의 단독 프로젝트로 관리한다.
* 커스터마이징 코드는 공통 코드와는 분리된 별도의 프로젝트로 관리하며, 저장소 위치도 다르다.
* 커스터마이징 개발의 경우, 커스터마이징 코드에 공통 코드를 Import Module 하여 개발한다.
나는 개인적으로 이런 낯선 개발 환경/방식을 스스로 풀어나가는 걸 좋아한다.
하지만, 문제는 새로 입사하신 분들 중에는 빠르게 정답만 알고 싶어하시는 분이 있다는 점이다.
그럴 때마다 듣는 사람도 심드렁하고, 알려주는 보람도 없는 일을 더 이상 하기 싫어졌다.
커스터마이징 코드 관리 전략을 스스로 생각하기에 좋은 패턴으로 개선한 내용을 소개하려고 한다.
[TO-BE]
* 공통 코드는 하나의 단독 프로젝트로 관리한다.
* 커스터마이징 코드는 공통 코드와 동일한 프로젝트에 관리하며, 저장소 위치도 동일하다.
* 커스터마이징 개발의 경우, 추가 작업 없이 바로 개발이 가능하다.
* 단, 커스터마이징 코드는 정해진 hierarchy 규칙에 따라 관리/개발한다.
AS-IS 구조의 개발이 안좋다고 말하려는 것이 아니다.
정답은 없다고 생각하며, 또 다른 하나의 방법을 제시하는 것이다.
글로만 전달하려고 하니, 괜한 오해가 있을까봐 말이 길어진다...
커스터마이징 너무 싫다... 조금만 줄이고 싶다...
프로젝트 준비
Common API 테스트 코드 작성 (+REST Docs)
Common API 테스트 코드 기반으로 실제 코드 작성
Customizing API 테스트 코드 작성 (+REST Docs)
Customizing API 테스트 코드 기반으로 실제 코드 작성
Common + Customizing API 패키징 (실제 코드 + 테스트 코드 + REST Docs)
위의 총 5-Step 에 걸쳐 정리할 예정이다.
다음 글에서는 간단한 Common API 테스트 코드 작성하여 REST Docs API 문서를 만들고, 이를 기반으로 실제 코드를 작성하도록 하겠다.
-
Touch background to close