Spring 공부-4
Spring 공부-4
페이징 기능
한 화면에 표시할 게시물이 많아져서 스크롤바를 내려야 하는 불편함이 생긴다. 이를 해결하기 위해 질문 목록 화면에 페이징 기능을 적용해 보자. 여기서 페이징(paging)이란 입력된 정보나 데이터를 여러 페이지에 나눠 표시하고, 사용자가 페이지를 이동할 수 있게 하는 기능을 말한다.
- 스프링 부트(Spring Boot)의 페이징 기능을 구현할 때 첫 페이지 번호는 1이 아닌 0이므로 기본값으로 0을 설정해야 한다.
- GET 방식에서는 값을 전달하기 위해서
?와&기호를 사용한다. 첫 번째 파라미터는?기호를 사용하고 그 이후 추가되는 값은&기호를 사용한다.
템플릿에 Page 클래스의 객체인 paging을 model에 설정하여 전달했다. paging 객체에는 다음과 같은 속성들이 있는데, 이 속성들은 템플릿에서 페이징을 처리할 때 필요하므로 미리 알아 두자.
| 속성 | 설명 |
|---|---|
paging.isEmpty |
페이지 존재 여부를 의미한다(게시물이 있으면 false, 없으면 true). |
paging.totalElements |
전체 게시물 개수를 의미한다. |
paging.totalPages |
전체 페이지 개수를 의미한다. |
paging.size |
페이지당 보여 줄 게시물 개수를 의미한다. |
paging.number |
현재 페이지 번호를 의미한다. |
paging.hasPrevious |
이전 페이지의 존재 여부를 의미한다. |
paging.hasNext |
다음 페이지의 존재 여부를 의미한다. |
이전 페이지가 없는 경우에는 ‘이전’ 링크가 비활성화(disabled)되도록 했다. ‘다음’ 링크의 경우도 마찬가지 방법으로 적용했다. 그리고 th:each 속성을 사용해 전체 페이지 수만큼 반복하면서 해당 페이지로 이동할 수 있는 ‘이전’, ‘다음’ 링크를 생성했다. 이때 반복하던 도중 요청 페이지가 현재 페이지와 같을 경우에는 active 클래스를 적용하여 페이지 링크에 파란색 배경이 나타나도록 했다.
타임리프의
th:classappend="조건식 ? 클래스_값"은 조건식이 참인 경우 ‘클래스_값’을class속성에 추가한다.
위 템플릿에 사용한 주요 페이징 기능을 표로 정리해 보았다.
| 페이징 기능 관련 주요 코드 | 설명 |
|---|---|
th:classappend="${!paging.hasPrevious} ? 'disabled'" |
이전 페이지가 없으면 ‘이전’ 링크를 비활성화한다. |
th:classappend="${!paging.hasNext} ? 'disabled'" |
다음 페이지가 없으면 ‘다음’ 링크를 비활성화한다. |
th:href="@{\|?page=${paging.number-1}\|}" |
이전 페이지 링크를 생성한다. |
th:href="@{\|?page=${paging.number+1}\|}" |
다음 페이지 링크를 생성한다. |
th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}" |
0부터 전체 페이지 수 만큼 이 요소를 반복하여 생성한다. 이때 현재 순번을 page 변수에 대입한다. |
th:classappend="${page == paging.number} ? 'active'" |
반복 구간 내에서 해당 페이지가 현재 페이지와 같은 경우 active 클래스를 적용한다. |
한 가지 더 설명하면, #numbers.sequence(시작 번호, 끝 번호)는 시작 번호부터 끝 번호까지 정해진 범위만큼 반복을 만들어 내는 타임리프의 기능이다.
게시물을 역순(최신순)으로 조회하려면 이와 같이 PageRequest.of 메서드의 세 번째 매개변수에 Sort 객체를 전달해야 한다. 작성 일시(createDate)를 역순(Desc)으로 조회하려면 Sort.Order.desc("createDate")와 같이 작성한다.
- 만약 작성 일시 외에 정렬 조건을 추가하고 싶다면
sort.add메서드를 활용해 sorts 리스트에 추가하면 된다.- 여기서 쓰인
desc는 내림차순을 의미하고,asc는 오름차순을 의미한다.
게시물 번호 공식 만들기
만약 질문 게시물이 12개라면 1페이지에는 가장 최근 게시물인 12번째~3번째 게시물이, 2페이지에는 2번째~1번째 게시물이 역순으로 표시되어야 한다. 질문 게시물의 번호를 역순으로 정렬하려면 다음 공식을 적용해야 한다.
게시물 번호 = 전체 게시물 개수 - (현재 페이지 * 페이지당 게시물 개수) - 나열 인덱스
| 항목 | 설명 |
|---|---|
| 게시물 번호 | 최종 표시될 게시물의 번호 |
| 전체 게시물 개수 | 데이터베이스에 저장된 게시물 전체 개수 |
| 현재 페이지 | 페이징에서 현재 선택한 페이지 |
| 페이지당 게시물 개수 | 한 페이지당 보여 줄 게시물의 개수 |
| 나열 인덱스 | for 문 안의 게시물 순서(나열 인덱스는 현재 페이지에서 표시할 수 있는 게시물의 인덱스이므로, 예를 들어 10개를 표시하는 페이지에서는 0~9, 2개를 표시하는 페이지에서는 0~1로 반복된다.) |
공식이 조금 복잡하니 질문 게시물이 12개인 상황을 예로 들어 설명해 보자. 현재 페이지가 0이면 게시물의 번호는 전체 게시물 개수 12에서 나열 인덱스 0~9를 뺀 12~3이 된다. 현재 페이지가 1이면 페이지당 노출되는 게시물 개수는 10이므로 12에서 10을 뺀 값인 2에 나열 인덱스 0~1을 다시 빼므로 게시물 번호는 2~1이 된다.
paging.getTotalElements는 전체 게시물 개수를 말한다. 필자의 경우에는 302개이다. paging.number는 현재 페이지 번호로 페이지를 변경할 때마다 달라진다. paging.size는 페이지당 게시물 개수로 여기서는 10으로 정해져 있다. 마지막으로 loop.index는 나열 인덱스로 0부터 시작한다.
다음 표는 템플릿에 사용한 공식의 상세 정보를 정리한 것이다.
| 항목 | 설명 |
|---|---|
paging.getTotalElements |
전체 게시물 개수를 의미한다. |
paging.number |
현재 페이지 번호를 의미한다. |
paging.size |
페이지당 게시물 개수를 의미한다. |
loop.index |
나열 인덱스를 의미한다(0부터 시작). |
스프링 시큐리티 설정하기
@Configuration은 이 파일이 스프링의 환경 설정 파일임을 의미하는 애너테이션이다. 여기서는 스프링 시큐리티를 설정하기 위해 사용했다. @EnableWebSecurity는 모든 요청 URL이 스프링 시큐리티의 제어를 받도록 만드는 애너테이션이다. 이 애너테이션을 사용하면 스프링 시큐리티를 활성화하는 역할을 한다. 내부적으로 SecurityFilterChain 클래스가 동작하여 모든 요청 URL에 이 클래스가 필터로 적용되어 URL별로 특별한 설정을 할 수 있게 된다. 스프링 시큐리티의 세부 설정은 @Bean 애너테이션을 통해 SecurityFilterChain 빈을 생성하여 설정할 수 있다.
다음은 인증되지 않은 모든 페이지의 요청을 허락한다는 의미이다. 따라서 로그인하지 않더라도 모든 페이지에 접근할 수 있도록 한다.
http
.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
.requestMatchers(new AntPathRequestMatcher("/**")).permitAll())
;
빈이란?
빈(bean)은 스프링에 의해 생성 또는 관리되는 객체를 의미한다. 우리가 지금껏 만들어 왔던 컨트롤러, 서비스, 리포지터리 등도 모두 빈에 해당한다. 또한 앞선 예처럼 @Bean 애너테이션을 통해 자바 코드 내에서 별도로 빈을 정의하고 등록할 수도 있다.
H2 콘솔 오류 수정하기
그런데 스프링 시큐리티를 적용하면 H2 콘솔 로그인 시 다음과 같은 403 Forbidden 오류가 발생한다. 403 Forbidden은 작동 중인 서버에 클라이언트의 요청이 들어왔으나, 서버가 클라이언트의 접근을 거부했을 때 반환하는 HTTP 오류 코드이다. 이 오류는 서버 또는 서버에 있는 파일 등에 접근 권한이 없을 경우에 발생한다.

