Spring 입문 주차 개인 과제

박영준·2022년 11월 29일
0

Spring

목록 보기
17/58

조건

  1. Entity를 그대로 반환하지 말고, DTO에 담아서 반환
  2. PostMan 사용: 서버가 반환하는 결과값을 더 쉽게 확인 할 수 있는 도구

요구사항

  1. 아래의 요구사항을 기반으로 Use Case 그려보기
  2. 전체 게시글 목록 조회 API
    • 제목, 작성자명, 작성 내용, 작성 날짜를 조회하기
    • 작성 날짜 기준으로 내림차순으로 정렬하기
  3. 게시글 작성 API
    • 제목, 작성자명, 비밀번호, 작성 내용을 저장하고
    • 저장된 게시글을 Client 로 반환하기
  4. 선택한 게시글 조회 API
    • 선택한 게시글의 제목, 작성자명, 작성 날짜, 작성 내용을 조회하기
      (검색 기능이 아닙니다. 간단한 게시글 조회만 구현해주세요.)
  5. 선택한 게시글 수정 API
    • 수정을 요청할 때 수정할 데이터와 비밀번호를 같이 보내서 서버에서 비밀번호 일치 여부를 확인 한 후
    • 제목, 작성자명, 작성 내용을 수정하고 수정된 게시글을 Client 로 반환하기
  6. 선택한 게시글 삭제 API
    • 삭제를 요청할 때 비밀번호를 같이 보내서 서버에서 비밀번호 일치 여부를 확인 한 후
    • 선택한 게시글을 삭제하고 Client 로 성공했다는 표시 반환하기

API 설계하기

기능MethodURLReturn
전체 게시글 목록 조회GET/boardList
선택한 게시글 조회GET/board/{id}Post
게시글 작성POST/boardPost
선택한 게시글 수정(변경)PUT/board/{id}Long
선택한 게시글 삭제DELETE/board/{id}Long

질문 & 답변

  1. 수정, 삭제 API의 request를 어떤 방식으로 사용하셨나요? (param, query, body)
  2. 어떤 상황에 어떤 방식의 request를 써야하나요?
  3. RESTful한 API를 설계했나요? 어떤 부분이 그런가요? 어떤 부분이 그렇지 않나요?
  4. 적절한 관심사 분리를 적용하였나요? (Controller, Repository, Service)
  5. API 명세서 작성 가이드라인을 검색하여 직접 작성한 명세서와 비교해보세요!

전체적인 패키지 및 파일 생성

1. BoardController

//Client <--DTO--> 여기!! Controller <--DTO--> Service <--DTO--> Repository <--Domain(Entity Class)--> DB
package com.sparta.hanghaeboard.controller;

import com.sparta.hanghaeboard.dto.BoardRequestDto;
import com.sparta.hanghaeboard.dto.BoardResponseDto;
import com.sparta.hanghaeboard.service.BoardService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController     //JSON 데이터 타입으로 response 해줄것이므로 -> 이걸 달아주지 않으면, 각각의 API에 responsebody를 달아줘야하는 번거로움
@RequiredArgsConstructor    //final 선언할때 스프링에게 알려줌

public class BoardController {

    //BoardService 와 연결
    private final BoardService boardService;

    //게시글 작성
    @PostMapping("/board")      //request 종류: POST
    //BoardResponseDto: 반환 타입. createBoard: 메소드명(원하는대로)
    //@RequestBody: POST 안에 저장된 body 값들을 key:value 형태(JSON 타입)로 짝지음. body 에 들어오는 데이터들을 가지고오는 역할 --> Controller 에서만 들어가는 부분
    //BoardRequestDto: JSON 타입으로 넘어오는 데이터를 받는 객체(데이터를 저장할 공간)
    //requestDto: 매개변수
    public BoardResponseDto createBoard(@RequestBody BoardRequestDto requestDto) {
        //매개변수 requestDto 를 메소드 createBoard 를 사용해서, boardService 로 반환(boardService 와 연결)
        return boardService.createBoard(requestDto);
    }

