리뷰 서비스 품질 개선

리뷰 서비스 품질 개선

1. 들어가며

서비스를 개발하다 보면 화면을 구현하는 것보다 그 이후를 고민하는 시간이 더 길어질 때가 있습니다.

최근 관광지 콘텐츠 서비스의 리뷰 기능을 개발하면서 그런 경험을 했습니다.

리뷰 기능은 사용자가 직접 후기를 작성하고 사진을 등록할 수 있는 기능입니다.

기능 자체는 비교적 단순해 보입니다. 사용자가 리뷰 내용을 입력하고 이미지를 업로드하면 서버에 저장하고, 저장된 데이터를 다시 화면에 보여주면 됩니다.

처음에는 리뷰가 정상적으로 등록되고 조회되는 것에 집중했습니다. 하지만 기능 구현을 마친 뒤 운영 환경을 기준으로 다시 살펴보니 생각보다 많은 질문이 생겼습니다.

  • 사용자가 예상하지 못한 문자열을 입력하면 어떻게 될까?

  • HTML 태그나 스크립트 형태의 입력이 저장되면 어떻게 될까?

  • 이미지가 아닌 파일을 이미지처럼 업로드하면 어떻게 될까?

  • 리뷰 수정 과정에서 파일 정보와 실제 저장 데이터가 서로 다른 상태가 되면 어떻게 될까?

개발 환경에서는 문제가 없어 보였던 기능도 운영 관점에서 다시 바라보니 보완해야 할 부분이 적지 않았습니다.

이번 글에서는 리뷰 기능을 개선하면서 적용했던 입력값 검증, 이미지 업로드 검증, 그리고 데이터 정합성 관리 경험을 공유하고자 합니다.

2. 운영 환경에서 발견한 문제

리뷰 기능은 사용자가 직접 콘텐츠를 생성하는 대표적인 기능입니다.

관리자 화면과 달리 사용자는 어떤 값을 입력할지 예측하기 어렵고, 개발자가 의도하지 않은 방식으로 기능을 사용할 수도 있습니다.

운영 환경을 기준으로 검토하면서 다음과 같은 문제를 발견했습니다.

특히 리뷰 기능은 사용자가 직접 입력하는 영역이기 때문에 정상적인 사용 시나리오뿐 아니라 예상하지 못한 입력과 예외 상황까지 함께 고려해야 했습니다.

구분

발견한 문제

영향

텍스트 입력

동일한 의미의 문자열이 다른 형태로 저장될 수 있음

데이터 품질 저하

사용자 입력

HTML 태그 및 스크립트 형태 입력 가능

보안 및 운영 위험

파일 업로드

MIME 타입만으로 이미지 여부 판단

잘못된 파일 업로드 가능

이미지 수정

파일 정보와 데이터 정보가 불일치할 수 있음

데이터 정합성 문제

3. 텍스트 입력값 검증 적용

가장 먼저 개선한 부분은 리뷰 본문과 작성자명에 대한 입력값 처리였습니다.

처음에는 사용자가 입력한 문자열을 그대로 저장하고 있었습니다.

하지만 같은 의미를 가진 문자열도 입력 방식에 따라 서로 다른 형태로 저장될 수 있다는 점이 눈에 띄었습니다.

대표적으로 전각 문자와 반각 문자는 사용자 입장에서는 비슷하게 보이지만 시스템에서는 서로 다른 데이터로 인식됩니다.

이러한 데이터가 누적되면 검색이나 데이터 관리 과정에서 예상하지 못한 문제가 발생할 수 있습니다.

이를 해결하기 위해 저장 전에 문자열 정규화 과정을 추가했습니다.

String normalized = Normalizer.normalize(value, Normalizer.Form.NFKC).trim();

if (normalized.indexOf('<') >= 0 || normalized.indexOf('>') >= 0) {
    throw new IllegalArgumentException("Review text cannot contain angle brackets.");
}

NFKC(Normalization Form Compatibility Composition)를 적용하여 문자열을 정규화하고, <, > 문자가 포함된 경우 저장을 제한하도록 수정했습니다.

처음에는 음란성 문구나 불필요한 태그 입력을 방지하기 위한 목적이 컸습니다.

하지만 검토를 진행하면서 HTML 태그나 스크립트 형태의 입력이 저장될 가능성도 함께 고려하게 되었습니다.

현재 서비스 구조에서 즉시 문제가 발생하지 않더라도, 저장된 데이터가 다른 화면이나 기능에서 활용될 경우 XSS(Cross Site Scripting)와 같은 문제가 발생할 가능성이 있습니다.

결과적으로 이 검증은 단순한 특수문자 제한이 아니라 데이터 품질과 서비스 안정성을 함께 고려한 작업이 되었습니다.

4. 프론트엔드와 백엔드의 이중 검증 구조

입력값 검증을 적용하면서 또 하나 고민했던 부분은 검증을 어디에서 수행할 것인가였습니다.

처음에는 백엔드에서만 검증해도 충분하다고 생각했습니다.

하지만 사용자는 저장 버튼을 누른 뒤에야 오류를 확인할 수 있었고, 사용자 경험 측면에서는 아쉬움이 있었습니다.

그래서 프론트엔드에서도 동일한 검증 규칙을 적용했습니다.

const hasUnsafeReviewText = (value: string) => {
  const normalized = value.normalize('NFKC');
  return normalized.includes('<') || normalized.includes('>');
};

