2026. 4. 10. 20:55ㆍ대우개발원 수업 내용/spring boot, framework
Ajax와 JSON
• Asynchronous JavaScript And XML – 비동기로 서버와의 통신 처리
• JavaScript에서 많은 역할을 수행하고 서버에서는 XML이나 JSON과 같이 순수한 데이터만 처리하는 방식
• 개발의 무게 중심 변화
• JSON(JavaScript Object Notation)
REST방식의 구현
• Ajax를 이용해서 클라이언트와 서버 사이의 통신이 가능하다.
• Ajax는 처음에는 브라우저에서 서버를 호출하는데 사용되었지만 모바일에서도 유용하다.
• Ajax로 데이터를 주고 받을 때는 문자열이 가장 적합하다.
• 문자열로 구조화된 데이터를 표현하기 위해서 JSON이라는 포맷이 사용된다.
• JSON은 ‘키:값’과 ‘{ }’를 이용해서 객체의 구조를 표현할 수 있다.
Swagger UI
• REST api document tool
• 약간의 설정으로 api 테스트 환경과 문서 작업을 처리할 수 있음
REST방식의 댓글 처리 준비
• 구현 단계
• URL의 설계와 데이터 포맷 결정
• 컨트롤러의 JSON/XML 처리
• 동작 확인
• JavaScript를 통한 화면 처리
1. 프로젝트 환경 구성 및 Swagger(API 문서) 적용
- 프로젝트 분리: 기존 b01 프로젝트를 복제하여 REST API 전용 b01Rest 프로젝트로 전환함.
- Swagger 환경 구축: Spring Boot 3.x 버전에 맞춰 구형 springfox 라이브러리를 제거하고,
최신 springdoc-openapi-starter-webmvc-ui 의존성을 추가하여 500 에러 충돌을 해결함. - API 문서 그룹화: SwaggerConfig를 생성하여 전체 API는 REST API로, 특정 경로(/api/)를 제외한
API는 COMMON API 그룹으로 나누어 가독성을 높임. - 문서화 어노테이션: 컨트롤러 메서드에 @Operation을 추가해 Swagger UI에서
API의 역할을 명확히 파악할 수 있도록 설정함.
2. REST API 기반 댓글(Reply) 기능 설계
- DTO 생성: 클라이언트와 서버 간 데이터 전송을 위한 ReplyDTO를 생성함 (댓글 번호, 게시글 번호, 내용, 작성자 등).
- REST 컨트롤러 생성: @RestController를 사용하여 화면(View)이 아닌 순수 데이터(JSON)만 반환하는 ReplyController를 만듦.
- 데이터 매핑: @PostMapping과 @RequestBody를 통해 클라이언트가 보낸 JSON 데이터를
자바 객체(ReplyDTO)로 변환하여 받도록 구현함.
3. 전역 예외 처리 (Global Exception Handling)
- Advice 클래스 도입: @RestControllerAdvice를 활용한 CustomRestAdvice를 생성하여, 컨트롤러 전역에서
발생하는 에러를 한 곳에서 통제함. - 에러 응답 규격화: 에러 발생 시 HTML 에러 페이지가 아닌, 에러 내용이 담긴 Map 객체를
HTTP 상태 코드(예: 400 Bad Request)와 함께 JSON 형태로 클라이언트에게 반환하도록 설정함.
4. 데이터 유효성 검증 (Validation)
- 제약 조건 설정: ReplyDTO 필드에 @NotNull(null 제한), @NotEmpty(빈 문자열 제한) 등을 적용하여
올바른 데이터만 받도록 필터링함. - 검증 로직 적용: 컨트롤러 메서드 매개변수에 @Valid를 붙여 데이터 검증을 지시하고, 결과를 BindingResult로 받음.
- 에러 던지기 (Throw): 검증을 통과하지 못한 에러가 존재할 경우 throw new BindException을 통해 고의로
예외를 발생시키고, 이를 3번의 CustomRestAdvice가 낚아채어 안전하게 처리하도록 흐름을 완성함.
💡 오늘의 성과 요약 화면 렌더링 방식(Controller)에서 벗어나, 데이터 중심의 REST API 구조를 세팅함.
요청받은 데이터를 꼼꼼히 검증(@Valid)하고, 문제가 생기면 서버가 뻗지 않도록 한 곳에서 에러를 잡아내어(@RestControllerAdvice) 프론트엔드에 깔끔하게 결과를 알려주는 백엔드의 핵심 흐름을 완성함.
b01 프로젝트에서 게시판 기능까지는 다 만들었으니
프로젝트를 복사해서 b01Rest라는 프로젝트를 만들어준다

