현재 서비스는 모노리식 아키텍쳐로 구성되어 있고, 8개의 도메인으로 구성되어 있다.
monolithic 아키텍쳐에서 microservice 아키텍쳐로 전환을 할 계획이다.
해당 도메인 하나 당 서버를 하나 만드는 것이 아닌, 비슷한 성격을 가진 도메인 별로 묶어서 3개의 서버를 만들기로 했다.
멤버
distributor, producer, singer
컨텐츠
contract, drama, ost
금융
revenue, settlement
따라서 비지니스 로직 관련 서버 3개, gateway 서버 1개, Eureka 서버 1개로 구성된 microService 아키텍쳐를 구성할 계획이다.
예를 들어 ost 객체 저장을 위해선 producer와 singer 정보가 모두 필요하다. (ost-singer 연관 관계) monolithic 구조에서는 같은 서버에 모든 엔티티 정보가 저장되어 있으므로 문제가 없지만, MSA에서는 다른 서버에 존재한다.
content 서버에도 singer 엔티티를 저장하고 관련 비지니스 로직을 작성해야 할까?
-> MSA로 전환한 의미 없음.
ostService에서 필요할때마다 singerService API 호출한다?
-> 성능도 떨어지고, 무엇보다 서비스 간 강한 결합 발생!
물리적으로 분산된 환경에서 어떻게 엔티티 매핑을 진행해야 할까?
@Entity
public class Ost {
...
// 제작사
@ManyToOne
@JoinColumn(name = "producerId")
private Producer producer;
// 가창자
@ManyToOne
@JoinColumn(name = "singerId")
private Singer singer;
...
}
@Service
@RequiredArgsConstructor
public class OstService {
...
public void register(OstRegisterDto ostRegisterDto) {
...
Long producerId = ostRegisterDto.getProducerId();
Producer producer = producerRepository.findById(producerId).orElseThrow();
Long singerId = ostRegisterDto.getSingerId();
Singer singer = singerRepository.findById(singerId).orElseThrow();
Ost ost = Ost.builder()
.drama(drama)
.producer(producer)
.singer(singer)
.title(ostRegisterDto.getTitle())
.build();
ostRepository.save(ost);
}
}
ost 등록할 때 member 서버에 있는 producer, singer가 필요하다.
현재 ost는 producer, singer와 연관 맺고 있는 상태.
-> Ost 엔티티에서 Producer, Singer의 직접 참조 끊고, 간접 참조 사용해서 의존성 제거.
@Entity
public class Ost {
...
// 제작사
private Long producerId;
// 가창자
private Long singerId;
...
}
하지만 아직 ostService에서 producerRepository, singerRepository 의존하고 있는 상황.
Ost ost = Ost.builder()
.drama(drama)
.producer(producerId)
.singer(singerId)
.title(ostRegisterDto.getTitle())
.build();
ostRepository.save(ost);
간접 참조를 이용해서 producer 대신 producerId를 필드로 갖게 함.
그럼 producerId는 어떻게 가져올까?
-> 서비스 간 통신 통해서 가져 옴.
동기와 비동기 방식이 있는데, 기존 HTTP, REST 요청과 같은 동기 방식을 선택했다.
다수 요청이 동시에 있을 시 비동기가 좋지만 익숙한 동기를 사용하고, 추후에 비동기 방식으로 변경해기로 결정했다.
RestTemplate의 경우 엄밀히 deprecated는 아니지만, 스프링에서도 WebClient를 권장하고 있기에, OpenFeign을 사용하기로 결정했다.
동기 방식은 다수 요청 들어왔을 때 지연 생길 수 있는 반면, 비동기 방식은 성능 좋지만 고려할 점이 많다.
간단하게 구현하기 위해 동기를 사용했지만, 실제 코드에서는 비동기 방식을 사용하는 것이 좋을 것!
OpenFeign은 어노테이션 기반 web service client 라이브러리이다.
마이크로서비스 간 통신에 사용.
Spring MVC, HttpMesageConverters를 지원해 기존 스프링 MVC에서 어노테이션 기반으로 HTTP 통신하는 방식과 유사하게 구현.
스프링 클라우드에서 제공되기 때문에 다른 기술(Eureka, 로드 밸런서 등)과 통합 쉽다.
호출하는 Ost의 Application에 @EnableFeignClients를 선언해 OpenFeign을 활성화 시킴.
ost를 register 할 때 singer의 id가 필요하기 때문에, Singer 서비스 호출해서 가져와야 함.
응답값은 Ost 서비스에서 Singer 정보 중 필요한 값만 생성해서 response dto로 전달하면 됨.
/*
Producer에서 API 반환 객체로 사용
Ost에서는 OpenFeign 인터페이스 생성 시 API 작성할때 반환 타입 선언에 사용
-> 따라서 두 곳에서 동일해야 하기 때문에 라이브러리로 만들어서 사용하는 것이 좋다
import 하여 양쪽에서 같게 사용
*/
@Component
@Getter
@Builder
@AllArgsConstructor
@RequiredArgsConstructor
public class SingerFeignResponse {
private Long singerId;
private String name;
}
SingerFeignResponse는 Ost와 Singer 양쪽에 모두 동일하게 생성해야 함.
따라서 라이브러리로 배포하는 것이 좋다!
Singer에서는 API 반환 객체로 SingerFeignResponse 사용,
// ost등 다른 서버에서 singer 정보 API 통해 가져와야 함
public SingerFeignResponse findSingerById(Long singerId) {
Singer singer = singerRepository.findById(singerId).orElseThrow();
return new SingerFeignResponse(singer.getId(), singer.getName());
}
public List<SingerFeignResponse> findAll() {
List<Singer> singerList = singerRepository.findAll();
List<SingerFeignResponse> singerFeignList = singerList.stream()
.map(singer -> new SingerFeignResponse(singer.getId(), singer.getName()))
.collect(Collectors.toList());
return singerFeignList;
}
Ost에서는 OpenFeign 인터페이스 생성 시 (singerFeignClient) 반환 타입 SingerFeignResponse으로 선언.
SingerFeignResponse singerFeignResponse = singerFeignClient.findSingerById(singerId);
Ost ost = Ost.builder()
.drama(drama)
.producerId(producerFeignResponse.getProducerId())
.singerId(singerFeignResponse.getSingerId())
.title(ostRegisterDto.getTitle())
.build();
ostRepository.save(ost);
Ost 서비스에서 Singer 서비스의 API(findById())를 호출해 Singer 정보를 얻기 위해 OpenFeign 인터페이스를 작성.
OpenFeign 인터페이스는 불러오는 쪽(Ost)에만 있으면 됨.
// name : 통신할 서비스의 Eureka 등록 이름
// path : RequestMapping의 value와 동일
// FeignClient끼리 name 같으면 contextId로 구분해야 함
@FeignClient(name = "member-service", contextId = "feignClientForSinger", path = "/member/singer")
public interface SingerFeignClient {
// FeintClient 설정해주면 마치 자신의 API 인것처럼 정의 가능. 세부 구현은 x, Singer 서비스에 되어있음
@GetMapping
SingerFeignResponse findSingerById(@RequestParam(value = "memberId") Long memberId);
}
<참고>