    //전체 게시글 목록 조회
    @GetMapping("/board")      //request 종류: GET
    //List 타입
    //BoardResponseDto: 반환 타입. getListBoards: 메소드명. (): 전부 Client 에게로 반환하므로 비워둠
    public List<BoardResponseDto> getListBoards() {
        //getListBoards 메소드를 사용해서, boardService 와 연결
        return boardService.getListBoards();
    }

    //선택한 게시글 조회
    @GetMapping("/board/{id}")      //request 종류: GET
    //BoardResponseDto: 반환 타입. getBoards: 메소드명
    //@PathVariable: URL 경로에 변수를 넣어주는 것
    //@PathVariable Long id: PathVariable 방식으로 id 값을 가져온다 --> 전체 게시글 목록에서 id 값으로 각각의 게시글을 구별
    public BoardResponseDto getBoards(@PathVariable Long id) {     //optional: 이게 왜 쓰였는지 기술매니저님께 여쭤보기!! //List 쓰면 안 되는 걸까?
        //id 값을 담은 getBoard 메소드를 사용해서, boardService 와 연결
        return boardService.getBoard(id);
    }

    //선택한 게시글 수정(변경)
    @PutMapping("/board/{id}")      //request 종류: PUT
    //Long: 반환 타입?  updateBoard: 메소드 명
    //@RequestBody: POST 안에 저장된 body 값들을 key:value 형태(JSON 타입)로 짝지음 --> Controller 에서만 들어가는 부분
    //BoardRequestDto: JSON 타입으로 넘어오는 데이터를 받는 객체
    //requestDto: 매개변수
    public BoardResponseDto updateBoard(@PathVariable Long id, @RequestBody BoardRequestDto requestDto) {       //이 부분만 Long 타입?  BoardResponseDto?
        //id 값을 담은 getBoard 메소드를 사용해서, boardService 와 연결
        return boardService.update(id, requestDto);    //(id, requestDto): requestDto 에 가져올 내용으로 id 값만 적어서 id만 나오는 건가????
    }

    //선택한 게시글 삭제
    //@DeleteMapping("/board/{id}")      //request 종류: DELETE
    @DeleteMapping("/board/{id}/{password}")
    //public Map<String,Object> deleteBoard(@PathVariable Long id, @RequestBody BoardRequestDto requestDto) {   //BoardResponseDto?  Map<String,Object>?
    public BoardResponseDto deleteBoard(@PathVariable Long id, @PathVariable String password) {
        System.out.println("password controller = " + password);    //추가
        return boardService.deleteBoard(id, password);       //requestDto? password?
    }
}

@RequiredArgsConstructor 가 없다면?

public class BoardController {

  private final BoardService boardService;

  @Autowired	//생성자 주입
  public BoardController (BoardService boardService) {
      this.boardService = boardService;
  }
...  
}  

해당 코드 목적?
BoardController 가 Bean 에 등록될 때, BoardService 도 같이 Bean 에 등록되도록 한다.

참고: @RequiredArgsConstructor 을 통한, 생성자 주입

2. BoardService

//Client <--DTO--> Controller <--DTO--> 여기!! Service <--DTO--> Repository <--Domain(Entity Class)--> DB
package com.sparta.hanghaeboard.service;

import com.sparta.hanghaeboard.dto.BoardRequestDto;
import com.sparta.hanghaeboard.dto.BoardResponseDto;
import com.sparta.hanghaeboard.entity.Board;
import com.sparta.hanghaeboard.repository.BoardRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;    //import jakarta.transaction.Transactional; 사용했더니 readOnly에 오류 발생
import java.util.ArrayList;                                            //-->원인: readOnly 기능을 제공하는 Transactional이 아니라 다른 동명의 Transactional를 import했기 때문
import java.util.List;

@Service        //서비스라는 걸 알려주는 어노테이션
@RequiredArgsConstructor    //final 선언할때 스프링에게 알려줌
public class BoardService {

