스프링 부트(Spring Boot)로 게시판 벡엔드 서버 만들기

Doyeon·2023년 2월 6일
1
post-thumbnail
post-custom-banner

스프링 부트(Spring Boot)로 게시판 벡엔드 서버를 만들어보자

Spring Boot를 기반으로 CRUD(Create, Read, Update, Delete) 기능이 포함된 REST API를 만들어보려고 한다.

데이터베이스는 h2 DB를 연결하고 Lombok과 JPA를 이용해서 게시글 데이터를 저장하고 조회할 것이다.

그럼 이제부터 간단한 REST API를 만들어보자!


#1. 기능 정의

  • 전체 게시글 목록 조회
    • 제목, 작성자명, 작성 내용, 작성 날짜를 조회하기
    • 작성 날짜 기준 내림차순으로 정렬하기
  • 게시글 작성
    • 제목, 작성자명, 비밀번호, 작성 내용을 저장하고
    • 저장된 게시글을 Client 로 반환하기
  • 선택한 게시글 조회
    • 선택한 게시글의 제목, 작성자명, 작성 날짜, 작성 내용을 조회하기
  • 선택한 게시글 수정
    • 수정을 요청할 때 수정할 데이터와 비밀번호를 같이 보내서 서버에서 비밀번호 일치 여부를 확인 한 후
    • 제목, 작성자명, 작성 내용을 수정하고 수정된 게시글을 Client 로 반환하기
  • 선택한 게시글 삭제 API
    • 삭제를 요청할 때 비밀번호를 같이 보내서 서버에서 비밀번호 일치 여부를 확인 한 후
    • 선택한 게시글을 삭제하고 Client 로 성공했다는 표시 반환하기

#2. 유스케이스 다이어그램 작성

유스케이스 다이어그램(Use-case Diagram)
: 시스템과 사용자의 상호작용을 다이어그램으로 표현한 것


#3. API 명세서 작성


#4. 프로젝트 dependency 및 database 설정

  • dependency
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
     implementation 'org.springframework.boot:spring-boot-starter-web'
     compileOnly 'org.projectlombok:lombok'
     developmentOnly 'org.springframework.boot:spring-boot-devtools'
     runtimeOnly 'com.h2database:h2'
     annotationProcessor 'org.projectlombok:lombok'
  • properties
    // h2 database 연결을 위한 설정
    spring.h2.console.enabled=true
    spring.datasource.url=jdbc:h2:mem:db;MODE=MYSQL;
    spring.datasource.username=sa
    spring.datasource.password=

#5. 도메인 클래스 설계(Entity 구현)

게시글에 필요한 속성을 모아 Board 라는 클래스를 만들어 entity로 구현할 것이다.

필요한 속성은 게시글을 구분할 { id, 제목, 내용, 작성자, 비밀번호, 작성일시, 수정일시} 다.

여기서 작성일시와 수정일시는 클라이언트가 직접 입력하는 것이 아니라, 글이 생성될 때 자동으로 일시를 부여해줄 것이다. 이를 위해 Timestamped 클래스를 만들 것이다.

  • board/entity/Timestamped.class
    @Getter
    @MappedSuperclass
    @EntityListeners(AuditingEntityListener.class)
    public class Timestamped {
    
        @CreatedDate
        private LocalDateTime createdAt;
    
        @LastModifiedDate
        private LocalDateTime modifiedAt;
    }
    • @MappedSuperClass : 공통으로 사용하는 맵핑 정보만 상속하는 부모 클래스에 선언
    • JPA가 시간에 대해 자동으로 값을 넣어주는 Auditing 기능을 사용해서 생성일시, 수정일시를 구현할 수 있다.
      • Auditing 기능을 사용하는 해당 클래스에는EntityListeners(AuditingEntityListener.class) 를,
      • @SpringBootApplication 이 있는 class 에 @EnableJpaAuditing 을 추가해야 한다.
    • @CraetedDate : 엔티티가 생성되어 저장될 때 시간을 자동 저장
    • @LastModifiedDate : 엔티티가 수정된 시간을 자동 저장
  • board/entity/Board.class
    @Getter
     @Entity
     @NoArgsConstructor
     public class Board extends Timestamped {
         @Id
         @GeneratedValue(strategy = GenerationType.AUTO)
         private Long id;
     
         @Column(nullable = false)
         private String title;
     
         @Column(nullable = false)
         private String contents;
     
         @Column(nullable = false)
         private String author;
     
         @Column(nullable = false)
         private String password;
     }
    • Board 클래스는 DB에 저장할 엔티티이므로 @Entity 를 붙인다.
    • 생성일시와 수정일시는 Timestpamped 를 상속받아 사용한다.
    • @Id@GeneratedValue 를 동시에 사용하면 JPA가 기본 키를 자동으로 생성해준다.
    • @Column : 객체 필드를 테이블의 컬럼으로 맵핑시킨다.