403 Forbidden 오류가 발생하는 이유를 좀 더 구체적으로 설명하면, 스프링 시큐리티의 CSRF 방어 기능에 의해 H2 콘솔 접근이 거부되기 때문이다. CSRF는 웹 보안 공격 중 하나로, 조작된 정보로 웹 사이트가 실행되도록 속이는 공격 기술이다. 스프링 시큐리티는 이러한 공격을 방지하기 위해 CSRF 토큰을 세션을 통해 발행하고, 웹 페이지에서는 폼 전송 시에 해당 토큰을 함께 전송하여 실제 웹 페이지에서 작성한 데이터가 전달되는지를 검증한다.
- 토큰이란 요청을 식별하고 검증하는 데 사용하는 특수한 문자열 또는 값을 의미한다.
- 세션이란 사용자의 상태를 유지하고 관리하는 데 사용하는 기능이다.
이 오류를 해결하기 전에 다음과 같이 질문 등록 화면을 열고 브라우저의 ‘페이지 소스 보기’ 기능을 이용하여 질문 등록 화면의 소스를 잠시 확인해 보자.

2) 그러면 다음과 같이 질문 등록 화면의 소스를 볼 수 있다.

다음과 같은 input 요소가 <form> 태그 안에 자동으로 생성된 것을 확인할 수 있다.
<input type="hidden" name="_csrf" value="ELCsIKgKv7yGzeFZsXxrCtAcVBiiS-lvBrP8b-1scsRpPlrWHJoKfFsw4ioyr-thtgFFfbLF6eSCUctFCYICYW2l4gC57o4W1"/>
스프링 시큐리티에 의해 이와 같은 CSRF 토큰이 자동으로 생성된다.
CSRF 토큰은 서버에서 생성되는 임의의 값으로 페이지 요청 시 항상 다른 값으로 생성된다. 때문에 여러분의 화면과 다소 차이가 있다.
스프링 시큐리티는 이런 식으로 페이지에 CSRF 토큰을 발행하여 이 값이 다시 서버로 정확하게 들어오는지를 확인하는 과정을 거친다. 만약 CSRF 토큰이 없거나 해커가 임의의 CSRF 토큰을 강제로 만들어 전송한다면 스프링 시큐리티에 의해 차단될 것이다. 정리하자면, H2 콘솔은 스프링 프레임워크가 아니므로 CSRF 토큰을 발행하는 기능이 없어 이와 같은 403 오류가 발생한 것이다.
H2 콘솔은 스프링과 상관없는 일반 애플리케이션이다.
스프링 시큐리티가 CSRF 처리 시 H2 콘솔은 예외로 처리할 수 있도록 다음과 같이 설정 파일을 수정하자.
[파일명:SecurityConfig.java]
(... 생략 ...)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
.requestMatchers(new AntPathRequestMatcher("/**")).permitAll())
.csrf((csrf) -> csrf
.ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**")))
;
return http.build();
}
}
이와 같이 /h2-console/로 시작하는 모든 URL은 CSRF 검증을 하지 않는다는 설정을 추가했다. 이렇게 수정하고 로컬 서버를 재시작한 후 다시 H2 콘솔에 접속해 보자.
4) 이제 CSRF 검증에서 예외 처리되어 로그인은 잘 수행된다. 하지만 다음과 같이 화면이 깨져 보인다.

