상세 컨텐츠

본문 제목

[JPA] 게시판 CRUD

JAVA/SPRING

by ranlan 2021. 3. 21. 09:46

본문

728x90

 

가장 먼저 간단한 게시판 기능을 구현하기로 하였다.

카테고리를 선택하고 글의 제목과 내용을 입력하여 게시물을 등록하고 조회, 수정, 삭제까지 할 수 있도록 하였다.

그 외의 이미지, 동영상 업로드나 수정 시 권한 확인 등의 문제는 차차 추가할 예정!

 

 

2021.03.22 - [study/spring boot & jpa] - RESTful API 만들기

 

[WEB] RESTful API 만들기

VIEW를 담당하는 컨트롤러를 제외한 모든 컨트롤러를 @RestController로 지정하여 RESTful API로 만들었다. @RestController는 별도의 View를 제공하지 않기 때문에 문제가 발생하는 상황에서 상태코드와 응

juran-devblog.tistory.com


 

도메인 설계

Board

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Board extends DateEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long boardNo;

    @JsonManagedReference
    @ManyToOne
    @JoinColumn(name = "member_no")
    private Member member; //작성자 정보

    @JsonManagedReference
    @ManyToOne
    @JoinColumn(name = "category_no")
    private Category category; //카테고리

    @JsonBackReference
    @OneToMany(mappedBy = "board", cascade = CascadeType.ALL)
    private List<Comment> comments = new ArrayList<>(); //댓글

    @Column(length = 500, nullable = false)
    private String boardTitle; //제목

    @Column(length = 1000, nullable = false)
    private String boardContent; //내용

    @Column(insertable = false, columnDefinition = "int default 0", nullable = false)
    private Integer boardRcmdCnt; //추천수

    @Column(insertable = false, columnDefinition = "int default 0", nullable = false)
    private Integer boardViewCnt; //조회수
}

- 작성자 Member와는 다대일 관계 →  Board (N) : Member (1)

- 카테고리 Category와도 다대일 관계 →  Board (N) : Category (1)

- 댓글 Comment과는 일대다 관계  →  Board (1) : Comment (N)

 

Category

@Entity
@Setter @Getter
@NoArgsConstructor
@AllArgsConstructor
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long categoryNo;

    @Column(length = 50, nullable = false)
    private String categoryName;

    @OneToMany(mappedBy = "category", cascade = CascadeType.ALL)
    private List<Board> boards = new ArrayList<>();

}

 

 

로직 구현

spring data jpa인 JpaRepository 인터페이스를 구현한 각 엔티티의 레퍼지토리로 DB에 접근

 

서비스 객체에서 레퍼지토리를 이용하여 서비스 로직 작성 (트랜잭션 스크립트 패턴)

- 트랜잭션 스크립트 패턴 : 서비스 계층에서 대부분의 비지니스 로직 처리

- 도메인 모델 패턴 : 엔티티가 비지니스 로직을 지니고 서비스 계층은 단순히 엔티티에 필요한 요청을 위임

 

Service객체 어노테이션 : @Service / @RequiredArgsConstructor

Controller객체 어노테이션 : @RestController / @Controller / @RequiredArgsConstructor

더보기

@RequiredArgsConstructor

초기화 되지않은 final 필드나, @NonNull 이 붙은 필드에 대해 생성자 자동 생성

주로 의존성 주입(Dependency Injection) 편의성을 위해 사용

 

BoardRepository

public interface BoardRepository extends JpaRepository<Board, Long> {

}

BoardRequest

@Getter @Setter
@AllArgsConstructor
@NoArgsConstructor
public class BoardRequest {

    @NotNull
    private String boardTitle;
    
    @NotNull
    private String boardContent;
    
    @NotNull
    private Long categoryNo;

}

글 제목과 내용, 카테고리 정보가 담긴 DTO를 따로 구성하여 글 작성과 수정 시 request body 에 담아 사용하였다.