#6. DTO 구현

DTO(Data Transfer Object)

  • 프로세스(계층) 간에 데이터를 보낼 때 사용하는 객체
    • ex) Service나 Controller로 데이터를 보낼 때
  • 데이터를 담는 용도이기 때문에 필드값, Getter, Setter만 존재하며 다른 메서드는 없다.
  • 원하는 데이터를 묶어서 하나의 요청으로 보낼 수 있다.

DTO에 어떤 데이터를 담을지 정하기 위해 위에서 만들었던 API 명세서를 확인해보자.

각각의 요청과 응답에 맞도록 DTO를 만들면 된다. 나중에 Service에서 로직을 만을 때, DTO에 담긴 데이터를 가져와야하니, @Getter를 붙여 만든다.

  • 먼저 request(요청) 부분을 보면, Board 의 필드 값을 받아 요청을 보낸다.

    • 게시글 작성 , 게시글 수정 : { title, contents, author, password }
    • 게시글 삭제 : { password }
  • 다음으로 response(응답) 부분을 보면, Board 의 필드값 또는 성공여부를 응답으로 보낸다.

    • 전체 목록 조회 : [ { id, title, contents, author, password, createdAt, modifiedAt } , … ]
    • 게시글 조회 , 게시글 작성 , 게시글 수정 : { id, title, contents, author, password, createdAt, modifiedAt }
    • 게시글 삭제 : { success }
  • board/dto/BoardRequestsDto

    @Getter
    public class BoardRequestsDto {
        private String title;
        private String contents;
        private String author;
        private String password;
    }
    • 요청 받을 때 담겨지는 데이터 필드 값을 모아 BoardRequestsDto 를 구현했다.
  • board/dto/BoardResponseDto

    @Getter
    @NoArgsConstructor
    public class BoardResponseDto {
        private Long id;
        private String title;
        private String contents;
        private String author;
        private LocalDateTime createdAt;
        private LocalDateTime modifiedAt;
    
        public BoardResponseDto(Board entity) {
            this.id = entity.getId();
            this.title = entity.getTitle();
            this.contents = entity.getContents();
            this.author = entity.getAuthor();
            this.createdAt = entity.getCreatedAt();
            this.modifiedAt = entity.getModifiedAt();
        }
    
    }
    • 게시글을 반환해야 하는 경우의 데이터 필드 값을 모아 BoardResponseDto 를 구현했다. password는 게시글 반환 시 보여지는 일이 없으므로 필드 값에서 제외했다.
    • Board 엔티티를 받아서 BoardResponseDto 객체로 만들기 위한 생성자를 추가했다.
  • board/dto/SuccessResponseDto

    @Getter
    public class SuccessResponseDto {
        private boolean success;
    
        public SuccessResponseDto(boolean success) {
            this.success = success;
        }
    }
    • 게시글을 삭제했을 때 성공 여부를 담아서 보낼 SuccessResponseDto 를 구현했다.
    • 성공여부를 담아서 객체를 만들어 반환하기 위해 생성자를 추가했다.

#7. CRUD 기능 구현

이제 드디어 게시판의 CRUD 기능을 하나씩 구현할 차례가 되었다.

먼저 API 명세서를 보면서, Controller에서 어떤 URL 주소를 호출받았을 때 어떤 메서드를 실행시켜야 하는지 정해야 한다.