    //BoardRepository 와 연결.
    //final: 서비스에게 꼭 필요함을 명시
    private final BoardRepository boardRepository;

    //게시글 작성
    @Transactional      //업데이트를 할 때, DB에 반영이 되는 것을 스프링에게 알려줌
    //BoardResponseDto: 반환 타입, createBoard: 메소드명(원하는대로)
    //BoardRequestDto: JSON 타입으로 넘어오는 데이터를 받는 객체
    //requestDto: 매개변수 --> controller 에서 넘어온
    public BoardResponseDto createBoard(BoardRequestDto requestDto) {
        //Board: Entity 명
        //매개변수 requestDto 를 넣을 새로운 board 객체 생성 --> 텅 빈 상태
        //가지고온 데이터(requestDto)를 넣음
        Board board = new Board(requestDto);	// 테이블의 한 줄이 됨
        //boardRepository 안에 데이터가 들어간 객체 board 를 save(저장 함수 역할)한다
        boardRepository.save(board);
        //데이터가 들어간 객체 board 를 BoardResponseDto 로 반환
        //(board): BoardResponseDto 에서 만든 생성자와 연결되는 부분 --> ()안에는 만들어진 생성자 형태 그대로 나타남
        //예시. (String msg, int statusCode) 여기 두 개를 넣어야 함. --> 여기선 board 1개 밖에 없으므로
        return new BoardResponseDto(board);
    }

    //전체 게시글 목록 조회
    @Transactional(readOnly = true)
    //BoardResponseDto: 반환 타입. getListBoards: 메소드명. (): 전부 Client 에게로 반환하므로 비워둠
    public List<BoardResponseDto> getListBoards() {
        //boardRepository 와 연결해서, 모든 데이터들을 내림차순으로, List 타입으로 객체 Board 에 저장된 데이터들을 boardList 안에 담는다
        List<Board> boardList =  boardRepository.findAllByOrderByModifiedAtDesc();      //주의. boards 와 board
        //boardResponseDto 를 새롭게 만든다 --> 텅 빈 상태 (빈 주머니 상태?)
        List<BoardResponseDto> boardResponseDto = new ArrayList<>();

        //반복문을 이용하여, boardList 에 담긴 데이터들을 객체 Board 로 모두 옮긴다
        for (Board board : boardList) {
            //board 를 새롭게 BoardResponseDto 로 옮겨담고, BoardResponseDto 를 boardResponseDto 안에 추가(add)한다
            boardResponseDto.add(new BoardResponseDto(board));
        }
        //최종적으로 옮겨담아진 boardResponseDto 를 반환
        return boardResponseDto;
    }

    //선택한 게시글 조회
    @Transactional(readOnly = true)
    //BoardResponseDto: 반환 타입. getBoard: 메소드명. (Long id): Client 에게로 반환할 값
    public BoardResponseDto getBoard(Long id) {
        //Board: Entity 명
        //boardRepository 와 연결해서, id 를 찾는다
        //orElseThrow: 예외 처리 --> 예외 발생 시, "아이디가 존재하지 않습니다." 를 출력
        Board board = boardRepository.findById(id).orElseThrow(
                () -> new IllegalArgumentException("아이디가 존재하지 않습니다.")       //RuntimeException? IllegalArgumentException?
        );
        //데이터가 들어간 객체 board 를 BoardResponseDto 로 반환
        return new BoardResponseDto(board);   //추가
    }

