[Spring] PostgreSQL tsvector 트러블슈팅

JUNYOUNG·2024년 12월 31일
post-thumbnail

1. 문제 상황

FastAPI에서 Spring으로 마이그레이션하던 중 다음과 같은 에러가 발생했다:


2024-12-31 10:08:03 WARN [http-nio-127.0.0.1-8000-exec-2] o.h.e.jdbc.spi.SqlExceptionHelper - SQL Error: 0, SQLState: 42804
2024-12-31 10:08:03 ERROR [http-nio-127.0.0.1-8000-exec-2] o.h.e.jdbc.spi.SqlExceptionHelper - ERROR: column "search_vector" is of type tsvector but expression is of type character varying
Hint: You will need to rewrite or cast the expression.

이 에러는 PostgreSQL의 tsvector 타입 컬럼(search_vector)에 character varying 타입 데이터를 직접 삽입하려 했기 때문에 발생했다.
기존 FastAPI 애플리케이션에서는 이러한 문제가 없었으나, Spring으로 마이그레이션하면서 이슈가 드러났다.

2. 원인 분석

PostgreSQL의 search_vector 컬럼은 전문 검색(Full Text Search)을 지원하기 위해 tsvector 타입으로 설정되어 있다.
Spring JPA는 엔티티 매핑 시 이를 일반 문자열(character varying)로 처리하려고 시도하였고, 타입 불일치로 인해 SQL 오류가 발생했다.

3. 해결 방법

3.1. 애플리케이션에서의 접근 방식

처음에는 애플리케이션 계층에서 search_vector 값을 적절히 변환하거나 삽입을 방지하려는 접근을 고려했다.
하지만 다음과 같은 이유로 더 적합한 방식은 아니라고 판단했다:

  • 애플리케이션 계층에서 변환 로직 추가 시 코드 복잡도가 증가.
  • 데이터베이스에 가까운 로직은 데이터베이스에서 처리하는 것이 유지보수성과 성능 측면에서 더 유리.

3.2. 데이터베이스 트리거를 활용한 해결

PostgreSQL 트리거를 사용하여 search_vector 값을 자동으로 업데이트하도록 설정했다.
이를 통해 애플리케이션 계층에서는 search_vector 컬럼을 업데이트하지 않고도 데이터베이스가 알아서 처리하도록 만들었다.

4. 트리거 설정

4.1. 트리거 함수 정의

CREATE FUNCTION users_search_vector_trigger() RETURNS trigger AS $$
BEGIN
    NEW.search_vector := to_tsvector('english', 
        COALESCE(NEW.username, '') || ' ' || 
        COALESCE(NEW.email, '')
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

4.2. 트리거 생성

CREATE TRIGGER users_search_vector_update
    BEFORE INSERT OR UPDATE ON users
    FOR EACH ROW
EXECUTE FUNCTION users_search_vector_trigger();

이 트리거는 users 테이블에 데이터가 삽입되거나 업데이트될 때마다 search_vector 컬럼을 자동으로 갱신한다.

5. 애플리케이션 계층 변경

5.1. search_vector 필드의 업데이트 방지

Spring JPA에서 search_vector 컬럼이 애플리케이션에서 수정되지 않도록 아래와 같이 설정한다:

@Column(name = "search_vector", columnDefinition = "tsvector", updatable = false)
private String searchVector;

5.2. 업데이트 로직 수정

@Transactional
public UserResponse updateUser(int userId, UserUpdate request) {
	User user = userRepository.findById(userId)
	.orElseThrow(() -> new CommonException(ErrorCode.USER_NOT_FOUND));
	
	if (request.getPassword() != null) {
	    request = UserUpdate.builder()
	            ...
	            ...
	}
	
	user.update(request);
	return userMapper.mapToDTO(user);
}

이제 search_vector 필드는 애플리케이션 코드에서 업데이트되지 않으며, 데이터베이스에서 트리거로 자동 처리된다.

6. 이점

6.1. 데이터 일관성 보장

  • 모든 데이터 변경이 데이터베이스 계층에서 일관되게 처리된다.
  • 애플리케이션 로직의 실수나 누락으로 인한 문제를 방지한다.

6.2. 성능 최적화

  • search_vector 갱신 로직이 데이터베이스 내부에서 처리되어 네트워크 오버헤드가 감소한다.
  • PostgreSQL의 전문 검색 인덱스(GIN 또는 GiST)와 결합해 성능을 극대화할 수 있다.

6.3. 코드 단순화

  • 애플리케이션 코드에서 불필요한 로직을 제거하여 가독성과 유지보수성이 향상된다.

7. 결론

이번 문제 해결 과정에서 중요한 것은 올바른 접근 방식을 선택하는 것이었다. 처음에는 Spring 애플리케이션에서 모든 것을 해결하려 했지만, 과감하게 데이터베이스 레벨의 해결책으로 방향을 전환했다.

PostgreSQL 트리거를 활용한 해결책은 단순히 문제를 해결하는 것을 넘어, 시스템 전체의 품질을 향상시켰다. 코드는 더 간결해졌고, 성능은 개선되었으며, 유지보수도 더 쉬워졌다.

이 경험은 문제를 한 걸음 뒤로 물러서서 바라보고, 빠르게 실행하며 개선하는 과정의 중요성을 다시 한 번 상기시켜 주었다. 현재 정식 출시 전이지만, 이러한 접근은 더 나은 시스템을 만들고 사용자에게 더 나은 서비스를 제공하는 기반이 될 것이다.

profile
Onward, Always Upward - 기록은 성장의 증거

0개의 댓글