Spring Lv1 과제
Spring Boot로 게시판 백엔드 서버를 만들어보았다. (로그인 기능은 X)
Entity를 그대로 반환하지 말고, DTO에 담아서 반환
JSON을 반환하는 API형태
PostMan 으로 서버가 반환하는 결과값을 쉽게 확인하기
Use Case 그려보기
전체 게시글 목록 조회 API
게시글 작성 API
선택한 게시글 조회 API
선택한 게시글 수정 API
선택한 게시글 삭제 API
실선 화살표
<<포함>> 점선 화살표
spring.datasource.url=jdbc:mysql://localhost:3306/memo
spring.datasource.username=root
spring.datasource.password=비밀번호
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
// 가독성 위해
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
// 추가
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQLDialect
첫 네줄
spring.jpa.hibernate.ddl-auto=update
세 줄
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQLDialect
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.1'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'com.SpringProject'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
...
dependencies {
// JPA 설정
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// MySQL
implementation 'mysql:mysql-connector-java:8.0.33'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// 추가
implementation group: 'org.javassist', name: 'javassist', version: '3.15.0-GA'
}
...
버전에 대한 정보를 담고 있다.
dependencies
@RestController
@RequestMapping("/api")
public class BoardController {
private final BoardService boardService; // 2. 넣어주기만 하면 된다
public BoardController(BoardService boardService) { // 1. 외부에서 이미 만들어두었던 boardService 를 파라미터로 받아서
this.boardService = boardService;
}
...
}
@RestController
Spring 3 Layer Annotation 中 하나 (Controller, Service, Repository)
각 계층 클래스를 Bean 으로 등록할 때 사용
해당 어노테이션 안에는 이미 @Component 가 추가돼있다
@RequestMapping("/api")
아래에서 구현된 각 API 의 URL 에 공통적으로 들어가는 부분
동일한 URL 부분의 반복을 줄여 줄 수 있다
BoardService 객체 호출 한 부분
boardService 객체를 호출한다 (인스턴스화)
단, bean 객체만 주입받을 수 있는데, Service를 빈 객체로 등록했기 때문에 가능하다
사용 목적
약한 결합을 위해
아래에 각 기능들을 구현할 때,
// 게시글 작성
@PostMapping("/board")
public BoardResponseDto createBoard(@RequestBody BoardRequestDto requestDto) {
return boardService.createBoard(requestDto);
}
@RequestBody
BoardRequestDto requestDto
// 게시글 전체 조회
@GetMapping("/board")
public List<BoardResponseDto> getBoardList() {
return boardService.getBoardList();
}
// 게시글 선택 조회
@GetMapping("/board/{id}")
public BoardResponseDto getBoard(@PathVariable Long id) {
return boardService.getBoard(id);
}
@PathVariable
// 게시글 수정
@PutMapping("/board/{id}")
public BoardResponseDto updateBoard(@PathVariable Long id, @RequestBody BoardRequestDto requestDto) {
return boardService.updateBoard(id, requestDto);
}
// 게시글 삭제
@DeleteMapping("/board/{id}") // password 를 주소창에 노출시키지 않고, body 로 받았다
public BoardResponseDto deleteBoard(@PathVariable Long id, @RequestBody BoardRequestDto requestDto) {
return boardService.deleteBoard(id, requestDto);
}
@Getter
public class BoardRequestDto {
private String title; // 제목
private String username; // 작성자명
private String password; // 비밀번호
private String contents; // 작성 내용
}
@Getter
public class BoardResponseDto {
private Long id; // 게시글 구분을 위한 id 값
private String title; // 제목
private String username; // 작성자명
private String contents; // 작성 내용
private LocalDateTime createdAt; // 게시글 생성 날짜
private LocalDateTime modifiedAt; // 게시글 수정 날짜
private String msg; // 게시글 삭제 시, 삭제 성공 메시지
public BoardResponseDto(Board board) {
this.id = board.getId();
this.title = board.getTitle();
this.username = board.getUsername();
this.contents = board.getContents();
this.createdAt = board.getCreatedAt();
this.modifiedAt = board.getModifiedAt();
}
// 게시글 삭제 시, 삭제 성공 메시지
public BoardResponseDto(String msg) {
this.msg = msg;
}
}
Board라는 Entity에 저장된 값들을 호출해서, getxxx()메서드를 이용해 BoardResponseDto의 필드에 담는다.
삭제 성공 메시지의 경우
DB에서 불러오지 않고, Service 단에서 BoardResponseDto 객체를 생성하면서 직접 입력할 것이므로 이렇게 작성했다.
@Service
public class BoardService {
private final BoardRepository boardRepository;
public BoardService(BoardRepository boardRepository) {
this.boardRepository = boardRepository;
}
...
}
@Service
(Controller 에서 Spring 3 Layer Annotation 中 하나 (Controller, Service, Repository) 에 대한 설명과 동일)
BoardRepository 객체 호출 한 부분
(Controller 에서 BoardService 객체 호출 한 부분에 대한 설명과 동일)
// 게시글 작성
public BoardResponseDto createBoard(BoardRequestDto requestDto) {
Board board = new Board(requestDto); // RequestDto -> Entity
Board saveBoard = boardRepository.save(board); // DB 저장
BoardResponseDto boardResponseDto = new BoardResponseDto(saveBoard); // Entity -> ResponseDto
return boardResponseDto;
}
RequestDto -> Entity
: requestDto 열쇠를 담아서, Board 객체로 변환
DB 저장
Entity -> ResponseDto
: saveBoard변수에 담긴 결과값을 담아서, BoardResponseDto 객체로 변환
return boardResponseDto
: 변환 후, 그 결과값을 반환
아래 두 줄을 다음처럼 간략히 표현할 수도 있다.
// 수정 전
BoardResponseDto boardResponseDto = new BoardResponseDto(saveBoard); // Entity -> ResponseDto
return boardResponseDto;
// 수정 후
return new BoardResponseDto(saveBoard);
// 게시글 전체 조회
public List<BoardResponseDto> getBoardList() {
return boardRepository.findAllByOrderByModifiedAtDesc().stream() // DB 에서 조회한 List -> stream 으로 변환
.map(BoardResponseDto::new) // stream 처리를 통해, Board 객체 -> BoardResponseDto 로 변환
.toList(); // 다시 stream -> List 로 변환
}
boardRepository.findAllByOrderByModifiedAtDesc()
: boardRepository의 findAllByOrderByModifiedAtDesc()메서드를 호출stream().map(BoardResponseDto::new).toList()
// 게시글 선택 조회
public BoardResponseDto getBoard(Long id) {
// 해당 id 가 없을 경우
Board board = boardRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException("아이디가 존재하지 않습니다.")
);
// 해당 id 가 있을 경우
return new BoardResponseDto(board);
}
// 게시글 수정
@Transactional
public BoardResponseDto updateBoard(Long id, BoardRequestDto requestDto) {
Board board = findBoard(id); // DB에 해당 id 의 게시글이 존재하는지 확인
// 비밀번호 일치 여부 확인
if (board.getPassword().equals(requestDto.getPassword())) {
board.update(requestDto); // 일치하면 게시글 수정
} else {
return new BoardResponseDto("비밀번호가 일치하지 않습니다.");
}
return new BoardResponseDto(board);
}
...
// id 일치 여부 확인 (공용 사용되는 부분)
private Board findBoard(Long id) {
return boardRepository.findById(id).orElseThrow(() ->
new IllegalArgumentException("선택한 게시글은 존재하지 않습니다.")
);
}
@Transactional
findBoard(id)
board.getPassword().equals(requestDto.getPassword())
// 게시글 삭제
public BoardResponseDto deleteBoard(Long id, BoardRequestDto requestDto) {
Board board = findBoard(id);
// 비밀번호 일치 여부 확인
if (board.getPassword().equals(requestDto.getPassword())) {
boardRepository.delete(board);
} else {
return new BoardResponseDto("비밀번호가 일치하지 않습니다.");
}
return new BoardResponseDto("게시글을 삭제했습니다."); // 게시글 삭제 시, 삭제 성공 메시지
}
// id 일치 여부 확인 (공용 사용되는 부분)
private Board findBoard(Long id) {
return boardRepository.findById(id).orElseThrow(() ->
new IllegalArgumentException("선택한 게시글은 존재하지 않습니다.")
);
}
findBoard(id)
(게시글 수정에서와 동일)
board.getPassword().equals(requestDto.getPassword())
public interface BoardRepository extends JpaRepository<Board, Long> {
List<Board> findAllByOrderByModifiedAtDesc();
}
JpaRepository<Board, Long>
findAllByOrderByModifiedAtDesc()
@Entity
@Getter
@Table(name = "board")
@NoArgsConstructor
public class Board extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "title", nullable = false)
private String title;
@Column(name = "username", nullable = false)
private String username;
@Column(name = "password", nullable = false)
private String password;
@Column(name = "contents", nullable = false, length = 500)
private String contents;
// 게시글 작성
public Board(BoardRequestDto requestDto) {
this.title = requestDto.getTitle();
this.username = requestDto.getUsername();
this.password = requestDto.getPassword();
this.contents = requestDto.getContents();
}
// 게시글 수정
public void update(BoardRequestDto requestDto) {
this.title = requestDto.getTitle();
this.username = requestDto.getUsername();
this.contents = requestDto.getContents();
}
}
@Entity
: JPA 가 관리할 수 있는 Entity 클래스로 지정
@Table(name = "board")
: 매핑할 테이블 이름을 지정
Board extends Timestamped
: Timestamped 클래스를 상속받는다
@NoArgsConstructor
: 참고: 생성자 - 7. 어노테이션
@Setter 를 사용하지 않았다.
this.password = requestDto.getPassword() 를 삭제했다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class Timestamped {
@CreatedDate
@Column(updatable = false)
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime createdAt;
@LastModifiedDate
@Column
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime modifiedAt;
}
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
: 해당 클래스에 Auditing 기능(자동으로 시간 넣기) 추가
@CreatedDate
: Entity 객체 생성 후 저장될 때, 시간이 자동 저장됨
@LastModifiedDate
: 조회한 Entity 객체 값 변경 시, 변경된 시간이 자동 저장됨
@Column(updatable = false)
@Temporal(TemporalType.TIMESTAMP)
: 날짜 데이터와 매핑 시에 사용
@EnableJpaAuditing
@SpringBootApplication
public class SpringProjectApplication {
...
}
@EnableJpaAuditing