Springboot를 활용한 아주아주 간단한 게시판 CRUD

2023. 4. 16. 03:23초기 과업/BackEnd

작성자알 수 없는 사용자

728x90
반응형

 

안녕하세요. 기깔나는 사람들에서 백앤드를 맡고있는 Hardy입니다.

 

 

오늘은 Springboot를 활용하여 간단한 게시판 CRUD를 소개하려고 합니다.

 

코드 로직은 전혀 복잡하지 않고 모두가 아시는 간단한 게시판 쓰기 , 삭제하기 , 조회하기 , 수정하기에 대해

 

간단하게 설명을 할 예정입니다.

 

JPA, QueryDsl을 사용하여 진행할 예정입니다!!

 

 


Board Entity 설계

 

간단한 게시판(Board)에 관한 엔티티 내용입니다.!

 

@Entity를 붙이게 되면 JPA의 구현체인 HIbernate가 해당 객체를 데이터베이스의 테이블과 매핑을 하고 , 객체의 속성들을 테이블의 컬럼과 매핑하게 됩니다.

 

엔티티에는 반드시 @Id가 존재해야 하며(JPA스펙) , public , protected로 선언된 매개변수가 없는 생성자가 필요합니다.

위에 코드에서는 @NoArgsConstructor라는 어노테이션으로 이를 대체 합니다!

 

여기서 왜 public , protected로 된 매개변수가 없는 생성자가 필요로 할까요???

 

그 이유는 Reflection을 사용하기 때문이에요!

 

Reflection을 사용하면 생성자의 매개변수에 대한 정확한 정보는 가져 올 수 없어요! 그렇기 때문에 기본생성자를 통해서 객체를 생성하게 된답니다.

 

 JPA가 엔티티를 생성 하기위해 Reflection을 사용하기 때문에 기본생성자가 필요로 합니다!!

 

그럼 여기서 또 질문!!! 왜 private는 안되나요??

 

그 이유는 JPA의 프록시랑 관련이 있어요!! 엄청 복잡한 내용이지만 간략적으로 설명 드릴께요! 이 부분은 한번 찾아보시는것도 좋아요!

 

Jpa에서 프록시 객체는 반드시 원본 class를 상속하여 만들게 됩니다!

 

이때 JPA에 의해 프록시 객체가 생성될때 상속하는 원본 class의 생성자를 호출하게 되는데.. private로 되어 있으면 호출을 할 수 없게 되고 즉 문제가 발생됩니다.

 

@Entity가 붙은 객체들은 Hibernate에 의해 데이터베이스 테이블과 매핑이 되기 때문에 @Column어노테이션을 통해 해당 속성에 속성을 둘 수 있습니다.


 

 

Repository 및 QueryDsl

우선 queryDsl에 대해 간략하게 설명 드릴께요!

 

Querydsl은 Java언어 기반의 쿼리빌더라고 생각하시면 됩니다.

 

QueryDsl을 사용하는 이유는 여러가지가 있지만 저는 크게 3가지라고 생각해요!

 

동적 쿼리 , 타입안정성 , 메소드 체이닝을 통한 쿼리 작성(객체지향) 이라고 생각해요!

 

동적쿼리는 쉽게 생각하면 실행 시점에 쿼리가 생성되어 실행되는거에요! 예는 검색이나 , 정렬이 있겠죠?

 

타입 안정성은 컴파일 시점에 타입 안정성을 알 수 있어요? 또한 Native query는 동작을 해봐야 런타임시점에 쿼리 오류를 발견할 수 있다는 단점이 있지만 QueryDsl은 전혀 걱정 NO!

 

그럼 이제 제가 사용한 QueryDsl을 설명 드릴께요!

 

BoardQueryDslRepository

간단한 조회 queryDsl

 

우선 검색은 제목과 내용을 포함하여 검색을 하도록 했어요!

 

또한 이해를 쉽게 하기 위해 검색조건이 있는지를 if문으로 처리하도록 했어요!

 

좋은 방식은 if문을 외부로 빼는게 좋겠죠!!

 

selectTitleAndContent는 검색조건에 따라 검색을 하고 페이징 처리를 할 수 있는 메소드에요!

이런게 바로 동적쿼리에 한 예시라고 할 수 있어요!

 

여기보시면 Q가 붙은 클래스가 보이시나요??

 

QueryDsl에서 사용하는 클래스로 , 엔티티 기반으로 쿼리를 생성할 수 있도록 도와 주는 클래스에요!

QueryDsl로 사용되어지는 엔티티들은 Qclass가 반드시 필요합니다! gradle기준으로 반드시 compile해주시면 

자동으로 생기실 꺼에요!

 

