반응형

인텔리제이에서 스프링부트 프로젝트 실행은 잘 되는데, 중지 버튼만 누르면 이런 오류 메시지가 뜨나요?

Execution failed for task ':RestDemoApplication.main()'.
> Build cancelled while executing task ':RestDemoApplication.main()'

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
For more on this, please refer to https://docs.gradle.org/8.4/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
BUILD FAILED in 32s
3 actionable tasks: 1 executed, 2 up-to-date

 

대충 읽어보면 gradle 버전이 호환이 되지 않는다는 내용입니다.

 

실행은 잘 되는데, 멈추기만 하면 이런 오류메시지가 뜹니다.

 

해당 문제는 프로젝트 설정에서 gradle로 실행되게 했기 때문입니다.

 

다음 절차를 따라주세요.

 

1.file > project structure로 들어가세요

 

그런 다음

 

project의 SDK가 java 11로 되어있는지 확인합니다.

 

2. 변경한 설정을 완료한 후 다시 File > Settings를 들어가세요

 그런다음

 

돋보기에 gradle을 검색합니다.

그럼 위와 같은 창이 뜹니다.

튜브가 위치한 곳의 언어를 IntelliJ IDEA로 변경해주세요

아마 처음에는 gradle로 초기값이 설정되어 있을거에요!

그리고 아래 gradle JVM역시 자바 11버전으로 설정해주세요

 

다 하신 후에는..

3. file > open > 현재 프로젝트의 build.gradle을 누르세요

 

그런다음 Open as Project로 열어주신 후 실행하시면 됩니다.!

 

 

그 결과...

 

 

 

실행을 멈추어도 오류 메시지가 뜨지 않고 finish 되었네요!

반응형
반응형
API ?

"API 만들어 본 적 있어?"

친구의 질문으로 시작된 본격 API 파헤치기.. 

여기서 API는 [ Application Programming Interface ]의 약자로, 서로 다른 애플리케이션 사이에서 데이터를 주고 받을 수 있게 해주는 중간 다리 역할 이라고 생각하면 된다.

 

API에는 RESTful API, SOAP API, JPA... 등등 여러 종류가 있다.

 

지금껏 내가 해왔던 프로젝트는 클라이언트와 서버간의 상호작용에 있어서는 전통적인 자바 애플리케이션 @controller를 사용했다. 즉, HTTP 메서드인 GET, POST, PUT, DELETE (소위 CRUD라고 함)를 활용해서 조작하지 않고, @Getmapping, @Postmapping으로 url을 잡고 사용해왔다.

이런 방식은 사실 상 서버 사이드 렌더링을 통해서 웹 페이지를 생성하고 제공하는데에 사용되기 때문에, 서버랑 클라이언트가 강력하게 결합되어 있어서 서버-클라이언트 간의 독립성은 떨어진다. (한 마디로 정말 간단한 웹 서비스 개발..)

[서버사이드 렌더링이 뭐고] ↴

더보기

서버 사이드 렌더링이 무슨말이냐구요?

 

서버 사이드 렌더링(Server-Side Rendering, SSR)은 웹 애플리케이션의 사용자 인터페이스(UI)를 서버에서 생성하고 초기 로드 시에 클라이언트에게 완전한 HTML 페이지를 제공하는 웹 개발 기술입니다. 이것은 클라이언트 사이드 렌더링(Client-Side Rendering, CSR)과 대조적입니다.

SSR의 작동 방식은 다음과 같습니다:

  1. 클라이언트에서 웹 페이지 요청을 서버로 보냅니다.
  2. 서버는 요청을 받아 해당 요청에 필요한 데이터를 데이터베이스에서 가져오거나 다른 외부 소스로부터 데이터를 가져옵니다.
  3. 서버는 서버 사이드 렌더링 엔진을 사용하여 사용자 인터페이스(UI) 템플릿을 렌더링하고, 데이터를 포함한 HTML 페이지를 생성합니다.
  4. 서버는 완전한 HTML 페이지를 클라이언트에게 반환합니다.
  5. 클라이언트는 받은 HTML을 렌더링하고 페이지를 화면에 표시합니다.

하지만 요즘 대부분의 서비스는 여러 형태의 클라이언트 플랫폼과 서버간의 통신이 이루어지는 방식이기 때문에, 서버와 클라이언트가 독립적이면서도 잘 통신할 수 있도록 하는 방식이 선호된다.

 

그런 방식이 대표적으로 RESTful API라고 할 수 있다.

 

그럼 RESTful API는 뭐야???

 

RESTful API?

RESTfult API는 [ Representational State Transferful Application Programming Interface ]의 약자로, URI에 자원의 정보를 나타내도록 하고, HTTP 메서드( GET, POST, PUT, DELETE  )를 사용해서 자원을 조작하는 것이다.

 

자원?? URI??? 조작???

 

한가지 예를 들어보자.

