깃허브에서 다른 프로젝트의 코드를 보다가 사용자가 입력한 문자열 값을 trim() 메서드로 앞뒤 공백을 제거하는 코드를 보았다. 이 코드를 보다보니 내 프로젝트에서도 특정 문자열에 대해서는 trim 이 필요하겠다고 생각이 들었다.

예를 들어 제목이나 닉네임 같이 중복이 허용되지 않는 필드가 있을 때, 만약 앞뒤 공백을 자르지 않고 사용자가 입력한 값을 그대로 저장한다면 ‘lucy’, ‘lucy ‘ 두 문자열은 서로 다른 값으로 인식되므로 둘 다 저장될 수 있다.

그래서 트리밍을 내 프로젝트에 간단히 추가하려고 보니 또다른 문제가 있었다. 바로 어디에서 문자열에 trim 을 적용할 것이냐는 문제였다.

일단 가장 간단히 생각해보면 데이터를 전달하는 객체인 DTO에 넣을 수도 있다. 반대로 DTO는 값 전달 객체로 두고, 서비스에서 비즈니스 로직의 일부로 트리밍을 할수도 있다. 두 방식의 장단점을 비교해보면 아래와 같다.

DTO

서비스(비즈니스 레이어)

이런 각각의 장단점이 있고, 결국 선택은 팀이나 개인의 철학과 스타일에 따라 달라질 수 있다고 생각한다. 하지만 개인적으로는 DTO 는 값 전달 객체, 서비스는 전달된 값을 정규화 하고 비즈니스 로직을 처리하는 레이어 라고 보는 것이 가장 단순하고 직관적이라고 생각하기 때문에 결론적으로 문자열 트리밍은 서비스 레이어에서 처리하기로 결정하였다.

그런데 서비스 레이어에서 생성, 업데이트 메서드 각각마다 trim 메서드를 호출한다면 중복이 아니냐 라고 생각할수도 있다. 하지만 이는 DTO도 마찬가지이다. 보통 DTO도 생성, 업데이트 전용 객체로 클래스를 분리하기 때문에 DTO와 서비스 레이어 어떤 곳에서 값을 가공하여도 중복이 발생한다는 점은 똑같다.

다만 서비스는 보통 클래스 안에서 메서드 단위로 요청을 분리하고, DTO는 클래스 단위로 요청을 분리하다보니 서비스 레이어쪽이 조금 더 중복이 잘 보일 뿐 결과적으로 DTO 에서도 중복이 발생하는 것은 마찬가지이기 때문에 둘 중 어느곳에서 값을 가공할까 를 결정할 때 중복은 고려대상이 되기 어렵다.

그리고 DTO 에서 값을 가공하고, 나머지 예외적으로 DTO를 거치지 않는 로직에서만 직접 값을 가공하는 것이 중복을 최소화 하는 방식이 아니냐고 말할수도 있다. 이는 맞는 말이긴 하지만, 개인적으로 어디에서는 DTO를 믿고, 어디에서는 직접 값을 가공하게 되면 나중에 수정이 일어날 때 도대체 어디에서 DTO를 거치지 않는 지 한번에 기억하기에는 좀 어렵다고 판단하였다. 사람은 시간이 지나면 항상 까먹을 수 있으므로, 차라리 값을 가공하는 로직은 무조건 비즈니스 레이어에서만 한다 라는 규칙만 기억하게 하는 것이 유지보수 지점은 늘어날지라도 더 실수 가능성을 줄일 수 있다고 보았다.

또한 어떤 방법을 사용하든 엔티티에서는 무조건 trim 을 하는 방식으로 결정하였다. 이유는 trim 같은 값을 가공하는 로직은 모든 값에 대해 적용되지 않는다. 그래서 어떤 값에 적용해야 하는 지 가끔 까먹을 수 있고, 이를 확실하게 방지하기 위해 엔티티에도 중복이긴 하지만 마지막 안전망으로 trim 을 적용하는 것이 좋겠다고 생각이 들었다.

예를 들어 소설의 제목을 저장할 때 제목이 중복되면 안된다 라는 규칙이 있다면, 실수로 trim 을 하지 않고 중복 체크를 했을 때 “제목1”, “제목1 “ 두 제목은 중복이 아닌 것으로 나오게 된다. 그래서 이 값을 그대로 엔티티에 넣어서 저장을 하려고 시도하게 되는데 만약 엔티티에 this.title = title.trim() 이라는 코드가 있었고, title에 UNIQUE 제약조건이 걸려있었다면 DB에 저장될 때 UNIQUE 제약조건 에러가 발생하게 된다.

이렇게 되면 초기에 버그를 바로 발견할 수 있어 더 안전한 애플리케이션을 개발할 수 있게 된다. 그래서 이런 이유로 엔티티에도 trim 을 하여 최종적으로 저장하도록 구현하였다.

그런데 이렇게 되면 이전에 말한 엔티티에서의 검증과 비슷한 맥락으로 유지보수 복잡도가 증가하지 않냐는 의문이 들수도 있다. 표면적으로는 비즈니스 레이어 + 엔티티 두 곳에서 중복으로 trim 을 호출하므로 맞는 말일 수 있다.

하지만 trim은 검증보다는 구현하기 위한 코드가 적다. 그냥 title.trim() 메서드를 호출하면 끝이라서 만약 trim 을 할 필요가 없어지더라도 그냥 엔티티 한 곳에서 trim 삭제, 나머지 비즈니스 레이어들에서 trim 삭제만 이루어지면 된다. 즉 검증보다는 실수 가능성이 적다.