반응형
💡 WHY?
  • 기존의 스프링부트 프로젝트는 로그인을 하지 않더라도 페이지의 주소만 알면 접근이 가능했던(물론 로그인 정보는 뜨지 않지만) 문제가 있었다.
  • 페이지 접근에 있어서 로그인 한 경우에만 로그인 페이지 그 외의 페이지들을 접근할 수 있도록 하는 기능이 필요하다.
  • 이를 해결하기 위해 '스프링부트 Security'를 적용해보자.

 

아직 spring security에 대한 기본 지식이 많이 부족하므로, 기본적인 것부터 하나씩 추가해보려고 한다.

 

 

Spring Security 기본 설정 시작
  • Gradle에 security 의존성을 추가해준다.
    (gradle 수정 후에는 항상 build(코끼리 누르기)해 줄 것을 잊지말 것!)
implementation 'org.springframework.boot:spring-boot-starter-security'//spring security 추가
  • security 클래스 추가하기.
    config 디렉토리 하에 security 디렉토리를 생성해준 후, LoginIdPwValidator.javaSpringSecurityConfig.java 클래스를 생성해준다.

config 클래스 추가

 

  • 일단 LoginIdPwValidator는 건너 뛴다.

  • SpringSecurityConfig 클래스를 아래와 같이 설정해준다.
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .defaultSuccessUrl("/main", true)//"/"로 가도 됨.
                .permitAll()
                .and()
                .logout();
    }
}
  • SpringSecurityConfig 클래스는 WebSecurityConfigurerAdapter를 상속받아 사용하고, WebSecurityConfigurerAdapter에 정의된 다양한 security함수들을 오버라이딩하여 목적에 맞게 사용할 수 있다.
  • 이 때 @Configuration과 @EnableWebSecurity 어노테이션을 추가해 주면 된다.
  • configure함수를 오버라이딩 한 것이고, 일단은 HttpSecurity 만을 인자로 가지도록 하였다.

 

  • 아래 함수의 역할은 '어떤 URI로 접근하든지 인증이 필요하다'는 것이다.
anyRequest().authenticated()
  • 아래 함수의 역할은 '폼 방식 로그인을 사용할 것'임을 나타내고, 아래 로그아웃도 추가해주었다.
formLogin()
  • 아래 함수의 역할은 '로그인이 완료되면 해당 URI로 이동할 것'임을 나타낸다.
defaultSuccessUrl("/main", true)

 

스프링 시큐리티 예외 처리

현재 상태는 모든 경로에 대해서 사용자 접근이 제한되어 있다. 

로그인 페이지나 소개 페이지 같은 페이지들은 로그인 없이도 접근 가능해야 한다.

따라서 특정 페이지에 대한 경로 제한을 풀어주는 예외처리가 필요하다.

로그인페이지의 URI는 "/"이므로 이 경로에 대해서만 접근을 허용하기 위해 다음과 같은 코드를 SpringSecurityConfig에 추가해 주어야 한다.

'.antMatchers("/").permitAll()'

여기서 .antMatchers("/")는 Spring Security에서 URL 경로에 대한 권한 설정을 하는 부분 중 하나이다. 이 설정은 "/" 경로에 대한 URL 접근을 나타낸다.

여기에 .permitAll()을 붙여주면 모든 사용자가 이 경로에 접근하도록 허용한다.

따라서 이 코드는 로그인페이지에 대해서는 예외적으로 모든 사용자가 로그인을 하지않아도 접근할 수 있게 해준다.

 

허용하고 싶은 페이지가 여러 개라면, .antMatchers("/","/a",..) 이런식으로 경로를 나열해주면 된다.

 

백엔드와 프론트엔드가 분리되지 않은 프로젝트(스프링부트에서 jsp나 타임리프를 붙여서 하나의 프로젝트로 백엔드+프론트엔드를 전부 처리하는 프로젝트)는 css나 이미지 파일 등의 경우 인증이 되지 않은 상태에서도 보여져야 할 수 있다. 이 경우에는 별도로 WebSecurity 하나를 인자로 갖는 configure를 오버라이딩 해서 예외 처리를 할 수 있다.

 