실행시킬 메서드의 로직은 Service에서 구현한다.

Service에서 로직을 구현할 때 Repository를 이용해서 실제 데이터를 가져오거나 저장할 수 있다.

각 기능 구현에 앞서 Controller, Service, Repository를 만들어놓자!

  • board/controller/BoardController
    @RestController
    @RequiredArgsConstructor
    public class BoardController {
    
        private final BoardService boardService;
    
    }
    • 호출된 URL에 따라 메서드를 실행시킬 때, Service에서 구현된 메서드를 실행시킬 것이므로 Service 객체를 선언한다.
  • board/service/BoardService
    @Service
    @RequiredArgsConstructor
    public class BoardService {
    
        private final BoardRepository boardRepository;
    
    }
    • Service에서는 실제 사용할 메서드의 로직을 구현할텐데, 데이터를 저장하거나 조회하려면 실제 데이터에 접근해야하므로 Repository 객체를 선언한다.
  • board/repository/BoardRepository
    @Repository
    public interface BoardRepository extends JpaRepository<Board, Long> {
    }
    • JPA를 사용해서 데이터베이스에 테이블 정보를 생성하고, 저장하고, 조회할 것이므로 JpaRepository 를 상속받는다. 이 때 제네릭스 타입은 <엔티티로 쓰는 클래스, id 타입> 이다.

전체 목록 조회

  • BoardController

    @GetMapping("/api/posts")
    public List<BoardResponseDto> getPosts() {
        return boardService.getPosts();
    }
    • GET 방식 “/api/posts” → getPosts (전체 목록을 가져온다.)
    • 게시글의 내용을 BoardResponseDto 에 담아 List 형태로 Client에 보낸다.
  • BoardService

    @Transactional(readOnly = true)
    public List<BoardResponseDto> getPosts() {
        return boardRepository.findAllByOrderByModifiedAtDesc().stream().map(BoardResponseDto::new).toList();
    }
    • BoardRepository 에서 수정일시 기준 내림차순으로 모든 데이터를 가져온다.
      • JPA가 findAll은 기본적으로 제공해주는데, 수정일시 기준 내림차순은 BoardRepository에서 따로 선언해줘야 한다.
    • BoardResponseDto 에서 Board 엔티티를 넣으면 BoardResponseDto 로 객체를 생성주는 생성자를 만들었기 때문에 map(BoardResponseDto::new) 를 통해 간편하게 dto로 바꿔줄 수 있다.
    • dto로 바꾼 데이터들을 toList() 를 사용해서 List로 바꾸어 리턴하면 된다.
  • BoardRepository

    @Repository
    public interface BoardRepository extends JpaRepository<Board, Long> {
        List<Board> findAllByOrderByModifiedAtDesc();
    }
    • ModifiedAt 내림차순 기준으로 모든 데이터를 가져오기 위해, findAllByOrderMyModifiedAtDesc() 메서드를 선언한다. 이렇게 Repository에 선언해주면 Service에서 메서드를 사용할 수 있다.

게시글 작성

  • BoardController

    @PostMapping("/api/post")
    public BoardResponseDto createPost(@RequestBody BoardRequestsDto requestsDto) {
        return boardService.createPost(requestsDto);
    }
    • POST 방식 “/api/post” → createPost (게시글을 작성한다.)
    • 게시글 내용이 담긴 BoardRequestsDto 를 Client로부터 받는다.
    • 작성된 게시글을 BoardResponseDto 에 담아 Client로 보낸다.
  • BoardService

    @Transactional
    public BoardResponseDto createPost(BoardRequestsDto requestsDto) {
        Board board = new Board(requestsDto);
        boardRepository.save(board);
        return new BoardResponseDto(board);
    }
    • 게시글 내용이 담긴 requestsDto를 받아서 Board 엔티티로 만들어준다.
      • 이 부분은, Board 클래스에서 BoardRequestsDto 를 받아서 Board 객체를 생성해주는 생성 자를 만들어줘야 한다.
      • Board
        public Board(BoardRequestsDto requestsDto) {
            this.title = requestsDto.getTitle();
            this.contents = requestsDto.getContents();
            this.author = requestsDto.getAuthor();
            this.password = requestsDto.getPassword();
        }
    • Board 엔티티 객체인 board를 boardRepository에 저장한다. → DB에 board 데이터가 저장된다.
    • board 엔티티를 넣어서 BoardResponseDto 객체를 만들어 반환한다.

