[Spring Cloud]마이크로서비스 간 통신 - MSA 적용기 (2)

HW·2024년 8월 29일
0

MSA

목록 보기
2/4

서론

이전 게시물에서 Eureka로 Client-Side Discovery를 구현했습니다.
현재까지 구현 내용은 다음과 같습니다.
1. user-service 마이크로서비스를 eureka로 register한 상태입니다.
2. config-service로 각 마이크로서비스의 중앙화된 설정을 구현하기 위해 뼈대를 잡아놨습니다.

다이어그램의 점선은 아직 구현되지 않은 부분을 의미하는데
이번 게시물에서는 추가로 language-service 마이크로서비스를 추가하여
language-user 간의 N:1 관계를 형성하고 각 마이크로서비스 간의 통신을 구현합니다.

본론

Entity 간의 연관관계 형성

Entity 간접 참조

먼저 Language Entity를 생성해줍니다.

public class Language {
  @Id
  @Column(name = "iso639_1", length =2, nullable = false)
  private String iso639_1;

  @Column(nullable = false)
  private String name;

ISO 639-1 표준에 따라 2자리 언어 코드로 기본 키를 생성합니다.

public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String username;

  @Column(length =2, nullable = false)
  private String language;

이에 따라 User Entity에서는 language 필드를 직접 가지고 있으며, Lanugage Entity의 기본 키와 동일한 형식을 취합니다. 의존 관계를 줄이기 위해 이와 같은 방법을 적용했습니다.

장점 (Pros)

  1. 서비스 독립성: 각 마이크로서비스는 자체 데이터를 완전히 소유하며, 다른 서비스에 의존하지 않습니다.
  2. 쿼리 성능: User 정보를 조회할 때 추가적인 조인 없이 언어 정보를 즉시 얻을 수 있습니다.
  3. 확장성: 언어 서비스와 사용자 서비스를 독립적으로 스케일링할 수 있습니다.
  4. 단순성: 구현이 간단하고 직관적입니다.

단점 (Cons)

  1. 데이터 중복: 동일한 언어 코드가 여러 User 레코드에 반복될 수 있습니다.
  2. 일관성 관리: Language 엔티티의 변경사항을 User 엔티티에 반영하는 것이 복잡할 수 있습니다.
  3. 제약 조건: 데이터베이스 레벨에서 외래 키 제약을 적용하기 어려울 수 있습니다.
  4. 유효성 검증: User 서비스에서 유효한 언어 코드인지 확인하는 추가 로직이 필요할 수 있습니다.

먼저, 이전에 언급된 몇 가지 단점들에 대해 다시 생각해봅시다:
1. 데이터 중복:
- User 레코드에 언어 코드가 중복 저장되는 것은 불가피하며, 이는 문제가 되지 않습니다. 이는 데이터의 특성상 자연스러운 현상입니다.
2. 일관성 관리:
- ISO 639-1 같은 표준 언어 코드를 사용함으로써 이 문제를 원천적으로 방지할 수 있습니다. 표준 코드는 변경될 가능성이 극히 낮습니다.
3. 외래 키 제약 조건:
- MSA 환경에서는 의도적으로 외래 키를 적용하지 않는 것이 일반적입니다. 이는 서비스 간 독립성을 유지하기 위한 전략적 선택입니다.
4. 유효성 검증:
- 표준 언어 코드만을 사용한다면, 추가적인 유효성 검증 로직이 필요하지 않을 수 있습니다. 입력 시점에서 간단한 형식 검사만으로 충분할 것입니다.

이러한 설계 방식으로 MSA 핵심 원칙인 서비스 간 낮은 결합도와 높은 응집도를 반영할 수 있습니다.

id를 통한 간접 참조 외에 user-service 내부에 @ManyToOne 관계의 LanguageInfo라는 별도의 Entity를 생성할 수 있습니다.

서비스 간 통신

서비스 간 통신의 종류는 다음과 같이 분류 됩니다.

RPC (Remote Procedure Call) 기반

  • gRPC
    • 동기식 및 비동기식 통신 모두 지원
    • 양방향 스트리밍 기능 제공
    • 효율적인 직렬화와 프로토콜 버퍼 사용

HTTP 클라이언트 기반

  • OpenFeign
    • Spring Cloud에서 제공하는 선언적 HTTP 클라이언트
    • 인터페이스와 어노테이션 기반의 간편한 사용
    • 동기식 통신 방식
    • 서비스 디스커버리, 로드 밸런싱과 쉽게 통합
  • RestTemplate
    • Spring Framework의 동기식 HTTP 클라이언트
    • RESTful 웹 서비스 호출에 사용
    • 다양한 HTTP 메서드 지원
    • 레거시 시스템에서 주로 사용 (현재는 WebClient 권장)
  • WebClient
    - 비동기 및 논 블로킹 방식

메시지 브로커 기반

