주요 기능의 구현을 마치고, 안정성을 높이기 위해 예외처리의 방법에 대해 더 공부하는 시간을 가져보았다.
현재 우리 팀의 프로젝트에서는 Service에서 request의 값이 유효한 값인지 확인하고 예외 발생시키는 방법으로 예외처리를 하고 있었다.
override fun createReply(feedId: Long, request: ReplyCreateRequest ): ReplyResponse {
if(request.userName.length !in 1 .. PASSWORD_MAX)
throw InputLengthException("UserName",request.userName.length,1,USERNAME_MAX)
else if(request.password.length !in PASSWORD_MIN .. PASSWORD_MAX)
throw InputLengthException("Password",request.password.length,PASSWORD_MIN,USERNAME_MAX)
else if(request.contents.length !in 1 .. CONTENTS_MAX)
throw InputLengthException("Contents",request.password.length,1,CONTENTS_MAX)
단순하게 보면 제대로 예외처리는 되었으니 문제 없다고 볼 수 있지만, 어느 계층에서 어떤 에러를 처리할 것인지에 대한 정책도 제대로 설정하지 않으면 프로젝트가 커질 수록 예외처리에 대한 내용이 중구난방이 되어 예외를 놓쳐버리거나 중복으로 체크하여 비효율적이 될 수 있다고 한다.
이에 대한 내용은 확실한 정답이 정해져 있는 것은 아니고 아직 이슈가 되고 있는 내용인듯 했다.
Validation(입력값 검증)의 최적의 장소는 어디일까?
https://starkying.tistory.com/entry/Model-객체에서만큼은-Validation을-필수로-하자
[우아한 테크 코스 5기] 가장 강력한 예외 처리 방법은 무엇이 있을까요?
https://heesangstudynote.tistory.com/109
아직은 이해하기 어려운 내용들이 많았지만 우리 프로젝트에서 적용시킨다면 크게 두 가지로 나눌 수 있다고 생각했다. 입력에 대한 검증과 비지니스 요구사항에 대한 검증.
이중 입력에 대한 검증은 Request 클래스에서 @field:NotBlank 와 @Min(1) @Max(5) 어노테이션을 설정해두고 Controller 에서 @RequestBody 앞에 @Valid 를 붙여 유효성을 검사할 수 있도록 하였다. 이것으로 1차적인 Request 입력 오류를 방지할 수 있었다.
비즈니스 요구사항에 대한 검증은 도메인에서 실시해보기로 하였다.
처음 시도한 방법은 아래와 같았다.
@Column(name="user_name")
var userName = _userName
.also{if(it.isEmpty() || it.length > USERNAME_MAX)
throw InputLengthException("UserName",_userName.length,1,USERNAME_MAX)
}
@JsonIgnore
@Column(name = "password")
var password = _password
.also{if(it.length < 4 || it.length > PASSWORD_MAX)
throw InputLengthException("Password",_password.length,PASSWORD_MIN,PASSWORD_MAX)
}
@Column(name = "content")
var content = _content
.also{if(it.isEmpty() || it.length > CONTENT_MAX)
throw InputLengthException("Content",_content.length,1,CONTENT_MAX)
}
클래스의 생성자로 _userName, _Password 등을 입력받은 뒤, 클래스의 내용에서 값의 유효성을 체크하는 방법이다.
하지만 이 방법은 문제가 있었다. 객체를 생성할 때는 제대로 처리되지만, 이미 생성된 객체를 업데이트할 때는 아래 과정이 수행되지 않는 것이다..
두번째 방법은 아예 validate() 메서드를 생성하여 검증이 필요할 때 마다 호출하도록 하는 것이었다. 이 방법이라면 create, update 두 상황에서 모두 한 메서드를 호출하여 확인할 수 있고, 이후에도 검증이 필요할 때 언제든 불러올 수 있어 적합한 방법으로 판단되었다.
init{
validate()
}
fun validate(){
if(userName.isEmpty() || userName.length > USERNAME_MAX)
throw InputLengthException("UserName",userName.length,1,USERNAME_MAX)
if(password.length < 4 || password.length > PASSWORD_MAX)
throw InputLengthException("Password",password.length,PASSWORD_MIN,PASSWORD_MAX)
if(content.isEmpty() || content.length > CONTENT_MAX)
throw InputLengthException("Content",content.length,1,CONTENT_MAX)
}
각 도메인에 알맞는 내용 값을 넣어 검증할 수 있고, 에러를 검증할 때는 어디에서 어떤 이유인지 메세지로 전달하도록 하였다.
다행히 모든 요청과 응답에 정상 작동 확인 후 PR 완료!
처음 예외처리에 대해 배울땐 그저 예외메세지를 발생시키는 것만 되면 끝이라고 생각했는데 항상 더 큰 규모의 개발을 염두해두고 진행해야 함을 다시 느꼈다. 어디서 어떻게 처리되느냐에 따라 의미가 달라질 수 있고, 안정성과 효율성뿐만 아니라 유지보수에서도 큰 차이를 가져올 내용이기 때문이다. 앞으로도 코드 한줄 한줄의 의미를 고민해보는 노력을 더해야 겠다.