선택한 게시글 조회

  • BoardController

    @GetMapping("/api/post/{id}")
    public BoardResponseDto getPost(@PathVariable Long id) {
        return boardService.getPost(id);
    }
    • GET 방식 “/api/post/{id}” → getPost (id에 해당하는 글을 조회한다.)
    • 선택한 게시물의 id를 param 형태로 Client로부터 받는다.
    • 조회된 게시글을 BoardResponseDto 에 담아 Client로 보낸다.
  • BoardService

    @Transactional
    public BoardResponseDto getPost(Long id) {
        return boardRepository.findById(id).map(BoardResponseDto::new).orElseThrow(
                () -> new IllegalArgumentException("아이디가 존재하지 않습니다.")
        );
    }
    • 게시글의 id를 가진 데이터를 boardRepository에서 찾아서 BoardResponseDto 객체로 만들어 반환한다.
    • 만약 boardRepository에 해당 id의 데이터가 없다면, 예외처리한다.

선택한 게시글 수정

  • BoardController

    @PutMapping("/api/post/{id}")
    public BoardResponseDto updatePost(@PathVariable Long id, @RequestBody BoardRequestsDto requestsDto) throws Exception {
        return boardService.updatePost(id, requestsDto);
    }
    • PUT 방식 “/api/post/{id}” → updatePost (id에 해당하는 글을 수정한다.)
    • Client로부터 선택한 게시물의 id는 param 형태로, 수정할 내용을 담은 requestsDto는 body 형태로 받는다.
    • 조회된 게시글을 BoardResponseDto 에 담아 Client로 보낸다.
  • BoardService

    @Transactional
    public BoardResponseDto updatePost(Long id, BoardRequestsDto requestsDto) throws Exception {
        Board board = boardRepository.findById(id).orElseThrow(
                () -> new IllegalArgumentException("아이디가 존재하지 않습니다.")
        );
        if (!requestsDto.getPassword().equals(board.getPassword()))
            throw new Exception("비밀번호가 일치하지 않습니다.");
    
        board.update(requestsDto);
        return new BoardResponseDto(board);
    }
    • 게시글의 id를 가진 데이터를 boardRepository에서 찾아서 Board 객체에 넣는다.
      • 만약 boardRepository에 해당 id의 데이터가 없다면, 예외처리한다.
    • Client에서 보낸 requestsDto의 패스워드와, boardRepository에서 가져온 board의 패스워드가 다르다면 예외처리한다.
    • id도 있고, 비밀번호도 일치한다면, board 내용을 update 하고, update된 board를 BoardResponseDto 에 담아 반환한다.

선택한 게시글 삭제

  • BoardController

    @DeleteMapping("/api/post/{id}")
    public SuccessResponseDto deletePost(@PathVariable Long id, @RequestBody BoardRequestsDto requestsDto) throws Exception {
        return boardService.deletePost(id, requestsDto);
    }
    • DELETE 방식 “/api/post/{id}” → deletePost (id에 해당하는 글을 삭제한다.)
    • Client로부터 선택한 게시물의 id는 param 형태로, 삭제 시 입력할 패스워드를 담은 requestsDto는 body 형태로 받는다.
    • 삭제 성공 여부를 SuccessResponseDto 에 담아 Client로 보낸다.
  • BoardService

    @Transactional
    public SuccessResponseDto deletePost(Long id, BoardRequestsDto requestsDto) throws Exception {
        Board board = boardRepository.findById(id).orElseThrow(
                () -> new IllegalArgumentException("아이디가 존재하지 않습니다.")
        );
    
        if (!requestsDto.getPassword().equals(board.getPassword()))
            throw new Exception("비밀번호가 일치하지 않습니다.");
    
        boardRepository.deleteById(id);
        return new SuccessResponseDto(true);
    }
    • 게시글의 id를 가진 데이터를 boardRepository에서 찾아서 Board 객체에 넣는다.
      • 만약 boardRepository에 해당 id의 데이터가 없다면, 예외처리한다.
    • Client에서 보낸 requestsDto의 패스워드와, boardRepository에서 가져온 board의 패스워드가 다르다면 예외처리한다.
    • id도 있고, 비밀번호도 일치한다면, boardRepository에서 해당 id 값을 지닌 데이터를 지운다.
    • 지운 후에 SuccessResponseDto에 성공여부를 담아 반환한다.

