배경
현재 프로젝트에서 짜면서 테스트 클래스에서 반복적으로 사용하는 기능들은 추상화해서 사용하고 있다.
AuthIntegrationTest
@Test
@DisplayName("로그인 성공")
void loginSuccessTest() throws Exception {
// given
LoginDto loginSuccessDto = StubData.MockMember.getLoginSuccessDto();
LoginResponse expectedResponseDto = StubData.MockMember.getLoginResponseDto();
// when
String uri = UriComponentsBuilder.newInstance().path(BASE_URL + "/login")
.build().toUri().toString();
String json = ObjectMapperUtils.asJsonString(loginSuccessDto);
ResultActions actions = ResultActionsUtils.getRequest(mvc, uri, json);
// then
LoginResponse responseDto = ObjectMapperUtils.actionsSingleResponseToLoginDto(actions);
assertThat(expectedResponseDto.getEmail()).isEqualTo(responseDto.getEmail());
assertThat(expectedResponseDto.getNickname()).isEqualTo(responseDto.getNickname());
assertThat(expectedResponseDto.getRole()).isEqualTo(responseDto.getRole());
actions
.andExpect(status().isOk())
.andDo(document("login-success",
getRequestPreProcessor(),
getResponsePreProcessor(),
getLoginSnippet(),
getLonginSuccessResponseSnippet()));
}
ObjectMapperUtils.actionsSingleResponseToLoginDto(actions) 코드의 ObjectMapperUtils 클래스는 JSON 문자열과 객체 간의 변환을 수행하는 유틸리티 클래스이다. ObjectMapperUtils 클래스를 살펴보자
ObjectMapperUtils
public class ObjectMapperUtils {
private static final ObjectMapper objectMapper = new ObjectMapper();
public static String asJsonString(final Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static LoginResponse actionsSingleResponseToLoginDto(ResultActions actions) throws Exception {
String response = resultActionsToResponseAsString(actions);
return objectMapper.readValue(response, LoginResponse.class);
}
public static MemberResponse actionsSingleResponseToMemberDto(ResultActions actions) throws Exception {
String response = resultActionsToResponseAsString(actions);
return objectMapper.registerModule(new JavaTimeModule()).readValue(response, Response.class);
}
public static OrderResponse actionsSingleResponseToOrderDto(ResultActions actions) throws Exception {
String response = resultActionsToResponseAsString(actions);
return objectMapper.registerModule(new JavaTimeModule()).readValue(response, Response.class);
}
private static String resultActionsToResponseAsString(ResultActions actions) throws UnsupportedEncodingException {
String response = actions.andReturn()
.getResponse()
.getContentAsString(StandardCharsets.UTF_8)
.substring(8);
return response.substring(0, response.length() - 1);
}
}
resultActionsToResponseAsString() 메서드는 인코딩 방식을 지정하고 ResponseDto로 역직렬화할 수 있도록 문자열을 자르는 작업을 하고 있다.
actionsSingleResponseTo~() 메서드들을 살펴보면 각각 LoginResponse, MemberResponse, OrderResponse DTO 객체를 반환하도록 하드코딩되어 있다. 3개의 메서드 반환 타입이 다를뿐 로직은 동일했다.
지금은 3개의 메서드뿐이지만 새로운 DTO 클래스가 추가될 때마다 새로운 메서드를 작성해야 하는 번거롭고 유연하지 않은 코드였다. 그래서 리팩토링 할 방법을 고민했다.
제네릭
해당 코드를 해결할 방법으로는 반환하는 객체를 하드코딩하지 않고 동적으로 처리할 수 있도록 하는 것이다. 그래서 제네릭을 사용해 리팩토링하기로 했다.
제네릭이란?
제네릭은 클래스나 메서드에서 사용될 타입을 미리 지정하지 않고, 실행 지점에 구체적인 타입을 결정할 수 있도록하는 기능이다. 제네릭은 클래스, 인터페이스, 메서드에서 사용할 수 있고 사용된는 타입 매개변수는 일반 매개변수처럼 사용할 수 있다.
그래 제네릭은 내부에서 타입을 지정하는 것이 아닌 외부에서 타입을 지정하는 것을 말한다.
제네릭을 사용하면 컴파일 시 타입 체크가 가능해지고, 타입 변환 오류를 줄일 수 있다. 또한, 코드 재사용성과 유지보수성을 높여준다. 제네릭은 < > 안에 타입 매개변수를 지정하여 사용한다.
제네릭으로 클래스나 메서드를 작성하면, 다양한 타입의 객체를 처리할 수 있도록 코드를 짤 수 있다. 이런 코드를 제네릭 코드라고 부른다.
리팩토링 구현
ObjectMapperUtils
public class ObjectMapperUtils {
private static final ObjectMapper objectMapper = new ObjectMapper();
public static String asJsonString(final Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String dtoToJsonString(Object obj) throws JsonProcessingException {
return objectMapper.registerModule(new JavaTimeModule()).writeValueAsString(obj);
}
public static <T> T actionsSingleToDto(ResultActions actions, Class<T> responseClass) throws Exception {
String response = resultActionsToResponseAsString(actions);
return objectMapper.registerModule(new JavaTimeModule()).readValue(response, responseClass);
}
private static String resultActionsToResponseAsString(ResultActions actions) throws UnsupportedEncodingException {
String response = actions.andReturn()
.getResponse()
.getContentAsString(StandardCharsets.UTF_8)
.substring(8);
return response.substring(0, response.length() - 1);
}
}
코드를 살펴보면 actionsSingleResponseTo~() 메서드는 모두 사라지고 actionsSingleToDto() 메서드만이 남아있다.
public static <T> T actionsSingleToDto(ResultActions actions, Class<T> responseClass) throws Exception {
String response = resultActionsToResponseAsString(actions);
return objectMapper.registerModule(new JavaTimeModule()).readValue(response, responseClass);
}
actionsSingleToDto() 메서드는 이전과는 다르게 매개변수로 ResultActions 객체뿐만 Class<T> 타입의 인자를 받아 T타입의 객체를 반환하고 있다.
여기서 제네릭 타입 T는 메서드를 호출하는 시점에 결정된다. 이 덕분에 다양한 타입의 객체를 반환할 수 있는 유연한 코드를 작성할 수 있다.
예를 들어, actionsSingleToDto() 메서드를 호출할 때, 두 번째 인자로
LoginResponse.class 를 전달하면 T 타입은 LoginResponse.class 로 결정되고, 해당 타입의 객체가 반환된다.
LoginResponse responseDto = ObjectMapperUtils.actionsSingleToDto(actions, LoginResponse.class);
마찬가지로 MemberResponse.class 를 전달하면 MemberResponse.class 객체가 반환된다.
MemberResponse responseDto = ObjectMapperUtils.actionsSingleToDto(actions, MemberResponse.class);
테스트
이전과 마찬가지로 테스트가 잘 통과된다!
마무리
제네릭에대해 공부는 해왔었지만 직접 제네릭을 사용해서 리팩토링하는건 처음이었다.
앞으로 제네릭을 잘사용하면 유연한 코드를 만들 수 있을 것 같다. 자주 사용해봐야지ㅎㅎ
'Java' 카테고리의 다른 글
Java - @JasonCreator로 DTO에서 유연하게 Enum Type 받기 (0) | 2023.04.26 |
---|---|
Java - 커스텀 애너테이션으로 유효성 검사하기 (0) | 2023.04.22 |
Java - @NotNull, @NotEmptty, @Notblank 차이점 알고 쓰시나요? (0) | 2023.04.04 |
Java - AES-128 양방향 암호화하기 (0) | 2023.03.29 |
Java - String 메소드 총정리! (0) | 2022.11.30 |