모든 요구사항을 한 엔드포인트로 처리하는 방법
🤔 서론
Java와 Spring으로 줍줍이라는 서비스를 개발하고 있다. 메시지 전송, 프로필 변경 같은 여러가지 슬랙 이벤트를 한 엔드포인트로만 관리해야 하는 상황이 발생했다. Front Controller? Handler Mapping? 뭘 사용해야 로직을 분기할 수 있지? 몇 개의 키워드만 주워듣고 팀원들 각자 해결할 방법을 생각해오기로 했다.
😂 상황 설명
"/test"라는 URL을 POST 메서드로 보내면 Controller의 testApi 메서드가 호출된다.
HTTP Message Body에 담긴 데이터 중에 "type"이라는 값에 따라 실행해야 하는 로직이 다르다.
@RestController
@RequestMapping("/test")
public class TestController {
private final TestAService aService;
private final TestBService bService;
public TestController(TestAService aService, TestBService bService) {
this.aService = aService;
this.bService = bService;
}
@PostMapping
public void testApi(@RequestBody TestDto testDto) {
if ("a".equals(testDto.getType())) {
aService.save(testDto);
}
bService.save(testDto);
}
}
로직을 분리하기 위해 아래와 같이 API를 아예 따로 만들고 싶다면?
// testDto.getType()이 "a"인 경우
@PostMapping
public void testApiA(@RequestBody TestDto testDto) {
aService.save(testDto);
}
// testDto.getType()이 "b"인 경우
@PostMapping
public void testApiB(@RequestBody TestDto testDto) {
bService.save(testDto);
}
😜 해결 시도
1. 쿼리 스트링을 받을 수 있다면...
- @RequestMapping의 속성 중 하나인 params 이용
- params를 이용하면 특정 값을 반드시 받아오거나, 특정 값과 일치하지 않는 등 여러가지 옵션 설정 가능
@PostMapping(params = "type=a")
public void testApiA(@RequestBody TestDto testDto) {
aService.save(testDto);
}
@PostMapping(params = "type=b")
public void testApiB(@RequestBody TestDto testDto) {
bService.save(testDto);
}
2. Filter 이용하기
- 커스텀 filter를 추가해 reqeust 값에 header 값을 임의로 추가
- @RequestMapping의 headers를 이용해 임의로 추가했던 header 값으로 분기 처리
실제로 내가 시도해본 방법은 아니고 팀원이 알려준 방법이다. (약간의 구현 과정이 까다로워보였지만 실제로 테스트를 통과시켰다고...)
@PostMapping(headers = "type=a")
public void testApiA(@RequestBody TestDto testDto) {
aService.save(testDto);
}
@PostMapping(headers = "type=b")
public void testApiB(@RequestBody TestDto testDto) {
bService.save(testDto);
}
3. controller에서 분기 처리하기
- Service를 interface화
- 여러 Service 목록에서 적합한 Service를 찾는 ServiceFinder 생성
- Controller에서는 ServiceFinder를 이용해 Service를 찾고, 로직 실행
TestService를 interface화하고 분기시킬 로직에 따라 해당 인터페이스를 구현한다.
public interface TestService {
void save(TestDto testDto);
boolean isSameType(String type);
}
ServiceFinder를 생성해 어떤 Service를 찾을 것인가에 대한 역할을 부여한다.
@Component
public class TestServiceFinder {
public final List<TestService> testServices;
// bean으로 등록된 TestService의 구현체들을 자동으로 찾아준다
public TestServiceFinder(final List<TestService> testServices) {
this.testServices = testServices;
}
public TestService find(String type) {
return slackEventServices.stream()
.filter(service -> service.isSameType(type))
.findAny()
.orElseThrow(IllegalArgumentException::new);
}
}
Controller에서는 ServiceFinder를 호출하고 로직을 실행시킨다.
@RestController
@RequestMapping("/test")
public class TestController {
private final TestServiceFinder testServiceFinder;
public TestController(TestServiceFinder testServiceFinder) {
this.testServiceFinder = testServiceFinder;
}
@PostMapping
public void testApi(@RequestBody TestDto testDto) {
testServiceFinder.find(testDto.getType())
.save(testDto);
}
}
🔻 다른 예제 살펴보기
위 방법은 사실 호돌맨님께서 조언해주신 방법이다. 설명을 위해 짜주신 코드가 너무 직관적이고 이해가 잘 가서 첨부한다. (정말 감사합니다)
🤭 어떤 방법이 좋을까?
1의 방법으로 해결할 수 있었다면 가장 편했을 것이다. 하지만 현재 프로젝트에서는 클라이언트가 쿼리스트링을 보내도록 제어할 수 없었다. 팀원들 사이에서 2, 3의 의견이 분분했다. 2는 HTTP 메시지를 임의로 수정한다는 것이 신뢰성을 낮추진 않을까? type은 메시지의 'body'에 담긴 데이터고 이에 따라 로직 분리가 필요하다는 것은 우리가 임의로 정한 규칙이라고 볼 수 있는데 HTTP 정보를 이용해서 분리하는 엔드 포인트를 마음대로 변경해도 되는가? 등의 우려가 있었기에 3으로 채택되었다. (프로젝트에 적용하는 과정 Pull Request 링크)
Thanks to ...
- 우아한테크코스 팀 줍줍, 코치 구구
- 개발바닥 호돌맨님, 개발바닥 단톡방 - 형덕님, 승일님