    //선택한 게시글 수정(변경)
    @Transactional      // 업데이트를 할 때, DB에 반영이 되는 것을 스프링에게 알려줌
    //BoardResponseDto 대신 ResponseEntity<?> 사용 가능 -> 어떤 클래스 타입이든 들어올 수 있음
    public ResponseEntity<?> update(Long id, BoardRequestDto requestDto) {
    	// DB 에 저장된 게시글이 있는지 확인 -> 있으면, 해당 데이터를 board 에 담음
        Board board = boardRepository.findById(id).orElseThrow(
                () -> new IllegalArgumentException("아이디가 존재하지 않습니다.")
        );
        //System.out.println(requestDto.getPassword());
        //System.out.println(board.getPassword());

		// 등록된 Password 와 수정 할 때 받은 Password 를 비교
        if (board.getPassword().equals(requestDto.getPassword())) {
            board.update(requestDto);

            //BoardResponseDto boardResponseDto = new BoardResponseDto(board);
            //return boardResponseDto;
            return ResponseEntity.ok(new BoardResponseDto(board));     //위의 두 줄을 한 줄에 쓴 형태(인라인화)

        } else {
            //return board.getId();
            return new ResponseEntity<>("비밀번호가 일치하지 않습니다.", HttpStatus.UNAUTHORIZED.value());
        }
    }

    //선택한 게시글 삭제
    @Transactional
    //public Map<String, Object> deleteBoard(Long id, BoardRequestDto requestDto) {       //, BoardRequestDto requestDto 추가      BoardResponseDto?? Map<String, Object>??
    public BoardResponseDto deleteBoard (Long id, String password) {
        Board board = boardRepository.findById(id).orElseThrow(
                () -> new IllegalArgumentException("게시글이 존재하지 않습니다.")
        );
        System.out.println("board password= " + board.getPassword());
        System.out.println("password = " + password);

        //Map<String, Object> response = new HashMap<>();      response? reponse?

        //if (requestDto.getPassword().equals(board.getPassword())) {
        if(board.getPassword().equals(password))
            boardRepository.deleteById(id);        //deleteById? delete?

        //response.put("success",true);
        return new BoardResponseDto("게시글 삭제 성공", HttpStatus.OK.value());
        //return response;

//        } else {
//        return new BoardResponseDto("비밀번호가 일치하지 않습니다.", HttpStatus.UNAUTHORIZED.value());
        //response.put("success", false);
        //return response;
    }
}

게시글 작성 (BoardService)

 public BoardResponseDto createBoard(BoardRequestDto requestDto) {
 	Board board = new Board(requestDto);	// 테이블의 한 줄
    
    // 저장. 그러나, dto 로 반환하도록 되어있음(반환 타입: BoardResponseDto)
      // 그래서, 일단 saveBoard 에 저장해두고, 이것을 dto 형태로 변환함
    Board saveBoard = boardRepository.save(board);	
    BoardResponseDto boardResponseDto = new BoardResponseDto(saveBoard);	// BoardResponseDto 는 Bean 으로 등록된 객체가 아닌 순수 자바 클래스이므로, new 를 사용해서 객체를 만들어줌 
    
    return BoardResponseDto;
}

게시글 작성 (BoardResponseDto)

public class BoardResponseDto {
    private String title;
    private String username;
    private String contents;
    private LocalDateTime modifiedAt;
    private LocalDateTime createdAt;
    
    public BoardResponseDto(Board saveBoard) {
    	this.title = saveBoard.getTitle();	//BoardResponseDto 클래스 내부에 저장된 위의 필드값 title 에 Board 엔티티에 저장된 값을 넣음
        this.username...
        ...
	}
}    

전체 게시글 목록 조회

public List<BoardResponseDto> getListBoards() {
	//BoardResponseDto 타입으로 반환하도록 되어있으므로, 반환할 타입의 List를 만든다.(boardList)
	List<BoardResponseDto> boardList = new ArrayList();
    //DB 에서 List 형식의 Board 를 모두 찾아서, boards 에 담는다
    List<Board> boards = boardRepository.findAll();
    //for 문을 통해, boards 를 하나씩 하나씩 board 에 담는다.
    for (Board board : boards) {
    	//새로운 생성자를 만들어서, Board 에 담긴 데이터들로 초기화시킨다.(BoardResponseDto.java 에서 초기화된다)
        //DB 입장에서는 이 코드가 row 1개
		BoardResposneDto responseDto = new BoardResposneDto(Board);
        //BoardResponseDto 타입만 들어갈 수 있는 List 에, 받아왔던 boards 가 하나씩 하나씩 Board 에 담겨, BoardResposneDto 타입으로 변환되면서, BoardResponseDto 타입만 들어갈 수 있는 List 에 들어가짐
        boardList.add(responseDto);
    }
    //반환 타입에 맞춰, board 가 아닌 boardList 로 반환
    return boardList;
}

