이전 게시물에서 Eureka로 Client-Side Discovery를 구현했습니다.
현재까지 구현 내용은 다음과 같습니다.
1. user-service 마이크로서비스를 eureka로 register한 상태입니다.
2. config-service로 각 마이크로서비스의 중앙화된 설정을 구현하기 위해 뼈대를 잡아놨습니다.
다이어그램의 점선은 아직 구현되지 않은 부분을 의미하는데
이번 게시물에서는 추가로 language-service 마이크로서비스를 추가하여
language-user 간의 N:1 관계를 형성하고 각 마이크로서비스 간의 통신을 구현합니다.
먼저 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의 기본 키와 동일한 형식을 취합니다. 의존 관계를 줄이기 위해 이와 같은 방법을 적용했습니다.
먼저, 이전에 언급된 몇 가지 단점들에 대해 다시 생각해봅시다:
1. 데이터 중복:
- User 레코드에 언어 코드가 중복 저장되는 것은 불가피하며, 이는 문제가 되지 않습니다. 이는 데이터의 특성상 자연스러운 현상입니다.
2. 일관성 관리:
- ISO 639-1 같은 표준 언어 코드를 사용함으로써 이 문제를 원천적으로 방지할 수 있습니다. 표준 코드는 변경될 가능성이 극히 낮습니다.
3. 외래 키 제약 조건:
- MSA 환경에서는 의도적으로 외래 키를 적용하지 않는 것이 일반적입니다. 이는 서비스 간 독립성을 유지하기 위한 전략적 선택입니다.
4. 유효성 검증:
- 표준 언어 코드만을 사용한다면, 추가적인 유효성 검증 로직이 필요하지 않을 수 있습니다. 입력 시점에서 간단한 형식 검사만으로 충분할 것입니다.
이러한 설계 방식으로 MSA 핵심 원칙인 서비스 간 낮은 결합도와 높은 응집도를 반영할 수 있습니다.
id를 통한 간접 참조 외에 user-service 내부에 @ManyToOne 관계의 LanguageInfo라는 별도의 Entity를 생성할 수 있습니다.
서비스 간 통신의 종류는 다음과 같이 분류 됩니다.
규모가 작은 애플리케이션이고 메시지 브로커를 사용하려고 추가 인프라를 구축하기엔 닭 잡는데에 소 잡는 칼을 쓰는 격이 될것 같습니다.
그래서 복잡한 설정 없이도 Spring Cloud에서 지원해주고 있는 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'
}
다른 Sping Cloud 기술과 비슷하게 @EnableFeignClients를 선언해줘야 합니다.
@SpringBootApplication
@EnableFeignClients
public class LanguageServiceApplication {
public static void main(String[] args) {
SpringApplication.run(LanguageServiceApplication.class, args);
}
}
@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를 통해 정보를 가져오도록 합니다.
@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 클래스를 작성합니다.
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와의 관계를 형성하며 서비스 간 통신을 구현했습니다. 주요 내용을 요약하면 다음과 같습니다:
다음 게시물로 MSA 개선 방향으로 API 게이트웨이를 도입하도록 하겠습니다.