Spring Boot를 기반으로 CRUD(Create, Read, Update, Delete) 기능이 포함된 REST API를 만들어보려고 한다.
데이터베이스는 h2 DB를 연결하고 Lombok과 JPA를 이용해서 게시글 데이터를 저장하고 조회할 것이다.
그럼 이제부터 간단한 REST API를 만들어보자!
유스케이스 다이어그램(Use-case Diagram)
: 시스템과 사용자의 상호작용을 다이어그램으로 표현한 것
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'
// 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로 구현할 것이다.
필요한 속성은 게시글을 구분할 { id, 제목, 내용, 작성자, 비밀번호, 작성일시, 수정일시} 다.
여기서 작성일시와 수정일시는 클라이언트가 직접 입력하는 것이 아니라, 글이 생성될 때 자동으로 일시를 부여해줄 것이다. 이를 위해 Timestamped
클래스를 만들 것이다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class Timestamped {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime modifiedAt;
}
@MappedSuperClass
: 공통으로 사용하는 맵핑 정보만 상속하는 부모 클래스에 선언Auditing
기능을 사용해서 생성일시, 수정일시를 구현할 수 있다.Auditing
기능을 사용하는 해당 클래스에는EntityListeners(AuditingEntityListener.class)
를,@SpringBootApplication
이 있는 class 에 @EnableJpaAuditing
을 추가해야 한다.@CraetedDate
: 엔티티가 생성되어 저장될 때 시간을 자동 저장@LastModifiedDate
: 엔티티가 수정된 시간을 자동 저장@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
: 객체 필드를 테이블의 컬럼으로 맵핑시킨다.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
를 구현했다.이제 드디어 게시판의 CRUD 기능을 하나씩 구현할 차례가 되었다.
먼저 API 명세서를 보면서, Controller에서 어떤 URL 주소를 호출받았을 때 어떤 메서드를 실행시켜야 하는지 정해야 한다.
실행시킬 메서드의 로직은 Service에서 구현한다.
Service에서 로직을 구현할 때 Repository를 이용해서 실제 데이터를 가져오거나 저장할 수 있다.
각 기능 구현에 앞서 Controller, Service, Repository를 만들어놓자!
@RestController
@RequiredArgsConstructor
public class BoardController {
private final BoardService boardService;
}
@Service
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;
}
@Repository
public interface BoardRepository extends JpaRepository<Board, Long> {
}
JpaRepository
를 상속받는다. 이 때 제네릭스 타입은 <엔티티로 쓰는 클래스, id 타입> 이다.BoardController
@GetMapping("/api/posts")
public List<BoardResponseDto> getPosts() {
return boardService.getPosts();
}
getPosts
(전체 목록을 가져온다.)BoardResponseDto
에 담아 List 형태로 Client에 보낸다.BoardService
@Transactional(readOnly = true)
public List<BoardResponseDto> getPosts() {
return boardRepository.findAllByOrderByModifiedAtDesc().stream().map(BoardResponseDto::new).toList();
}
BoardRepository
에서 수정일시 기준 내림차순으로 모든 데이터를 가져온다.findAll
은 기본적으로 제공해주는데, 수정일시 기준 내림차순은 BoardRepository
에서 따로 선언해줘야 한다.BoardResponseDto
에서 Board
엔티티를 넣으면 BoardResponseDto
로 객체를 생성주는 생성자를 만들었기 때문에 map(BoardResponseDto::new)
를 통해 간편하게 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);
}
createPost
(게시글을 작성한다.)BoardRequestsDto
를 Client로부터 받는다.BoardResponseDto
에 담아 Client로 보낸다.BoardService
@Transactional
public BoardResponseDto createPost(BoardRequestsDto requestsDto) {
Board board = new Board(requestsDto);
boardRepository.save(board);
return new BoardResponseDto(board);
}
Board
엔티티로 만들어준다.Board
클래스에서 BoardRequestsDto
를 받아서 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 데이터가 저장된다.BoardResponseDto
객체를 만들어 반환한다.BoardController
@GetMapping("/api/post/{id}")
public BoardResponseDto getPost(@PathVariable Long id) {
return boardService.getPost(id);
}
getPost
(id에 해당하는 글을 조회한다.)BoardResponseDto
에 담아 Client로 보낸다.BoardService
@Transactional
public BoardResponseDto getPost(Long id) {
return boardRepository.findById(id).map(BoardResponseDto::new).orElseThrow(
() -> new IllegalArgumentException("아이디가 존재하지 않습니다.")
);
}
BoardResponseDto
객체로 만들어 반환한다.BoardController
@PutMapping("/api/post/{id}")
public BoardResponseDto updatePost(@PathVariable Long id, @RequestBody BoardRequestsDto requestsDto) throws Exception {
return boardService.updatePost(id, requestsDto);
}
updatePost
(id에 해당하는 글을 수정한다.)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);
}
Board
객체에 넣는다.BoardResponseDto
에 담아 반환한다.BoardController
@DeleteMapping("/api/post/{id}")
public SuccessResponseDto deletePost(@PathVariable Long id, @RequestBody BoardRequestsDto requestsDto) throws Exception {
return boardService.deletePost(id, requestsDto);
}
deletePost
(id에 해당하는 글을 삭제한다.)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);
}
Board
객체에 넣는다.Postman을 사용해서 서버에 요청을 보내고 반환 결과를 확인할 수 있다.
https://github.com/dev-dykim/Spring-project-board.git
@PathVariable
로 id를 받도록 param 방식을 사용하였다.title
, contents
, author
, passwrod
} 를 받아야 하고, 삭제할 때는 { password
} 를 받아야 하므로, 서버에서 @RequestBody
로 데이터를 넘길 수 있도록 body 방식을 사용하였다./api/post/{id}
/api/post/id/{id}/name/{name}
/books/123
→ 123번 책 정보를 가져온다.@PathVariable
로 받는다.api/post?key=value&key2=value2
/books?genre=novel
→ 장르가 소설인 책 목록을 가져온다.RESTful
: REST API 의 설계 의도를 명확하게 지킴으로써, 각 구성 요소들의 역할이 완벽하게 분리되어 있어, URI만 보더라도 리소스를 명확하게 인식할 수 있도록 표현한 설계 방식post
리소스 중심으로 API를 설계하였다. → 메서드 기능은 http 메서드에서 미리 정의되어 있으므로, URI에는 대상이 되는 리소스(post
)만 담도록 설계하였다.Controller
: URL 맵핑을 통해 특정 메서드가 호출되도록 한다.Service
: 비지니스 로직을 수행한다.Repository
: 데이터베이스에 저장하고 조회하는 기능을 수행한다.JpaRepository
를 상속받아 Board
엔티티를 DB에 저장하도록 구현했다.기능
, 메서드
, URL
, Request
, Response
항목으로 작성했다.