b01을 카피 했기때문에 프로젝트 이름도 b01로 되어 있고 하위 폴더도 이름이 다 그대로 되어 있다.
우선 바꾸지 않고 진행
bulid.gradle에
implementation("io.springfox:springfox-swagger-ui:3.0.0")
Swagger UI 를 사용하기 위한dependency(의존성) 추가

SwaggerConfig.java 클래스를 생성


Spring Boot Swagger(OpenAPI) 문서 그룹화 설정 코드
1. @Configuration
- 스프링 실행 시 이 클래스를 설정 파일로 인식하도록 지정함.
2. restApi() 메서드
- 적용 대상: 프로젝트 내의 모든 API 경로 (/**)
- 역할: 모든 API를 가져와 'REST API'라는 그룹으로 묶어줌.
3. commonApi() 메서드
- 적용 대상: 모든 경로를 가져오되, /api/ 로 시작하는 경로는 제외함.
- 역할: 핵심 API가 아닌 나머지 API들만 모아 'COMMON API'라는 그룹으로 묶어줌.
4. 결과
- Swagger 문서 페이지에서 우측 상단 메뉴를 통해 'REST API'(전체)와 'COMMON API'(일부 제외) 그룹을 선택해서 볼 수 있음
package com.example.b01.config;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfig {
@Bean
public GroupedOpenApi restApi(){
return GroupedOpenApi.builder()
.pathsToMatch("/**")
.group("REST API")
.build();
}
@Bean
public GroupedOpenApi commonApi() {
return GroupedOpenApi.builder()
.pathsToMatch("/**/*")
.pathsToExclude("/api/**/*")
.group("COMMON API")
.build();
}
}
http://localhost:8080/swagger-ui/index.html
을 실행하면 아래처럼 사진이 뜸

BoardController.java에 아래 어노테이션 추가

@Operation
package com.example.b01.controller;
import com.example.b01.dto.BoardDTO;
import com.example.b01.dto.PageRequestDTO;
import com.example.b01.dto.PageResponseDTO;
import com.example.b01.service.BoardService;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping("/board")
@Log4j2
@RequiredArgsConstructor
public class BoardController {
private final BoardService boardService;
@Operation
@GetMapping("/list")
public void list(PageRequestDTO pageRequestDTO, Model model){
PageResponseDTO<BoardDTO> responseDTO = boardService.list(pageRequestDTO);
log.info(responseDTO);
model.addAttribute("responseDTO", responseDTO);
}
@GetMapping("/register")
public void registerGET() {
}
@PostMapping("/register")
public String registerPost(@Valid BoardDTO boardDTO, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
log.info("board POST register.........");
if(bindingResult.hasErrors()) {
redirectAttributes.addFlashAttribute("errors",
bindingResult.getAllErrors());
return "redirect:/board/register";
}
log.info(boardDTO);
Long bno = boardService.register(boardDTO);
redirectAttributes.addFlashAttribute("result", bno);
return "redirect:/board/list";
}
// @GetMapping("/read")
@GetMapping({"/read", "/modify"})
public void read(Long bno, PageRequestDTO pageRequestDTO, Model model) {
BoardDTO boardDTO = boardService.readOne(bno);
log.info(boardDTO);
model.addAttribute("dto", boardDTO);
}
@PostMapping("/modify")
public String modify(@Valid BoardDTO boardDTO,
BindingResult bindingResult,
PageRequestDTO pageRequestDTO,
RedirectAttributes redirectAttributes){
log.info("board modify post.........." + boardDTO);
if(bindingResult.hasErrors()) {
log.info("has errors..........");
String link = pageRequestDTO. getLink();
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
redirectAttributes.addAttribute("bno", boardDTO.getBno());
return "redirect:/board/modify?"+link;
}
boardService.modify(boardDTO);
redirectAttributes.addFlashAttribute("result", "modified");
redirectAttributes.addAttribute("bno", boardDTO.getBno());
return "redirect:/board/read";
}
@GetMapping("remove")
public String remove(Long bno, RedirectAttributes redirectAttributes) {
log.info("remove post...." + bno);
boardService.remove(bno);
redirectAttributes.addFlashAttribute("result", "removed");
return "redirect:/board/list";
}
}
다시 실행하면
아래 board-controller가 추가된 모습