3. BoardRepository

//Client <--DTO--> Controller <--DTO--> Service <--DTO--> 여기!! Repository <--Domain(Entity Class)--> DB
package com.sparta.hanghaeboard.repository;

import com.sparta.hanghaeboard.entity.Board;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

//게시글 작성
//jpa: 미리 검색 메소드를 정의 해 두는 것, 메소드를 호출하는 것만으로 스마트한 데이터 검색 가능
//JpaRepository: Entity 에 있는 데이터를 조회, 저장, 변경, 삭제 할때 Spring JPA에서 제공하는 Repository라는 인터페이스를 정의해 해당 Entity의 데이터를 사용.
//@Repository: JpaRepository 내부에 자동으로 Bean 으로 등록될 수 있는 옵션이 들어가 있으므로, 생략 가능하고 Bean 으로 자동 등록됨
public interface BoardRepository extends JpaRepository<Board, Long> {       //<Entity 클래스 이름, ID 필드 타입>

    //전체 게시글 목록 조회
    List<Board> findAllByOrderByModifiedAtDesc();       //모두 불러와 id에 대해 내림차순 정렬
}

4. Board

//Client <--DTO--> Controller <--DTO--> Service <--DTO--> Repository <-- 여기!! Domain(Entity Class)--> DB
//Board 테이블 생성
package com.sparta.hanghaeboard.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.sparta.hanghaeboard.dto.BoardRequestDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter     //setter는 repository에서 자동으로 해주기 때문에 설정 안 함
@Entity     //데이터베이스 기준으로 테이블 역할을 하는 것을 스프링에게 알려줌 --> 데이터베이스로 데이터를 날린다
@NoArgsConstructor  //기본생성자를 자동으로 만듦

//Board: Entity 클래스명 <-- Timestamped 를 상속받는다
public class Board extends Timestamped {

    //필드
    //게시글 작성
    //필드값: Id, title(제목), username(작성자), contents(내용), password
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)     //id 자동 증가명령
    private Long id;

    @Column(nullable = false)   // 컬럼 값이고 반드시 값이 존재해야 한다.
    private String title;

    @Column(nullable = false)
    private String username;

    @Column(nullable = false)
    private String contents;
    @JsonIgnore //데이터를 주고받을 때, 해당 데이터는 'ignore' 돼서 응답값에 보이지 않게됨
    @Column(nullable = false)
    private String password;

    //생성자
    //게시글 작성
    //Board: 객체명 --> boardRepository 안에 데이터가 들어간 객체 board 와 연결
    //requestDto: 객체 board 의 값을 넣어줄 생성자
    //requestDto 안에 title, username, contents, password 값을 넣을 것이다. (길만 열어둔 상태? 통로? 주머니? 느낌?)
    // BoardService.java 에서 Board board = new Board(requestDto); 을 만들었기 때문에 필요함
    public Board(BoardRequestDto requestDto) {        //BoardService.java 에서 객체 Board의 값을 넣어줄 생성자 requestDto를 만듦
        this.title = requestDto.getTitle();           //주의!! gettitle 이 아닌, getTitle
        this.username = requestDto.getUsername();	//위의 필드값 username 에 Client에게서 받아온 값(requestDto)을 넣음
        this.contents = requestDto.getContents();
        this.password = requestDto.getPassword();
    }

    //선택한 게시글 수정(변경)
    public void update(BoardRequestDto requestDto) {   //responseDto? boardRequestDto?    boardResponseDto? requestDto? 왜?
        this.title = requestDto.getTitle();    //responseDto? boardRequestDto?     boardResponseDto?
        this.username = requestDto.getUsername();
        this.contents = requestDto.getContents();
        this.password = requestDto.getPassword();
    }
}

