Develop/Spring+JPA

모든 요구사항을 한 엔드포인트로 처리하는 방법

연로그 2022. 7. 8. 13:46
반응형

🤔 서론

 

 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 ...

  • 우아한테크코스 팀 줍줍, 코치 구구
  • 개발바닥 호돌맨님, 개발바닥 단톡방 - 형덕님, 승일님
반응형