Project/DrugDoctor
(Spring) 공공데이터포털 데이터 조회 로직 리펙토링
우봉수
2023. 7. 30. 14:56
의약품의 이름 정보를 쿼리에 담아 요청을 보내면 해당 하는 알약의 정보를 반환 받는 기존의 로직을
수강한 강의 영상을 바탕으로 개선한 과정 기록
기존 로직
- controller에 비지니스 로직이 섞여 있는 모습
package _PF026.DrDrug.controller;
import _PF026.DrDrug.dto.MedicationDto;
import _PF026.DrDrug.dto.ResponseDto;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.DefaultUriBuilderFactory;
import reactor.core.publisher.Mono;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
@RestController
public class DrugOpenAPIController {
private final static String BASE_URL = "http://apis.data.go.kr/1471000/DrbEasyDrugInfoService/getDrbEasyDrugList";
private final String API_KEY = "your key"
@GetMapping(value = "/MedicationDto/{itemName}", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<MedicationDto> getDrugInfo(@PathVariable String itemName){
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);
String encodedItemName = URLEncoder.encode(itemName, StandardCharsets.UTF_8);
WebClient webClient = WebClient.builder()
.uriBuilderFactory(factory)
.baseUrl(BASE_URL)
.build();
Mono<ResponseDto> response = webClient.get()
.uri(uriBuilder -> uriBuilder
.queryParam("serviceKey", API_KEY)
.queryParam("pageNo", "1")
.queryParam("numOfRows", "1")
.queryParam("entpName", "")
.queryParam("itemName", encodedItemName)
.queryParam("itemSeq", "")
.queryParam("efcyQesitm", "")
.queryParam("useMethodQesitm", "")
.queryParam("atpnWarnQesitm", "")
.queryParam("atpnQesitm", "")
.queryParam("intrcQesitm", "")
.queryParam("seQesitm", "")
.queryParam("depositMethodQesitm", "")
.queryParam("openDe", "")
.queryParam("updateDe", "")
.queryParam("type", "json")
.build())
.retrieve()
.bodyToMono(ResponseDto.class);
return response.flatMap(this::convertToMedication);
}
private Mono<MedicationDto> convertToMedication(ResponseDto responseDTO) {
List<MedicationDto> medicationDtos = responseDTO.getBody().getItems();
if (medicationDtos != null && !medicationDtos.isEmpty()) {
return Mono.just(medicationDtos.get(0));
} else {
return Mono.empty();
}
}
}
Rest API 방식으로 url/MedicationDto/{itemName} 으로 요청을 보내면
itemName에 해당 하는 의약품 정보를 공공포털 api를 이용하여 get 요청을 보내 의약품에 대한 정보를 얻는다.
얻은 정보를 전용 Dto 객체로 파싱하여 저장하고 다른 계층에 적합한 Dto 객체로 다시 변환하여 전송한다.
1차 개선
- SOLID 원칙에서(S) Single Principle Responsibility 원칙에 따라서
- 한 클래스는 하나의 책임을 가져야 한다.
- 공공포털 조회 기능을 service 로직으로 분리
package _PF026.DrDrug.service;
import _PF026.DrDrug.dto.MedicationDto;
import _PF026.DrDrug.dto.ResponseDto;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.DefaultUriBuilderFactory;
import reactor.core.publisher.Mono;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Service
public class DrugOpenAPIService {
private final static String BASE_URL = "http://apis.data.go.kr/1471000/DrbEasyDrugInfoService/getDrbEasyDrugList";
private final String API_KEY = "your api key";
public Mono<MedicationDto> getDrugInfo(String itemName){
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);
String encodedItemName = URLEncoder.encode(itemName, StandardCharsets.UTF_8);
WebClient webClient = WebClient.builder()
.uriBuilderFactory(factory)
.baseUrl(BASE_URL)
.build();
Mono<ResponseDto> response = webClient.get()
.uri(uriBuilder -> uriBuilder
.queryParam("serviceKey", API_KEY)
.queryParam("pageNo", "1")
.queryParam("numOfRows", "1")
.queryParam("entpName", "")
.queryParam("itemName", encodedItemName)
.queryParam("itemSeq", "")
.queryParam("efcyQesitm", "")
.queryParam("useMethodQesitm", "")
.queryParam("atpnWarnQesitm", "")
.queryParam("atpnQesitm", "")
.queryParam("intrcQesitm", "")
.queryParam("seQesitm", "")
.queryParam("depositMethodQesitm", "")
.queryParam("openDe", "")
.queryParam("updateDe", "")
.queryParam("type", "json")
.build())
.retrieve()
.bodyToMono(ResponseDto.class);
return response.flatMap(this::convertToMedication);
}
private Mono<MedicationDto> convertToMedication(ResponseDto responseDTO) {
List<MedicationDto> medicationDtos = responseDTO.getBody().getItems();
if (medicationDtos != null && !medicationDtos.isEmpty()) {
return Mono.just(medicationDtos.get(0));
} else {
return Mono.empty();
}
}
}
- 컨트롤러 코드 수정
package _PF026.DrDrug.controller;
import _PF026.DrDrug.dto.MedicationDto;
import _PF026.DrDrug.service.DrugOpenAPIService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
public class DrugOpenAPIController {
private final DrugOpenAPIService drugOpenAPIService;
@Autowired
public DrugOpenAPIController(DrugOpenAPIService drugOpenAPIService) {
this.drugOpenAPIService = drugOpenAPIService;
}
@GetMapping(value = "/MedicationDto/{itemName}", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<MedicationDto> getDrugInfo(@PathVariable String itemName){
return drugOpenAPIService.getDrugInfo(itemName);
}
}
2차 개선
- SOLID 원칙에서(D) Dependency Inversion Principle 원칙에 따라서 다시 수정
- 구현체가 아닌 인터페이스에 의존함으로써 결합력을 낮추고 응집력을 높힐 수 가 있다.
- 변경된 컨트롤러
package _PF026.DrDrug.controller;
import _PF026.DrDrug.dto.MedicationDto;
import _PF026.DrDrug.service.DrugService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
public class DrugOpenAPIController {
private final PublicApiService publicApiService;
@Autowired
public DrugOpenAPIController(DrugService drugService) {
this.publicApiService = drugService;
}
@GetMapping(value = "/MedicationDto/{itemName}", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<Object> getDrugInfo(@PathVariable String itemName){
return publicApiService.getDrugInfo(itemName);
}
}
- 추가되고 변경된 service 로직
package _PF026.DrDrug.service;
import reactor.core.publisher.Mono;
public interface PublicApiService {
Mono<Object> getInfo(String query);
}
package _PF026.DrDrug.service;
import _PF026.DrDrug.dto.MedicationDto;
import _PF026.DrDrug.dto.ResponseDto;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.DefaultUriBuilderFactory;
import reactor.core.publisher.Mono;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Service
public class DrugOpenAPIService implements PublicApiService {
private final static String BASE_URL = "http://apis.data.go.kr/1471000/DrbEasyDrugInfoService/getDrbEasyDrugList";
private final String API_KEY = "your api key";
public Mono<Object> getDrugInfo(String itemName){
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);
String encodedItemName = URLEncoder.encode(itemName, StandardCharsets.UTF_8);
WebClient webClient = WebClient.builder()
.uriBuilderFactory(factory)
.baseUrl(BASE_URL)
.build();
Mono<ResponseDto> response = webClient.get()
.uri(uriBuilder -> uriBuilder
.queryParam("serviceKey", API_KEY)
.queryParam("pageNo", "1")
.queryParam("numOfRows", "1")
.queryParam("entpName", "")
.queryParam("itemName", encodedItemName)
.queryParam("itemSeq", "")
.queryParam("efcyQesitm", "")
.queryParam("useMethodQesitm", "")
.queryParam("atpnWarnQesitm", "")
.queryParam("atpnQesitm", "")
.queryParam("intrcQesitm", "")
.queryParam("seQesitm", "")
.queryParam("depositMethodQesitm", "")
.queryParam("openDe", "")
.queryParam("updateDe", "")
.queryParam("type", "json")
.build())
.retrieve()
.bodyToMono(ResponseDto.class);
return response.flatMap(this::convertToMedication);
}
private Mono<MedicationDto> convertToMedication(ResponseDto responseDTO) {
List<MedicationDto> medicationDtos = responseDTO.getBody().getItems();
if (medicationDtos != null && !medicationDtos.isEmpty()) {
return Mono.just(medicationDtos.get(0));
} else {
return Mono.empty();
}
}
}
3차 개선
- Lombok 라이브러리를 사용하여 controller 코드 간결화
- @RequiredArgsConstructor를 사용하여 빈으로 등록된 클래스를 이용해 의존성 주입
- 이 어노테이션은 클래스 내부에 final 또는 @NonNull이 붙은 필드에 대해 생성자를 자동으로 생성
- 생성자가 명시적으로 존재하지 않아도, Lombok 라이브러리가 컴파일 시점에 생성자를 추가해준다.
- 따라서 @Autowired 어노테이션이 없어도 스프링이 자동으로 의존성 주입을 해준다.
@RestController
@RequiredArgsConstructor
public class DrugOpenAPIController {
private final PublicApiService drugOpenAPIService;
@GetMapping(value = "/MedicationDto/{itemName}", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<Object> getDrugInfo(@PathVariable String itemName){
return drugOpenAPIService.getInfo(itemName);
}
}
- 해당 코드만 봐서는 구현체가 어떤 클래스로 주입받았는지 생략이 되어 판별하기가 쉽지않다, 그 판별 원리는 다음과 같다.
- 우선적으로 스프링 컨테이너가 해당 타입의 빈을 찾는다. (위에서는 PublicApiService 타입)
- 해당 타입의 빈이 하나만 있다면 그 빈을 주입한다.
- 여러개의 빈이 있다면 빈의 이름(메소드 이름, @Bean 어노테이션의 이름 속성)을 사용하여 빈을 선택
- 만약 일치하는 이름의 빈이 없다면 NoUniqueBeanDefinitionException 발생
- @Primary 어노테이션을 사용하면, 동일한 타입의 빈이 여러 개 있을 때 기본적으로 선택되는 빈을 지정할 수 있다.
- @Qualifier 어노테이션을 사용하면, 동일한 타입의 빈이 여러 개 있을 때 주입될 빈을 명시적으로 지정할 수 있다.
코드 관련 링크
https://suhanlim.tistory.com/222
https://suhanlim.tistory.com/110
도움을 받은 강의 링크
좋은 지식 공유해주셔서 감사합니다 김영환 멘토님