Share
Sign In
기술이야기
RestTemplate Request/Response 저장하기
전지훈
👍
Created by
  • 전지훈
Created at
들어가며
개인적인 경험으로, 회사에서 진행하는 프로젝트 중에서 오직 단독 시스템으로 동작하는 서비스는 없었던 것 같다. 대부분은 외부 시스템과 연동하게 되며, MSA를 주력으로 사용하는 부서라면 더더욱 많은 연동을 하게 될 것이다. 연동해야 하는 시스템이 많다는 것은 그만큼 관리 포인트가 늘어나는 것이고, 그에 따라 맞닥뜨려야 할 오류 또한 증가하게 된다. 일부 요청 추적(Request Tracing)을 지원하는 DataDog, PinPoint, Jaeger 등의 서비스를 사용하여 요청/응답을 간략하게 확인할 수 있는 툴을 사용한다면 좋겠지만, 그렇지 않고 독자적으로 시스템에서 외부 시스템을 호출할 때의 로그를 남기고 싶어 하는 부서도 있다. 이 글에서는 그중에서도 RestTemplate을 사용하여 외부 시스템과 연동할 때, 요청/응답에 대해 로그를 DB로 남기는 것을 설명하려 한다.
예제 프로젝트 세팅
이 글에서는 기본적으로 Kafka의 값에는 Json 형식으로 메시지를 저장하는 부분에서 나아가 Json 부분을 암호화하고, 복호화하는 것을 추가하여 설명하려고 한다. 우선 Json 값을 저장하는 예제 프로젝트로 아래와 같이 Producer와 Consumer 세팅하였다.
build.gradle
plugins { id 'java' id 'org.springframework.boot' version '3.3.1' id 'io.spring.dependency-management' version '1.1.5' } group = 'co.kr.ignite' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { useJUnitPlatform() }
DelegateController
@RequestMapping("/") @RestController public class DelegateController { private final RestTemplate restTemplate; public DelegateController(RestTemplateBuilder builder) { this.restTemplate = builder.rootUri("https://httpbin.org").build(); } @GetMapping public HttpBinResponse delegatedGet() { return restTemplate.getForObject("/get", HttpBinResponse.class); } public record HttpBinResponse(Map<String, String> args, Map<String, String> headers, String origin, String url) { } }
http://localhost:8080을 호출할 때 RestTemplate을 사용하여, http://httpbin.org/get을 호출하고, 이에 대한 응답을 HttpBinResponse로 정의하여 반환한다. 로컬에서 애플리케이션을 실행시켜 아래와 같이 동작하는 것을 확인할 수 있다.
RestTemplate 요청/응답 로그를 DB에 저장하기
1. 사용자 정의 ClientHttpRequestInterceptor 작성하기
Spring에서는 ClientHttpRequestInterceptor를 통해, RestTemplate이 요청/응답을 보내기 전/후의 동작을 정의할 수 있다. 인터페이스는 다음과 같다.
@FunctionalInterface public interface ClientHttpRequestInterceptor { ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException; }
HttpRequest: 요청에 대한 객체이며, HttpHeaders getHeaders(), URI getURI(), HttpMethod getMethod()를 가지는 인터페이스다.
body: payload에 해당하는 byte의 배열이다.
execution: execution.execute(request, body)을 수행해야 실제로 요청을 수행하고, interceptor 메서드에서 반환해야 할 응답 결과(ClientHttpResponse)를 얻을 수 있다.
우선 아래와 같이 LoggingClientHttpRequestInterceptor를 작성하자.
@Slf4j public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { return null; } }
2. 요청 로그 남기기
요청 로그를 남기기 위해서 아래와 같이 LoggingClientHttpRequestInterceptor를 변경하자.
@Slf4j public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { log.info("request:\n\tURI: {}\n\tMETHOD: {}\n\tHEADERS: {}", request.getURI(), request.getMethod(), request.getHeaders()); return execution.execute(request, body); } }
HttpRequest.toString()은 오버라이딩 되어있지 않아, 그대로 로그를 남긴다면 인스턴스의 해시 코드 값을 반환하게 된다. 각각 URI, Method, Headers의 getter를 호출하여 원하는 값을 얻을 수 있다.
request: URI: http://httpbin.org/get METHOD: GET HEADERS: [Accept:"application/json, application/*+json", Content-Length:"0"]
3. 응답 로그 남기기
응답 로그를 남기기 위해서 다시 아래와 같이 LoggingClientHttpRequestInterceptor를 변경하자.
@Slf4j public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { log.info("request:\n\tURI: {}\n\tMETHOD: {}\n\tHEADERS: {}", request.getURI(), request.getMethod(), request.getHeaders()); ClientHttpResponse response = execution.execute(request, body); log.info("response:\n\tSTATUS_CODE: {}\n\tBODY: {}", response.getStatusCode(), response.getBody().readAllBytes()); return response; } }
이번에 위 인터셉터를 적용한 RestTemplate을 호출하면 아래와 같이 정상적인 로그를 확인할 수 있다.
request: URI: http://httpbin.org/get METHOD: GET HEADERS: [Accept:"application/json, application/*+json", Content-Length:"0"] response: STATUS_CODE: 200 OK BODY: [123, 10, 32, 32, 34, ...]
다만 브라우저에서는 비어있는 화면을 확인할 수 있을 것이다.
위와 같이, 로그는 남겨지나 실제 응답은 비어있는 이유는 Java Servlet 기반의 Filter에서도 같은 동작을 보이는데, ClientHttpResponse.getBody()가 InputStream을 반환하여 1회성으로만 동작하기 때문이다. 따라서 우리는 ClientHttpResponse.getBody()를 어딘가 캐시할 필요가 있다.
4. BodyCachingClientHttpResponse 구현하기
패턴과도 관련이 있는 이야기인데, 인터페이스를 사용하는 객체는 래핑(Wrapping)해서 대부분은 특정 구현체로 동작시키고 일부만 조합(Composition)을 이용해서 기존 동작을 유지하도록 할 수 있다.(위임자 패턴) 우리는 응답을 나타내는 getBody()을 재정의하여 실제 응답을 캐시하고, 항상 새로운 ByteArrayInputStream을 반환하도록 재정의할 수 있다.
💬
Java가 능숙한 사람들은 이미 왜 항상 새로운 ByteArrayInputStream을 사용하는지 의아해 할 수도 있다. ByteArrayInputStream은 재사용 가능하기 때문이다. 다만 여기서는 간단하게 구현하는 예제를 보여주기 위해 reset()을 사용하여 재사용하기보다는 새로운 객체를 만들어 반환한다. 당연 성능상으로는 재사용하여 GC나 메모리 부담을 조금이나마 줄일 수 있으나, 코드 상으로는 새로 생성하는 것이 가독성을 높다.
public class BodyCachingClientHttpResponse implements ClientHttpResponse { @Delegate private final ClientHttpResponse delegation; private byte[] cachedBody; public BodyCachingClientHttpResponse(ClientHttpResponse delegation) { this.delegation = delegation; } @NonNull @Override public InputStream getBody() throws IOException { if (cachedBody == null) { InputStream body = delegation.getBody(); cachedBody = body.readAllBytes(); } return new ByteArrayInputStream(cachedBody); } }
Lombok을 사용하여 기존의 ClientHttpResponse가 구현해야 하는 메서드는 모두 생성자로 주입받은 delegation으로 위임하고, getBody() 부분만 우리가 원하는 동작을 하도록 최초 호출 시에 응답을 캐시하고, 이후에는 new ByteArrayInputStream(cachedBody)를 반환하도록 재정의하였다.
이를 적용하여 LoggingClientHttpRequestInterceptor를 아래와 같이 바꾼다.
@Slf4j public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { log.info("request:\n\tURI: {}\n\tMETHOD: {}\n\tHEADERS: {}", request.getURI(), request.getMethod(), request.getHeaders()); ClientHttpResponse response = new BodyCachingClientHttpResponse(execution.execute(request, body)); log.info("response:\n\tSTATUS_CODE: {}\n\tBODY: {}", response.getStatusCode(), response.getBody().readAllBytes()); return response; } }
이후에는 이전과 같이 정상적으로 응답이 반환되는 것을 확인할 수 있다.
5. Entity로 변환하기
요청/응답에 대해 DB에 저장하기 위해 엔티티를 작성하자. 엔티티는 요청/응답/오류의 3가지 개념을 가지는 것으로 아래와 같이 작성한다. 실제 코드를 예제로 담기에는 Lombok과 JPA의 애너테이션 사용이 많아 가독성이 떨어지기 때문에 애너테이션을 선언한 부분은 제거하였다.
public class RestTemplateLog { private Long id; private RequestLog requestLog; private ResponseLog responseLog; private ExceptionLog exceptionLog; private LocalDateTime createdAt; public static RestTemplateLog error(HttpRequest request, byte[] body, Exception e) { return RestTemplateLog.builder() .requestLog(RequestLog.create(request, body)) .exceptionLog(ExceptionLog.create(e)) .build(); } public static RestTemplateLog create(HttpRequest request, byte[] body, ClientHttpResponse response) { return RestTemplateLog.builder() .requestLog(RequestLog.create(request, body)) .responseLog(ResponseLog.create(response)) .build(); } public static class RequestLog { String method; String uri; String headers; String body; private static RequestLog create(HttpRequest request, byte[] body) { return new RequestLog(request.getMethod().name(), request.getURI().toString(), request.getHeaders().toString(), Optional.ofNullable(body).map(String::new).orElse(null)); } } public static class ResponseLog { Integer statusCode; String headers; String body; private static ResponseLog create(ClientHttpResponse response) { if (response == null) { return null; } return new ResponseLog(response.getStatusCode().value(), response.getHeaders().toString(), Optional.ofNullable(response.getBody().readAllBytes()).map(String::new).orElse(null)); } } public static class ExceptionLog { String name; String message; public static ExceptionLog create(Exception e) { return new ExceptionLog(e.getClass().getSimpleName(), e.getMessage()); } } }
6. Exception에 대해서
위의 엔티티에서 ExceptionLog를 정의하였다. 가끔 외부 연동을 할 때, 분명 json으로 응답을 하는 것임에도 불구하고 4xx, 5xx의 응답, 혹은 배포로 인해 nginx 등에서 가용 서비스가 없거나 할 때, html로 응답이 오는 경우가 있다.(필자만 겪은 것이 아니리라) 그 외에도 Timeout 등에 의해서 execution.execute가 실패할 때도 있다. 때문에 예외를 저장해야 하며, 이러한 경우를 위해 아래와 같이 로그를 남기도록 LoggingClientHttpRequestInterceptor를 작성해야 한다.
@RequiredArgsConstructor public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { private final RestTemplateLogRepository restTemplateLogRepository; @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { try { ClientHttpResponse response = new BodyCachingClientHttpResponse(execution.execute(request, body)); restTemplateLogRepository.save(RestTemplateLog.create(request, body, response)); return response; } catch (Exception e) { restTemplateLogRepository.save(RestTemplateLog.error(request, body, e)); throw e; } } }
이제 각각 Timeout이 발생하는 경우, 역직렬화가 실패하는 경우, 정상 호출이 된 경우에 대해 아래와 같이 DB에 남게 된다.
URI
METHOD
REQUEST_HEADERS
STATUS_CODE
RESPONSE_HEADERS
RESPONSE_BODY
EXCEPTION_NAME
EXCEPTION_MESSAGE
http://httpbin.org/delay/2
GET
[Accept:"application/json"]
SocketTimeoutException
Read timed out
http://httpbin.org/get2
GET
[Accept:"application/json"]
404
[Content-Type:"text/html"]
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> <title>404 Not Found</title> <h1>Not Found</h1> <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
http://httpbin.org/get
GET
[Accept:"application/json"]
200
[Content-Type:"application/json"]
{ "args": {}, "headers": { "Accept": "application/json, application/*+json", "Host": "httpbin.org", "User-Agent": "Java/21.0.2", "X-Amzn-Trace-Id": "Root=1-66795cd0-7b00005c54b125fa1063d2d1" }, "origin": "58.87.60.234", "url": "http://httpbin.org/get" }
Spring Boot에서 RestTemplate에 Interceptor를 넣는 2가지 방법
Spring Document에 의하면, Spring Boot에서 RestTemplate을 커스터마이징하는 방법은 총 3가지이다. 다만 필자는 주로 아래의 2가지 방법을 애용한다. 커스터마이징하는 방법은 주로 전역으로 설정할 것인가, 아니면 주입받는 곳에서 설정할 것인가의 범위를 기준으로 나뉜다.
1. RestTemplateCustomizer를 사용하여 전역 설정하기
애플리케이션에서 사용할 RestTemplateBuilder를 스프링 빈으로 등록하여, 커스터마이징한 설정으로 애플리케이션 전역에서 사용할 수 있다.
@RequiredArgsConstructor @Configuration public class IgniteRestTemplateConfiguration { @Bean public RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer configurer, RestTemplateLogRepository restTemplateLogRepository) { return configurer.configure(new RestTemplateBuilder()) .additionalInterceptors(new LoggingClientHttpRequestInterceptor(restTemplateLogRepository)); } }
첨언. RestTemplateBuilderConfigurer?
위의 내용이 어떻게 동작하는지 알기 위해서는, spring-boot-starter-web의 RestTemplateAutoConfiguration을 살펴볼 필요가 있다. 아래와 같이 2개의 빈을 등록해 주고 있다.
public class RestTemplateAutoConfiguration { @Bean @Lazy public RestTemplateBuilderConfigurer restTemplateBuilderConfigurer(ObjectProvider<HttpMessageConverters> messageConverters, ObjectProvider<RestTemplateCustomizer> restTemplateCustomizers, ObjectProvider<RestTemplateRequestCustomizer<?>> restTemplateRequestCustomizers) { RestTemplateBuilderConfigurer configurer = new RestTemplateBuilderConfigurer(); configurer.setHttpMessageConverters((HttpMessageConverters)messageConverters.getIfUnique()); configurer.setRestTemplateCustomizers(restTemplateCustomizers.orderedStream().toList()); configurer.setRestTemplateRequestCustomizers(restTemplateRequestCustomizers.orderedStream().toList()); return configurer; } @Bean @Lazy @ConditionalOnMissingBean public RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer restTemplateBuilderConfigurer) { RestTemplateBuilder builder = new RestTemplateBuilder(new RestTemplateCustomizer[0]); return restTemplateBuilderConfigurer.configure(builder); } }
RestTemplateBuilderConfigurer는 Spring에 의해 자동 설정되는데, 생성할 때 Spring으로부터 messageConverters, restTemplateCustomizers와 restTemplateRequestCustomizer를 주입받는다. 이는 즉 개발자가 아래 코드로도 전역 설정을 할 수 있음을 암시한다.
@RequiredArgsConstructor @Configuration public class IgniteRestTemplateConfiguration { @Bean public RestTemplateCustomizer restTemplateCustomizer(RestTemplateLogRepository restTemplateLogRepository) { return restTemplate -> { List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors(); interceptors.addLast(new LoggingClientHttpRequestInterceptor(restTemplateLogRepository)); restTemplate.setInterceptors(interceptors); }; } }
2. RestTemplateBuilder를 주입받아 설정하기
spring-boot-starter-web을 사용하면 RestTemplateBuilder를 빈에 주입받을 수 있으며, 이를 통해 개별 빈마다 다른 RestTemplate 설정을 추가할 수 있다.
@Component public class HttpBinClient { private final RestTemplate restTemplate; public HttpBinClient(RestTemplateBuilder builder, RestTemplateLogRepository restTemplateLogRepository) { this.restTemplate = builder.rootUri("http://httpbin.org") .additionalInterceptors(new LoggingClientHttpRequestInterceptor(restTemplateLogRepository)) .build(); } }
마무리
실질적으로는 RestTemplate을 사용한 요청/응답 로그를 DB로 쌓는 것에 대해 설명을 했지만, 속마음으로는 저렇게 기능이 동작하는 것 외의 RestTemplate을 어떻게 커스터마이징할 수 있는지, RestTemplate에 대한 Spring Boot Auto-Configuration이 어떻게 동작하는지에 대해 더 관심을 가져 주었으면 한다. 최근에는 FeignClient를 사용하는 프로젝트 또한 많이 경험하는데, Spring 이미 통합되어 있는 만큼 비슷한 사용성을 경험할 수 있으리라 생각된다. Spring Triangle의 한 꼭짓점이 PSA인 만큼, 다른 외부 호출 서비스가 통합되어도 이러한 사용성이 유지되며, 이를 응용할 수 있을 것이라 생각된다.
Subscribe to '이그나이트'
Welcome to '이그나이트'!
By subscribing to my site, you'll be the first to receive notifications and emails about the latest updates, including new posts.
Join SlashPage and subscribe to '이그나이트'!
Subscribe
👍
김재우
머신러닝(Machine Learning) 입문: 타이타닉 생존자 예측 문제로 배우는 ML 기초
머신러닝 소개 머신러닝, ML 엔지니어가 하는 일 머신러닝(Machine Learning)은 인공지능(AI)의 한 분야로, 컴퓨터가 데이터로부터 스스로 학습하여 패턴을 인식하고 예측을 수행할 수 있게 해주는 기술입니다. 전통적인 프로그래밍 방식은 규칙을 직접 코딩하지만, 머신러닝은 데이터로부터 규칙을 스스로 학습합니다. 이를 통해 복잡한 패턴 인식, 예측, 의사결정 등이 가능해집니다. 메일에서 스팸 메일을 찾아내는 간단한 일에서부터, 의료 분야에서는 질병 예측과 진단, 금융 분야에서는 사기 탐지, 그리고 자율주행 자동차 같은 것들이 머신러닝을 통해 이루어집니다. 머신러닝은 크게 정답이 포함된 데이타로 학습하는 지도학습, 데이타의 패턴을 스스로 찾아내는 비지도학습, 환경과 상호작용하며 보상을 최대화 하는 강화학습. 이렇게 3가지 유형으로 나눌 수 있습니다. 머신러닝 모델은 이런 학습과정을 거쳐 만들어진 규칙입니다. 입력 데이터를 받아 출력을 생성하는 함수와 같은 것입니다. 예를 들어, 스펨 메일 필터링 모델은 입력 메일의 특징을 분석하여 스펨 메일인지 아닌지를 예측하는 알고리즘입니다 머신러닝 모델을 개발하고 배포하는 ML 엔지니어는 데이터를 수집하고 분석하며, 머신러닝 모델을 만들고, 모델을 배포하여 실제 업무에 적용합니다. 데이타의 양이 커서, 데이타에 노이즈나 결측치가 있어서 어려움을 격기도 하고, A하드웨어에서 만든 모델이 B하드웨어에서 제대로 작동안하는 문제를 해결하기도 합니다. 이 글에서는 ML 엔지니어의 기본 업무인 데이터 분석과 모델 구축 과정을 타이타닉 생존자 예측 문제를 예로 들어 설명하겠습니다. 이를 통해 ML 엔지니어의 실제 업무와 그 과정에서 겪는 다양한 도전 과제들을 이해할 수 있을 것입니다. 타이타닉 문제로 시작하기 타이타닉 문제는 1912년 타이타닉 호 침몰 사고 당시의 실제 데이터를 기반으로 승객의 나이, 성별, 탑승 티켓 등급 등의 데이타로 해당 승객이 생존했는지 여부를 예측하는 것입니다. 데이터 규모가 크지 않아(12개의 컬럼, 891건의 데이터) 초보자도 다루기 쉬우며, 이진 분류(생존/비생존) 문제라 평가가 용이합니다. 또한 숫자, 범주형 등 다양한 유형의 데이터와 결측치(누락된 데이타)를 포함하고 있어, 데이터 전처리 과정을 연습할 수 있습니다. 이 문제는 기계학습 파이프라인의 전체 과정을 경험할 수 있는 대표적인 예제입니다. 일단 손으로 풀어보고, 그 다음 실제 머신러닝 모델을 만들어보도록 하겠습니다. 손(Excel)으로 하는 머신러닝 타이타닉 문제의 데이터 파일은 kaggle 에서 다운로드 받으실 수 있습니다. 회원 가입 후 파일을 다운로드 하시면 되고, train.csv 모델 훈련용으로 사용하는 데이터 파일입니다. test.csv 모델의 검증용으로 사용하는 데이터 파일입니다. gender_submission.csv kaggle 에 업로드 해 만들어진 모델을 평가하기 위한 파일입니다. 모델 구축 후 모델의 결과를 이 파일에 저장하고 업로드하면 만들어진 모델의 성능을 확인할 수 있습니다. 데이터 살펴보기 파일을 엑셀에서 열어보면 위와 같은 데이터가 있고, 각 컬럼의 의미는 아래와 같습니다. PassengerId: 승객 ID Pclass: 티켓 클래스 Name: 이름
  • 김재우
