6. DTO 설정과 서비스 계층 만들기
DTO(Data Transfer Object) : 계층 간 데이터 교환을 목적으로 하는 객체
DTO는 상황에 따라 여러 개로 만들어도 됨
package org.zerock.apiserver.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TodoDTO {
private long tno;
private String title;
private String content;
private boolean complete;
private LocalDateTime dueDate;
}
TodoDTO.java
TodoDTO 클래스는 데이터를 담는 데이터 전송 객체이고, 주로 API 서버에서 클라이언트와의 데이터 교환에 사용함
package org.zerock.apiserver.service;
import jakarta.transaction.Transactional;
import org.zerock.apiserver.domain.Todo;
import org.zerock.apiserver.dto.TodoDTO;
@Transactional
public interface TodoService {
TodoDTO get(Long tno);
default TodoDTO entityToDTO(Todo todo){
return TodoDTO.builder()
.tno(todo.getTno())
.title(todo.getTitle())
.content(todo.getContent())
.complete(todo.isComplete())
.dueDate(todo.getDueDate())
.build();
}
default Todo dtoToEntity(TodoDTO todoDTO){
return Todo.builder()
.tno(todoDTO.getTno())
.title(todoDTO.getTitle())
.content(todoDTO.getContent())
.complete(todoDTO.isComplete())
.dueDate(todoDTO.getDueDate())
.build();
}
}
TodoService.java
TodoService 인터페이스
- get(Long tno) 메소드를 통해 Todo 데이터 조회
- @Transactional 애노테이션은 이 인터페이스를 구현하는 클래스의 모든 메소드에 트랜잭션 관리가 적용됨을 의미 -> 데이터베이스의 일관성과 무결성 보장
- entityToDTO와 dtoToEntity 메소드를 통해 도메인 엔티티와 데이터 전송 객체 간의 변환을 처리를 진행
package org.zerock.apiserver.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
import org.zerock.apiserver.domain.Todo;
import org.zerock.apiserver.dto.TodoDTO;
import org.zerock.apiserver.repository.TodoRepository;
import java.util.Optional;
@Service
@Log4j2
@RequiredArgsConstructor
public class TodoServiceImpl implements TodoService{
private final TodoRepository todoRepository;
@Override
public TodoDTO get(Long tno) {
Optional<Todo> result = todoRepository.findById(tno);
Todo todo = result.orElseThrow();
return entityToDTO(todo);
}
}
TodoServiceImpl.java
TodoServiceImpl 클래스는 TodoService 인터페이스를 구현한 서비스 계층의 클래스
=> TodoService 인터페이스에서 정의한 메소드를 실제로 구현함
package org.zerock.apiserver.service;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@Log4j2
public class TodoServiceTests {
@Autowired
TodoService todoService;
@Test
public void testGet() {
Long tno = 50L;
log.info(todoService.get(tno));
}
}
TodoServiceTests.java
TodoServiceTests 클래스는 TodoService 인터페이스를 구현한 서비스 클래스인 TodoServiceImpl의 기능을 테스트하기 위한 클래스
테스트를 실행해보면 정상적으로 DTO로 변환됨
-> 조회 완료
7. 서비스 계층 만들기
register 등록 구현
default Todo dtoToEntity(TodoDTO todoDTO){
return Todo.builder()
.tno(todoDTO.getTno())
.title(todoDTO.getTitle())
.content(todoDTO.getContent())
.complete(todoDTO.isComplete())
.dueDate(todoDTO.getDueDate())
.build();
}
TodoService.java
package org.zerock.apiserver.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
import org.zerock.apiserver.domain.Todo;
import org.zerock.apiserver.dto.TodoDTO;
import org.zerock.apiserver.repository.TodoRepository;
import java.util.Optional;
@Service
@Log4j2
@RequiredArgsConstructor
public class TodoServiceImpl implements TodoService{
private final TodoRepository todoRepository;
@Override
public TodoDTO get(Long tno) {
Optional<Todo> result = todoRepository.findById(tno);
Todo todo = result.orElseThrow();
return entityToDTO(todo);
}
@Override
public Long register(TodoDTO dto) {
Todo todo = dtoToEntity(dto);
Todo result = todoRepository.save(todo);
return result.getTno();
}
@Override
public void modify(TodoDTO dto) {
}
@Override
public void remove(Long tno) {
}
}
TodoServiceImpl.java
Todo를 등록하고 tno를 부여하는 기능을 추가함
package org.zerock.apiserver.service;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.zerock.apiserver.dto.TodoDTO;
import java.time.LocalDate;
import java.time.LocalDateTime;
@SpringBootTest
@Log4j2
public class TodoServiceTests {
@Autowired
TodoService todoService;
@Test
public void testGet() {
Long tno = 50L;
log.info(todoService.get(tno));
}
@Test
public void testRegister() {
TodoDTO todoDTO = TodoDTO.builder()
.title("Title...")
.content("Content...")
.dueDate(LocalDate.of(2024,10,01).atStartOfDay())
.build();
log.info(todoService.register(todoDTO));
}
}
TodoServiceTests.java
insert문이 실행되었고 102번으로 만들어진 것을 확인할 수 있음
-> 등록 처리까지 성공
modify, remove
@Override
public void modify(TodoDTO dto) {
Optional<Todo> result = todoRepository.findById(dto.getTno());
Todo todo = result.orElseThrow();
todo.changeTitle(dto.getTitle());
todo.changeContent(dto.getContent());
todo.changeComplete(dto.isComplete());
todo.changeDueDate(dto.getDueDate());
todoRepository.save(todo);
}
@Override
public void remove(Long tno) {
todoRepository.deleteById(tno);
}
todo 데이터를 수정하고 삭제하는 기능
- TodoDTO를 받아 해당 tno에 해당하는 Todo 엔티티를 조회하고, 필드를 업데이트한 후 저장
- 특정 tno에 해당하는 할 일을 데이터베이스에서 삭제
8. 페이지 처리를 위한 DTO 설계
package org.zerock.apiserver.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
@SuperBuilder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {
@Builder.Default
private int page = 1;
@Builder.Default
private int size = 10;
}
superBuilder : Lombok 라이브러리에서 제공하는 애노테이션, 클래스 상속 구조에서도 동작 가능
package org.zerock.apiserver.dto;
import lombok.Data;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@Data
public class PageResponseDTO<E> {
private List<E> dtolist;
private List<Integer> pageNumlist;
private PageRequestDTO pageRequestDTO;
private boolean prev, next;
private int totalCount, prevPage, nextPage, totalPage, current;
public PageResponseDTO(List<E> dtolist, PageRequestDTO pageRequestDTO, long total) {
this.dtolist = dtolist;
this.pageRequestDTO = pageRequestDTO;
this.totalCount = (int) total;
//끝페이지 end
int end = (int)(Math.ceil(pageRequestDTO.getPage() / 10.0)) * 10;
int start = end - 9;
int last = (int) (Math.ceil(totalCount/(double)pageRequestDTO.getSize()));
end = end > last ? last : end;
this.prev = start > 1;
this.next = totalCount > end * pageRequestDTO.getSize();
this.pageNumlist = IntStream.rangeClosed(start, end).boxed().collect(Collectors.toList());
this. prevPage = prev ? start-1 : 0;
this.nextPage = next ? end +1 : 0;
}
}
페이지의 사이즈는 10이라고 가정
현재페이지/10.0 -> 소수점 올림 -> 10을 곱함
이렇게 계산한 값 = 페이지의 끝번호(계산한 값에서 9를 뺀 것은 시작 페이지 번호)
=> 페이징 처리하는 로직임
9. Querydsl 검색처리
오류를 방지하기 위해 Test 코드를 주석 처리
PageResponseDTO<TodoDTO> getList(PageRequestDTO pageRequestDTO);
TodoService.java에 해당 코드 추가
PageRequestDTO는 검색과 관련된 모든 데이터를 가지고 있는 DTO이다.
PageRequestDTO에서 전달된 페이지 정보를 바탕으로 TodoDTO 리스트를 포함한 PageResponseDTO 객체를 반환하고 이 객체는 페이징된 리스트를 제공
@Override
public PageResponseDTO<TodoDTO> getList(PageRequestDTO pageRequestDTO) {
//JPA
Page<Todo> result = todoRepository.search1(pageRequestDTO);
//Todo List -> TodoDTO List
List<TodoDTO> dtoList = result
.get()
.map(todo -> entityToDTO(todo)).collect(Collectors.toList());
PageResponseDTO<TodoDTO> responseDTO =
PageResponseDTO.<TodoDTO>withAll()
.dtoList(dtoList)
.pageRequestDTO(pageRequestDTO)
.total(result.getTotalElements())
.build();
return responseDTO;
}
TodoServiceImpl.java
getList 메서드는 PageRequestDTO에 따라 Todo 엔티티를 조회하고, 이를 TodoDTO로 변환한 후 페이징된 결과를 담은 PageResponseDTO 객체를 반환
JPA를 사용하여 데이터베이스에서 Todo를 가져오고, 이를 DTO로 변환하여 클라이언트에게 전달한다.
TodoServiceTests.java
todoService의 getList 메서드를 호출하여, 102개의 페이지 목록이 출력된다.
page를 11로 지정해서 테스트를 해도 정상적으로 11까지 출력되는 것을 확인할 수 있다.
10. REST 방식 컨트롤러 만들기(1)
package org.zerock.apiserver.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.zerock.apiserver.dto.TodoDTO;
import org.zerock.apiserver.service.TodoService;
@RestController
@Log4j2
@RequiredArgsConstructor
@RequestMapping("/api/todo")
public class TodoController {
private final TodoService todoService;
@GetMapping("/{tno}")
public TodoDTO get(@PathVariable("tno") Long tno) {
return todoService.get(tno);
}
}
Controller 패키지에 TodoController 클래스를 생성한다.
/api/todo/{tno}와 같은 URL을 통해 tno에 대한 정보를 조회하는 기능
@RestController를 추가하여 REST API 요청을 처리하도록 한다.
REST API : HTTP 프로토콜을 기반의 클라이언트와 서버 간에 데이터를 주고받는 방식
실행을 하고
url을 입력해서 잘 출력되는지 확인한다.
@GetMapping("/list")
public PageResponseDTO<TodoDTO> list(PageRequestDTO pageRequestDTO){
log.info("list............." + pageRequestDTO);
return todoService.getList(pageRequestDTO);
}
pathvariable은 항상 동일할 때 사용하고, /list?page=3처럼 변경 가능성이 있을 경우는 queryString을 사용해야 한다.
Get 방식의 list도 추가를 한다.
11. @RestControllerAdvice
REST API를 사용했기 때문에 발생하는 예외를 처리하고 전역적인 응답 처리를 위해 @RestControllerAdvice를 사용한다.
RestController에 문제가 생겼을 때 예외처리를 한다.
controller 패키지 아래에 advice 패키지를 생성하고 CustomControllerAdvice 클래스를 작성한다.
package org.zerock.apiserver.controller.advice;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Map;
import java.util.NoSuchElementException;
@RestControllerAdvice
public class CustomControllerAdvice {
@ExceptionHandler(NoSuchElementException.class)
public ResponseEntity<?> notExist(NoSuchElementException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("msg", e.getMessage()));
}
}
HTTP 상태 코드 404 Not Found와 함께 예외 메시지를 JSON 형식으로 클라이언트에게 전달하는 코드이다.
서버를 다시 실행하고 없는 페이지 번호를 입력하면 404 에러를 볼 수 있다.
페이지 번호를 잘못 입력했을 때 MethodArgumentNotValidException가 발생한다.
이것을 방지하기 위해 ExceptionHandler를 작성한다.
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> notExist(MethodArgumentNotValidException e){
return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).body(Map.of("msg", e.getMessage()));
}
MethodArgumentNotValidException 예외를 처리하는 메서드를 추가
406 Not Acceptable 상태 코드를 반환한다.
Postman으로도 응답을 확인할 수 있다.
같은 응답이지만 Postman은 재사용이 가능해서 유용함..
12. REST 방식 컨트롤러 만들기(2)
package org.zerock.apiserver.controller.formatter;
import org.springframework.format.Formatter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class LocalDateFormatter implements Formatter<LocalDate> {
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// LocalDate를 문자열로 변환
@Override
public String print(LocalDate date, Locale locale) {
return formatter.format(date);
}
// 문자열을 LocalDate로 변환
@Override
public LocalDate parse(String text, Locale locale) {
return LocalDate.parse(text, formatter);
}
}
LocalDate를 문자열로, 문자열을 LocalDate로 변환하기 위한 포맷터
날짜 변환 처리
우클릭 -> Generate -> Override Methods
Method 오버라이드할 수 있는 것이 많아진다.
addFormatters만 선택
package org.zerock.apiserver.config;
import jakarta.servlet.ServletConfig;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.zerock.apiserver.controller.formatter.LocalDateFormatter;
@Configuration
@Log4j2
public class CustomServletConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
log.info("---------------------------");
log.info("addFormatters");
registry.addFormatter(new LocalDateFormatter());
}
}
위에서 만든 LocalDateFormatter에서는 어노테이션이 없었다.
따라서 해당 코드에 LocalDateFormatter를 애플리케이션의 전역 포맷터로 추가하여, LocalDate 객체를 자동으로 문자열로 변환하거나 문자열을 LocalDate로 변환할 수 있게 하는 기능을 제공
그리고 확인하기 위한 log 코드로 추가하였다.
POST 요청을 통해 Todo를 등록할 수 있는 register를 추가했다.
클라이언트의 요청 body에서 JSON 데이터를 받아 TodoDTO 객체로 변환하고, Todo을 등록한 후 TNO를 반환
addFormatters가 출력되어 정상적으로 작동하는 것을 확인했다.
private LocalDate dueDate;
TodoDTO.java에서 LocalDateTime이라고 선언했던 부분이 오류가 나서 위와 같이 수정하고 연결된 Service 코드들을 수정하고 진행했다...
{
"title":"Sample Title",
"content":"Sample Contemt",
"dueDate": "2024-10-02"
}
Post 전송을 했을 때 TNO가 잘 출력되고
insert문도 잘 실행이 된다.
여기까지 RestController와 Register 구현이 완료!
13. REST 컨트롤러 - 수정/삭제, CORS설정
@PutMapping("/{tno}")
public Map<String, String> modify(@PathVariable("tno") Long tno,
@RequestBody TodoDTO todoDTO) {
todoDTO.setTno(tno);
todoService.modify(todoDTO);
return Map.of("RESULT", "SUCCESS");
}
@DeleteMapping("/{tno}")
public Map<String, String> remove(@PathVariable Long tno) {
todoService.remove(tno);
return Map.of("RESULT", "SUCCESS");
}
TodoController.java에서 수정과 삭제 코드를 작성
@PutMapping과 @DeleteMapping 어노테이션을 사용하여 수정 및 삭제
작업에 성공하면 {"RESULT": "SUCCESS"} 형식의 JSON 응답을 반환한다.
CORS(Cross-Origin Resource Sharing)는 웹 브라우저에서 다른 도메인(origin) 간의 리소스 요청을 제어하는 보안 메커니즘
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 모든 경로에 대해 CORS를 허용
.maxAge(500) // preflight 요청의 캐싱 시간을 500초로 설정
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 허용할 HTTP 메서드 설정
.allowedOrigins("*"); // 모든 도메인에서 요청 허용
}
}
CustomServletConfig 코드에서 CORS로 서버가 다른 출처의 웹 페이지에서 오는 요청을 허용하거나 거부할 수 있는 방법을 설정해 주었다.
'Web' 카테고리의 다른 글
5. 상품 API 서버 구성하기 - 1 (0) | 2024.10.08 |
---|---|
4. 리액트와 API 서버 통신 (0) | 2024.10.06 |
3. 스프링부트와 API 서버 - 1 (0) | 2024.10.01 |
2. React-Router (0) | 2024.09.24 |
1. 개발환경 설정 (0) | 2024.09.24 |