회원(회원정보)이 있고, 상품(상품정보)이 있다고 가정하자. 각각은 자원이라고 부른다. 

회원 정보에 대해서 조회(GET)를 할수 있고, 새로운 회원 정보를 생성(POST)할 수 있고, 회원 정보를 수정(PUT)할 수 있고, 회원 정보를 삭제(DELETE)할 수 있다.

mapping할 때마다 우리는 URI를 적었다. 그 URI를 자원에 따라서 패턴화 하고, 적절한 HTTP 메서드를 사용함으로써 동작을 정의할 수있다.

  

즉 이와 같이 쓸 수 있다.

  • GET /members: 모든 회원 목록 조회.
  • GET /members/{id}: 특정 회원 조회.
  • POST /members: 새로운 회원 생성.
  • PUT /members/{id}: 특정 회원 수정.
  • DELETE /members/{id}: 특정 회원 삭제.
  • GET /products: 모든 상품 목록 조회.
  • GET /products /{id}: 특정 상품 조회.
  • POST /products : 새로운 상품 생성.
  • PUT /products /{id}: 특정 상품 수정.
  • DELETE /products /{id}: 특정 상품 삭제.

 이렇게 한 자원에 대해서 '/자원'으로 패턴화 하는 과정을 거치고, 그 안의 세부 동작이나 자원에 대해서는 /이하에 붙여서 URI를 통해 자원의 상태를 나타내어 가독성을 높혀주는 것이다.

 

특징을 보면 자원이 모두 복수명사 형태로 표현된 것을 확인 할 수 있다.

리소스 명은 동사보다는 명사를 사용하도록 하는 것이 바람직한 표현 방식이다.

 

따라서 명심하자.

 

GET /members/delete/1  //잘못된 표현

이와 같이 동작을 나타내는 delete를 리소스에 작성하는 것이 아니다.

 

DELETE /members/1  //옳게 수정된 표현

 자원은 명사로만 두고, 동작은 HTTP 메서드로 표현하는 것이다.

 

(URI 설계의 자세한 내용은 아래의 블로그를 참고하자.)

 

 

개발 초보를 위한 RESTful API 설계 가이드

초보자를 위한 RESTful API 설계 가이드를 작성해보았습니다.

velog.io

 

스프링부트에서 코드 상 어떻게 쓰이는지 살펴보자.

@RestController
@RequestMapping("/api/members")
public class MemberRestController {
    // 멤버 API 정의
    
    @GetMapping
    public ResponseEntity<List<MemberDTO>> getAllMembers() {
        // 모든 멤버 목록 조회 로직
        List<MemberDTO> members = memberService.getAllMembers();
        return new ResponseEntity<>(members, HttpStatus.OK);
    }

    @GetMapping("/{id}")
    public ResponseEntity<MemberDTO> getMember(@PathVariable Long id) {
        // 특정 멤버 조회 로직
        MemberDTO member = memberService.getMemberById(id);
        return new ResponseEntity<>(member, HttpStatus.OK);
    }

    @PostMapping
    public ResponseEntity<MemberDTO> createMember(@RequestBody MemberDTO memberDTO) {
        // 새로운 멤버 생성 로직
        MemberDTO createdMember = memberService.createMember(memberDTO);
        return new ResponseEntity<>(createdMember, HttpStatus.CREATED);
    }