jpaQueryFactory
.select(QBoard.board)
.from(QBoard.board)
.where(QBoard.board.isDeleted.eq(false))
.orderBy(getOrders(pageable, QBoard.board.getRoot()))
.offset(pageable.getPageNumber())
.limit(pageable.getPageSize())
.fetch();

 

해당 코드를 보시면 Native쿼리를 쓴거 처럼 메소드 채이닝으로 알아보기 쉽게 되어있죠?

 

select 절에는 Qclass를 통해서 원하는 컬럼만 뽑아 낼 수 있고 , 저렇게 board 전체를 가지고 올 수 있답니다.

 

from절에는 어떤 엔티티를 대상으로 데이터를 가져 올지 설정해요

 

where절에는 원하는 조건을 넣을 수 있어요! 저는 게시판 삭제는 deleted라는 컬럼을 넣어 주었기 때문에 해당 컬럼의 값이 false인 값만 가지고 오도록 했어요! 즉 native query에서 where절에 들어갈 내용을 체이닝방식으로 설정해주시면 됩니다! natvie 쿼리에서 지원하는건 거의 지원해요 (and , eq  등등)

 

orderBy를 통해서 정렬하고자 하는 컬럼을 정렬 할 수 있어요! 

 

offset과 limit는 페이징 처리를 하기 위함이에요!!! 이렇게 동적 쿼리를 생성할 수 있어요!

 

 

BoardRepository

repository

 

JPA에서 지원하는 JpaRepository를 상속하면 JpaRepository가 지원하는 간단한 메소드를 통해 쿼리를 사용할 수 있어요!

아래는 JpaRepository가 지원하고 있는 메소드 들이에요!

jpaRepository

제네릭 값으로는 해당 Repository를 적용할 엔티티객채 , 엔티티 객체에서 사용하는 id속성 타입을 넣어주시면 됩니다.

 

이제 테스트 코드를 작성해 볼까요??

 

우선 BoardQueryDslRepository부터 생성해 볼께요!

 

테스트 코드

 

@DataJpaTest어노테이션을 사용하면 테스트를 위한 JPA관련 빈들이 자동으로 등록이 되요!

dataSource , entityManager 등등 테스트에 필요한것들이 자동으로 빈으로 등록되고 이를 기반으로 테스트 코드가 돌아가요!

 

해당 테스트 코드를 설명하면 제목과 , 내용을 둘다 검색을 하는 코드에요!

해당 테스트를 위해 먼저 Board를 저장했어요!

 

그 이후 DI된 boardQuerydslRepository의 메소드를 호출해서 값을 체크 하고 있어요!

현재는 리턴괸 결과의 숫자만 확인하지만!! 좀더 구체적으로 작성해주시는게 좋아요!

 

정렬 테스트

 

여기서 위에 코드랑 다른점은 정렬을 지정해줬어요!

정렬은 PageRequest를 사용하여 불러올 데이터 개수와 Sort를 지정해 주었어요!!!

 

1. 게시판 데이터 저장

2. 정렬할 데이터 , 페이징에 대한 정보를 담은 PageRequest생성

3. boardQuerydslRepository의 selectTitleAndContent메소드 호출

4. 검증

이런 절차를 가지고 있어요!

 

이 두코드 만으로도 정렬과 조회를 모두 테스트 해줬어요! 원하는 기능이 있으면 여기서 더 테스트 코드를 작성하시면 됩니다!!!

 

여기까지 Repository , QueryDsl , QueryDsl테스트 코드를 한번 봤어요!

 

밑에서는 각각 CRUD 에 대한 service , controller 레이어를 설명할께요!!!


 

게시판 생성하기

 

 

controller

 

게시판 저장은 클라이언트로 부터 Body로 게시판 내용을 받아서 저장하도록 구현했어요!

클라이언트로 부터 Body를 받아서 Dto에 매핑을 위해 @RequestMapping을 붙여줬어요!

또한 겅즘 처리를 위해 @Valid어노테이션을 사용했어요

 

@Valid어노테이션이 작동하는 과정은 간략하게 설명 드릴께요!

 

클라이언트로 부터 요청이 들어오면 HandlerMapping을 통해 요청이 처리될 컨트롤러를 조회해요!

그 이후 HandlerAdapter를 통해 컨틀롤러를 호출해요! 이때 @Valid어노테이션이 있는지 확인을 하고 

있다면 Validator를 통해서 검증을 진행합니다!

 

여기서 Principal은 스프링 시큐리티를 사용했기에 현재 요청을 보낸 인증 처리된 사용자를 위해 사용했어요! 

 

requestDto

 

 

Validator에 의해 검증되는 것은 @NotBlank , @NotNull로 검증을 위한 어노테이션이 붙은 필드값을 검증을 진행해요!

 

검증이 실패하면 어떻게 될까요?  오류를 처리하는 클래스를 보여줄께요!

advisor

@RestControllerAdivce 어노테이션이 붙은 클래스를 작성해줍니다!

