이번 챕터에서는 각 마이크로 서비스 간의 통신을 어떻게 할지에 대해서 알아보고 적용해보도록 하겠습니다.
0. 기존 Board-Service 리팩토링(Member 의존성 제거)
제 프로젝트에서는 다음과 같이 Member-Board가 1:N 관계였었습니다.
이전 멀티 모듈 구성 시에는 편의를 위해 이러한 의존성을 제거하지 않고
board-service 모듈이 member-service 모듈을 import 했었습니다.
MSA 구조에서는 Board-Service Application은 Member-Service 모듈을 참조하는 소스코드가 없어야 합니다.
이제 완벽한 의존성 제거를 위해 member-service 모듈의 import도 제거하고 공통 모듈만 import하도록 리팩토링했습니다.
member-service 모듈을 제거함으로써 Board-Service에 있는 Member 관련 소스 코드들이 모두 컴파일 에러가 뜨게 되는데요.
이 상황에서, Board-Service에서 Member를 참조하는 소스 코드들을 제거해야합니다.
0-1. Entity 의존 관계 제거
Entity의 의존 관계를 제거하는 방법에 대해서는 떠오르는 방법이 많았습니다.
그래서 Entity 설정에 대해서는 이번 포스팅에 다루지 않고 별도로 포스팅했습니다.
[MSA] MSA에서 JPA Entity의 연관관계를 어떻게 설정할까?
결론만 말하면, 저는 Board Entity에서 Member 직접참조를 끊고 Member의 정보를 가지는 Entity를 만들어서 참조하도록 했습니다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Board extends BaseEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn
private MemberInfo memberInfo;
...
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberInfo {
@Id @GeneratedValue
private Long id;
private String nickname;
...
}
이렇게 해서 Entity 단에서의 Member 의존성을 제거했습니다.
0-2. Service 로직 의존 관계 제거
이제 Board-Service에서 남은 의존은 Service(계층) 비즈니스 로직 단의 Member 로직에 있습니다.
여러 비즈니스 로직이 존재하지만, 편의상 게시글을 작성하는 메소드만 살펴봅시다.
@Service
@Transactional
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;
private final MemberRepository memberRepository;
public Long writeBoard(final String loginId, final BoardWriteRequest request) {
final Member member = memberRepository.findByLoginId(loginId)
.orElseThrow(MemberException.NotFoundMemberException::new);
final Board board = new Board(member, request.title(), request.content());
final Board savedBoard = boardRepository.save(board);
return savedBoard.getId();
}
}
이처럼 MemberRepository를 BoardService가 의존하고,
게시글 작성 비즈니스 로직 실행 시 MemberRepository에서 loginId에 해당하는 멤버를 찾아서 사용하는 로직이 있습니다.
여기서 Member와의 의존을 끊기 위해서는 다음과 같이 제거해야 합니다.
@Service
@Transactional
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;
public Long writeBoard(final String loginId, final BoardWriteRequest request) {
// memberId를 어떻게 가져올까?
final Board board = new Board(memberId, request.title(), request.content());
final Board savedBoard = boardRepository.save(board);
return savedBoard.getId();
}
}
MemberRepository의 의존을 제거하고, 게시글 작성 로직에서도 Member 관련 로직을 제거했습니다.
이때, 위의 엔티티에서 Member 의존을 끊을 때 간접참조로 Member 대신 memberId를 필드로 가졌습니다.
그렇다면 이렇게 Member 관련 의존을 제거하고 나니, 다음과 같은 의문이 생기게 됩니다.
Board-Service Application의 비즈니스 로직에서 Member 관련 정보(memberId)는 어떻게 가져오지?
이와 같은 질문에 대한 답은 '서비스 간 통신'을 해서 가져오는 것입니다.
이제 서비스 간 통신을 통해 Board-Service에서 Member 관련 정보를 가져오는 작업을 진행해보도록 하겠습니다.
1. 서비스 간 통신 방식의 종류
실제로 서비스 간 통신을 하기전에, 어떤 통신 방식으로 리팩토링을 진행해야 할지 먼저 종류를 살펴봅시다.
서비스 간 통신의 종류는 크게 동기 방식과 비동기 방식으로 나뉘게 됩니다.
- 동기 방식
- RestTemplate
- OpenFeign
- 비동기 방식
- WebClient
- 메시지 큐 사용(Kafka, RabbitMQ, ActiveMQ 등)
따라서, 서비스 간 통신 방식 결정을위해서는 동기 방식으로 서비스를 호출할지 비동기 방식으로 서비스를 호출할지를 결정해야 합니다.
동기 방식은 기존 HTTP / REST 요청이 동기 방식이기 때문에 익숙한 방식이지만 다수의 요청이 들어왔을 때 지연이 길어질 수 있습니다.
하지만 비동기 방식은 여러 요청을 동시에 처리할 수 있기 때문에 지연이 적고 성능이 좋지만 이에 따라 고려할 점이 많습니다.
이러한 상황에서 저는 애플리케이션의 규모가 작기도 하고 익숙하고 비교적 쉽게 구현할 수 있는 동기 방식을 선택하였습니다.
추후에 시간이 된다면 비동기 방식으로 리팩토링하여 동기 방식과 비교를 해보는 것도 좋을 것 같습니다.
동기 방식 중에서는 RestTemplate과 OpenFeign이 존재합니다.
- RestTemplate : Spring에서 제공하지만, Spring에서 권장하지 않음(WebClient를 권장)
- OpenFeign : Spring Cloud에서 지원하고, 높은 수준의 추상화로 간단하게 통신 가능
옛날부터 RestTemplate을 자주 써왔지만, RestTemplate이 Deprecated까지는 아니고 maintenance mode가 됨에 따라서
사용하지 않는 것을 권장하는 것을 볼 수 있었습니다.
또한, Spring Cloud 환경에서 MSA를 구축하고 있기도 하고 가독성이 좋은 OpenFeign을 사용하여
서비스 간 통신을 구현해보도록 하겠습니다.
2. OpenFeign이란?
Spring Cloud OpenFeign의 공식문서에는 다음과 같이 소개하고 있습니다.
Feign은 어노테이션 기반의 Web Service Client로,
어노테이션 방식으로 동작함에 따라 Web Service Client를 쉽게 구성할 수 있습니다.
Spring MVC을 지원하고, Spring Web에서 사용되는 HttpMessageConverters도 지원합니다.
요약하자면, OpenFeign은 어노테이션 기반으로 Spring MVC 및 HttpMesageConverters를 지원함에 따라
기존 Spring MVC에서 어노테이션 기반으로 HTTP 통신을 했던 방식과 유사하게 구현할 수 있습니다.
따라서 다른 방식과 비교해서 비교적 간단하게 구현이 가능합니다.
또한 Spring Cloud에서 제공되기 때문에 다른 기술들(Eureka, Circuit Breaker, Load Balancer)와 통합이 쉽습니다.
3. OpenFeign 적용하기
본격적으로 서비스 간 통신에 OpenFeign을 적용해봅시다.
3-1. Spring Cloud OpenFeign 의존성 추가
다른 서비스를 호출할 쪽에서 OpenFeign 의존성을 추가해줍니다.
저는 Board 서비스에서 Member 서비스를 호출할 것이므로, Board 서비스에 OpenFeign 의존성을 추가했습니다.
ext {
set('springCloudVersion', "2023.0.0")
}
dependencies {
...
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
3-2. OpenFeign 활성화
OpenFeign을 활성화하기 위해서는 다른 Spring Cloud 기술들과 비슷하게 @EnableFeignClients를 선언해주면 됩니다.
Board 서비스 쪽의 실행 Application에 해당 어노테이션을 선언하여 활성화했습니다.
...
@EnableFeignClients
@SpringBootApplication(scanBasePackages = {"com.example.boardservice", "com.example.shboardcommon"})
public class BoardServiceApplication {
public static void main(String[] args) {
SpringApplication.run(BoardServiceApplication.class, args);
}
}
3-3. OpenFeign 관련 클래스 생성
OpenFeign 관련 클래스를 생성하기 전에, 현재 제 프로젝트의 서비스 호출 Context를 이해해봅시다.
@Service
@Transactional
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;
public Long writeBoard(final String loginId, final BoardWriteRequest request) {
// memberId를 어떻게 가져올까?
final Board board = new Board(memberId, request.title(), request.content());
final Board savedBoard = boardRepository.save(board);
return savedBoard.getId();
}
}
위의 코드에서 memberId를 가져오는 코드를 Member 서비스를 호출하여 가져와야 합니다.
호출 시 응답으로 Board 서비스에서 Member 정보 중에 필요한 값만을 생성해서 DTO로 전달하면 됩니다.
3-3-1. Member, Board 통신할 Response Dto 생성
위의 코드는 memberId만 필요하지만, Board 서비스의 다른 로직에서 member의 nickname이 필요했습니다.
따라서, memberId와 nickname을 가지는 ResponseDto를 Member와 Board에 모두 생성했습니다.
public record MemberFeignResponse(Long memberId, String nickname) {
}
※ Feign 통신 Response 관리?
Member에서는 API 반환 객체로 MemberFeignResponse를 사용하고,
Board에서는 OpenFeign 인터페이스 생성 시 API를 작성할 때 반환 타입을 선언하기 위해 MemberFeignResponse가 필요합니다.
이때, Member의 MemberFeignResponse와 Board의 MemberFeignResponse는 동일해야 합니다.
그런데, 다른 모듈(서비스)에 존재하기 때문에 하나를 수정하면 2개가 같이 변경되어야 합니다.
이러한 상황을 어떻게 해결할 수 있을까 해서 찾아봤는데, 아래의 링크 답변에서 찾을 수 있었습니다.
두 개의 서비스에 사용되는 Feign Response Dto를 라이브러리로 만들고 해당 라이브러리를 의존하는 방식으로 관리한다고 합니다.
이 방법 말고도 생각나는 방법으로는, 현재 멀티 모듈 환경인만큼 Feign Dto 관련 공통 모듈을 생성해서
해당 모듈을 사용하는 각 마이크로 서비스에 import 하는 방법도 간단할 것 같습니다!
3-3-2. Member Controller API 작성
기존 모놀리식 구조에서 Member API는 회원가입, 로그인 API만 존재했었습니다.
현재는 Board 서비스에서 Member 정보를 API를 통해 가져와야 하므로 추가적으로 Member Controller에 API를 생성했습니다.
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/members")
public class MemberApiController {
private final MemberService memberService;
...
@GetMapping
public MemberFeignResponse findMemberIdByLoginId(@RequestParam("loginId") final String loginId) {
log.info("open feign communication success!");
return memberService.findMemberByLoginId(loginId);
}
...
}
이후 통신 테스트를 위해 info 로그도 추가해줬습니다.
3-3-3. OpenFeign 인터페이스 작성
Board 서비스에서 Member 서비스의 API를 호출하여 Member의 정보를 얻기 위한 OpenFeign 인터페이스를 작성했습니다.
@FeignClient(name = "member-service", path = "/api/members")
public interface MemberFeignClient {
@GetMapping
MemberFeignResponse findMemberIdByLoginId(@RequestParam("loginId") final String loginId);
...
}
인터페이스를 생성하고, @FeignClient를 붙여서 활성화해줬습니다.
메소드로는 통신할 Member Controller API를 그대로 작성해줍니다.
- @FeignClient name 옵션 : 통신할 서비스의 Eureka 등록 이름
- @FeignClient path 옵션 : RequestMapping의 value와 동일 (공통 URI 추출)
3-3-4. 서비스를 호출하는 쪽(Board)에서 OpenFeign 인터페이스 사용하여 통신
서비스를 호출하는 Board 서비스의 Service 로직에서 OpenFeign을 주입받아 사용해봅시다.
@Service
@Transactional
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;
private final MemberFeignClient memberFeignClient;
...
public Long writeBoard(final String loginId, final BoardWriteRequest request) {
// Feign 통신을 통해 FeignResponse(memberId) 받아오기
final MemberFeignResponse response = memberFeignClient.findMemberIdByLoginId(loginId);
final Board board = new Board(response.memberId(), request.title(), request.content());
final Board savedBoard = boardRepository.save(board);
return savedBoard.getId();
}
}
MemberFeignClient를 주입받아서 작성했던 API를 호출하면 통신해서 FeignResponse를 받을 수 있습니다!
3-3-5. 서비스 통신 테스트
이제 API Gateway를 통해 Board 서비스의 게시글 작성 API를 호출하여 Member 서비스와 통신이 제대로 되는지 테스트해봅시다.
API Gateway 포트인 9001번으로 다음과 같이 요청을 보냈습니다.
위와 같이 201 Created로 성공하는 것을 알 수 있습니다.
Member 서비스 Application의 로그를 확인해보면, 다음과 같이 Feign 통신이 정상적으로 성공함을 알 수 있습니다.
여기까지 서비스 간 통신으로 OpenFeign을 적용해봤습니다.
이것으로 이번 챕터는 마무리하겠습니다!
🎯 Github Repository 링크 (전체 코드)
https://github.com/sh111-coder/sh-board-msa
📘 Monolithic to MSA 전체 목차
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (1) MSA란?
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (2) 멀티 모듈 구성하기
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (3) Service Discovery 패턴 적용하기(feat. Spring Cloud Eureka)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (4) API Gateway 구현(feat. Spring Cloud Gateway)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (5) 서비스 간 통신하기(feat.Spring Cloud OpenFeign)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (6) 각 서비스의 설정 파일 관리하기(feat. Spring Cloud Config)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (7) 서비스 장애 대응 Circuit Breaker 구현(feat. Resilience4J)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (10) MSA 전환 후 비교 및 회고 + 마무리
Reference
https://mangkyu.tistory.com/278