* DTO 객체의 경우 getter와 기본 생성자는 필수

 

추가설명)

- VIEW 템플릿은 JSP를 이용하였으며 대부분 ajax를 통해 데이터를 호출하였다.

- json형태로 주고 받기 위해 뷰를 담당하는 컨트롤러가 아닌 API 컨트롤러들은 모두 @RestController로 선언

- 성공 시 응답 객체로 ApiResponse에 상태와 데이터를 담아 반환하였다.

 

ResponseEntity

@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ApiResponse {

    private boolean success; // 성공여부
    private String dateTime; // 시간
    private String errorCode; // 에러코드
    private String detail; // 에러 메세지
    private Map<String, Object> data = new HashMap<>(); // 반환할 데이터

   public ApiResponse(boolean success) {
        this.dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        this.success = success;
    }

    public ApiResponse(boolean success, String detail) {
        this.dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        this.success = success;
        this.detail = detail;
    }

    public ApiResponse(boolean success, String errorCode, String detail) {
        this.dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        this.success = success;
        this.errorCode = errorCode;
        this.detail = detail;
    }

    public void putData(String key, Object value) {
        this.data.put(key, value);
    }

}

 

 

1) 게시판 글 작성 (CREATE)

BoardService

@Transactional
public void registerBoard(BoardRequest boardRequest, Member member, Category category) {

    Board board = new Board();
    LocalDateTime now = LocalDateTime.now();

    board.setBoardTitle(boardRequest.getBoardTitle());
    board.setBoardContent(boardRequest.getBoardContent());
    board.setCategory(category);
    board.setRegDate(now);
    board.setUpdateDate(now);
    board.setMember(member);

    boardRepository.save(board);
}

새로운 Board 객체를 생성하여 BoardRequest로 받아온 값을 설정해준 뒤 저장

 

선언적 트랜잭션 처리 @Transactional

-  하나의 트랜젹션으로 묶어서 처리

-  도중 에러 발생 시 자동으로 롤백

-  클래스에 @Transactional 선언 시 모든 메서드에 적용되나 각 메서드에 붙은 트랜잭션 어노테이션에 대한 속성을 우선으로 따름

 

BoardApiController

@PostMapping("register")
public ResponseEntity<?> boardRegister(@Validated @RequestBody BoardRequest boardRequest,
                                      Authentication authentication) throws Exception {

    Member member = memberService.loadUserByUsername(authentication.getPrincipal().toString());
    Category category = categoryService.findByCategoryNo(boardRequest.getCategoryNo()).get();
    boardService.registerBoard(boardRequest, member, category);

    ApiResponse apiResponse = new ApiResponse(true, "게시글 등록");

    return ResponseEntity.ok(apiResponse);
}

스프링 시큐리티를 적용하였기 때문에 현재 작성하고 있는 사용자에 대한 정보는 Authentication 객체로 받아옴

→ 해당 객체의 Principal(아이디)을 통해 member객체를 찾음

더보기

스프링 시큐리티

 

Authentication : 사용자 정보가 담긴 객체

- Principal : 사용자 이름(아이디)

- Credential: 비밀번호

 

loadUserByUsername(String username) : MemberService가 상속받은 UserDetailsService의 메서드로 오버라이딩하여 로직 구현

 

 

2) 게시판 글 조회 (READ)

BoardService

@Transactional(readOnly = true)
public List<Board> findAllBoard() {

    	return boardRepository.findAll();
}

@Transactional(readOnly = true)
public Board findBoardByNo(Long boardNo) {

	return  boardRepository.findById(boardNo).orElseThrow(() -> new NoSuchElementException());
}

findAllBoard() : 게시글 목록 전체 조회

findBoardByNo(Long boardNo) : boardNo으로 해당 게시글 상세 내용 조회

 

