선언적 웹 서비스 클라이언트입니다.
HttpMessageConvertersSpring Cloud는 Spring MVC 주석에 대한 지원과 Spring Web에서 기본적으로 사용되는 동일한 사용에 대한 지원을 추가합니다 . Spring Cloud는 Eureka, Spring Cloud CircuitBreaker 및 Spring Cloud LoadBalancer를 통합하여 Feign을 사용할 때 부하 분산된 http 클라이언트를 제공합니다.
현재 상황은 북마크를 담당하는 서버가 북마크를 하기전에 Auth서버에게 해당 유저가 있는지 요청을 합니다.
📌 Feign Client 코드는 이러합니다.
@FeignClient(
name = "${api.authentication-server.name}",
url = "${api.authentication-server.url}",
configuration = {FeignRetryConfiguration.class}
)
public interface MemberFeignClient {
@RequestMapping(
method = RequestMethod.GET,
value = "/api/member/exist/{memberId}",
consumes = MediaType.APPLICATION_JSON_VALUE
)
Boolean memberExistsByMemberId(@PathVariable("memberId") Long memberId);
}
member id로 해당 유저가 있는지 물어봅니다.
📌 feign client가 fallback을 사용할 수 있게 만들어 주었습니다.
@Service
public class MemberFeignClientService {
private final MemberFeignClient memberFeignClient;
public MemberFeignClientService(MemberFeignClient memberFeignClient) {
this.memberFeignClient = memberFeignClient;
}
@CircuitBreaker(name = "existMember", fallbackMethod = "throwMemberFeignEx")
public Boolean existByMemberId(Long memberId) {
return memberFeignClient.memberExistsByMemberId(memberId);
}
private Boolean throwMemberEx(Throwable t) {
tthrow new IllegalArgumentException("페인 클라이언트 에러!");
}
}
북마크는 한명의 유저가 뉴스를 찜하는 기능입니다.
📌 북마크를 저장하는 코드입니다.
@Service
public class BookmarkCommandService implements BookmarkCommand {
private final NewsJpaRepository newsJpaRepository;
private final MemberFeignClientService memberFeignClientService;
private final BookmarkJpaRepository bookmarkJpaRepository;
public BookmarkCommandService(
NewsJpaRepository newsJpaRepository,
MemberFeignClientService memberFeignClientService,
BookmarkJpaRepository bookmarkJpaRepository
) {
this.newsJpaRepository = newsJpaRepository;
this.memberFeignClientService = memberFeignClientService;
this.bookmarkJpaRepository = bookmarkJpaRepository;
}
@Override
public Bookmark saveBookmark(Long newsId, Long memberId) {
if (!memberFeignClientService.existByMemberId(memberId)) {
throw new IllegalArgumentException("해당 id의 멤버가 존재하지 않습니다.");
}
if (!newsJpaRepository.existsById(newsId)) {
throw new IllegalArgumentException("해당 id의 뉴스가 존재하지 않습니다.");
}
Bookmark bookmark = new Bookmark(
newsId,
memberId
);
Bookmark savedBookmark = bookmarkJpaRepository.save(bookmark);
return savedBookmark;
}
}
이제는 테스트 코드를 작성해 보겠습니다.
📌 가짜 구현체
public class MemberFeignClientImpl implements MemberFeignClient {
@Override
public Boolean memberExistsByMemberId(Long memberId) {
if (memberId == null) {
return Boolean.FALSE;
}
if (memberId > 0) {
return Boolean.TRUE;
}
return Boolean.FALSE;
}
}
📌 테스트 코드
@Test
@DisplayName("북마크를 저장하면 Bookmark객체의 id값이 생긴다.")
public void given_news_id_and_member_id_when_then() {
News save = newsJpaRepository.save(
new News(
"title",
"des",
1L,
"com",
"com",
Instant.now(),
Instant.now()
)
);
Bookmark bookmark = bookmarkCommand.saveBookmark(1L, save.getNewsId());
assertThat(bookmark.getBookmarkId()).isGreaterThan(0L);
}
음... 해결된거 같으면서도 테스트의 이치에는 맞지 않는것 같다.
gradle 추가
testImplementation 'com.github.tomakehurst:wiremock-jre8:2.30.1'
@ActiveProfiles({"test"})
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@DisplayName("북마크 서비스레이어 통합 테스트")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class BookmarkServiceMockTest extends AbstractContainerTestBase {
@Autowired
private BookmarkCommandService bookmarkCommandService;
static final int PORT = 8080;
public static WireMockServer wireMockServer = new WireMockServer(options().port(PORT));
@Autowired
private PersistenceHelper persistenceHelper;
@DynamicPropertySource
public static void addUrlProperties(DynamicPropertyRegistry registry) {
registry.add("api.authentication-server.url", () -> "localhost:" + PORT);
}
@BeforeAll
public static void beforeAll() {
wireMockServer.start();
WireMock.configureFor("localhost", PORT);
}
@AfterAll
public static void afterAll() {
wireMockServer.stop();
}
@AfterEach
public void afterEach() {
wireMockServer.resetAll();
}
@Test
@DisplayName("북마크 저장을 하면 북마크가 생긴다.")
public void given_memberid_newsid_when_save_bookmark_then_is_not_null() {
Long memberId = 1L;
wireMockServer.stubFor(get(urlMatching("/api/member/exist/"+memberId))
.willReturn(aResponse()
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.withBody("true"))
);
News persist = persistenceHelper.persist(NewsFixture.createNews());
Bookmark bookmark = bookmarkCommandService.saveBookmark(1L, memberId);
assertThat(bookmark).isNotNull();
}
@Test
@DisplayName("Auth서버랑 연결되지 않으면 에러를 반환한다.")
public void when_bookmark_save_then_throw_error() {
assertThatThrownBy(() -> bookmarkCommandService.saveBookmark(1L, 1L))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("페인 클라이언트 에러!");
}
}
@ActiveProfiles({"test"})
@DisplayName("북마크 API 통합 테스트")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class BookmarkControllerIntegrationTest extends AbstractContainerTestBase {
private MockMvc mockMvc;
@Autowired
private BookmarkCommandAPI bookmarkCommandAPI;
static final int PORT = 8080;
public static WireMockServer wireMockServer = new WireMockServer(options().port(PORT));
@DynamicPropertySource
public static void addUrlProperties(DynamicPropertyRegistry registry) {
registry.add("api.authentication-server.url", () -> "localhost:" + PORT);
}
@BeforeAll
public static void beforeAll() {
wireMockServer.start();
WireMock.configureFor("localhost", PORT);
}
@AfterAll
public static void afterAll() {
wireMockServer.stop();
}
@AfterEach
public void afterEach() {
wireMockServer.resetAll();
}
@BeforeEach
public void setup() {
this.mockMvc = MockMvcBuilders
.standaloneSetup(bookmarkCommandAPI)
.build();
}
@Autowired
private PersistenceHelper persistenceHelper;
@Test
@DisplayName("북마크를 저장하면 Status코드가 201이다.")
public void given_news_id_when_save_then_status_created() throws Exception {
wireMockServer.stubFor(get(urlMatching("/api/member/exist/1"))
.willReturn(aResponse()
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.withBody("true"))
);
News news = persistenceHelper.persist(NewsFixture.createNews());
Long newsId = news.getNewsId();
ResultActions response = mockMvc.perform(MockMvcRequestBuilders.post("/api/bookmark/save/1/"+newsId)
.contentType(MediaType.APPLICATION_JSON)
);
response.andExpect(MockMvcResultMatchers.status().isCreated());
}
}
서비스 통합테스트와 마찬가지로 WireMock을 사용하였습니다.
MSA를 구축하는건 모놀리식 보다 공수가 몇배는 드는것 같습니다...
물론 그럼에도 불구하고 팀 사이즈, 어플리케이션의 사이즈에 따라 MSA가 독이될 수도 약이 될 수도 있을것 같습니다.
그리고 제가 잘못 작성한게 있다면 지적해주시면 정말 감사하겠습니다!
https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/
https://www.baeldung.com/introduction-to-wiremock
https://kafcamus.tistory.com/55
너무 감동적이네요 ㅠㅠ