이와 같은 오류가 발생하는 원인은 H2 콘솔의 화면이 프레임(frame) 구조로 작성되었기 때문이다. 즉, H2 콘솔 UI(user interface) 레이아웃이 이 화면처럼 작업 영역이 나눠져 있음을 의미한다. 스프링 시큐리티는 웹 사이트의 콘텐츠가 다른 사이트에 포함되지 않도록 하기 위해 X-Frame-Options 헤더의 기본값을 DENY로 사용하는데, 프레임 구조의 웹 사이트는 이 헤더의 값이 DENY인 경우 이와 같이 오류가 발생한다.
스프링 부트에서 X-Frame-Options 헤더는 클릭재킹 공격을 막기 위해 사용한다. 클릭재킹은 사용자의 의도와 다른 작업이 수행되도록 속이는 보안 공격 기술이다.
5) 이 문제를 해결하기 위해 다음과 같이 설정 파일을 수정하자.
[파일명:SecurityConfig.java]
package com.mysite.sbb;
(... 생략 ...)
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
(... 생략 ...)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
.requestMatchers(new AntPathRequestMatcher("/**")).permitAll())
.csrf((csrf) -> csrf
.ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**")))
.headers((headers) -> headers
.addHeaderWriter(new XFrameOptionsHeaderWriter(
XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)))
;
return http.build();
}
}
이와 같이 URL 요청 시 X-Frame-Options 헤더를 DENY 대신 SAMEORIGIN으로 설정하여 오류가 발생하지 않도록 했다. X-Frame-Options 헤더의 값으로 SAMEORIGIN을 설정하면 프레임에 포함된 웹 페이지가 동일한 사이트에서 제공할 때에만 사용이 허락된다.
6) 이제 다시 H2 콘솔로 로그인하면 우리에게 익숙한 화면이 등장한다. 즉, 정상으로 동작하는 것을 확인할 수 있다.
스프링 시큐리티를 사용하면 웹 프로그램(애플리케이션)의 보안을 강화하고 사용자 인증 및 권한 부여를 효과적으로 관리할 수 있으며, 외부 공격으로부터 시스템을 보호하는 데 도움을 얻을 수 있다.
댓글남기기