사이트에서 간단하게 Excute를 눌러서 테스트를 할 수 있음
페이지와 사이즈에 값을 넣고 테스트 하면
정상적으로 실행되는 모습
REST방식의 댓글 처리 준비
화면(View)에서 받아온 데이터나 화면으로 보낼 데이터를 담는 전송 객체로 사용하기 위한 ReplyDTO.java 생성


댓글 데이터를 계층 간(View, Controller, Service 등)에 전달하기 위한 DTO(Data Transfer Object) 클래스 코드
필드 (변수) 구성
rno: 댓글의 고유 번호 (Primary Key 역할).
bno: 해당 댓글이 달린 원본 게시글의 번호.
replyText: 사용자가 작성한 댓글의 실제 내용.
replyer: 댓글을 작성한 사람의 아이디 또는 이름.
regDate: 댓글이 처음 등록된 날짜와 시간.
modDate: 댓글이 마지막으로 수정된 날짜와 시간.
package com.example.b01.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ReplyDTO {
private Long rno;
private Long bno;
private String replyText;
private String replyer;
private LocalDateTime regDate;
private LocalDateTime modDate;
}
ReplyController을 생성


Rest방식으로 return하는 어노테이션을 추가함
@RestController
댓글 데이터를 처리하기 위한 REST 컨트롤러 기본 뼈대 코드
클래스 설정
- @RestController: 화면(View) 대신 순수 데이터(JSON 등)를 응답하는 컨트롤러로 설정함.
- @RequestMapping("/replies"): 이 클래스의 모든 요청 주소가 /replies 로 시작하도록 매핑함.
- @Log4j2: 서버 콘솔에 로그를 찍기 위한 어노테이션임.
- @RequiredArgsConstructor: 나중에 Service 클래스 등을 주입받을 때 필요한 생성자를 자동 생성해주는 Lombok 어노테이션임.
`ReplyController` 내부의 `register` 메서드(댓글 등록 기능) 코드 설명
1. @Operation(summary = "...")
- Swagger API 문서 화면에서 이 API의 역할을 "POST 방식으로 댓글 등록"이라고 보여주기 위한 어노테이션
2. @PostMapping(...)
- HTTP 메서드 중 POST 요청을 처리함.
- `consumes = MediaType.APPLICATION_JSON_VALUE`: 클라이언트가 보내는 데이터가 반드시
JSON 형식이어야만 처리하겠다고 제한하는 설정임.
3. 매개변수: @RequestBody ReplyDTO replyDTO
- 클라이언트(프론트엔드)가 요청 본문(Body)에 담아 보낸 JSON 데이터를 자바 객체인 `ReplyDTO`로 자동 변환해서 받아옴
4. 반환타입: ResponseEntity<Map<String,Long>>
- 결과 데이터(`Map`)와 함께 HTTP 상태 코드(ex. 200 OK)를 클라이언트에게 명확하게 전달하기 위한 반환 타입
5. 내부 실행 로직
- `log.info(replyDTO);`: 클라이언트가 보낸 댓글 데이터가 잘 들어왔는지 서버 콘솔에 로그를 찍어 확인.
- `Map<String, Long> resultMap = Map.of("rno", 111L);`: 아직 DB에 저장하는 진짜 로직이 없으므로,
임시로 "댓글 번호(rno) 111번이 생성되었다"고 가정한 결과 데이터를 만듦.
- `return ResponseEntity.ok(resultMap);`: HTTP 상태 코드 200(정상)과 함께 방금 만든
임시 결과 데이터(`{"rno": 111}`)를 JSON 형태로 프론트엔드에 응답함.
package com.example.b01.controller;
import com.example.b01.dto.ReplyDTO;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/replies")
@Log4j2
@RequiredArgsConstructor
public class ReplyController {
@Operation(summary = "POST 방식으로 댓글 등록")
@PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String,Long>> register(@RequestBody ReplyDTO
replyDTO){
log.info(replyDTO);
Map<String, Long> resultMap = Map.of("rno", 111L);
return ResponseEntity.ok(resultMap);
}
}
실행해서
Restapi로 변경
ok라고 보임