#8. 구현 결과

Postman을 사용해서 서버에 요청을 보내고 반환 결과를 확인할 수 있다.

  • 게시글 저장

  • 전체 목록 조회
  • 선택한 게시글 조회
  • 선택한 게시글 수정
  • 선택한 게시글 삭제

완성 깃헙 링크

https://github.com/dev-dykim/Spring-project-board.git


질문과 답변

  1. 수정, 삭제 API의 request를 어떤 방식으로 사용하셨나요? (param, query, body)
    • param, body 방식을 사용하였다.
    • 수정, 삭제 모두 대상 게시글의 id를 받아야 하므로 서버에서 @PathVariable로 id를 받도록 param 방식을 사용하였다.
    • 수정할 때는 수정할 내용 { title, contents, author, passwrod } 를 받아야 하고, 삭제할 때는 { password } 를 받아야 하므로, 서버에서 @RequestBody로 데이터를 넘길 수 있도록 body 방식을 사용하였다.
  2. 어떤 상황에 어떤 방식의 request를 써야하나요?
    • param
      • 주소에서 포함된 변수를 받는다.
      • /api/post/{id}
      • /api/post/id/{id}/name/{name}
      • 식별할 데이터에 대한 정보를 받아올 때 적절하다.
        • /books/123 → 123번 책 정보를 가져온다.
      • 서버에서 @PathVariable 로 받는다.
    • query
      • 엔드포인트에서 물음표(?) 뒤에 key=value 형태로 변수를 담는다.
      • api/post?key=value&key2=value2
      • 정렬이나 필터링이 필요한 경우 적절하다.
        • /books?genre=novel → 장르가 소설인 책 목록을 가져온다.
    • body
      • URL에는 보이지 않는 오브젝트 데이터(JSON, XML 등)를 담는다.
      • 객체를 바로 담아서 보낼 경우 적절하다.
  3. RESTful한 API를 설계했나요? 어떤 부분이 그런가요? 어떤 부분이 그렇지 않나요?
    • RESTful : REST API 의 설계 의도를 명확하게 지킴으로써, 각 구성 요소들의 역할이 완벽하게 분리되어 있어, URI만 보더라도 리소스를 명확하게 인식할 수 있도록 표현한 설계 방식
      • 메서드 기능이나 뷰가 아닌, 리소스(데이터) 중심의 API를 구성해야 한다. → 게시글이라는 post 리소스 중심으로 API를 설계하였다. → 메서드 기능은 http 메서드에서 미리 정의되어 있으므로, URI에는 대상이 되는 리소스(post)만 담도록 설계하였다.
  4. 적절한 관심사 분리를 적용하였나요? (Controller, Repository, Service)
    • Controller : URL 맵핑을 통해 특정 메서드가 호출되도록 한다.
      • http 메서드와 함께 특정 URL로 요청이 올 때, 특정 메서드가 실행되게 구현했다.
    • Service : 비지니스 로직을 수행한다.
      • 메서드의 기능을 직접 구현하며 repository에서 데이터를 가져오거나 저장한다.
    • Repository : 데이터베이스에 저장하고 조회하는 기능을 수행한다.
      • JpaRepository 를 상속받아 Board 엔티티를 DB에 저장하도록 구현했다.
  5. API 명세서 작성 가이드라인을 검색하여 직접 작성한 API 명세서와 비교해보세요!
    • 기능, 메서드, URL, Request, Response 항목으로 작성했다.
profile
🔥
post-custom-banner

0개의 댓글