(원래 지연로딩 때문에 getOne을 주로 사용하였지만 Exception핸들링에 대해서도 알아야 할 거 같아서 한번 써 보았다.  getOne 사용과 에러 핸들링에 대해서는 공부가 더 필요한 것 같다..ㅜㅜ)

 

* api를 만들어 응답 결과를 DTO를 사용하지 않고 엔티티를 바로 받아오는 것은 매우 안좋다고 한다 (후에 수정 필요!)

   - 불필요한 정보를 모두 받아옴

   - 보안상의 이유

   - api 스펙 변화 등등

 

BoardAipController

@GetMapping("list")
public ResponseEntity<?> boardListAll() throws Exception {

    ApiResponse apiResponse = new ApiResponse(true, "게시글 전체 조회");
    apiResponse.putData("boardList", boardService.findAllBoard());

    return ResponseEntity.ok(apiResponse);
}

@GetMapping("info/{boardNo}")
public ResponseEntity<?> boardDetailInfo(@PathVariable(name = "boardNo") Long boardNo) throws Exception{

    ApiResponse apiResponse = new ApiResponse(true, "게시글 상세정보 조회");
    apiResponse.putData("boardInfo", boardService.findBoardByNo(boardNo));

    return ResponseEntity.ok(apiResponse);
}

전체조회와 url로 넘긴 boardNo을 통해 해당 게시글의 상세 정보를 조회

 

 

3) 게시판 글 수정 (UPDATE)

Jpa 실전활용 강의를 보던 중 setter를 남발하는 것은 안좋다는 것을 보고😥 수정 기능은 엔티티에 구현하였다.

 

Board

public void updateBoard (String title, String content, Category category) {
    this.boardTitle = title;
    this.boardContent = content;
    this.category = category;
}

Board 엔티티에 작성한 게시글 수정 비지니스 로직

 

BoardService

@Transactional
public void updateBoard(BoardRequest boardRequest, Long boardNo) {
    Board board = boardRepository.getOne(boardNo);
    board.updateBoard(boardRequest.getBoardTitle(),boardRequest.getBoardContent(), categoryRepository.getOne(boardRequest.getCategoryNo()));
}

boardNo으로 수정할 board를 찾고 엔티티에 작성된 메서드를 통해 내용 수정

 

BoardApiController

@PostMapping("edit/{boardNo}")
public ResponseEntity<?> boardEdit(@Validated @PathVariable(name = "boardNo") Long boardNo,
                                  @RequestBody BoardRequest boardRequest) throws Exception {

    boardService.updateBoard(boardRequest, boardNo);
    ApiResponse apiResponse = new ApiResponse(true, "게시글 수정");

    return ResponseEntity.ok(apiResponse);
}

boardNo는 url에 담아 받았고 수정할 내용은 등록과 동일하게 BoardRequest 객체로 받았다.

 

 

4) 게시판 글 삭제 (DELETE)

삭제는 jpa 덕분에 매우 간단했다.

 

BoardService

@Transactional
public void deleteByNo(Long boardNo) {

	boardRepository.deleteById(boardNo);
}

BoardApiController

@GetMapping("delete/{boardNo}")
public ResponseEntity<?> boardDelete(@PathVariable(name = "boardNo") Long boardNo) throws Exception {

    boardService.deleteByNo(boardNo);
    ApiResponse apiResponse = new ApiResponse(true, "게시글 삭제");

    return ResponseEntity.ok(apiResponse);
}

url에 담긴 boardNo으로 해당 게시글을 조회하고 삭제

 

 

 

728x90

'JAVA > SPRING' 카테고리의 다른 글

[ERROR] 순환참조 문제  (0) 2021.04.14
[WEB] RESTful API 만들기  (0) 2021.03.22
[SPRING SECURITY] 스프링 부트 환경설정  (0) 2021.03.14
[SPRING SECURITY] 스프링 시큐리티란  (0) 2021.03.14
[SPRING BOOT] 디렉터리 구조  (0) 2021.03.13

관련글 더보기

댓글 영역