위에서 설명한 두가지 예외처리에 대해 적용하면 아래와 같다.

 

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/").permitAll()//로그인 페이지를 제외하고 나머지 페이지는 접근 금지
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .defaultSuccessUrl("/main", true)//"/"로 가도 됨.
                .permitAll()
                .and()
                .logout();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/static/**");
    }
}

 

내 프로젝트의 경우 /static 폴더 하에 바로 img들이 있고, css 파일이 있으므로 static폴더 하위에 대해 보안 설정에 있어 모두 예외처리를 해주도록 하였다.

 

 

 

 

 

 

오늘은 여기까지.

 

📒 아래의 블로그를 참고함.
https://nahwasa.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-Spring-Security-%EA%B8%B0%EB%B3%B8-%EC%84%B8%ED%8C%85-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0
 

스프링부트 Spring Security 기본 세팅 (스프링 시큐리티)

[ 2023-02-10 추가 ] 스프링부트 3.0 이상에 적용하실 경우 '스프링부트 3.0이상 Spring Security 기본 세팅 (스프링 시큐리티)' 글을 참고해주세요. 버전 상관없이 시큐리티 기본 세팅을 익히실 경우에도

nahwasa.com

 

반응형
반응형

한 페이지에서 큰 단위로 조회 결과가 나오는 경우 한눈에 보기 어렵고 원하는 결과를 찾기도 어렵다.

이를 위해 일정한 단위만큼 나누어 페이지를 넘기도록 하는 기능을 구현하고자 한다.

 

회원 정보를 출력하는 메인 페이지에서 페이징 기능을 추가하도록 하였다.

 

스프링 프레임워크에서 제공하는 Pageable 인터페이스를 사용해보자.

 


Pageable 인터페이스?

 

Pageable 인터페이스는 페이지 번호, 페이지 크기, 정렬 조건 등을 설정할 수 있는 메서드를 제공한다. 이를 통해 검색 결과를 페이지 단위로 나누고 원하는 페이지를 가져오는 등의 작업을 수행할 수 있다.

 

  • 주요 메서드와 기능
  • getPageNumber(): 현재 페이지 번호를 반환합니다. 0부터 시작합니다.
  • getPageSize(): 페이지 크기(한 페이지에 포함되는 아이템 수)를 반환합니다.
  • getOffset(): 현재 페이지의 시작 인덱스(오프셋)를 반환합니다. 페이지 번호와 페이지 크기를 이용하여 계산됩니다.
  • getSort(): 정렬 조건을 반환합니다.
  • next(): 다음 페이지를 반환합니다.
  • previousOrFirst(): 이전 페이지를 반환합니다. 첫 번째 페이지인 경우 첫 번째 페이지를 반환합니다.
  • first(): 첫 번째 페이지를 반환합니다.
  • hasNext(): 다음 페이지가 있는지 여부를 확인합니다.
  • hasPrevious(): 이전 페이지가 있는지 여부를 확인합니다.

페이징 기능 구현을 위해 아래의 글을 참고하였다.

 

9. 스프링부트와 타임리프로 게시판 만들기 - 페이징

페이징 - 백엔드 개념 부분 페이징이란? 페이징이란 여러 게시물을 볼 때 일정 갯수 이상이 넘어가면 다음 페이지에 존재할 수 있게 하는 것을 의미합니다. 위와 같이 게시글이 일정 갯수가 넘어

velog.io

 

1. @PageableDefault으로 페이지 정보 view로 전달

환자 정보 리스트를 화면에 출력하는 main페이지가 요청될 때 페이지 정보를 view에 함께 전달해야 한다.

@GetMapping("/main") //환자 정보 리스트
public String main(Model model, HttpSession session, @PageableDefault(page = 0, size = 10, sort = "id", direction = Sort.Direction.ASC) Pageable pageable) {

    Page<PatientEntity> patientEntityList = memberService.findAll(session, pageable);
    int nowPage = patientEntityList.getPageable().getPageNumber()+1;
    int startPage = Math.max(nowPage-4, 1);
    int endPage = Math.min(startPage+9, patientEntityList.getTotalPages());

    model.addAttribute("nowPage", nowPage);
    model.addAttribute("startPage", startPage);
    model.addAttribute("endPage", endPage);
    model.addAttribute("patientList", patientEntityList);
    return "main2";
}
  • @PageableDefault(page = 0, size = 10, sort = "id", direction = Sort.Direction.ASC) Pageable pageable
    : Pageable 인터페이스에서 page는 0부터 시작한다. 한 페이지에 보일 레코드의 개수(size)를 10으로 설정하였다. sort는 정렬 기준으로, PatientEntity의 id 필드를 기준으로 정렬한다. direction은 정렬 방식으로 DESC(내림차순)와 ASC(오름차순)으로 설정할 수 있다. 이 초기값을 바탕으로 Pageable 인터페이스를 생성한다.
  • Page<PatientEntity> patientEntityList = memberService.findAll(session, pageable);
    : 페이지 기능으로 하기 전에는 List형태였으나, 이제 Pageable을 사용하기 때문에, Page로 가져올 것이다. 타입 역시 Entity타입으로 가져온다. 
    서비스의 findAll함수를 pageable을 추가한 것에 맞게 수정해야 한다!!!
  • int nowPage = patientEntityList.getPageable().getPageNumber()+1
    : 현재페이지에 대한 정보이다. pageable은 페이지가 0부터 시작하므로 +1을 해줌으로써 1페이지부터 시작하도록 한다.
  • int startPage = Math.max(nowPage-4, 1)
    : 표시될 시작페이지에 대한 정보이다. 현재 페이지 앞에 최대 4개까지 보이도록 하고, 4개보다 적다면 1페이지부터 표시되도록 한다.
  • int endPage = Math.min(startPage+9, patientEntityList.getTotalPages())
    :표시될 마지막페이지에 대한 정보이다. 시작페이지에 9를 더한 값의 페이지까지 보이도록 하여 총 10개만 보이도록 하였고, 최대 페이지를 넘지는 않도록 하였다.
  • 각 값들을 model의 attribute로 view에 전달한다.

 

2. 서비스의 findAll()함수에 Pageable 기능을 하도록 수정

public Page<PatientEntity> findAll(HttpSession session, Pageable pageable){
        Long Id = (Long) session.getAttribute("loginId");
//        List<PatientEntity> patientEntityList = patientRepository.findByMemberEntity_Id(Id);
//        List<PatientDTO> patientDTOList = new ArrayList<>();
//        for(PatientEntity patientEntity: patientEntityList){//여러개의 entity를 여러개의 dto로 하나씩 담기위해
//            patientDTOList.add(PatientDTO.toPatientDTO(patientEntity));
//        }
        return patientRepository.findByMemberEntity_Id(Id, pageable);
    }

서비스 함수의 반환 타입을 Page 형태로 PatientEntity들이 전달되도록 수정하였다. 그리고 Pageable 인터페이스를 인자로 받는다.

주석처리된 부분은 페이징 처리 전에 List형태로 받았던 코드이다.

레포지토리의 findByMemberEntity_Id함수를 수정하여 그 반환형태 자체를 반환하도록 하였다.

 

 

3. 레포지토리 findByMemberEntity_Id함수 수정하기

Page<PatientEntity> findByMemberEntity_Id(Long id, Pageable pageable);

pageable 정보를 넘겨주면 조회결과 레코드를 10개씩 나누어 페이지로 알아서 반환해준다.

 

 

4.view에서 페이징 처리하기!

<div class="card-footer py-4">
  <nav aria-label="...">
    <ul class="pagination justify-content-end mb-0">
      <li class="page-item disabled">
        <a class="page-link" href="#" tabindex="-1">
          <i class="fas fa-angle-left"></i>
          <span class="sr-only">Previous</span>
        </a>
      </li>
      <th:block th:each="page:${#numbers.sequence(startPage,endPage)}">
        <li class="page-item" th:class="${page == nowPage} ? 'page-item active' : 'page-item'">
          <a class="page-link" th:if="${page != nowPage}" th:href="@{/main(page=${page-1})}" th:text="${page}"></a>
          <a class="page-link" th:if="${page == nowPage}" th:text="${page}" style="color:white; font-weight:bold;"></a>
        </li>
      </th:block>
      <li class="page-item">
        <a class="page-link" href="#">
          <i class="fas fa-angle-right"></i>
          <span class="sr-only">Next</span>
        </a>
      </li>
    </ul>
  </nav>
</div>
  • 첫번째 li는 이전 페이지로 이동하는 버튼을 구현한것이다
  • 두번째 li가 현재 페이지를 포함한 10개의 페이지 버튼을 보이도록 구현한것이다.
    th:each를 통해 model로 전달된 startPage와 endPage의 정보를 가지고, 각 페이지를 표시하도록 한다.
    각 페이지를 순회하면서 현재 활성화된 페이지와 같으면, page-link 스타일을 가지면서, 페이지 번호는 white색이며, 볼드체로 표시되도록 하였고, 활성화된 페이지가 아니면, page-link스타일을 가지면서 페이지 번호에 해당 페이지 링크를 걸어주었다.
  • 세번째 li가 다음 페이지로 이동하는 버튼을 구현한 것이다.

 

5. 구현결과  

페이지 버튼 성공!!

5페이지 까지는 1이 첫번째고, 10이 최대며, 그다음부터는 1씩 늘어나며 shift되는 것을 확인하였다!

잘 동작한다!!! 디자인도 잘 적용되었다!

반응형

반응형

서비스 함수에서 jpa repository에 정의된 쿼리 함수를 호출하는데에서 위와 같은 "No EntityManager with actual transaction available for current thread - cannot reliably process 'remove' call" 오류가 발생하였다.

 

오류가 발생한 코드의 위치

remove 명령을 실행하는 과정에서 트랜잭션과 관련된 문제가 발생한 것을 확인할 수 있다.

 

알아보니 이 오류는 스프링 트랜잭션 관련 문제로, 현재 스레드에 실제 트랜잭션이 없기 때문에 'remove' 호출을 신뢰할 수 없다는 것을 의미한 것이었다.

 

위 오류는 다음 두 가지 상황에서 발생할 수 있는데,

 

첫번째, 스프링의 트랜잭션 관리 설정이 올바르게 구성되어 있는지?

예를 들어, @EnableTransactionManagement 어노테이션이 설정되어 있는지 확인하고, 트랜잭션 관리자가 적절히 설정되어 있는지 확인해야 한다!

 

두번째, 트랜잭션 어노테이션이 메서드나 클래스에 적용되지 않은 경우

@Transactional 어노테이션을 메서드나 클래스에 추가하여 트랜잭션을 활성화해서 해결할 수 있다!

 

나의 경우 두번째에 해당!!!!

 

나의 코드를 보면, 트랜잭션 경계를 벗어난 상황에서 레포지토리 함수를 호출하고 있다. 

스프링에서 트랜잭션은 일반적으로 서비스 레이어에서 관리되는데, 다른 레이어(컨트롤러)에서 트랜잭션 경계를 벗어나서 호출하면 이러한 오류가 발생할 수 있다.

따라서 트랜잭션 범위 내에서 해당 호출을 수행하도록 코드를 구성해야 한다!

 

아래와 같이 해결하였다.

 

서비스 레이어에서 트랜잭션을 관리하도록 어노테이션 추가

 

 

이전에 비슷한 오류가 발생하였을 때, 레포지토리 함수 자체에 @transactional 어노테이션을 추가하여 해결한 적이 있다.

 

그렇다면, 레포지 함수 자체에 어노테이션을 추가하는 것과, 이를 호출하는 서비스 함수에 어노테이션을 추가하는 것 중 어느것이 더 적절한 방법일까?

 

일반적으로 서비스 계층에서 @Transactional 어노테이션을 추가하는 것이 더 좋은 설계이다!!!

 

이유는 다음과 같다

 

  1. 레이어 간의 역할 분리 : 서비스 계층은 비즈니스 로직을 처리하는 데 책임이 있으며, 트랜잭션 관리 역시 서비스 계층에서 이루어져야 한다. 따라서 트랜잭션 관리는 서비스 계층의 역할이며, 레포지토리는 데이터 액세스에 집중해야 한다. 이를 위해 트랜잭션 관리는 서비스 계층에 위임하는 것이 좋다!
  2. 트랜잭션 경계의 명확성: 서비스 계층에서 @Transactional 어노테이션을 추가하면 트랜잭션의 범위가 서비스 메서드의 호출과 일치하게 된다. 이는 트랜잭션 경계를 명확하게 설정하여 예기치 않은 동작을 방지하는 데 도움이 된다.
  3. 트랜잭션 관리의 유연성: 서비스 계층에서 트랜잭션 관리를 담당하면 여러 레포지토리 메서드를 호출하는 복잡한 비즈니스 로직에서도 단일 트랜잭션으로 묶을 수 있다. 이는 일관된 데이터 처리와 롤백을 보장하여 데이터의 무결성을 유지하는 데 도움이 된다.

따라서, 가능하다면 레포지토리에서 직접 @Transactional 어노테이션을 추가하는 대신 해당 함수를 호출하는 서비스 계층에서 @Transactional 어노테이션을 추가하는 것이 좋다.

 

이렇게 하면 역할과 책임이 분리되며, 트랜잭션 관리가 명확하고 유연하게 이루어질 수 있다!!!

 

"트랜잭션 관리를 명확히 하기!!!"

반응형

+ Recent posts