이번 글에서는 코드의 중복 문제와 코드 재사용성에 대한 문제점을 살펴보겠습니다.
@RestController
@RequestMapping
public class UserController {
@RequestMapping("addUser")
public String addUser(UserParam userParam) {
if (StringUtils.isEmpty(userParam.getUserName())) {
return "사용자 이름을 입력해주세요";
}
if (StringUtils.isEmpty(userParam.getPhone())) {
return "휴대폰 번호를 입력해주세요";
}
if (userParam.getPhone().length() > 11) {
return "휴대폰 번호는 11자리를 초과할 수 없습니다";
}
if (StringUtils.isEmpty(userParam.getEmail())) {
return "이메일을 입력해주세요";
}
// 다른 매개 변수 검사를 생략했습니다
// todo : 사용자 정보 테이블에 삽입
return "SUCCESS";
}
}
위의 코드는 사용자를 추가하는 기능과 사용자 정보를 편집하는 기능이 있습니다. 두 가지 기능 모두 사용자의 정보를 검증하고, 그 결과에 따라 성공 또는 실패 메시지를 반환합니다.
그러나 두 기능 모두 동일한 검증 로직을 갖고 있습니다. 따라서 두 메서드에서 중복 코드가 발생합니다. 이렇게 중복 코드가 발생하면 유지 보수가 어려워질 뿐만 아니라, 코드의 재사용성도 떨어지게 됩니다.
이러한 문제를 해결하기 위해 코드의 중복을 제거하고 코드의 재사용성을 높이기 위해 검증 로직을 별도의 메서드로 분리해야 합니다. 이렇게 하면 두 메서드에서 동일한 검증 로직을 반복하지 않아도 되므로 유지 보수가 용이해집니다.
따라서 검증 로직을 별도의 메서드로 분리하여 다음과 같이 코드를 수정할 수 있습니다:
개선 된 코드
@RestController
@RequestMapping
public class UserController {
@RequestMapping("addUser")
public String addUser(UserParam userParam) {
String validationMessage = validateUserParam(userParam);
if (validationMessage != null) {
return validationMessage;
}
// todo: insert user information into database
return "SUCCESS";
}
@RequestMapping("editUser")
public String editUser(UserParam userParam) {
String validationMessage = validateUserParam(userParam);
if (validationMessage != null) {
return validationMessage;
}
// todo: update user information in database
return "SUCCESS";
}
private String validateUserParam(UserParam userParam) {
if (StringUtils.isEmpty(userParam.getUserName())) {
return "사용자 이름을 입력하세요.";
}
if (StringUtils.isEmpty(userParam.getPhone())) {
return "전화번호를 입력하세요.";
}
if (userParam.getPhone().length() > 11) {
return "전화번호는 11자 이하여야 합니다.";
}
if (StringUtils.isEmpty(userParam.getEmail())) {
return "이메일을 입력하세요.";
}
// other validation logic
return null;
}
}
이렇게 코드를 수정하면 중복 코드를 제거하고 코드의 재사용성을 높일 수 있습니다. 이제 검증 로직이 필요한 모든 메서드에서 validateUserParam() 메서드를 호출하기만 하면 됩니다.
우리는 validation 라이브러리를 설치하고 조금 더 쉽게 유효값을 체크 할수 있다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
이후 아래와 같이 코딩 할수 있다.
public class UserParam {
@NotNull(message = "사용자 이름은 필수 입력사항입니다.")
private String userName;
@NotNull(message = "핸드폰 번호는 필수 입력사항입니다.")
@Max(value = 11, message = "11자리 이내의 핸드폰 번호를 입력해주세요.")
private String phone;
@NotNull(message = "이메일은 필수 입력사항입니다.")
private String email;
// 생략...
...
}
아래는 @Validated 어노테이션이 추가된 UserParam 파라미터 객체를 사용하여 BindingResult 객체에 에러 메시지를 수신하는 코드입니다.
@RequestMapping("addUser")
public String addUser(@Validated UserParam userParam, BindingResult result) {
List<FieldError> fieldErrors = result.getFieldErrors();
if (!fieldErrors.isEmpty()) {
return fieldErrors.get(0).getDefaultMessage();
}
//todo: 사용자 정보를 데이터베이스에 삽입
return "SUCCESS";
}
인터페이스 통일 응답 객체 반환
프로젝트 코드에서 컨트롤러 레이어의 응답 결과를 보면 다음과 같은 것이 있을 수 있습니다.
@RequestMapping("/hello")
public String getStr(){
return "hello, 산달라떼를 좋아하는 소년";
}
// 반환:
hello, 산달라떼를 좋아하는 소년
또는 다음과 같은 것도 있을 수 있습니다.
@RequestMapping("queryUser")
public UserVo queryUser(String userId) {
return new UserVo("666", "산달라떼를 좋아하는 소년");
}
// 반환:
{"userId":"666","name":"산달라떼를 좋아하는 소년"}
인터페이스 반환 결과가 일치하지 않으면, 프론트엔드 처리가 불편하며, 코드도 유지보수하기 어려워집니다. 예를 들어 작은 달팽이는 결과를 처리할 때 Result를 사용하고, 큰 달팽이는 Response를 사용하는 것을 좋아합니다. 이러한 코드가 얼마나 혼란스러울지 상상할 수 있습니다.
그래서 백엔드 개발자로서, 프로젝트의 응답 결과는 통일된 표준 반환 형식이 필요합니다. 일반적으로 표준 응답 메시지 객체는 어떤 속성을 가지고 있나요?
code: 응답 코드 message: 응답 결과 data: 반환 데이터
응답 코드는 일반적으로 열거형으로 표시
public enum CodeEnum {
/**작업 성공**/
SUCCESS("0000","작업 성공"),
/**작업 실패**/
ERROR("9999","작업 실패"),;
/**
* 사용자 정의 상태 코드
**/
private String code;
/**사용자 정의 설명**/
private String message;
CodeEnum(String code, String message){
this.code = code;
this.message = message;
}
public String getCode() {
return code;
}
public String getMessage() {
return message;
}
}
응답객체를 한번 만들어보겠습니다.
public class BaseResponse<T> {
// 상태 코드 (0000: 성공, 9999: 실패)
private String code;
// 결과 메시지
private String message;
// 반환할 데이터
private T data;
// 성공 응답 생성
public static <T> BaseResponse<T> success(T data) {
BaseResponse<T> response = new BaseResponse<>();
response.setCode(CodeEnum.SUCCESS.getCode());
response.setMessage(CodeEnum.SUCCESS.getMessage());
response.setData(data);
return response;
}
// 실패 응답 생성
public static <T> BaseResponse<T> fail(String code, String message) {
BaseResponse<T> response = new BaseResponse<>();
response.setCode(code);
response.setMessage(message);
return response;
}
// 코드 설정
public void setCode(String code) {
this.code = code;
}
// 메시지 설정
public void setMessage(String message) {
this.message = message;
}
// 데이터 설정
public void setData(T data) {
this.data = data;
}
}
controller 를 한번 더 개선해보 겠습니다.
@RequestMapping("/hello")
public BaseResponse<String> getStr(){
return BaseResponse.success("hello,산달라떼를 좋아하는 소년");
}
// 출력 결과
{"code":"0000","message":"작업 성공","data":"hello,산달라떼를 좋아하는 소년"}
@RequestMapping("queryUser")
public BaseResponse<UserVo> queryUser(String userId) {
return BaseResponse.success(new UserVo("666", "산달라떼를 좋아하는 소년"));
}
// 출력 결과
{"code":"0000","message":"작업 성공","data":{"userId":"666","name":"산달라떼를 좋아하는 소년"}}
아참 그리고 예외처리 부분도 공통으로 한번 해보겠습니다.
public class BizException extends RuntimeException {
private String retCode;
private String retMessage;
public BizException() {
super();
}
public BizException(String retCode, String retMessage) {
this.retCode = retCode;
this.retMessage = retMessage;
}
public String getRetCode() {
return retCode;
}
public String getRetMessage() {
return retMessage;
}
}
controller 쪽에서 아래와 같은 코드가 있을겁니다.
@RequestMapping("/query")
public BaseResponse<UserVo> queryUserInfo(UserParam userParam) {
try {
return BaseResponse.success(userService.queryUserInfo(userParam));
} catch (BizException e) {
//doSomething
} catch (Exception e) {
//doSomething
}
return BaseResponse.fail(CodeEnum.ERROR.getCode(),CodeEnum.ERROR.getMessage());
}
뭐 정상 작동되고 아무런 문제 없어 보입니다. 네 그렇습니다. 아무런 문제는 없죠. 하지만 try …catch 블럭이 너무 많을것으로 예상됩니다. 로직이 더 복잡해진다고 하면 5개 넘어 갈듯합니다.
spring의 @RestControllerAdvice 를 이용하여 Controller의 AOP 를 작성해 볼겠습니다. 일반적으로 @RestControllerAdvice 와 @ExceptionHandler를 결합해서 사용합니다.
@RestController
public class UserController {
@Autowired
private UserService userService;
// 사용자 정보 조회 API
@RequestMapping("/query")
public BaseResponse<UserVo> queryUserInfo1(UserParam userParam) {
return BaseResponse.success(userService.queryUserInfo(userParam));
}
}
@Service
public class UserServiceImpl implements UserService {
// 사용자 정보 조회 메소드에서 발생할 수 있는 비즈니스 예외 처리
@Override
public UserVo queryUserInfo(UserParam userParam) throws BizException {
throw new BizException("6666", "테스트 예외 클래스");
}
}
@RestControllerAdvice 을 추가 합니다.
@RestControllerAdvice(annotations = RestController.class)
public class ControllerExceptionHandler {
}
추가적으로 BizException 타입을 가로채고 싶다면, @ExceptionHandler 어노테이션으로 수정한 새로운 메서드를 추가합니다.
@RestControllerAdvice(annotations = RestController.class)
public class ControllerExceptionHandler {
// 비즈니스 예외 처리 메소드
@ExceptionHandler(BizException.class)
@ResponseBody
public BaseResponse<Void> handler(BizException e) {
System.out.println("비즈니스 예외 발생: " + e.getRetCode() + " " + e.getRetMessage());
return BaseResponse.fail(CodeEnum.ERROR.getCode(), CodeEnum.ERROR.getMessage());
}
}
결론적으로
여기서 알수 있는 것들:
- 더욱 우아하고 간결하며 유지보수하기 쉬운 코드를 작성하기 위해서는 매개변수 유효성 검사, 통일된 응답 객체 반환, 예외 처리를 통일해야 합니다.
- 매개변수 유효성 검사를 보다 간결하게 작성하기 위해서는 어노테이션을 사용할 수 있습니다.
- 통일된 응답 객체는 일반적으로 상태 코드, 설명 메시지 및 반환 데이터를 포함해야 합니다.
- 컨트롤러 레이어에서 전체 예외 처리를 통일하기 위해서는 @RestControllerAdvice와 @ExceptionHandler를 함께 사용할 수 있습니다.
- 추가적으로, 진보된 사용자는 자체적으로 커스텀 어노테이션을 구현할 수 있으며, @RestControllerAdvice의 구현 원리를 살펴보는 것이 좋습니다. 이는 사실상 하나의 aspect 어노테이션으로 작동하기 때문입니다.
내저장소 바로가기 luxury515
'Back-end > 기타' 카테고리의 다른 글
RequestBodyAdvice 와 ResponseBodyAdvice 사용법 (0) | 2023.04.17 |
---|---|
@Transactionl 이 무용지물이 된 케이스 (0) | 2023.04.17 |
필터(Filter) vs 인터셉터(Intercepter) (0) | 2023.04.17 |
on duplicate key update (0) | 2023.04.16 |
AWS KMS 관련 세팅 (0) | 2023.04.14 |