마이크로서비스를 구축함에 있어 개발자의 우선 고려 사항을 살펴본다.
- 서비스 엔드포인트를 노출하도록 엔드포인트 매핑용 스프링 부트 컨트롤러 클래스 구현
- 타 언어 메시지를 적용할 수 있는 국제화 구현
- 사용자가 서버와 상호 적용할 수 있도록 충분한 정보를 제공하는 스프링 HATEOAS 구현
마이크로서비스 출입구 만들기:스프링 부트 컨트롤러
왜 마이크로서비스에 JSON을 사용하는가?
여러 프로토콜을 사용하여 HTTP 기반 마이크로서비스 간 데이터를 통신할 수 있지만 JSON은 다음 몇 가지의 이유로 사실상 표준이 되었다.
- JSON은 가볍고 텍스토로 데이터 표현이 가능하다. 또한, 쉽게 읽고 사용할 수 있다.
- JSON은 자바스크립트에 사용되는 기본 직렬화 프로토콜이다. 프로그래밍 언어로 자바스크립트가 급격하게 성장하고 자바스크립트에 크게 의존하는 SPIA(단일 페이지 인터넷 애플리케이션)도 함께 비약적으로 증가했기 때문에 JSON은 REST 기반 애플리케이션을 구축하는 데 적합했다. JSON은 프로트엔드 웹 클라이언트가 서비스를 호출하는 데 사용했다.
- 서비스 간 통신 하는데 JSON보다 더 효율적인 다른 매커니즘과 프로토콜이 있다. 아파치 쓰리프트(Thrift) 프레임워크를 이용하면 바이너리 프로토콜로 상호 통신할 수 있는 다언어 서비스를 구축할 수 있다. 아파치 아프로(Avro)는 클라이언트와 서버 호출 간 데이터를 바이너리 포맷으로 상호 변환할 수 있는 데이터 직렬화 프로토콜이다. 전송할 데이터 크기를 최소화할 필요가 있다면 이런 프로토콜을 검토하기 바란다. 하지만 마이크로서비스에서는 JSON을 사용하는 것이 효과적이고 서비스 소바자 및 클라이언트 간 디버깅을 위해 또 다른 통신 계층을 삽입할 필요가 없다.
엔드포인트 이름이 중요하다
코드 작성을 진행하기 전에 프로젝트 내에 서비스가 노출하는 엔드포인트에 대한 표준을 수립했는지 확인해야 한다. 마이크로서비스 URL(Uniform Resource Locator)은 서비스 의도, 서비스가 관리하는 리소스, 서비스 안에서 관리되는 리소스 간 관계를 명확히 커뮤니케이션하는데 사용해야 한다.
- 서비스가 표현하는 리소스에 명확한 URL 이름을 사용하라 : URL을 정의하는 데 표준 형식을 사용하면 API의 직관성과 사용 편의성이 향상된다. 따라서 일관된 규칙을 사용하라.
- URL을 사용해서 리소스간 관계를 설정하라 : 대개 마이크로서비스 내 리소스 사이에는 부모-자식 관계가 생긴다. 이 관계에서 자식 리소스는 부모 리소스의 컨텍스트 밖에는 존재하지 않는다. 따라서 리소스를 위한 별도의 마이크로서비스는 없을 것이다. 이러한 관계를 표현하는 데 URL을 사용하라.
- URL 버전 체계를 일찍 세워라 : URL과 엔드포인트는 서비스 소유자와 서비스 소비자 간 계약을 의미한다. 일반적 패턴 중 하나는 모든 엔드포인트 앞에 버전 번호를 붙이는 것이다. 조기에 버전 체계를 수립하고 준수하자. 소비자가 URL을 이미 사용한 후에 URL 버전 체계 ( 예를 들어 URL 매핑에서 /v1/ 을 사용)를 개량하는 것은 매우 어려운 일이다.
/*
스프링 부트에 이 서비스는 REST 기반 서비스이며, 응답은 JSON 으로
서비스 요청 및 자동으로 직렬화 및 역직렬화할 것이라고 지정한다.
*/
@RestController
/*
이 클래스의 모든 엔드포인트가 /v1/organization/{organizationId}/license 에서 시작하도록 노출한다.
*/
@RequiredArgsConstructor
@RequestMapping(value="v1/organization/{organizationId}/license")
public class LicenseController {
private final LicenseService licenseService;
@RequestMapping(value="/{licenseId}",method = RequestMethod.GET)
public ResponseEntity<License> getLicense(@PathVariable("organizationId") String organizationId,
@PathVariable("licenseId") String licenseId) {
License license = licenseService.getLicense(licenseId, organizationId);
return ResponseEntity.ok(license);
}
@PutMapping
public ResponseEntity<String> updateLicense(@PathVariable("organizationId") String organizationId, @RequestBody License request) {
return ResponseEntity.ok(licenseService.updateLicense(request, organizationId));
}
@PostMapping
public ResponseEntity<String> createLicense(@PathVariable("organizationId") String organizationId, @RequestBody License request,
@RequestHeader(value = "Accept-Language",required = false) Locale locale) {
return ResponseEntity.ok(licenseService.createLicense(request, organizationId, locale));
}
@DeleteMapping(value="/{licenseId}")
public ResponseEntity<String> deleteLicense(@PathVariable("organizationId") String organizationId, @PathVariable("licenseId") String licenseId) {
return ResponseEntity.ok(licenseService.deleteLicense(licenseId, organizationId));
}
}
@Service
@RequiredArgsConstructor
public class LicenseService {
private final MessageSource messages;
public License getLicense(String licenseId, String organizationId){
License license = new License();
license.setId(new Random().nextInt(1000));
license.setLicenseId(licenseId);
license.setOrganizationId(organizationId);
license.setDescription("Software product");
license.setProductName("Ostock");
license.setLicenseType("full");
return license;
}
public String createLicense(License license, String organizationId, Locale locale){
String responseMessage = null;
if(!StringUtils.isEmpty(license)) {
license.setOrganizationId(organizationId);
responseMessage = String.format(messages.getMessage("license.create.message",null,locale), license.toString());
}
return responseMessage;
}
public String updateLicense(License license, String organizationId){
String responseMessage = null;
if(!StringUtils.isEmpty(license)) {
license.setOrganizationId(organizationId);
responseMessage = String.format(messages.getMessage("license.update.message", null, null), license.toString());
}
return responseMessage;
}
public String deleteLicense(String licenseId, String organizationId){
String responseMessage = null;
responseMessage = String.format(messages.getMessage("license.delete.message", null, null),licenseId, organizationId);
return responseMessage;
}
}
GET, PUT, DELETE, POST 동사에 대한 메서드를 구현했다면 이제 국제화를 진행할 수 있다.
국제화 추가하기
국제화는 애플리케이션을 다른 언어에 적응할 수 있도록 하는 필수 요구 사항이다. 여기에서 주요 목표는 콘텐츠를 제공하는 애플리케이션을 개발하는 것이다.
@Configuration
public class ResourceBundleMessageSource {
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver localeResolver = new SessionLocaleResolver();
localeResolver.setDefaultLocale(Locale.US);
return localeResolver;
}
@Bean
public org.springframework.context.support.ResourceBundleMessageSource messageSource() {
org.springframework.context.support.ResourceBundleMessageSource messageSource = new org.springframework.context.support.ResourceBundleMessageSource();
messageSource.setUseCodeAsDefaultMessage(true);
messageSource.setBasenames("messages");
return messageSource;
}
}
-- Controller 코드 내 생성
@RequestHeader(value = "Accept-Language",required = false) Locale locale)
-- SERVICE 코드 내 생성
public String createLicense(License license, String organizationId, Locale locale){
String responseMessage = null;
if(!StringUtils.isEmpty(license)) {
license.setOrganizationId(organizationId);
responseMessage = String.format(messages.getMessage("license.create.message",null,locale), license.toString());
}
return responseMessage;
}
위 코드에서 강조해야 할 3가지 사항이 있다. 첫 번째는 컨트롤러 자체에서 로케일을 전달 받을 수 있다는 것이다. 두 번째는 전달받은 매개변수를 사용해서 messages.getMessage("license.create.message", null, locale)을 호출할 수 있다는 것이고, 세번째는 messages.getMessage("license.create", null, null) 처럼 로케일 없이도 호출할 수 있다는 것이다. 이 시나리오에서 애플리케이션은 부트스트랩에 기정의된 디폴트 로케일을 사용한다. 위 코드에서 요청이 포함된 Accept-Language 헤더에서 언어를 수신받기 위해 컨트롤러의 createLicense() 메서드가 포함되어 있다. 위 코드에서 요청 헤더 값을 매개변수에 매핑하는 @RequestHeader 애너테이션을 사용하는 것이다. createLicense() 메서드는 요청 헤더인 Accept-Language에서 로케일을 가져온다. 이 서비스 매개변수는 필수가 아니므로 지정하지 않으면 디폴트 로케일을 사용한다.
관련 링크를 표시하는 스프링 HATEOAS 구현
스프링 HATEOAS는 해당 원칙(리소스와 관련된 링크를 표시하는)을 준수하는 API를 생성하는 작은 프로젝트이다. 이 원칙에 따르면 각 서비스 응답과 함께 가능한 다음 단계 정보도 제공하며, 클라이언트 다음 단계로 가이드할 수 있어야 한다. 이 프로젝트는 핵심 또는 필수 기능은 아니지만 주어진 리소스의 모든 API에 대한 완전한 가이드를 원한다면 훌륭한 방안이다.
"_links": {
"self" : {
"href" : "http://localhost:8080/v1/organization/1/license/1"
}
"createLicense" : {
"href" : "http://localhost:8080/v1/organization/1/license"
}
"updateLicense" : {
"href" : "http://localhost:8080/v1/organization/1/license"
}
"deleteLicense" : {
"href" : "http://localhost:8080/v1/organization/1/license/1"
}
}
-- dependency 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
의존성을 추가했다면 RepresentationModel<License>를 확장하기 위해 License 클래스를 업데이트해야 한다.
@Getter @Setter @ToString
public class License extends RepresentationModel<License> {
private int id;
private String licenseId;
private String description;
private String organizationId;
private String productName;
private String licenseType;
}
RepresentationModel 은 License 모델 클래스에 링크를 추가할 수 있게 한다. 이제 모든 설정이 완료되었으니 LicenseController 클래스에 대한 링크를 조회하기 위해 HATEOAS 구성 정보를 생성해 보자.
@RequestMapping(value="/{licenseId}",method = RequestMethod.GET)
public ResponseEntity<License> getLicense(@PathVariable("organizationId") String organizationId,
@PathVariable("licenseId") String licenseId) {
License license = licenseService.getLicense(licenseId, organizationId);
license.add(
linkTo(methodOn(LicenseController.class).getLicense(organizationId, license.getLicenseId())).withSelfRel(),
linkTo(methodOn(LicenseController.class).createLicense(organizationId, license, null)).withRel("createLicense"),
linkTo(methodOn(LicenseController.class).updateLicense(organizationId, license)).withRel("updateLicense"),
linkTo(methodOn(LicenseController.class).deleteLicense(organizationId, license.getLicenseId())).withRel("deleteLicense")
);
return ResponseEntity.ok(license);
}
add() 는 RepresentationModel 클래스 메서드이며, linkTo() 메서드는 LicenseController 클래스를 검사해서 루트 매핑을 더고, methodOn() 메서드는 대상 메서드에 더미 호출을 수행하여 메서드 매핑을 가져온다. 두 메서드 모두 org.springframework.hateoas.server.mvc.WebMvcLinkBuilder의 정적 메서드이다. 이제 실행되고 기본 골격을 갖춘 서비스가 마련되었다. 하지만 개발 관점에서 보면 아직 와전한 서비스는 아니다. 좋은 마이크로서비스 설계는 서비스를 명확히 정의된 비즈니스 로직과 데이터 액세스 계층으로 나누는 것을 망설이지 않는다. 이후 내용에서 이 서비스를 계속 반복하여 다루면서 어떻게 구조를 잡아 나갈지 자세히 살펴 볼 것이다.
'마이크로서비스' 카테고리의 다른 글
마이크로서비스 데브옵스 (0) | 2024.11.11 |
---|---|
마이크로서비시 아키텍처 설계 (0) | 2024.10.14 |
클라우드 네이티브 마이크로서비스 구축을 위한 12 팩터 앱 (0) | 2024.10.10 |
스프링 클라우드란? (1) | 2024.10.07 |
마이크로서비스 개발 패턴 (3) | 2024.09.10 |