프론트엔드에서는 사용자가 입력하는 즉시 검증하여 빠른 피드백을 제공하고, 백엔드에서는 저장 직전 동일한 규칙으로 최종 검증을 수행하도록 구성했습니다.

각 영역의 역할은 다음과 같습니다.

구분

역할

프론트엔드

입력 즉시 검증 및 사용자 피드백 제공

백엔드

저장 직전 최종 검증 및 데이터 보호

이 구조를 적용한 이후 사용자 경험과 데이터 안정성을 모두 확보할 수 있었습니다.

특히 프론트엔드 검증만으로는 API 직접 호출이나 우회 요청을 막을 수 없기 때문에 백엔드 검증은 반드시 필요하다는 점을 다시 확인할 수 있었습니다.

5. MIME 타입 검증에서 실제 이미지 검증으로

이번 작업에서 가장 고민이 많았던 부분은 이미지 업로드 검증이었습니다.

초기 구현에서는 파일의 MIME 타입만 확인하고 있었습니다.

예를 들어 image/png, image/jpeg와 같은 타입으로 전달되면 이미지 파일이라고 판단하는 방식이었습니다.

당시에는 충분하다고 생각했습니다.

하지만 검토 과정에서 “이미지라고 전달된 파일이 정말 이미지일까?”라는 질문이 생겼습니다.

MIME 타입은 클라이언트가 전달하는 정보일 뿐 실제 파일 내용을 보장하지 못합니다.

이를 보완하기 위해 프론트엔드에서는 파일 시그니처(Signature)를 확인하여 JPEG, PNG, GIF, BMP 형식인지 먼저 검증하도록 수정했습니다.

그리고 백엔드에서는 실제로 이미지를 디코딩할 수 있는지 확인하는 검증 로직을 추가했습니다.

private boolean isDecodableRasterImage(MultipartFile file) {
    try (InputStream inputStream = file.getInputStream()) {
        BufferedImage image = ImageIO.read(inputStream);

        return image != null
                && image.getWidth() > 0
                && image.getHeight() > 0;
    } catch (IOException e) {
        return false;
    }
}

검증 기준을 MIME 타입에서 실제 이미지 데이터로 확장함으로써, 이미지가 아닌 파일이 업로드되는 상황을 보다 안정적으로 방지할 수 있었습니다.

6. 이미지 수정 시 데이터 정합성 관리

개인적으로 가장 인상 깊었던 작업은 이미지 수정 기능 개선이었습니다.

사용자는 리뷰 수정 과정에서 다음과 같은 작업을 수행할 수 있습니다.

  • 기존 이미지 유지

  • 기존 이미지 일부 삭제

  • 새로운 이미지 추가

  • 기존 이미지 전체 삭제

사용자 입장에서는 단순한 수정 기능처럼 보이지만 실제로는 여러 데이터가 함께 변경됩니다.

실제 파일, 파일 식별 정보(clipId), 썸네일 정보, 리뷰 데이터가 항상 같은 상태를 유지해야 했습니다.

만일 일부 정보만 변경된다면 실제 파일은 삭제되었는데 리뷰 데이터는 남아 있거나, 반대로 데이터는 삭제되었지만 파일은 계속 존재하는 상황이 발생할 수 있습니다.

이를 방지하기 위해 유지할 파일과 삭제할 파일을 명확히 구분하고, 관련 데이터가 함께 변경되도록 로직을 정리했습니다.

또한 마지막 이미지가 삭제되는 경우에는 썸네일 정보와 관련 식별 정보도 함께 제거하도록 개선했습니다.

이 작업을 진행하면서 기능 구현보다 데이터 간 관계를 일관되게 유지하는 것이 더 중요할 수 있다는 점을 체감할 수 있었습니다.

7. 적용 결과

이번 개선 작업을 통해 다음과 같은 효과를 얻을 수 있었습니다.

개선 항목

적용 결과

문자열 정규화

데이터 일관성 향상

HTML 태그 차단

XSS 발생 가능성 감소

이미지 검증 강화

잘못된 파일 업로드 방지

프론트·백엔드 이중 검증

사용자 경험 및 안정성 향상

데이터 정합성 관리

파일 정보 불일치 감소

개별 작업만 보면 작은 수정처럼 보일 수 있습니다.

하지만 실제 운영 환경에서는 이러한 작은 검증 로직 하나가 서비스 품질에 큰 영향을 준다는 것을 경험할 수 있었습니다.

8. 마무리

이번 작업을 진행하면서 기능 구현과 운영 안정성은 서로 다른 관점이라는 것을 다시 느꼈습니다.

화면이 정상적으로 동작하는 것만으로는 서비스가 완성되었다고 말하기 어렵습니다.

사용자가 예상하지 못한 입력을 하더라도 시스템이 안정적으로 동작할 수 있어야 하고, 여러 데이터가 항상 일관된 상태를 유지할 수 있어야 합니다.

특히 입력값 검증과 데이터 정합성 관리는 사용자가 직접 체감하기 어려운 영역입니다.

하지만 이러한 보이지 않는 부분들이 결국 서비스 품질을 결정한다고 생각합니다.

이번 경험을 통해 기능 구현뿐 아니라 데이터가 어떻게 저장되고 관리되는지, 운영 과정에서 어떤 문제가 발생할 수 있는지까지 함께 고민하는 습관의 중요성을 다시 한번 느낄 수 있었습니다.

앞으로도 기능 구현에 그치지 않고 운영 환경까지 고려하는 개발을 지속적으로 실천해 나가고자 합니다.

zero

Site footer