try it out를 눌러서


사진 처럼 입력 후 Excute
rno에 111이 들어가고 ok가 뜸
로그에도 정상적으로 뜬다

controller 패키지에 advice 패키지를 추가하고
CustomRestAdvice클래스 추가


REST 컨트롤러에서 발생하는 예외(에러)를 한 곳에서 모아 처리하는 전역 예외 처리 코드
클래스 설정
- @RestControllerAdvice: 프로젝트 내의 모든 @RestController에서 발생하는 예외를 가로채서 처리하도록 지정함.
(JSON 형식으로 응답) - @Log4j2: 에러 발생 시 로그를 기록하기 위해 사용함.
- 메서드 설정 (handleBindException)
- @ExceptionHandler(BindException.class):
컨트롤러로 들어오는 데이터(DTO 등)가 검증(@Valid)을 통과하지 못해 BindException이 발생했을 때 이 메서드가 실행됨. - @ResponseStatus(HttpStatus.EXPECTATION_FAILED):
기본적으로 HTTP 상태 코드를 417(Expectation Failed)로 설정함.
내부 실행 로직
- log.error(e);: 어떤 에러가 났는지 서버 콘솔에 붉은 글씨로 로그를 남김.
- Map<String, String> errorMap: 프론트엔드에 전달할 에러 내용을 담을 빈 공간(Map)을 생성함.
- if(e.hasErrors()): 실제 검증 에러가 존재하는지 확인함.
- bindingResult.getFieldErrors().forEach(...): 여러 개의 에러 항목을 하나씩 꺼내어,
에러가 발생한 필드 이름(예: title)과 에러 원인 코드(예: NotBlank)를 errorMap에 저장함.
결과 반환
- return ResponseEntity.badRequest().body(errorMap);: 최종적으로 HTTP 상태 코드 400(Bad Request)과 함께,
에러가 발생한 필드 내역이 담긴 errorMap을 JSON 형태로 프론트엔드에 응답함.
(※ 이 코드가 실행되면 상단의 417 설정 대신 400 상태 코드로 전송됨)
package com.example.b01.controller.advice;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@Log4j2
@RestControllerAdvice
public class CustomRestAdvice {
@ExceptionHandler(BindException.class) // 폼 데이터 검증(@Valid) 실패로 인해 발생하는 BindException을 처리
@ResponseStatus(HttpStatus.EXPECTATION_FAILED) // HTTP 응답 상태를 417 EXPECTATION_FAILED로 설정
public ResponseEntity<Map<String, String>> handleBindException(BindException e) {
log.error(e);
Map<String, String> errorMap = new HashMap<>();
if(e.hasErrors()){ // 에러 발생 시 에러가 발생한 필드명과 에러 코드가 errorMap에 저장
BindingResult bindingResult = e.getBindingResult();
bindingResult.getFieldErrors().forEach(fieldError -> {
errorMap.put(fieldError.getField(), fieldError.getCode());
});
}
return ResponseEntity.badRequest().body(errorMap);
// 필드 오류 정보를 JSON 형태로 반환하고 HTTP 400 Bad Request 응답 생성
}
}
ReplyDTO를 수정
유효성 검사 어노테이션(Validation Annotation) 또는 검증 어노테이션을 추가