해당 어노테이션이 작성된 클래스는 컨트롤러전역에서 발생할 수 있는 예외를 잡아 처리해주는 annotation이라고 생각하시면 좋아요!

 

@ExceptionHandler에는 처리될 exception을 지정해주면 해당 exception이 발생하면 해당 메소드로들어와서 처리가 됩니다.@ExceptionHandler는 aop로 동작하게 되면 exception이 발생하면 스프링이 가로채서 해당 메소드를 호출하게 해줍니다! 

 

해당 메소드안에서는 적정한 응답값을 생성하여 클라이언트에 전달하게 됩니다.

 

Aop는 중요한 내용이기에 꼭 찾아보세요!!!!!!

 

 

이렇게 검증이 된 데이터를 받아서 boardService.update()메소드를 호출하여 처리를 진행합니다!

 

서비스 코드 보여 드릴께요!

 

service
findMember

@Transactional을 붙인 이유는 디비에 쓰기 작업을 하기 때문에 save 이후 로직에서 exception이 발생하면 롤백을 위함이 커요! 

 

클라이언트로 부터 받은 boardRequestDto클래스에 ofBoard()메소드를 통해서 Board앤티티에 필요한 데이터를 담아서 엔티티를 반환해요! 해당 엔티티는 아직 영속성 컨텍스트에서 관리 되지 않아요!

 

findMember를 호출해서 해당 요청을 보낸 클라이언트의 데이터가 디비에 있는지 조회를 해요! 조회를 하는 이유는 사용자가 디비에 저장된 사용자인지 확인을 위함이며 , 게시판과 연관관계를 설정하기 위해 조회를 해요!

여기서 repository의 findById를 통해 조회를 하면 Optional을 반환해요! 그렇기에 없는 경우에는 exception을 던지도록 orElaseThrow를 통해 exception을 발생하도록 했어요!  

 

그 이후 Member객체를 가져온후 만들어진 board.initMemeber를 통해 Member와 Board의 연괄 관계를 설정해 주었어요! 이렇게 되면 board가 저장 될때 memberId도 들어가게 된 답니다!

board안에 있는 메소드로 member와 연관관계 설정

 

그 이후 repository를 통해 board를 저장해 주었어요! 이렇게 되면 저장 작업은 완료가 됩니다!

 

 


 

게시판 업데이트하기

 

 

controller

 

@PutMapping어노테이션은 리소스를 업데이트 하는 Http Put메소드 요처을 처리하는 어노테이션이에요!

 

@PathVariable은 URL에 포함된 값을 변수에 할당할 때 사용하는 어노테이션이요! 아래와 같이 URL이 있다면 1을 받기 위해 사용하는 어노테이션이에요!!

/api/1

 

그 이외에는 게시글 저장 컨트롤러와 받는 데이터는 다르지 않아요!!

 

 

 

service update

 

URL로 부터 받은 ID값을 통해 Board객체를 조회해와요!

위에서 설명했듯이 repository의 id값을 통해 검색해 오면 Optional을 반환하하기 없는 경우에는 Exception을 발생 시키도록 했어요!

 

그 이후 요청한 클라이언트의 아이디 값으로 Member객체도 조회해 옵니다!

 

게시판 수정은 작성자가 동일한 경우에만 수정할 수 있게 하기 위해 조회해온 Board와 Member객체를 equal비교를 진행했어요!

 

여기서 발생하는 Exception들은 모두 위에서 기재한 @RestControllerAdvice에서 처리되요! 벗뜨 처리하기 위해서는 

@RestControllerAdvice로 지정된 클래스안에 @ExceptionHandler로 발생할 Exception를 등록해주어야 해요!

 

그 이후 변경된 데이터를 board.change 메소드로 적용하도록 했어요! 그럼 board에 변경된 내용이 적용이 되었어요

 

change메소드

 

그 이후 컨트롤러에 반환될 값을 return합니다.

 

여기서 중요한게 Update메소드 안에서 변경할 내용을 디비에 적용을 해야하는데 repository.update라는 메소드가 왜 없을까요??

 

그 이유는 영속성 컨텍스를 아시면 바로 이해가 되실꺼요!

 

영속성 컨텍스트 기능중에 변경감지라는게 있어요!

변경감지는 엔티티에 변경이 발생하면 영속성 컨텍스트에 있는 원본 엔티티랑 비교 후 변경된 쿼리를 쓰기 지연소에 저장을하고 flush가 발생하면 데이터베이스에 적용해요! 그렇기에 따로 업데이트 메소드를 호출하지 않아도 됩니다 참 편하죠!!!

 


 

게시판 삭제하기

 

여기서 삭제는 Hard삭제가 아니 Soft삭제를 진행했어요!

 

Hard삭제는 디비에서 Delete를 활용하여 row를 완전히 삭제하는걸 의미에요

