Github에 Playground(놀이터)라는 이름으로 Repository를 하나 만들었습니다.
앞으로 이 Repository는 제 소스코드 저장소가 될 예정입니다.
그 시작으로 예전에 gitlab에 저장해 뒀던 Java에서 Rest API를 호출하는 모듈을 리팩터링 해봤습니다.
그전에.. 얼마 전에 봤던 인상 깊은 글의 내용을 소개하고자 합니다.
얼마 전에 커리어리라는 커뮤니티에서 한 시니어 개발자 분이 쓴 "코드 가독성과 빼기의 미학"이라는 제목의 글을 봤다.
사실 내용은 어느정도 알고 있는 내용이었고, 여기서 가장 큰 수확은 "코딩 주짓수"라는 재밌는 단어를 알게 된 것이다.
요약하자면 "코딩 주짓수"하지 말고 읽기 쉬운 코드를 작성하라는 것이다.
구글 코딩 가이드라인을 보면, 코드 가독성(Readability)를 강조한다고 한다. 읽기 쉬운 코드를 작성해야, 본인이 나중에 본인의 코드를 볼 때도 도움이 되고, 타인이 볼 때도 도움이 되어 유지보수성 향상에 도움이 된다는 것이다.
회사에서 신입으로 입사 후 시니어분께 처음 주입받은 개념 중 하나는 "코딩 주짓수 하지 마라"와 비슷한 내용의 개념이었다. 너무 과한 모듈화, 추상화는 오히려 유지보수성을 해치고 코드를 이해할 수 없게 만든다는 말이었다.
점차 그 논리를 이해해가고 있다.
정리하자면 아래와 같다.
- 코드가 단순할 수록 이해하기 쉽고, 수정할 때 실수를 줄일 수 있으며, 팀 협업에서 더 효율적이다.
- 불필요하게 복잡한 코드는 버그를 유발하기 쉽고, 이해하기 어려워서 비효율적이다.
- 아직 필요하지 않은 것들을 과하게 고려하는 건 오히려 불필요한 코드만 생성될 뿐이다.
- 과도한 설계나 복잡한 논리를 피하고 간단한 해결책을 찾는 게 낫다.
- Simple is Best
이번 작업 역시 코딩 주짓수를 시도해봤으나 결국은 어느 정도 간단한 형태로 되돌아오게 되었습니다...
Github: RestApiService
이 코드는 Lombok에 의존합니다. 참고하세요.
사용법
RestApiService restApiService = new RestApiService();
// 1. url
String url = "https://jsonplaceholder.typicode.com/comments";
// 2. query parameters
QueryParams queryParams = new QueryParams()
.add("postId", "1")
.add("page", "2");
// 3. Http Headers
ExtraHeaders headers = new ExtraHeaders()
.add("Content-Type", "application/json")
.add("access_token", "test-123-123");
// 4. Request Body
PostAddDto postUpdateDto = new PostAddDto(); // some-object
// 5. execute
ApiResponse response = restApiService.post(url, queryParams, headers, postUpdateDto);
1. url
base url을 세팅해줍니다.
물론 이 url에 query parameters까지 모두 포함시켜도 됩니다.
ex) https://jsonplaceholder.typicode.com/comments?postId=1&page=2
2. QueryParams
Map<String, String>을 감싸고 있습니다.
query parameter 들을 key - value 형태로 추가할 수 있습니다.
add와 동시에 validation이 되어 잘못된 값은 무시합니다.
3. ExtraHeaders
Map<String, String>을 감싸고 있습니다.
Http Header에 추가할 값들을 key - value 형태로 추가할 수 있습니다.
마찬가지로, add와 동시에 validation이 되어 잘못된 값은 무시합니다.
4. Request Body
HttpRequest Body에 해당하는 객체를 넣습니다.
GET, DELETE의 경우에도 사용은 가능하게 해 뒀습니다.
5. 실행
RestApiService는 get(), post(), put(), delete() 메서드를 지원합니다.
여러 가지 상황으로 오버로딩 되어있고, 없다면 매개변수를 넣고, 사용하지 않는 매개변수는 null로 대입하여 사용할 수 있습니다.
소스코드
class RestApiService
@Slf4j
public class RestApiService {
private final RestTemplate restTemplate = new RestTemplate();
/**
* execute HTTP GET Request
*
* @param url : base url
* @param queryParams : query parameters
* @param extraHeaders : Http Headers
* @param data : Http Body (optional)
* @return ApiResponse(status, body, errorMessage)
*/
public ApiResponse get(String url, QueryParams queryParams, ExtraHeaders extraHeaders, Object data) {
return this.getResult(HttpMethod.GET, url, queryParams, extraHeaders, data);
}
public ApiResponse get(String url) {
return this.get(url, null, null, null);
}
public ApiResponse get(String url, ExtraHeaders extraHeaders) {
return this.get(url, null, extraHeaders, null);
}
// POST
public ApiResponse post(String url, QueryParams queryParams, ExtraHeaders extraHeaders, Object data) {
return this.getResult(HttpMethod.POST, url, queryParams, extraHeaders, data);
}
public ApiResponse post(String url, Object body) {
return this.post(url, null, null, body);
}
public ApiResponse post(String url, ExtraHeaders extraHeaders, Object body) {
return this.post(url, null, extraHeaders, body);
}
// PUT
public ApiResponse put(String url, QueryParams queryParams, ExtraHeaders extraHeaders, Object data) {
return this.getResult(HttpMethod.PUT, url, queryParams, extraHeaders, data);
}
public ApiResponse put(String url, Object body) {
return this.put(url, null, null, body);
}
public ApiResponse put(String url, ExtraHeaders extraHeaders, Object body) {
return this.put(url, null, extraHeaders, body);
}
// DELETE
public ApiResponse delete(String url, QueryParams queryParams, ExtraHeaders extraHeaders, Object data) {
return this.getResult(HttpMethod.DELETE, url, queryParams, extraHeaders, data);
}
public ApiResponse delete(String url, Object body) {
return this.delete(url, null, null, null);
}
public ApiResponse delete(String url, ExtraHeaders extraHeaders) {
return this.delete(url, null, extraHeaders, null);
}
/**
* HTTP Request
*
* @param httpMethod GET / POST / PUT / DELETE
* @param baseUrl Request URL
* @param queryParameters query parameter map
* @param extraHeaders http header map
* @param body Request Body
* @return ApiResponse(status, body, errorMessage)
*/
private ApiResponse getResult(HttpMethod httpMethod,
String baseUrl,
QueryParams queryParameters,
ExtraHeaders extraHeaders,
Object body) {
//baseUrl validation
if (baseUrl == null || baseUrl.isEmpty()) {
throw new IllegalArgumentException("Base URL cannot be null or empty value");
}
try {
String url = RestApiUtils.buildFullUrl(baseUrl, queryParameters);
HttpHeaders headers = RestApiUtils.generateHttpHeaders(extraHeaders);
log.info("[{}] API Request: URL=[{}], Headers=[{}], Body=[{}]", httpMethod, url, headers, body);
long startTime = System.currentTimeMillis();
HttpEntity<Object> request = new HttpEntity<>(body, headers);
ResponseEntity<String> response = restTemplate.exchange(url, httpMethod, request, String.class);
log.info("[{}] API Response: Status=[{}], TimeElapsed={}ms, Body=[{}]",
httpMethod, response.getStatusCode(), System.currentTimeMillis() - startTime, response.getBody());
return new ApiResponse(response.getStatusCode(), null, response.getBody());
} catch (HttpClientErrorException | HttpServerErrorException e) {
return new ApiResponse(e.getStatusCode(), e.getResponseBodyAsString(), e.getMessage());
} catch (RestClientException e) {
return new ApiResponse(HttpStatus.INTERNAL_SERVER_ERROR, null, e.getMessage());
} catch (Exception e) {
return ApiResponse.unexpectedError();
}
}
}
class QueryParams
/**
* Http Request Url 에 포함될 query parameter 값들을 감싼 클래스
* <br> - 설계시, Map data 에는 잘못된 값이 들어갈 수 없도록 한다.
*/
@ToString
@Getter
public class QueryParams {
private Map<String, String> data;
public QueryParams() {
this.data = new HashMap<>();
}
public QueryParams(Map<String, String> data) {
if(data == null){
this.data = new HashMap<>();
return;
}
for(Map.Entry<String, String> entry : data.entrySet()){
this.add(entry.getKey(), entry.getValue());
}
}
public QueryParams add(String key, String value) {
if(!RestApiUtils.validateKeyValue(key, value)){
return this;
}
this.data.put(key, value);
return this;
}
public String getQueryParameterString(){
return "";
}
}
class ExtraHeaders
/**
* Http Request Header 에 포함될 값들을 감싼 클래스
* <br> - 설계시, Map data 에는 잘못된 값이 들어갈 수 없도록 한다.
*/
@ToString
@Getter
public class ExtraHeaders {
Map<String, String> data;
public ExtraHeaders() {
this.data = new HashMap<>();
}
public ExtraHeaders(Map<String, String> data) {
if(data == null){
this.data = new HashMap<>();
return;
}
for(Map.Entry<String, String> entry : data.entrySet()){
this.add(entry.getKey(), entry.getValue());
}
}
public ExtraHeaders add(String key, String value){
if(!RestApiUtils.validateKeyValue(key, value)){
return this;
}
this.data.put(key, value);
return this;
}
}
class ApiResponse
@ToString
@Getter
@Setter
@AllArgsConstructor
public class ApiResponse {
private HttpStatus status;
private String errorMessage;
private Object body;
public static ApiResponse unexpectedError(){
return new ApiResponse(HttpStatus.INTERNAL_SERVER_ERROR, "unexpected error",null);
}
}
class RestApiUtils
public class RestApiUtils {
/**
* HTTP Header 객체 생성
* @param extraHeaders ExtraHeaders Object
* @return
*/
public static HttpHeaders generateHttpHeaders(ExtraHeaders extraHeaders) {
if (extraHeaders == null || extraHeaders.getData() == null || extraHeaders.getData().isEmpty()) {
return new HttpHeaders();
}
HttpHeaders headers = new HttpHeaders();
for (Map.Entry<String, String> entry : extraHeaders.getData().entrySet()) {
headers.add(entry.getKey(), entry.getValue());
}
return headers;
}
/**
* 전체 URL 생성 (base url + query params)
* @param queryParams QueryParams Object
* @return full url with query parameters
*/
public static String buildFullUrl(String baseUrl, QueryParams queryParams){
if(baseUrl == null || baseUrl.isEmpty()){
throw new IllegalArgumentException("Base Url must not be null or empty");
}
if(queryParams == null || queryParams.getData() == null || queryParams.getData().isEmpty()){
return baseUrl;
}
//make queryParameter String
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
for (Map.Entry<String, String> entry : queryParams.getData().entrySet()) {
builder.queryParam(entry.getKey(), entry.getValue());
}
String queryParameterString = builder.build().toString();
if(baseUrl.contains("?")){
return baseUrl + queryParameterString.replaceAll("\\?", "&");
}
return baseUrl + queryParameterString;
}
/**
* key, value String validation
* @param key Map key
* @param value Map value
* @return Boolean result
*/
public static boolean validateKeyValue(String key, String value){
if(key == null || key.isEmpty()){
return false;
}
if( value == null || value.isEmpty()) {
return false;
}
return true;
}
}
'Projects & Playground' 카테고리의 다른 글
[java/Playground] Apache POI 엑셀 파일 다루기 (0) | 2024.09.04 |
---|---|
[Java/Spring] 백엔드 개발 프로젝트 명세서 (Fitness Record) (1) | 2023.11.19 |
[Java/Spring]초보 백엔드 개발자 게시판 프로젝트 - 명세서 만들어보기 (2) | 2023.05.29 |