SpringBoot를 사용하여 기본적인 CRUD 기능이 포함된 REST API를 만들어봅니다.
REST API
Representational state transfer의 약자로
분산 시스템에서 리소스를 표현하고 다루기 위한 규칙을 제시합니다.
간단한 예제이기 때문에 H2데이터 베이스를 사용합니다.
Lombok과 JPA를 사용하여 게시글을 저장 및 조회, 수정, 삭제합니다.
유스케이스 다이어그램
사용자와 시스템 간의 상호작용을 보여주며, 주요 기능과 시나리오를 나타냅니다.
[유스케이스 다이어그램 사진넣기]

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'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// h2 database 설정
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:db;MODE=MYSQL;
spring.datasource.username=sa
spring.datasource.password=
게시글을 관리하는 Board라는 Entity를 구현합니다.
Board Entity는 id, title, content, author, password, createdAt, modifiedAt 속성을 가집니다.
createdAt, modifiedAt 속성은 Timestamped Entity를 @MappedSuperclass 애노테이션을 통해 속성만 상속 받습니다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class Timestamped {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime modifiedAt;
}
@Mappedsuperclass : 공통 매핑 정보가 필요할 때, 부모 클래스에 선언하고 속성만 상속 받아서 사용할 수 있습니다.Auditing 기능 사용법 및 설명AuditingEntityListener.class : Spring Data JPA에서 제공하는 클래스로, 엔티티의 변경 사항을 추적하고 관리하는 데 사용됩니다.@EntityListeners(AuditingEntityListener.class) 어노테이션을 엔티티 클래스에 추가하여 해당 클래스가 엔티티 리스너에 의해 관리되어야 함을 명시합니다.@SpringBootApplication이 있는 class에 @EnableJpaAuditing을 추가합니다.@CreatedDate : 엔티티 생성시간을 자동으로 저장합니다.@LastModifiedDate : 엔티티가 수정된 시간을 자동으로 저장합니다.
@Getter
@Entity
@NoArgsConstructor
public class Board extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String content;
@Column(nullable = false)
private String author;
@Column(nullable = false)
private String password;
}
@Entity : JPA가 관리하는 엔티티 클래스, DB의 레코드와 1:1로 매핑이 됩니다.Timestamped를 상속받아 사용합니다.@Id와 @GeneratedValue를 사용해 JPA가 기본키를 관리하게 합니다.@Column : 클래스의 필드와 데이터베이스 테이블의 컬럼을 매핑하는 데 사용됩니다.DTO(Data Tranfer Object)
계층간 데이터 전송을 위한 객체
단순히 데이터 전송을 위한 객체이기 때문에 비즈니스 로직이 포함되면 안된다.
DTO에 어떤 데이터를 담을 지 확인하기 위해 위의 API 명세를 확인 합니다.
Board의 필드 값을 받아 요청을 보낸다.게시글 작성, 게시글 수정 : { title, content, author, password }게시글 삭제 : { password }Board의 필드값 또는 성공여부를 응답으로 보낸다.전체 목록 조회 : [{ id, title, content, author, createdAt, modifiedAt }, {...}]게시글 조회, 게시글 작성, 게시글 수정 : { id, title, content, author, createdAt, modifiedAt }게시글 삭제 : { success }@Getter
public class BoardRequestDTO {
private String title;
private String content;
private String author;
private String password;
BoardRequestDTO를 구현합니다.@Getter
@NoArgsConstructor
public class BoardResponseDTO {
private Long id;
private String title;
private String content;
private String author;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
public BoardResponseDTO(Board board) {
this.id = board.getId();
this.title = board.getTitle();
this.content = board.getContent();
this.author = board.getAuthor();
this.createdAt = board.getCreatedAt();
this.modifiedAt = board.getModifiedAt();
}
}
Board 엔티티를 받아 BoardResponseDTO 객체로 만들기 위한 생성자를 추가했습니다.@Getter
public class SuccessResponseDTO {
private boolean success;
public SuccessResponseDTO(boolean success) {
this.success = success;
}
}
SuccessResponseDTO를 구현했습니다.각 기능 구현을 하기 전에 미리 Controller, Service, Repository를 만들어 놓겠습니다.
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class BoardController {
private final BoardService boardService;
}
@RestController를 사용합니다./api 라는 uri는 공통으로 사용하기 때문에 @RequestMapping을 사용해서 class 레벨에 선언해줍니다@RequiredArgsConstructor와 final 제어자를 사용해서 DI를 받습니다.@Service
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;
}
boardRepository DI를 받습니다.BoardRepository를 사용합니다.@Repository
public interface BoardRepository extends JpaRepository<Board, Long> {
}
JpaRepository를 상속받습니다.JpaRepository<Entity, ID타입>입니다.@GetMapping("/posts")
public List<BoardResponseDTO> getPosts() {
return boardService.getPosts();
}
getPosts 메소드가 실행됩니다.List<BoardResponseDTO>로 응답합니다.@Transactional(readOnly = true)
public List<BoardResponseDTO> getPosts() {
return boardRepository.findAllByOrderByModifiedAtDesc()
.stream()
.map(BoardResponseDTO::new)
.toList();
}
BoardRepository를 통해 수정일시 기준 내림차순(최근 글 먼저)으로 모든 데이터를 가져온다.findAll메소드는 제공해 주지만 findAllBy{...}는 사용자가 선언 해야한다.BoardResponseDTO에서 Board를 매개변수로 받아서 BoardResponseDTO 필드로 할당해주는 생성자를 만들었기 때문에 map(BoardResponseDTO::new)를 통해 간편하게 DTO로 변환 가능하다.toList()를 사용해서 불변 List로 리턴합니다. 조회만 하고 변경을 할 필요가 없기 때문입니다.@Repository
public interface BoardRepository extends JpaRepository<Board, Long> {
List<Board> findAllByOrderByModifiedAtDesc();
}
findAllByOrderByModifiedAtDesc() 메소드를 선언합니다.@PostMapping("/post")
public BoardResponseDTO createPost(@RequestBody BoardRequestDTO boardRequestDTO) {
return boardService.createPost(boardRequestDTO);
}
createPost가 실행됩니다.BoardRequestDTO를 받아 게시물을 생성하는 서비스 계층의 메소드 boardService.createPost(boardRequestDTO)로 넘깁니다.@Transactional
public BoardResponseDTO createPost(BoardRequestDTO boardRequestDTO) {
Board board = new Board(boardRequestDTO);
boardRepository.save(board);
return new BoardResponseDTO(board);
}
BoardRequestDTO를 Entity로 변환해서 게시물 저장 로직을 실행합니다.Board 클래스에 생성자를 만들어줍니다.public Board(BoardRequestDTO boardRequestDTO) {
this.title = boardRequestDTO.getTitle();
this.content = boardRequestDTO.getContent();
this.author = boardRequestDTO.getAuthor();
this.password = boardRequestDTO.getPassword();
}BoardResponseDTO이기 때문에 Board 엔티티를 BoardResponseDTO로 변환해서 반환합니다.@GetMapping("/post/{id}")
public BoardResponseDTO getPost(@PathVariable Long id) {
return boardService.getPost(id);
}
getPost가 실행됩니다.(ID에 해당하는 글을 조회합니다.)BoardResponseDTO에 담아 응답합니다. @Transactional
public BoardResponseDTO getPost(Long id) {
return boardRepository.findById(id).map(BoardResponseDTO::new)
.orElseThrow(() -> new IllegalArgumentException("아이디가 존재하지 않습니다."));
}
map()을 통해 새로운 BoardResponseDTO 객체를 반환합니다.IllegalArgumentException()을 발생시킵니다. @PutMapping("/post/{id}")
public BoardResponseDTO updatePost(@PathVariable Long id, @RequestBody BoardRequestDTO boardRequestDTO) throws Exception {
return boardService.updatePost(id, boardRequestDTO);
}
updatePost가 실행됩니다.boardRequestDTO는 body 형태로 받습니다.BoardResponseDTO에 담아서 응답합니다.@Transactional
public BoardResponseDTO updatePost(Long id, BoardRequestDTO boardRequestDTO) throws Exception {
Board board = boardRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("아이디가 존재하지 않습니다."));
if (!boardRequestDTO.getPassword().equals(board.getPassword())) {
throw new InvalidPasswordException();
}
board.update(boardRequestDTO);
return new BoardResponseDTO(board);
}
id를 boardRepository.findById()에 넘기면 JPA가 id에 맞는 Board 객체를 찾아서 반환합니다.id로 Board 객체를 찾을 수 없다면 예외를 발생시킵니다.BoardRequestDTO의 패스워드와 id로 찾아온 기존의 Board객체의 패스워드가 일치하지 않으면, 예외를 발생시킵니다.public class InvalidPasswordException extends RuntimeException {
public InvalidPasswordException() {
super("비밀번호가 일치하지 않습니다.");
}
}id로 객체를 찾을 수 있고, 패스워드도 일치한다면, board의 내용을 update하고, BoardResponseDTO에 update된 board를 담아서 반환합니다.@DeleteMapping("/post/{id}")
public SuccessResponseDTO deletePost(@PathVariable Long id, @RequestBody BoardRequestDTO boardRequestDTO) {
return boardService.deletePost(id, boardRequestDTO);
}
deletePost가 실행됩니다.SuccessResponseDTO에 담아서 응답합니다.@Transactional
public SuccessResponseDTO deletePost(Long id, BoardRequestDTO boardRequestDTO) {
Board board = boardRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("아이디가 존재하지 않습니다."));
if (!boardRequestDTO.getPassword().equals(board.getPassword())) {
throw new InvalidPasswordException();
}
boardRepository.deleteById(id);
return new SuccessResponseDTO(true);
}
id를 boardRepository.findById()에 넘기면 JPA가 id에 맞는 Board 객체를 찾아서 반환합니다.id로 Board 객체를 찾을 수 없다면 예외를 발생시킵니다.id가 존재하고, 비밀번호도 일치한다면, boardRespository에서 해당 id 값을 가진 데이터를 지웁니다.SuccessResponseDTO에 담아서 반환합니다.Postman을 사용해서 요청을 보내고 반환 결과를 확인합니다.