Soft삭제는 디비에서 Delete를 활용하여 row를 완전히 삭제하는것이 아니라 삭제여부 필드를 주고 해당 필드값을 업데이트를 하도록 하는거에요!

 

delete

 

해당 컨트롤러는 삭제를 진행하는 컨트롤러에요!

삭제할 board의 id를 @PathVariable로 받았어요! @PathVariadble은 위에 설명했어요!

받은 id값으로 boardService.deleteBoard()메소드를 호출 했어요

 

deleteMethod

 

board를 조회한 후 없다면 Exception을 발생하도록 했어요!

 

만약 조회된 데이터가 있다면 board의 delete메소드를 호출했어요

board엔티티에 delete메소드

여기서 보시면 soft삭제를 진행했어요! isDeleted의 값만 true값으로 변경하면서 삭제되었다고 처리를 해줬어요!

 

여기도 마찬가지로 findBy로 조회를 해왔기 때문에 영속성 컨테스트에 의해 관리되는 객체가 됩니다!

 

그 이후 해당 엔티티의 속성값이 변경이 되면 영속석 컨테스트의 자동감지로 update문이 쓰기 지연장소에 들어가고 flush되는 시점에 isDeleted속성을 업데이트 하는 쿼리를 발생 시켜요!!

 

 

 


 

게시판  조회하기

 

이제 마지막으로 조회를 하는 부분을 설명하도록 할께요!!!

 

select Controller

@GetMapping은 Http Get Method를 매핑하겠다는 어노테이션이에요!

 

Pageable객체는 페이징 처리 , 정렬 등을 처리할 수 있게 해당 데이터를 담고 있는 클래스에요!

 

@RequestParam을 통해 해당 컨트롤러 호출시 검색어도 받도록 했어요! 보시면 required라는 속성이 있는데 해당 속성은 반드시 파라미터로 받을것인지에 대한 유무를 나타내요!

 

 

boardService에 searchBoard메소드

 

 

먼저 taget은 검색어를 의미해요!

 

selectCount메소드를 통해  검색어를 기준으로 모든 게시글의 개수를 가지고 와요! 이유는 클라이언테 스크롤이라든지 , 페이지네이션 처리를 하기 위함으로 해당 데이터를 생성해서 클라이언트에 던져 줍니다.

 

selectTitleAndContent메소드를 통해서 검색어 및 페이징 처리를 한 데이터를 디비에서 조회 후 결과를 가지고 옵니다.

 

그 이후 스트림을 통해 중간연산자로 Map처리를 하고 리스트를 생성합니다.

 

Stream이란???

stream의 사전적 정의는 흐름이라는 의미를 가지고 있어요! java에서 stream도 간단하게 생각하면 데이터 흐름이라고 생각하면 됩니다!! 배열 , 리스트 등등의 데이터 요소의 흐름을 관리하고 , 중간 연산자 , 최종 연산자 등으로 원하는 결과를 도출해 낼 수 있어요! 또한 람다를 사용하여 처리를 할 수 도 있습니다!

 

중간 연산이란 관리하는 데이터 요소들을 중간에 처리하는 연산이라고 생각하면 됩니다. 

예로는 filter , map이 존재해요! filter는 데이터 요소중 원하는 값을 추출해내는 연산이며 , map은 데이터 요소를 특정 형태로 변환하거나 , 요소의 특정한 필드값만 도출해낼 수 있어요! 이런 것들이 중간 연산이며 , 중간연산은 새로운 stream을 반환하기 때문에 메소드 채이닝을 통해 여러 중간연산을 수행 할 수 있어요!

또한 중간 연산은 바로 실행되는게 아니라 최종연산이 호출했을때 실행이 된다는점!!

 

최종연산이란 중간연산들을 통해 만들어지고 , 필터링된 데이터 요소를 최종적으로 소모하여 원하는 결과를 도출해 내거나 , 특정값만 가지고 오거나 하는 마지막 작업을 수행하는 연산이라고 생각하시면 됩니다! 또한 최종연산을 사용한 stream은 다시 재사용 할 수 가 없어요! 최종연산을 수행하면 stream이 닫히거든요!

 

위 코드에서는 map이 중간 연산에 해당 해요!

map을 통해서 스트림의 데이터 요소를 BoardResponseDto로 변환하는 작업을 수행합니다.

 

위코드에서는 toList()가 최종연산에 해당해요

내부적으로 리스트 객체를 생성하고 , 해당 리스트에 요소들을 전부 넣어서 변경 불가능한 리스트를 반환하는 역할을 합니다.

 

 

여기까지 간단한 게시판 CRUD에 대한 Controller , Service , repository , Entity에 대해서 간략한 설명을 마치겠습니다

 

감사합니다 :)

 

 

 

 

 

728x90
반응형