전지훈
Spring Kafka 메시지 암호화
들어가며 개발 업무를 진행하면 종종 개인정보에 대해 다룰 때가 생긴다. 보통 이러한 민감정보는 DB, Redis, Kafka 혹은 로깅에서도 암호화하여 저장하는 것이 권장된다. 이 글에서는 스프링으로 Kafka를 사용할 때, 본문 내용에 대해 암호화하여 저장하는 방법을 다루고자 한다. Json 역/직렬화 예제 이 글에서는 기본적으로 Kafka의 값에는 Json 형식으로 메시지를 저장하는 부분에서 나아가 Json 부분을 암호화하고, 복호화하는 것을 추가하여 설명하려고 한다. 우선 Json 값을 저장하는 예제 프로젝트로 아래와 같이 Producer와 Consumer 세팅하였다. producer와 consumer를 분리하였는데 이는 Spring Kafka에서 제공하는 JsonSerializer/Deserializer의 지원에 대해 설명하기 위해 분리하였다. producer의 코드들 ProducerChatMessage Kafka에 값으로 저장할 내용을 아래와 같이 정의했다. ProducingController Http 요청을 받아 ProducerChatMessage를 Kafka로 전송하는 부분으로 아래와 같이 정의한다. application.yaml Kafka에 대한 설정과 위에서 정의한 ProducerChatMessage를 아래와 같이 Json으로 직렬화하면서 별칭(alias)을 줄 수 있다. spring.kafka.producer.value-serializer와 spring.kafka.consumer.value-deserializer의 값으로 JsonSerializer, JsonDeserializer를 사용할 수 있다. 위 두 개의 클래스를 지정할 때 spring.kafka.[producer|consumer].properties.spring.json.type.mapping으로 위 예제와 같이 콜론(:) 구분으로 별칭을 줄 수 있다. 이러한 별칭은 추후 클래스를 다른 패키지로 변경하거나, 다른 클래스를 사용하게 될 때에도 유연하게 대응할 수 있도록 도와준다. 참고: Spring Boot / Reference / Messaging / Apache Kafka Support 별칭은 Kafka 메시지의 헤더로 저장되며, JsonSerializer가 헤더를 생성, JsonDeserializer가 헤더를 제거하는 식으로 동작한다. ex) key: __TypeId__, value: message 위와 같은 방법으로 아래 예제와 같이, 값에 인터페이스를 받도록 대응할 수 있다. 예제를 단순화하기 위해서 if/else를 사용했지만 실무에서는 사용하지 않기를 바란다. consumer의 코드들 ConsumerChatMessage producer 프로젝트의 ProducerChatMessage에 대응하는 모델을 ConsumerChatMessage로 작성한다. IgniteKakfaConsumer 단순하게 메시지를 받아 로그를 남기는 부분을 consumer로 작성한다.
  • 전지훈