  • RabbitMQ
    • 복잡한 라우팅 지원
    • 신뢰성 있는 메시징 제공
    • 다양한 메시징 패턴 구현 가능
  • ActiveMQ
    • JMS (Java Message Service) 구현체
    • 다양한 프로토콜 지원
  • NATS
    • 경량화 및 고성능 지향
    • 클라우드 네이티브 환경에 적합
  • Kafka
    • 대규모 데이터 스트리밍에 특화
    • 이벤트 소싱 및 로그 집계에 적합
    • 높은 처리량과 확장성 제공

규모가 작은 애플리케이션이고 메시지 브로커를 사용하려고 추가 인프라를 구축하기엔 닭 잡는데에 소 잡는 칼을 쓰는 격이 될것 같습니다.
그래서 복잡한 설정 없이도 Spring Cloud에서 지원해주고 있는 Open Feign을 사용하도록 합니다.

Open Feign 적용하기

의존성 추가

다른 서비스를 호출할 마이크로서비스에 OpenFeign 의존성을 추가합니다.
저는 language-service에서 user-service를 호출하므로, language-service에 OpenFeign 의존성을 추가합니다.

ext {
	set('springCloudVersion', "2023.0.3")
}

dependencies {

	implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

}

Open Feign 활성화

다른 Sping Cloud 기술과 비슷하게 @EnableFeignClients를 선언해줘야 합니다.

@SpringBootApplication
@EnableFeignClients
public class LanguageServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(LanguageServiceApplication.class, args);
	}

}

User Controller API 작성

@GetMapping("/language/{languageId}")
  public ResponseEntity<List<UserResponse>> findAllUsersByLanguage(
      @PathVariable("languageId") String languageId
  ){
    return ResponseEntity.ok(from(userService.findAllUsersByLanguage(languageId)));
  }

기존 모놀리식 구조의 API를 구현하듯 user-service에 API를 작성하고
language-service 해당 API를 통해 정보를 가져오도록 합니다.

Open Feign 인터페이스 작성

@FeignClient(name = "user-service", url = "${application.config.users-url}")
public interface UserClient {

  @GetMapping("/language/{languageId}")
  List<UserFeignResponse> findAllUsersByLanguage(@PathVariable("languageId") String languageId);

}

여기서 새로운 DTO UserFeignResponse를 생성했는데
이는 user-service의 UserResponse와 동일한 필드를 가집니다.

이 때, 두 객체는 다른 모듈에 분리되어 작성되었으므로
하나의 객체 수정 시 2개의 객체를 수정해야하는 번거로움이 있습니다.

이럴 경우 별도의 라이브러리 형태로 DTO를 만들어 공유할 수 있습니다.

shared-dto-library/ 
├── src/ 
│ └── main/ 
│ └── java/ 
│ └── com/ 
│ └── example/ 
│ └── shareddto/ 
│ ├── UserDto.java 
│ └── LanguageDto.java 
├── pom.xml 
└── README.md

위와 같은 구조로 별도의 모듈을 생성해 여러 서비스에 공유할 DTO 클래스를 작성합니다.

Open Feign 인터페이스 호출

private final UserClient userClient;

public LanguageWithUsersInfo findLanguage(RetrieveLanguageCommand command) {  
  Language language = languageRepository.findById(command.getLanguageId()); 
  List<UserFeignResponse> users = userClient.findAllUsersByLanguage(command.getLanguageId());  
  return LanguageWithUsersInfo.from(language, users);  
}

서비스를 호출하는 Language 서비스의 findLanguage 메서드에서 Open Feign을 주입받아 사용합니다.

결론

이번 게시물에서는 language-service 마이크로서비스를 추가하고, user-service와의 관계를 형성하며 서비스 간 통신을 구현했습니다. 주요 내용을 요약하면 다음과 같습니다:

  1. Entity 간 연관관계 형성:
    • Language와 User Entity를 생성하고, 간접 참조 방식을 통해 느슨한 결합을 구현했습니다.
    • 이 방식은 서비스 독립성, 쿼리 성능, 확장성 등의 장점을 제공합니다.
  2. 서비스 간 통신 방식 선택:
    • 다양한 통신 방식을 검토했습니다.
    • 애플리케이션의 규모와 요구사항을 고려하여 OpenFeign을 선택했습니다.
  3. OpenFeign 적용:
    • 의존성 추가 및 활성화
    • User Controller API 작성
    • OpenFeign 인터페이스 작성 및 호출 구현
  4. DTO 공유 전략:
    • 서비스 간 DTO 중복 문제를 해결하기 위해 shared-dto-library 모듈 생성 방법을 제시했습니다.

다음 게시물로 MSA 개선 방향으로 API 게이트웨이를 도입하도록 하겠습니다.

profile
예술융합형 개발자🎥

0개의 댓글