5. Timestamped

//자동으로 현재 시간을 보여줌
package com.sparta.hanghaeboard.entity;

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Getter     //Getter가 없으면 작동이 안 됨
@MappedSuperclass       //상속했을 때, 자동으로 컬럼으로 인식
// Springboot 에 @EnableJpaAuditing 을 추가해줘야지만, 아래 코드가 정상 작동함
@EntityListeners(AuditingEntityListener.class)     //생성,수정 시간을 자동으로 반영하도록 설정

public class Timestamped {
    //createdAt, modifiedAt 컬럼 2개를 가진다
    @CreatedDate        //생성일자
    @Column(updatable = false)	//처음 작성된 날짜는 다음 날짜로 업데이트되더라도 변화 X (기존값 유지를 위해)
    private LocalDateTime createdAt;

    @LastModifiedDate       //마지막 수정일자
    private LocalDateTime modifiedAt;
}

6. BoardRequestDto

package com.sparta.hanghaeboard.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

//게시글 작성
@Getter
@Setter
@NoArgsConstructor      //기본생성자를 자동으로 만듦

//BoardRequestDto: 테이블의 데이터에 접근할 때, 완충재 역할
public class BoardRequestDto {
    //Client 가 요청(request)한 데이터들(title, username, contents, password --> 이 객체들 안에 데이터가 저장됨)을 DB 로 넘긴다
    private String title;
    private String username;
    private String contents;
    private String password;    //문자열도 섞여있으므로 String
}

@Getter(접근자), @Setter(설정자) 를 사용하지 않는다면?

	public void setTitle(String title) {
    	this.title = title;
    }
    
    public void ...
    ...

이렇게 일일이 다 정의해줘야함

7. BoardResponseDto

package com.sparta.hanghaeboard.dto;

import com.sparta.hanghaeboard.entity.Board;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor

//넘어오는 데이터들(title, username, contents, modifiedAt, createdAt, id)을 Client 쪽으로 넘긴다
//--> GET 방식을 이용해서, 최종 출력된 값은 이 형태로 보인다(view)
public class BoardResponseDto {
    private String title;
    private String username;
    private String contents;
    //private String password;
    private LocalDateTime modifiedAt;
    private LocalDateTime createdAt;
    private Long id;
    private String msg;
    private int statusCode;

    //생성자
    //객체 board 안에 데이터들을 담아둔다 (길만 열어둔 상태? 통로? 주머니? 느낌?)
    public BoardResponseDto(Board board) {
        this.title = board.getTitle();
        this.username = board.getUsername();
        this.contents = board.getContents();
        //this.password = board.getPassword();
        this.createdAt = board.getCreatedAt();
        this.modifiedAt = board.getModifiedAt();
        this.id = board.getId();
    }

    public BoardResponseDto(String msg, int statusCode) {
        this.msg = msg;
        this.statusCode = statusCode;
    }
}

BoardRequestDto, BoardResponseDto, ResponseDto 가 있을 때, ResponseDto 에서 제네릭을 사용하는 이유?
데이터에 int, String 등 타입이 달라질 수 있기 때문에 유동적으로 사용하기 위함

// ResponseDto<T> 은 모든 값이 들어올 수 있는 가변성을 가지게 됨
public class ResponseDto<T> {
	T data;
    String msg;
    int statusCode;
}

참고: 제네릭


Trouble Shooting

해결법 1
Edit Configurations(구성 편집) - environment variable(환경 변수) - server.port='8090'(''에는 원하는 포트번호를 넣으면 됨)으로 port번호를 변경
해결법 2: 강제 종료
cmd에서 netstat -ano를 입력 - 이 중에서 로컬 주소 8080을 찾고, 그것의 PID 번호를 찾기 - 작업 관리자에서 PID 번호를 확인하고, 해당 프로그램을 강제 종료하기

profile
개발자로 거듭나기!

0개의 댓글