전지훈
Spring Properties를 외부 저장소와 연동하기
들어가며 SaaS(Software As A Service)가 보편화되며 아마 많은 개발자가 이를 구현하기 위해 Twelve-Factor를 읽어보았을 것으로 생각된다. 이 글은 Twelve-Factor에서 설정에 대해 어떻게 Spring Boot를 사용하는 개발자들이 외부의 설정 저장소에 접근하여 설정을 읽을 수 있을지 구현에 관해 이야기하고자 한다. 이미 대부분의 스타트업들은 클라우드 환경에서 시작하고 있으며, 국내 IT 대기업들은 자체 클라우드를 구축하고, IT 업계 외의 사업을 주로 하는 기업들도 클라우드로 옮겨가고 있다. 주된 방향성은 클라우드 환경에 있지만 아직 온프레미스를 주로 사용하는 조직도 있기에, 여기서는 온프레미스와 클라우드 환경에 따라 어떻게 설정을 달리할 수 있는지 다뤄보려고 한다. Spring Externalized Configuration 본문에서 다루고자 하는 모든 개발 방법의 기반은 Spring Documentation에서 이미 기술하고 있는 내용에 대한 응용이다. 또한 외부에서 설정을 읽는 부분에 대한 많은 지원을 이미 Spring Cloud Config에서 제공하고 있으니, 사용하려는 환경이 이미 Spring Cloud Config에서 지원하고 있다면 사용하는 것이 좋다. 이 글은 Spring Cloud Config보다는 좀 더 바퀴를 만드는 방법에 대해 다룬다. 본문이 도움될 만 한 상황 아래와 같은 상황에서 이 글이 도움이 될 것이라 생각된다. Spring Cloud Config를 조금 더 가볍게 모듈화하거나, 자신의 조직에 맞게 구성하고 싶은 경우. 전체 혹은 일부 설정에 대해 Spring Cloud Config에서 지원하지 않는 외부 설정 저장소를 사용하려는 경우. 혹은 아래와 같이 application.properties를 커스터마이징하려는 경우. 외부 환경에서 설정을 개발자가 직접 호출하도록 개발하기 Spring Cloud Config에서 지원하지 않는 환경에 설정을 저장하고, 불러와야 하는 경우가 있다. 사내에서 자체 구축한 시스템을 사용하는 경우이다. 이런 경우 EnvironmentPostProcessor를 진입점으로 간주하고 원하는 결과를 이룰 수 있다. EnvironmentPostProcessor EnvironmentPostProcessor는 ApplicationContext가 초기화되기전에 애플리케이션의 환경 설정을 변경할 수 있으며 아래 인터페이스를 가지고 있다. ConfigurationEnvironment: getPropertySources()를 통해 MutablePropertySources를 사용할 수 있으며, 여기서 property를 추가하거나, 우선순위를 조절하여 덮어쓸 수 있다. 따라서 내부 구현을 통해 외부 설정 저장소에서 설정값을 불러와 우선순위를 높여 PropertySources에 저장하면 외부 설정 저장소의 설정값을 애플리케이션에서 참조할 수 있다. CSV 예제 단순한 예제로 CSV에서 설정을 읽어와 사용하는 경우를 구현해 보자. custom.greeting이라는 설정에 Hello world!를 출력하고자 한다. application.yaml HelloWorldController.java CsvEnvironmentPostProcessor.java
  • 전지훈