    @PutMapping("/{id}")
    public ResponseEntity<MemberDTO> updateMember(@PathVariable Long id, @RequestBody MemberDTO memberDTO) {
        // 특정 멤버 수정 로직
        MemberDTO updatedMember = memberService.updateMember(id, memberDTO);
        return new ResponseEntity<>(updatedMember, HttpStatus.OK);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteMember(@PathVariable Long id) {
        // 특정 멤버 삭제 로직
        memberService.deleteMember(id);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}

각 객체의 CRUD를 위해서 하나의 RESTController로 정의되는 것을 확인할 수 있다.

어노테이션에 주목하자.

@RestController
@RequestMapping("/api/members")

@RestController를 통해서 RESTful API를 사용하겠다는 것을 나타내고 있다.

@RequestMapping을 통해서 URI 패턴을 정의하기 시작했다. 회원에 대해서는 /api/members가 기본 URI로 동작할 것이다.

그리고 각각의 CRUD 동작에 맞추어서 @Getmapping @Postmapping @Putmapping @Deletemapping을 사용하고 있는 것을 확인할 수 있다.

 

여기서 또 중요한 점!!!

 

기존 프로젝트에서는 CRUD결과 html 페이지(main.html 이런 파일 자체)를 반환했었는데에 반해(그래서 더욱 결합이 된 것일지도..), RESTful API 방식에서는 ResponseEntity를 반환하고 있다!!!??

 

ResponseEntity ?

ResponseEntity는 Spring Framework에서 제공하는 클래스로, HTTP 응답을 나타내는 객체이다. 이 클래스를 사용하여 클라이언트에게 HTTP 응답을 구성하고 전달할 수 있다.

 

왜 이 객체를 반환하는 것일까?

 

그건 바로 RESTful API 방식에서의 HTTP 응답 형식은 JSON이나 XML같은 데이터 형식을 사용함으로써 클라이언트에 응답하기 때문이다. 따라서 반드시 반환 형태를 JSON 또는 XML형태의 데이터로 주어야 한다.

 

ResponseEntity는 다음과 같은 기능을 제공한다. 

  1. HTTP 응답 상태 코드 설정
  2. HTTP 응답 헤더 설정
  3. 응답 본문 데이터 설정
  4. 응답 타입 설정

 

이것도 코드를 통해서 쓰임을 살펴보자.

 

@GetMapping
    public ResponseEntity<List<MemberDTO>> getAllMembers() {
        // 모든 멤버 목록 조회 로직
        List<MemberDTO> members = memberService.getAllMembers();
        return new ResponseEntity<>(members, HttpStatus.OK);
    }

 매핑 결과 ResponseEntity 객체를 new로 생성해서 members의 정보와 함께 HttpStatus.OK라는 것을 담아 반환하고 있다.

members는 조회 결과를 담은 데이터를 반환해 준 것일테고, HttpStatus.OK가 조금 생소하다.

 

HttpStatus.OK가 바로 ResponseEntity의 기능 1번인 HTTP 응답 상태 코드를 나타내는데, 의미는 200 OK 상태를 나타내는 코드이다.=말 그대로 OK= 요청이 성공적으로 처리되었음

HttpStatus.NOT_FOUND라는 코드는 404 not found 상태를 반환하라는 의미이다.= 요청한 리소스를 찾을 수 없음

(상태별로 코드가 정리된 표도 위 참고 블로그에 있으니 참고하자.)

 

상태 정보를 넘기는 이유가 뭘까?

상태코드 ?

상태 코드는 서버가 클라이언트에게 요청을 처리한 결과를 전달하는 수단이다. 클라이언트는 상태 코드를 통해 요청이 성공했는지, 실패했는지, 어떤 종류의 오류가 발생했는지 등을 파악할 수 있다.

 

클라이언트는 상태 코드를 기반으로 다음 단계를 결정할 수 있다. 예를 들어, 성공적인 응답(예: 200 OK)일 경우 데이터를 표시하고, 오류 응답(예: 404 Not Found)일 경우 오류 메시지를 표시하거나 다른 조치를 취할 수 있다.

 

이 외에도 다양한 이유가 있지만, 서버와 클라이언트 간의 정확하고 원활한 상호작용을 위해서가 중점이라고 할 수 있겠다.

 

 

마지막으로 한가지 더 잡고 넘어가야 할 부분이 있다.

 

바로 '세션' 문제인데, 기존의 프로젝트 코드(전통적인 웹 애플리케이션 방식)에서는 @HttpSession을 통해서 로그인 상태를 세션을 통해 유지할 수 있었다.

근데 RESTful API방식은 상태를 관리하지 않는 stateless 방식을 따른다. 각 요청이 독립적이고, 서버는 클라이언트의 상태를 유지하지 않기 때문에, 클라이언트는 요청을 할 때 요청 정보를 함께 제공해 주는 과정이 필요하다!!

 

예를 들어서 인증 정보는 요청 헤더에 토큰 또는 인증 정보를 포함하여 전송할 수 있다. 토큰 기반 인증을 사용해 사용자 인증을 하고 상태 관리를 할 수 있는 것이다.

 

RESTful API에서의 토큰 기반 인증:

  1. 사용자가 서버에 로그인하면 서버는 사용자에게 액세스 토큰(access token)을 발급합니다.
  2. 클라이언트는 액세스 토큰을 안전한 방식으로 저장하고 각 요청에 포함시켜 서버로 보냅니다. 일반적으로 요청의 헤더에 포함됩니다.
  3. 서버는 액세스 토큰을 검증하고, 유효한 토큰인 경우 해당 사용자를 식별하고 요청을 처리합니다.

요청의 헤더가 어디일까..~

 

일반적으로 사용되는 토큰 기반 인증 방식 중에는 OAuth 2.0 및 JWT(Json Web Token)가 있습니다. 이러한 인증 방식을 사용하여 RESTful API에서 사용자 인증 및 세션 관리를 구현할 수 있습니다.

 

OAuth...!!! 이번 정처기 실기에 나왔던 개념인데, 이걸 먼저 공부했더라면 맞힐 수 있었을텐데..

 

토큰 관련해서는 좀 더 공부가 필요할 것으로 보인다. 과거에 express랑 node.js로 개발했을 때 로그인에서 한번 쓴 것 같기도 한데(토큰 유효 시간 설정하고 그랬었음), 기억이 잘 나지 않은 것 보니.. RESTful API로 한번 프로젝트 파서 직접 해봐야겠다!!!

 

할거 +1됨.

 

 

 

반응형
반응형
💡 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