PostgreSQL을 사용하여 데이터를 정렬할 때, 한글이 포함된 컬럼의 정렬이 예상과 다르게 동작하는 경우가 발생할 수 있습니다. 예를 들어, ORDER BY 구문을 사용하여 한글이 포함된 컬럼을 정렬할 때, 한글이 정상적으로 "가나다" 순으로 정렬되지 않고 엉뚱한 순서로 나오는 상황을 겪을 수 있습니다.
이 문제의 원인은 데이터베이스가 한글 정렬에 적합한 collation 설정을 사용하지 않았기 때문입니다.
Collation이란?
Collation은 데이터베이스에서 문자열을 비교하고 정렬할 때 사용하는 규칙을 정의하는 설정입니다. PostgreSQL에서는 데이터베이스를 생성할 때 collation 설정을 지정할 수 있습니다. 기본적으로 en_US.UTF-8과 같은 영어 기반의 collation이 설정되어 있는 경우, 한글 정렬이 제대로 되지 않는 문제가 발생할 수 있습니다.
문제 원인 분석
PostgreSQL에서 데이터베이스의 collation이 한글 정렬에 적합하지 않은 값으로 설정된 경우, 한글이 올바르게 정렬되지 않습니다. 예를 들어, en_US.UTF-8로 설정된 데이터베이스는 한글을 영어 알파벳과 동일한 기준으로 정렬하기 때문에, 우리가 기대하는 "가나다" 순으로 정렬되지 않습니다.
이 문제를 해결하려면 데이터베이스를 생성할 때 올바른 collation을 설정해야 합니다.
해결 방법
1. 데이터베이스 생성 시 collation 설정하기
PgAdmin에서 생성시 Character type 을 C로 해준다
또는
데이터베이스를 처음 생성할 때 collation을 ko_KR.UTF-8로 설정하면 한글이 올바르게 정렬됩니다. 이 방법은 데이터베이스를 처음부터 설정하는 경우에 적합합니다.
CREATE DATABASE mydb
WITH ENCODING='UTF8'
LC_COLLATE='ko_KR.UTF-8'
LC_CTYPE='ko_KR.UTF-8'
TEMPLATE=template0;
2. 이미 생성된 데이터베이스에서 collation 변경하기
이미 데이터베이스가 생성된 후에는 collation을 변경하기가 어렵습니다. 이 경우, 데이터베이스를 백업하고 새로운 collation 설정으로 데이터베이스를 다시 생성해야 합니다.
UPDATE PG_DATABASE SET DATCOLLATE = 'ko_KR.utf8',
WHERE DATNAME='[데이터베이스명]';
3. 쿼리 수준에서 collate 옵션 사용하기
만약 데이터베이스 전체를 재설정할 수 없는 상황이라면, 개별 쿼리에서 collate 옵션을 사용하는 방법도 있습니다. 이 방법은 쿼리마다 collation을 지정하여 한글이 올바르게 정렬되도록 합니다.
SELECT * FROM store
ORDER BY store_nm COLLATE "ko_KR.utf8";
이 방법을 사용하면, 데이터베이스의 기본 collation을 변경하지 않고도 한글 정렬 문제를 해결할 수 있습니다.
상황을 간단히 설명하자면, 사용자가 리뷰를 제출한 후 바로 로그아웃하고 다시 동일한 페이지를 방문할 때, 페이지의 일부만 렌더링되는 문제가 발생했습니다
그런데 이 문제 해결에 대해 보기 전에 CSRF 토큰에 대한 이해가 필요하니 CSRF 에대해 이해해보도록 합시다
CSRF와 CSRF 토큰에 대한 이해와 구현
CSRF란 무엇인가?
CSRF는 사용자가 신뢰하는 웹 애플리케이션에서 사용자도 모르게 공격자가 조작한 요청을 서버로 전송하게 하는 공격 방식입니다. 이 공격의 핵심은 사용자가 이미 인증된 상태에서 발생한다는 점입니다.
예시로 보는 CSRF 공격
사용자가 은행 웹사이트에 로그인한 상태에서, 공격자가 보낸 이메일이나 메시지에 포함된 링크를 클릭하면 사용자는 자신도 모르게 공격자가 설계한 악의적인 요청을 은행 서버에 보내게 됩니다. 예를 들어, 링크를 클릭함으로써 사용자의 계좌에서 공격자의 계좌로 돈이 이체될 수 있습니다.
이때 중요한 점은 공격자는 사용자의 세션을 악용하여 요청을 보낼 뿐, 서버의 응답 내용은 확인할 수 없다는 것입니다. 그럼에도 불구하고, 이 방식은 공격자가 사용자 권한으로 중요한 작업을 수행할 수 있기 때문에 매우 위험합니다.
CSRF 토큰이란?
CSRF 공격을 방지하기 위한 주요 방법 중 하나가 바로 CSRF 토큰을 사용하는 것입니다. CSRF 토큰은 웹 애플리케이션이 각 사용자 세션마다 고유하게 생성하는 임의의 난수입니다.
이 토큰은 사용자가 서버에 요청을 보낼 때마다 함께 전송되며, 서버는 이 토큰을 검증하여 요청이 실제로 해당 세션의 사용자에 의해 발생했는지를 확인합니다.
CSRF 토큰의 동작 원리
토큰 발급: 사용자가 웹 애플리케이션에 접속하면, 서버는 사용자 세션에 고유한 CSRF 토큰을 생성하여 저장합니다.
토큰 전달: 서버는 사용자에게 HTML 페이지를 전달할 때, 이 CSRF 토큰을 숨겨진 폼 필드(<input type="hidden">)에 포함시켜 클라이언트에게 전송합니다.
요청 시 토큰 전송: 사용자가 폼을 제출하거나 서버로 요청을 보낼 때, 해당 요청에 이 숨겨진 필드의 CSRF 토큰이 포함되어 전송됩니다
토큰 검증: 서버는 요청을 받을 때, 요청에 포함된 토큰과 세션에 저장된 토큰을 비교합니다. 두 토큰이 일치하면 요청을 처리하고, 그렇지 않으면 요청을 거부합니다
Spring Security, Thymeleaf 및 세션 관리 이슈 해결 방법
사전 지식
Spring Security의 세션 생성 전략:
기본적으로 Spring Security는 "필요 시 생성" 전략을 사용합니다. 즉, 보안이 필요한 시점에 세션을 생성합니다.
Spring Session 생성 제한:
세션은 response가 커밋되기 이전에만 생성될 수 있습니다. 즉, 응답이 반환된 후에는 세션을 새로 생성할 수 없습니다.
Spring Security의 폼 로그아웃 기능:
기본적으로 서버 측 세션만 무효화되고 클라이언트의 쿠키 값에 있는 세션 ID는 그대로 유지됩니다. 이 세션 ID는 더 이상 유효하지 않은 세션을 참조하게 됩니다.
Thymeleaf의 렌더링 방식:
Thymeleaf는 Spring MVC와 함께 동작하며, 요청당 하나의 스레드에서 동기적으로 처리됩니다. 컨트롤러가 모든 로직을 처리한 후 Thymeleaf가 HTML을 렌더링합니다.
Thymeleaf는 메모리 최적화를 위해 기본적으로 8KB의 버퍼를 사용하여 렌더링된 HTML 파일을 CHUNKED 방식으로 나눠서 전송합니다. 버퍼가 가득 차면 전송을 시작하고, 남은 부분을 계속 렌더링합니다.
CSRF 보안 및 Thymeleaf와의 연동:
CSRF 보호를 위해 Spring Security와 Thymeleaf가 협력하여 자동으로 처리합니다.
Thymeleaf는 POST, PUT, DELETE 요청에 대한 폼 태그를 렌더링할 때 hidden 태그로 csrf_라는 이름의 태그를 추가합니다.
Spring Security는 CSRF 필터를 통해 클라이언트가 보유한 세션 ID에 매핑된 세션이 유효한지 확인하고, 유효하지 않거나 세션이 없을 경우 새로운 세션을 생성하여 CSRF 토큰을 부여합니다.
클라이언트가 요청을 보낼 때 세션에 저장된 CSRF 토큰과 폼의 csrf_ 태그 값을 비교하여 유효성을 판단합니다.
문제 상황
환경
세션 생성 전략은 기본값인 "필요 시 생성".
로그인 방식은 폼 로그인 방식.
문제가 발생하는 조건
로그아웃을 한 뒤, 음식점 세부 페이지에 접근.
해당 페이지에는 댓글이 2개 이상 있어 데이터 양이 증가.
문제 증상
음식점 세부 페이지가 부분적으로 잘려서 클라이언트에 반환됩니다.
페이지를 다시 방문하더라도 문제가 반복되며, 다른 세부 페이지를 들린 후에는 정상 동작합니다.
이는 해당 페이지를 메인화면에서 재방문하더라도 반복해서 일어나며, 다른 아무 세부 음식점을 들렸다가 오면 정상작동합니다
문제 상황 분석
로그아웃 후 클라이언트의 상태:
로그아웃 후에도 클라이언트 측 쿠키에는 여전히 세션 ID가 남아 있지만, 해당 세션은 서버 측에서 무효화된 상태입니다. 즉, 더 이상 유효한 세션과 매핑되지 않습니다.
음식점 세부 페이지에는 POST 요청을 보내는 폼이 있으며, 이로 인해 CSRF 토큰이 필요합니다.
Thymeleaf는 CSRF 토큰을 요청하지만, 클라이언트가 여전히 유효하지 않은 세션 ID를 서버에 전달하게 되어, 서버는 새로운 세션을 생성하고 CSRF 토큰을 부여합니다.
Thymeleaf의 CHUNKED 응답과 CSRF 문제:
댓글이 2개 이상인 상황에서는 데이터 양이 증가하여 Thymeleaf가 CHUNKED 방식을 통해 HTML을 전송하게 됩니다.
Thymeleaf는 응답을 버퍼에 저장하고 8KB가 가득 차면 일부분을 전송한 후 남은 부분을 계속 렌더링합니다.
그러나 세션은 응답이 커밋되기 전까지만 생성할 수 있습니다. 이미 CHUNKED로 일부 응답이 전송된 후, CSRF 토큰을 추가하기 위해 새로운 세션이 필요할 경우 세션을 생성할 수 없게 됩니다.
이로 인해 페이지의 하단 부분이 제대로 렌더링되지 않고 잘린 상태로 클라이언트에 전달됩니다.
다른 페이지를 방문하면 정상 동작하는 이유:
문제를 겪은 후 다른 페이지를 방문하면 새로운 세션이 생성되어 CSRF 토큰이 정상적으로 부여됩니다. 이후 문제 있는 페이지에 다시 접근하면 CSRF 토큰을 재생성할 필요가 없으므로 정상 동작합니다.
해결책
세션 생성 전략 변경:
모든 요청에 대해 세션을 생성하도록 설정할 수 있습니다. 그러나 이는 메모리 소비가 늘어나 비효율적일 수 있습니다.
Thymeleaf 버퍼 크기 조정:
Thymeleaf의 기본 버퍼 크기인 8KB를 늘리거나 CHUNKED 방식을 사용하지 않도록 전체 파일을 한 번에 반환하도록 설정할 수 있습니다. 이 방법은 페이지 크기가 클 경우 메모리 사용량이 증가하므로 주의가 필요합니다.
spring.thymeleaf.buffer-size=64KB
CSRF 토큰을 직접 처리:
CSRF 토큰을 직접 관리하거나 필터를 커스텀하여 CHUNKED 렌더링 과정에서 세션 문제를 방지할 수 있습니다.
또한 Ajax를 활용하여 CSRF 토큰을 동적으로 받아오는 방식으로 처리할 수 있습니다.
Thymeleaf 커밋 시점 조정:
문제의 원인이 타임리프의 응답 커밋 시점과 스프링 시큐리티의 동작 순서 충돌에서 비롯된 만큼, 해결 방법은 타임리프의 커밋 시점을 조정하는 것입니다.
타임리프가 HTML을 처리하는 동안 부분적인 출력을 지연시키도록 설정을 변경하면, 이 문제가 해결됩니다.
설정 파일에 아래 내용을 추가하여 타임리프가 전체 HTML을 처리할 때까지 응답을 커밋하지 않도록 설정할 수 있습니다:
# Thymeleaf 설정 spring.thymeleaf.servlet.produce-partial-output-while-processing=false
이 설정을 통해 타임리프는 전체 HTML이 처리될 때까지 응답을 커밋하지 않게 되며, 스프링 시큐리티가 CSRF 토큰을 폼에 추가할 수 있는 시간을 확보하게 됩니
냠톨릭은?
현재 운영측에서 사용자를 차단 하기위한 세션 전략 변경과 위의 해결책 적용으로 인해 1, 4번 해결책이 모두 적용되어 있다. 또한 거의 첫번째로 버퍼에 들어가는게 거의 확실한 nav bar에서 meta csrf 태그를 발견했는데 이에 의해 3번도 적용되어 있다
결론
이 문제는 Spring Security와 Thymeleaf의 세션 및 CSRF 처리 과정에서 발생하는 특수한 케이스입니다. 클라이언트는 세션 자체를 가지고 있지 않고, 단지 세션 ID를 쿠키로 보유하며, 로그아웃 후에도 이 세션 ID는 남아 있을 수 있지만 서버에서 더 이상 유효하지 않다는 점을 유념해야 합니다. 이를 해결하기 위해 세션 생성 전략을 조정하거나 Thymeleaf의 버퍼 크기를 조절하는 등의 해결책을 사용할 수 있습니다.
이러한 구조적 문제는 보통 예측하기 어려운 부분이니, 실무에서는 항상 페이지 성능과 메모리 최적화를 함께 고려하는 것이 중요합니다.