- @NotNull: null만 에러 처리.
- @NotEmpty: null과 ""(빈 문자열) 에러 처리.
package com.example.b01.dto;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ReplyDTO {
private Long rno;
@NotNull
private Long bno;
@NotEmpty
private String replyText;
@NotEmpty
private String replyer;
private LocalDateTime regDate;
private LocalDateTime modDate;
}
ReplyController를 수정

데이터를 검증(Validation)하는 기능이 추가된 버전
- 매개변수 추가
- @Valid: 파라미터로 들어오는 ReplyDTO 내부의 제약 조건(예: @NotNull, @NotBlank 등)을 검사하도록 지시.
- BindingResult: @Valid로 검증한 결과(에러 내역 등)를 담아두는 객체.
- 예외 처리 로직 추가
- throws BindException: 이 메서드에서 검증 에러가 나면 직접 처리하지 않고 밖으로 던지겠다고 선언함.
- if(bindingResult.hasErrors()): 클라이언트가 보낸 데이터에 에러(조건 위반)가 있는지 확인함.
- throw new BindException(bindingResult): 에러가 있다면 강제로 예외를 발생시킴. 발생한 예외는 이전에 만들어둔
전역 예외 처리기(CustomRestAdvice)로 넘어가서 안전하게 처리됨.
- 반환 타입 변경
- 기존 ResponseEntity 대신 순수한 Map 객체를 반환하도록 변경됨.
- @RestController 환경에서는 Map만 반환해도 스프링이 알아서 상태 코드 200(정상)과 함께 JSON 형식으로 변환하여
프론트엔드로 응답하므로 코드가 간결해짐.
결론 클라이언트가 보낸 데이터를 무조건 받지 않고, 조건에 맞는지 먼저 검사(@Valid)한 뒤 틀린 값이 있으면
에러 처리를 담당하는 곳(Advice)으로 보내 버리는 코드
public class ReplyController {
@Operation(summary = "POST 방식으로 댓글 등록")
@PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
// public ResponseEntity<Map<String,Long>> register(@RequestBody ReplyDTO
// replyDTO){
// log.info(replyDTO);
// Map<String, Long> resultMap = Map.of("rno", 111L);
// return ResponseEntity.ok(resultMap);
// }
public Map<String,Long> register(@Valid @RequestBody ReplyDTO replyDTO,
BindingResult bindingResult)throws BindException {
log.info(replyDTO);
if(bindingResult.hasErrors()){
throw new BindException(bindingResult);
}
Map<String, Long> resultMap = new HashMap<>();
resultMap.put("rno",111L);
return resultMap;
}
}
bulid.gradle을 수정
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.13'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
description = 'b01'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testCompileOnly 'org.projectlombok:lombok'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testAnnotationProcessor 'org.projectlombok:lombok'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.1.0'
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation("org.modelmapper:modelmapper:3.1.1")
// Source: https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui
// implementation("io.springfox:springfox-swagger-ui:3.0.0")
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
}
tasks.named('test') {
useJUnitPlatform()
}
그리고 실행해보면

정상적으로 400 error가 뜨는것을 확인
'대우개발원 수업 내용 > spring boot, framework' 카테고리의 다른 글
| 자바 스프링 부트 8일차 b01Rest 댓글 페이징 및 CRUD 프론트엔드 연동 구현 (1) | 2026.04.15 |
|---|---|
| 자바 스프링 부트 7일차 b01Rest REST API 서버 구축부터 Axios를 활용한 프론트엔드 통신 준비 (0) | 2026.04.14 |
| 자바 스프링 부트 5일차 b01 CRUD, 유효성 검사 (1) | 2026.04.09 |
| 자바 스프링 부트 4일차 b01 (0) | 2026.04.06 |
| 자바 스프링 부트 3일차 b01 Spring Data JPA 기본 및 CRUD (0) | 2026.04.06 |
