****수많은 문제들이 발생해서 업로드가 매우 늦어졌습니다.... 마지막 문제는 아직도 해결못했기때문에 다음 글로 이어집니다...
*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성
- Windows11(윈도우 11) 환경
- 자바 JDK 17 버전 설치 https://yungenie.tistory.com/11
[Java] 차근차근 Java 설치하기 (JDK17, Window 11)
자바 개발 도구 설치 방법에 대해서 알아보겠습니다. Java17은 LTS(Long Term Support : 장기 지원) 릴리즈로 1년 후까지 기술 지원 및 버그를 개선한 서비스를 제공받을 수 있습니다. 업데이트 버전을 꾸
yungenie.tistory.com
- 스프링 부트 4.31.0 사용 - STS(Spring Tool Suite) 설치(Spring Tools for Eclipse - https://spring.io/tools)
=> https://priming.tistory.com/147 참고
[Windows] Spring Tool Suite 4(STS 4) 다운로드 및 설치
STS란?Spring Tool Suite(STS)는 스프링 프로젝트를 생성하고, 개발할 수 있게 해주는 도구입니다. STS 설치 과정에 대해 설명드리겠습니다. 설치 파일 다운로드STS 공식 사이트에서 설치 파일을 다운로
priming.tistory.com
- MySQL Community Server 8.0.42 설치 https://dev.mysql.com/downloads/mysql/
MySQL :: Download MySQL Community Server
Select Version: 9.3.0 Innovation 8.4.5 LTS 8.0.42 Select Operating System: Select Operating System… Microsoft Windows Ubuntu Linux Debian Linux SUSE Linux Enterprise Server Red Hat Enterprise Linux / Oracle Linux Fedora Linux - Generic Oracle Solaris mac
dev.mysql.com
- Gradle
**STS에서 Gradle 프로젝트 생성한 과정
*** 함께 보면 좋은 글
스프링 부트 핵심 가이드(장정우 지음) - 스프링 부트 개요
1. 스프링 프레임워크자바(Java) 기반 애플리케이션 프레임워크로, 엔터프라이즈급(기업 환경 대상 개발) 애플리케이션을 위한 다양한 기능 제공-> 오픈소스 경량급 애플리케이션 프레임워크로
keep-programming-study.tistory.com
스프링 부트 핵심 가이드(장정우 지음) - 개발에 앞서 알면 좋은 기초 지식
1. 서버 간 통신마이크로서비스 아키텍처에서 한 서버가 다른 서버에 통신을 요청하는 것을 의미-> 한 대는 서버/다른 한 대는 클라이언트가 됨 가장 많이 사용되는 방식은 HTTP/HTTPS 방식(TCP/IP, SOA
keep-programming-study.tistory.com
2024.03.26 - [컴퓨터공학 공부/SQLD (SQL 전문가 가이드)] - SQLD 자격증 공부 데이터 모델링의 이해-엔터티, 속성(SQL 전문가가이드)
SQLD 자격증 공부 데이터 모델링의 이해-엔터티, 속성(SQL 전문가가이드)
*본 게시물은 2013년도 SQL 전문가 가이드 교재(일명 '노랭이')를 참고하여 공부하고 정리한 게시물입니다 1과목 데이터 모델링의 이해: 제1장 데이터 모델링의 이해 1. 엔터티 1) 정의 업무에서 관
keep-programming-study.tistory.com
JAVA/JSP 14. 데이터베이스 - 특징, 오라클 설치(Oracle Database 11gR2 Express Edition), 사용자 계정 생성 및
1. 데이터베이스의 특징우리가 매일 PC나 스마트폰을 통해 접하는 거의 모든 웹 애플리케이션에서 사용함매일 업데이트되는 뉴스나 날씨 등의 정보는 데이터베이스가 없다면 클라이언트에 전달
keep-programming-study.tistory.com
스프링 부트 핵심 가이드(장정우 지음) - 데이터베이스 연동 1: ORM(Object Relational Mapping) 특징, JPA(Ja
*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성 Windows11(윈도우 11) 환경자바 JDK 17 버전 설치 https://yungenie.tistory.com/11 [Java] 차근차근 Java 설치하기 (JDK17, Window 11)자바 개발 도구 설치 방법
keep-programming-study.tistory.com
스프링 부트 핵심 가이드(장정우 지음) - 데이터베이스 연동 2: 프로젝트에 MySQL Community Server
*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성 Windows11(윈도우 11) 환경자바 JDK 17 버전 설치 https://yungenie.tistory.com/11 [Java] 차근차근 Java 설치하기 (JDK17, Window 11)자바 개발 도구 설치 방법
keep-programming-study.tistory.com
**** 이 책에서 구현하는 스프링부트 애플리케이션의 구조
- 데이터베이스와 밀접한 관련이 있는 데이터 액세스 레이어까지는 데이터 교환에 엔티티 객체를 사용하고,
클라이언트와 가까워지는 다른 레이어에서는 데이터 교환에DTO 객체를 사용하는 것이 일반적 - 아래 그림은 서비스와 DAO의 사이에서 엔티티로 데이터를 전달하는 것인데,
회사나 개발 그룹 내 규정에 따라 DTO를 사용할 수도 있음 - 아래 그림에서는 각 레이어 사이의 큰 데이터의 전달을 표현했는데,
단일 데이터나 소량의 데이터를 전달하는 경우 DTO나 엔티티를 사용하지 않기 함
1. DAO(Data Access Object) 설계
- 데이터베이스에 접근하기 위한 로직을 관리하기 위한 객체
-> 비즈니스 로직 동작 과정에서 데이터를 조작하는 기능 수행 - 책에서는 서비스 레이어와 리포지토리의 중간 계층을 구성하는 역할로 사용(유지보수에 유리)
- 스프링 프레임워크나 스프링 MVC 사용자는 리포지토리라는 개념을 사용하지 않고, DAO 객체로 데이터베이스에 접근
1) DAO 클래스 생성
(1) ProductDAO.java
// ProductDAO.java
package com.example.demo.jpa.data.dao;
import com.example.demo.jpa.data.entity.Product;
// Product 엔티티에 대한 데이터 접근 로직
// -> 기본적인 CRUD를 다루기 위해 인터페이스에 메서드 정의
public interface ProductDAO {
/*
* 새로운 Product를 데이터베이스에 삽입
* @param product 삽입할 Product 객체
* @return 삽입된 Product 객체 (예: 자동 생성된 ID 포함)
*/
Product insertProduct(Product product);
/*
* 고유 번호를 기반으로 Product를 조회
* @param number 조회할 Product의 고유 번호
* @return 조회된 Product 객체 (없으면 null)
*/
Product selectProduct(Long number);
/*
* 특정 Product의 이름을 업데이트
* @param number 업데이트할 Product의 고유 번호
* @param name 새로운 이름
* @return 업데이트된 Product 객체
* @throws Exception 업데이트 실패 시 예외 발생
*/
Product updateProductName(Long number, String name) throws Exception;
/*
* 특정 Product를 삭제
* @param number 삭제할 Product의 고유 번호
* @throws Exception 삭제 실패 시 예외 발생
*/
void deleteProduct(Long number) throws Exception;
}
(2) ProductDAOImpl.java
// ProductDAOImpl.java
package com.example.demo.jpa.data.dao.impl;
import java.time.LocalDateTime;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.example.demo.jpa.data.dao.ProductDAO;
import com.example.demo.jpa.data.entity.Product;
import com.example.demo.jpa.data.repository.ProductRepository;
/**
* ProductDao 인터페이스의 구현 클래스로,
* Spring Data JPA의 ProductRepository를 활용하여
* Product 엔티티에 대한 CRUD 작업을 수행
*/
@Component // Spring이 이 클래스를 Bean으로 등록할 수 있도록 지정
public class ProductDAOImpl implements ProductDAO {
private final ProductRepository productRepository;
/**
* 생성자 주입 방식으로 ProductRepository를 주입받음(테스트와 유지보수에 유리)
* @param productRepository Product 엔티티에 대한 JPA Repository
*/
@Autowired
public ProductDAOImpl(ProductRepository productRepository) {
this.productRepository = productRepository;
}
/**
* 새로운 Product를 데이터베이스에 저장
* @param product 저장할 Product 객체
* @return 저장된 Product 객체
*/
@Override
public Product insertProduct(Product product) {
return productRepository.save(product);
}
/**
* 고유 번호를 기반으로 Product를 조회
* findById로 실제 DB에서 즉시 조회
* @param number 조회할 Product의 고유 번호
* @return 조회된 Product 객체
*/
@Override
public Product selectProduct(Long number) {
// findById는 null을 방지하기 위해 Optional을 반환하며, 존재 여부를 안전하게 확인
// -> Product를 리턴하기 위해 .get()을 붙임
return productRepository.findById(number).get();
}
/**
* 특정 Product의 이름을 업데이트(존재하지 않는 경우 예외 발생)
* @param number 업데이트할 Product의 고유 번호
* @param name 새로운 이름
* @return 업데이트된 Product 객체
* @throws Exception Product가 존재하지 않을 경우
*/
@Override
public Product updateProductName(Long number, String name) throws Exception {
// findById는 null을 방지하기 위해 Optional을 반환하며, 존재 여부를 안전하게 확인
Optional<Product> selectedProduct = productRepository.findById(number);
Product updatedProduct = null;
if (selectedProduct.isPresent()) {
Product product = selectedProduct.get();
product.setName(name);
product.setUpdatedAt(LocalDateTime.now());
updatedProduct = productRepository.save(product); // 변경사항 저장
} else {
throw new Exception("Product not found with number: " + number);
}
return updatedProduct;
}
/**
* 특정 Product를 삭제(존재하지 않는 경우 예외 발생)
* @param number 삭제할 Product의 고유 번호
* @throws Exception Product가 존재하지 않을 경우
*/
@Override
public void deleteProduct(Long number) throws Exception {
Optional<Product> selectedProduct = productRepository.findById(number);
if (selectedProduct.isPresent()) {
Product product = selectedProduct.get();
productRepository.delete(product);
} else {
throw new Exception("Product not found with number: " + number);
}
}
}
*DAO와 DAOImpl로 분리하는 이유
- 관심사의 분리 (Separation of Concerns): 비즈니스 로직과 데이터 접근 로직을 분리하여 각 책임을 명확히 함
- 유지보수 용이: 데이터베이스 변경이 생겨도 DAO만 수정하면 되므로 전체 시스템에 영향 최소화
- 테스트 용이성: DAO를 인터페이스로 정의하면, 테스트 시 Mock 객체로 대체하여 유닛 테스트가 쉬워짐
- 재사용성 증가: 여러 서비스나 컨트롤러에서 동일한 DAO를 재사용
2. 서비스와 컨트롤러 설계(클라이언트 요청과 연결하기 위함)
1) 필요한 DTO 클래스 생성
- 필요에 따라 builder 메서드와 hashCode/equals 메서드 추가 가능
Builder | 유연하고 가독성 높은 객체 생성 | 필드가 많거나, 선택적 초기화가 필요할 때 |
equals/hashCode | 객체 비교 및 해시 기반 자료구조 사용 | DTO를 Set/Map에 넣거나 비교할 때 |
(1) ProductDTO.java
// ProductDTO.java
package com.example.demo.jpa.data.dto;
/**
* 클라이언트와 서버 간의 데이터 전달을 위한 객체
* Entity와는 달리 필요한 데이터만 담아 전송하며, 비즈니스 로직이나 DB 매핑과는 분리
* 주로 Controller <-> Service <-> Client 사이에서 사용
*/
public class ProductDTO {
// 상품 이름
private String name;
// 상품 가격
private int price;
// 상품 재고 수량
private int stock;
/**
* 모든 필드를 초기화하는 생성자
* @param name 상품 이름
* @param price 상품 가격
* @param stock 상품 재고
*/
public ProductDTO(String name, int price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
// Getter 및 Setter 메서드들
/**
* 캡슐화: 내부 상태를 보호하고 제어된 접근 제공
* 유효성 검사: 잘못된 값의 설정을 방지
* 유지보수성: 내부 구현 변경 시 외부 영향 최소화
* 프레임워크: 호환 다양한 라이브러리와의 연동을 위해 필요
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public int getStock() {
return stock;
}
public void setStock(int stock) {
this.stock = stock;
}
}
(2) ProductResponseDTO.java
// ProductResponseDTO.java
package com.example.demo.jpa.data.dto;
/**
* 클라이언트에게 전달할 상품 정보를 담는 응답용 DTO
* Entity와 분리된 구조로, 필요한 정보만 선택적으로 전달하며 보안성과 유연성을 높임
*/
public class ProductResponseDTO {
// 상품 고유 번호 (ID)
private Long number;
// 상품 이름
private String name;
// 상품 가격
private int price;
// 상품 재고 수량
private int stock;
// 기본 생성자
public ProductResponseDTO() {}
/**
* 모든 필드를 초기화하는 생성자
* @param number 상품 고유 번호
* @param name 상품 이름
* @param price 상품 가격
* @param stock 상품 재고 수량
*/
public ProductResponseDTO(Long number, String name, int price, int stock) {
this.number = number;
this.name = name;
this.price = price;
this.stock = stock;
}
// Getter 및 Setter 메서드들
/**
* 캡슐화: 내부 상태를 보호하고 제어된 접근 제공
* 유효성 검사: 잘못된 값의 설정을 방지
* 유지보수성: 내부 구현 변경 시 외부 영향 최소화
* 프레임워크 호환: 다양한 라이브러리와의 연동을 위해 필요
*/
public Long getNumber() {
return number;
}
public void setNumber(Long number) {
this.number = number;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public int getStock() {
return stock;
}
public void setStock(int stock) {
this.stock = stock;
}
}
2) 서비스 클래스(서비스 레이어) 만들기
- 도메인 모델(Domain Model)을 활용하여 애플리케이션 핵심 기능 제공
- Service는 비즈니스 로직을 담당하고, DAO는 데이터 접근을 담당하며,
Controller는 사용자 요청을 받고, Service는 그 요청을 처리하며, DAO는 DB와 통신
-> 각 계층이 명확히 분리되어 있어 코드의 가독성과 유지보수성이 높아짐 - DAO와 마찬가지로 추상화해서 구성
(1) ProductService.java
// ProductService.java
package com.example.demo.service;
import com.example.demo.jpa.data.dto.ProductDTO;
import com.example.demo.jpa.data.dto.ProductResponseDTO;
/**
* 상품 관련 비즈니스 로직을 정의
* Controller와 DAO 사이에서 데이터를 가공하거나 검증하는 역할을 수행하며, 구현체(ProductServiceImpl)를 통해 실제 로직 실행
*/
public interface ProductService {
/**
* 상품 번호를 기반으로 상품 정보를 조회
* @param number 조회할 상품의 고유 번호
* @return 조회된 상품 정보를 담은 ProductResponseDTO
*/
ProductResponseDTO getProduct(Long number);
/**
* 새로운 상품 정보를 저장
* @param productDTO 저장할 상품 정보를 담은 DTO
* @return 저장된 상품 정보를 담은 ProductResponseDTO
*/
ProductResponseDTO saveProduct(ProductDTO productDTO);
/**
* 특정 상품의 이름을 변경
* @param number 변경할 상품의 고유 번호
* @param name 새로운 상품 이름
* @return 변경된 상품 정보를 담은 ProductResponseDTO
* @throws Exception 상품이 존재하지 않거나 변경 실패 시 예외 발생
*/
ProductResponseDTO changeProductName(Long number, String name) throws Exception;
/**
* 특정 상품을 삭제
* @param number 삭제할 상품의 고유 번호
* @throws Exception 상품이 존재하지 않거나 삭제 실패 시 예외 발생
*/
void deleteProduct(Long number) throws Exception;
}
(2) ProductServiceImpl.java
// ProductServiceImpl.java
package com.example.demo.service.impl;
import java.time.LocalDateTime;
import org.springframework.beans.factory.annotation.Autowired;
import com.example.demo.jpa.data.dao.ProductDAO;
import com.example.demo.jpa.data.dto.ProductDTO;
import com.example.demo.jpa.data.dto.ProductResponseDTO;
import com.example.demo.jpa.data.entity.Product;
import com.example.demo.service.ProductService;
/**
* ProductService 인터페이스의 구현 클래스
* 비즈니스 로직을 처리하며, DAO를 통해 DB와 통신하고 DTO를 통해 Controller와 데이터를 주고받음
*/
public class ProductServiceImpl implements ProductService {
// DAO를 통해 DB 접근을 수행
private final ProductDAO productDAO;
/**
* 생성자 주입 방식으로 ProductDAO를 주입
* @param productDAO 상품 데이터 접근 객체
*/
@Autowired
public ProductServiceImpl(ProductDAO productDAO) {
this.productDAO = productDAO;
}
/**
* 상품 번호를 기반으로 상품 정보를 조회하고, 응답 DTO로 변환하여 반환
* @param number 조회할 상품의 고유 번호
* @return ProductResponseDTO 조회된 상품 정보
*/
@Override
public ProductResponseDTO getProduct(Long number) {
Product product = productDAO.selectProduct(number);
ProductResponseDTO productResponseDTO = new ProductResponseDTO();
productResponseDTO.setNumber(product.getNumber());
productResponseDTO.setName(product.getName());
productResponseDTO.setPrice(product.getPrice());
// stock 필드 누락 시 추가 필요
productResponseDTO.setStock(product.getStock());
return productResponseDTO;
}
/**
* 클라이언트로부터 전달받은 DTO를 기반으로 상품을 생성하고 저장
* 저장된 상품 정보를 응답 DTO로 변환하여 반환
* @param productDTO 저장할 상품 정보
* @return ProductResponseDTO 저장된 상품 정보
*/
@Override
public ProductResponseDTO saveProduct(ProductDTO productDTO) {
Product product = new Product();
product.setName(productDTO.getName());
product.setPrice(productDTO.getPrice());
product.setStock(productDTO.getStock());
product.setCreatedAt(LocalDateTime.now());
product.setUpdatedAt(LocalDateTime.now());
Product savedProduct = productDAO.insertProduct(product);
ProductResponseDTO productResponseDTO = new ProductResponseDTO();
productResponseDTO.setNumber(savedProduct.getNumber());
productResponseDTO.setName(savedProduct.getName());
productResponseDTO.setPrice(savedProduct.getPrice());
productResponseDTO.setStock(savedProduct.getStock());
return productResponseDTO;
}
/**
* 특정 상품의 이름을 변경하고, 변경된 정보를 응답 DTO로 반환
* @param number 변경할 상품의 고유 번호
* @param name 새로운 상품 이름
* @return ProductResponseDTO 변경된 상품 정보
* @throws Exception 상품이 존재하지 않거나 변경 실패 시 예외 발생
*/
@Override
public ProductResponseDTO changeProductName(Long number, String name) throws Exception {
Product changedProduct = productDAO.updateProductName(number, name);
ProductResponseDTO productResponseDTO = new ProductResponseDTO();
productResponseDTO.setNumber(changedProduct.getNumber());
productResponseDTO.setName(changedProduct.getName());
productResponseDTO.setPrice(changedProduct.getPrice());
productResponseDTO.setStock(changedProduct.getStock());
return productResponseDTO;
}
/**
* 특정 상품을 삭제
* @param number 삭제할 상품의 고유 번호
* @throws Exception 상품이 존재하지 않거나 삭제 실패 시 예외 발생
*/
@Override
public void deleteProduct(Long number) throws Exception {
productDAO.deleteProduct(number);
}
}
3) 컨트롤러 클래스 만들기
(1) ProductController.java
// ProductController.java
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.example.demo.jpa.data.dto.ChangeProductNameDTO;
import com.example.demo.jpa.data.dto.ProductDTO;
import com.example.demo.jpa.data.dto.ProductResponseDTO;
import com.example.demo.service.ProductService;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
/**
* 상품 관련 HTTP 요청을 처리하는 REST 컨트롤러
* 클라이언트로부터 요청을 받아 Service 계층에 전달하고, 처리 결과를 ResponseEntity로 감싸서 응답
*/
@RestController // RESTful 웹 서비스 컨트롤러로 지정
@RequestMapping("/product") // 모든 요청의 기본 경로 설정
public class ProductController {
// 비즈니스 로직을 처리할 Service 객체
private final ProductService productService;
/**
* 생성자 주입 방식으로 ProductService를 주입
* @param productService 상품 서비스 객체
*/
@Autowired
public ProductController(ProductService productService) {
this.productService = productService;
}
/**
* 상품 조회 API
* GET /product?number=1
* @param number 조회할 상품의 고유 번호
* @return 상품 정보를 담은 응답 DTO
*/
@GetMapping()
public ResponseEntity<ProductResponseDTO> getProduct(Long number) {
ProductResponseDTO productResponseDTO = productService.getProduct(number);
return ResponseEntity.status(HttpStatus.OK).body(productResponseDTO);
}
/**
* 상품 생성 API
* POST /product
* @param productDTO 클라이언트가 전달한 상품 정보
* @return 저장된 상품 정보를 담은 응답 DTO
*/
@PostMapping()
public ResponseEntity<ProductResponseDTO> createProduct(@RequestBody ProductDTO productDTO) {
ProductResponseDTO productResponseDTO = productService.saveProduct(productDTO);
return ResponseEntity.status(HttpStatus.OK).body(productResponseDTO);
}
/**
* 상품 이름 변경 API
* PUT /product
* @param changeProductNameDTO 상품 번호와 새로운 이름을 담은 DTO
* @return 변경된 상품 정보를 담은 응답 DTO
* @throws Exception 상품이 존재하지 않거나 변경 실패 시 예외 발생
*/
@PutMapping()
public ResponseEntity<ProductResponseDTO> changeProductName(@RequestBody ChangeProductNameDTO changeProductNameDTO) throws Exception {
ProductResponseDTO productResponseDTO = productService.changeProductName(
changeProductNameDTO.getNumber(), changeProductNameDTO.getName());
return ResponseEntity.status(HttpStatus.OK).body(productResponseDTO);
}
/**
* 상품 삭제 API
* DELETE /product?number=1
* @param number 삭제할 상품의 고유 번호
* @return 삭제 성공 메시지
* @throws Exception 상품이 존재하지 않거나 삭제 실패 시 예외 발생
*/
@DeleteMapping()
public ResponseEntity<String> deleteProduct(Long number) throws Exception {
productService.deleteProduct(number);
return ResponseEntity.status(HttpStatus.OK).body("정상적으로 삭제되었습니다.");
}
}
(2) ChangeProductNameDTO.java
//ChangeProductNameDTO.java
package com.example.demo.jpa.data.dto;
public class ChangeProductNameDTO {
// 변경할 대상 상품의 고유 번호
private Long number;
// 새로 설정할 상품 이름
private String name;
/**
* 모든 필드를 초기화하는 생성자
* @param number 상품 고유 번호
* @param name 새로운 상품 이름
*/
public ChangeProductNameDTO(Long number, String name) {
this.number = number;
this.name = name;
}
/**
* 기본 생성자 (직렬화/역직렬화 및 프레임워크 사용을 위해 필요)
*/
public ChangeProductNameDTO() {}
// Getter 및 Setter 메서드들
/**
* 캡슐화: 내부 상태를 보호하고 제어된 접근 제공
* 유효성 검사: 잘못된 값의 설정을 방지
* 유지보수성: 내부 구현 변경 시 외부 영향 최소화
* 프레임워크: 호환 다양한 라이브러리와의 연동을 위해 필요
*/
public Long getNumber() {
return number;
}
public void setNumber(Long number) {
this.number = number;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
*Service와 ServiceImpl로 분리하는 이유
- 인터페이스 기반 설계로 유연성 확보: Service는 인터페이스로 구현체(ServiceImpl)와 분리되어 있으며,
구현체를 쉽게 교체하면서 다형성 활용 가능 - 테스트 용이성: 단위 테스트 시 실제 구현체 대신 Service를 Mockito로 mocking하여 DB 접근 없이 테스트
- 확장성과 유지보수성: 인터페이스를 사용하여 새로운 기능을 추가하거나 변경할 때 기존 코드에 영향을 최소화
4) Swagger API로 동작 확인
(1) Swagger API 추가 방법
스프링 부트 핵심 가이드(장정우 지음) - REST API 명세를 문서화하는 방법(Swagger), 로깅 라이브러리
*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성 Windows11(윈도우 11) 환경자바 JDK 17 버전 설치 https://yungenie.tistory.com/11 [Java] 차근차근 Java 설치하기 (JDK17, Window 11)자바 개발 도구 설치 방법
keep-programming-study.tistory.com
(2) localhost8080/swagger-ui/index.html로 접속해보기 -> 오류발생...
Error starting ApplicationContext. To display the condition evaluation report re-run your
application with 'debug' enabled.
2025-07-23 14:28:32.939 [main] ERROR o.s.boot.SpringApplication - Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name
'entityManagerFactory' defined in class path resource
[org/springframework/boot/autoconfigure
/orm/jpa/HibernateJpaConfiguration.class]: [PersistenceUnit: default] Unable to build
Hibernate SessionFactory; nested exception is org.hibernate.exception.JDBCConnectionException:
Unable to open JDBC Connection for DDL execution
[Communications link failure]
- Spring Boot 애플리케이션이 데이터베이스에 연결하지 못해서 ApplicationContext를 시작하지 못했다는 뜻으로,
보통 '서비스'에서 MySQL8을 우클릭해서 '시작'을 누르면 해결됨
- 또 다른 문제 발생
-> ProductServiceImpl에 @Service 어노테이션을 붙이는 걸 깜빡함
2025-07-23 14:36:15.379 [main] INFO o.s.b.a.l.ConditionEvaluationReportLogger -
Error starting ApplicationContext.
To display the condition evaluation report re-run your application with 'debug' enabled.
2025-07-23 14:36:15.402 [main] ERROR o.s.b.d.LoggingFailureAnalysisReporter -
*************************** APPLICATION FAILED TO START ***************************
Description: Parameter 0 of constructor in com.example.demo.controller.ProductController
required a bean of type 'com.example.demo.service.ProductService' that could not be found.
Action: Consider defining a bean of type 'com.example.demo.service.ProductService'
in your configuration.
- 서버 재시작후 드디어 성공!!
(3) 이번에는 localhost8080/swagger-ui/index.html에서 POST API 동작 오류...
2025-07-23 14:43:28.659 [http-nio-8080-exec-1] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet]
- Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
[Request processing failed: java.lang.IllegalStateException: Cannot resolve parameter names
for constructor public
com.example.demo.jpa.data.dto.ProductDTO(java.lang.String,int,int)] with root cause
java.lang.IllegalStateException: Cannot resolve parameter names for constructor public
com.example.demo.jpa.data.dto.ProductDTO(java.lang.String,int,int)
at org.springframework.util.Assert.state(Assert.java:101)
- Spring은 JSON을 Java 객체로 변환할 때, 생성자의 파라미터 이름을 알아야 매핑할 수 있음
-> Java는 기본적으로 런타임에 파라미터 이름 정보를 유지하지 않기 때문에, 이 정보를 알 수 없어 바인딩 실패- ProductDTO에 기본 생성자를 추가해야 함
- 그러나 또 오류 발생.. Spring Boot 애플리케이션에서 ProductDTO를 받아 Product 엔티티로 변환할 때 name 필드가 null
=> Hibernate가 null을 DB에 넣으려다 실패- 유효성 검사 어노테이션 @Valid를 컨트롤러에 추가
Hibernate: insert into product (created_at,name,price,stock,updated_at) values (?,?,?,?,?)
2025-07-23 14:58:18.119 [http-nio-8080-exec-8] WARN o.h.e.jdbc.spi.SqlExceptionHelper - SQL Error: 1048, SQLState: 23000
2025-07-23 14:58:18.119 [http-nio-8080-exec-8] ERROR o.h.e.jdbc.spi.SqlExceptionHelper - Column 'name' cannot be null
2025-07-23 14:58:18.152 [http-nio-8080-exec-8] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.DataIntegrityViolationException: could not execute statement [Column 'name' cannot be null] [insert into product (created_at,name,price,stock,updated_at) values (?,?,?,?,?)]; SQL [insert into product (created_at,name,price,stock,updated_at) values (?,?,?,?,?)]; constraint [null]] with root cause
java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:109)
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:114)
at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:990)
at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1168)
at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1103)
at com.mysql.cj.jdbc.ClientPreparedStatement.executeLargeUpdate(ClientPreparedStatement.java:1450)
at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdate(ClientPreparedStatement.java:1086)
at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61)
at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:194)
at org.hibernate.id.insert.GetGeneratedKeysDelegate.performMutation(GetGeneratedKeysDelegate.java:116)
at org.hibernate.engine.jdbc.mutation.internal.MutationExecutorSingleNonBatched.performNonBatchedOperations(MutationExecutorSingleNonBatched.java:47)
at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.execute(AbstractMutationExecutor.java:55)
at org.hibernate.persister.entity.mutation.InsertCoordinatorStandard.doStaticInserts(InsertCoordinatorStandard.java:194)
at org.hibernate.persister.entity.mutation.InsertCoordinatorStandard.coordinateInsert(InsertCoordinatorStandard.java:132)
at org.hibernate.persister.entity.mutation.InsertCoordinatorStandard.insert(InsertCoordinatorStandard.java:95)
at org.hibernate.action.internal.EntityIdentityInsertAction.execute(EntityIdentityInsertAction.java:85)
at org.hibernate.engine.spi.ActionQueue.execute(ActionQueue.java:682)
at org.hibernate.engine.spi.ActionQueue.addResolvedEntityInsertAction(ActionQueue.java:293)
at org.hibernate.engine.spi.ActionQueue.addInsertAction(ActionQueue.java:274)
at org.hibernate.engine.spi.ActionQueue.addAction(ActionQueue.java:324)
- 여전히 상품 이름이 찍히지 않는 문제 발생...
-> @Valid를 통해 ProductDTO를 검증했으나, name 필드가 null이라서 @NotBlank 조건을 만족하지 못해 검증 실패- System.out.println()을 ProductServiceImpl.java의 saveProduct 메서드에 넣고,
ProductDTO와 ProductResponseDTO에 필드의 값을 문자열로 조합해서 리턴하는 toString()을 추가하여
콘솔창에 찍히는지 확인하면서 원인 파악
- System.out.println()을 ProductServiceImpl.java의 saveProduct 메서드에 넣고,
2025-07-23 15:42:10.388 [http-nio-8080-exec-9] WARN
o.s.w.s.m.s.DefaultHandlerExceptionResolver -
Resolved [org.springframework.web.bind.MethodArgumentNotValidException:
Validation failed for argument [0] in public
org.springframework.http.ResponseEntity<com.example.demo.jpa.data.dto.ProductResponseDTO>
com.example.demo.controller.ProductController.createProduct
(com.example.demo.jpa.data.dto.ProductDTO):
[Field error in object 'productDTO' on field 'name': rejected value [null];
codes [NotBlank.productDTO.name,NotBlank.name,NotBlank.java.lang.String,NotBlank];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
codes [productDTO.name,name]; arguments []; default message [name]];
default message [상품 이름은 필수입니다.]] ]
- 콘솔창에 아무것도 안찍힘..!
-> createProduct() 메서드 호출 전에 Spring이 productDTO.name이 null임을 감지하고, 예외를 던져 메서드 실행을 막은 것- 예외 메시지를 Swagger에 표시하는 클래스를 추가하기...
package com.example.demo;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.stream.Collectors;
/**
* 애플리케이션 전역에서 발생하는 예외를 처리하는 클래스
* 특히 유효성 검사 실패(MethodArgumentNotValidException)에 대한 응답을 커스터마이징하여
* 클라이언트에게 명확한 오류 메시지를 전달합
*/
@RestControllerAdvice // 모든 컨트롤러에 적용되는 전역 예외 처리기
public class GlobalExceptionHandler {
/**
* 유효성 검사 실패 예외를 처리합니다.
* @param ex MethodArgumentNotValidException: @Valid 검증 실패 시 발생하는 예외
* @return ResponseEntity<String>: 사용자에게 전달할 오류 메시지를 포함한 400 응답
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException ex) {
// 필드 오류들을 스트림으로 처리하여 "필드명: 메시지" 형식으로 연결
String errorMessage = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
// 400 Bad Request 응답과 함께 오류 메시지 반환
return ResponseEntity.badRequest().body("입력 오류: " + errorMessage);
}
}
- 아예 Swagger 페이지에서 입력한 값이 인식이 안 되는거였다..
-> 진짜 원인: IntelliJ IDEA에서는 기본적으로 -parameters 옵션을 자동으로 설정해주는데, STS에서는 그렇지 않기 때문!!
- Jackson은 JSON을 Java 객체로 바꿀 때 "name"이라는 JSON 필드를 생성자의 name 파라미터에 넣으려 하지만,
Java가 파라미터 이름을 버렸기 때문에, Jackson은 "name"이 arg0인지 arg1인지 알 수 없어지기 때문에
-parameters 옵션이 필요함
- Jackson은 JSON을 Java 객체로 바꿀 때 "name"이라는 JSON 필드를 생성자의 name 파라미터에 넣으려 하지만,
- build.gradle에 -parameters 옵션 추가(저장 후엔 우클릭해서 Gradle -> Refresh Gradle Project 잊지 않기!!)
-> 그래도 안됨...
tasks.withType(JavaCompile) {
options.compilerArgs << "-parameters"
}
- cmd에서 프로젝트 루트로 이동한 후 javap -p 명령어로 -parameters 옵션이 적용되었는지 확인
-> 아래 이미지를 보면 생성자의 파라미터 이름이 name, price, stock이 아니라 그냥 타입 정보만 있고 이름이 없으며,
이는 -parameters 옵션이 실제로 반영되지 않았다는 뜻
=> CMD에서 curl -X POST http://localhost:8080/product -H "Content-Type: application/json" -d "{\"name\": \"노트북\", \"price\": 1500000, \"stock\": 10}" 명령어로 직접 시도해봤는데도 X
- 진짜 마지막 시도.... ProductDTO 코드 수정
-> 안됨... 콘솔창엔 아예 아무것도 안찍힘
@JsonCreator Jackson에게 “이 생성자를 쓰라”고 알려줌 @JsonProperty("name") JSON 필드 "name"을 해당 파라미터에 매핑
- 진짜진짜 마지막으로... 컨트롤러 코드를 테스트용 최소로만 구성해보기!!
-> 성공! 컨트롤러 호출 자체는 잘 됨
(4) 여전히 localhost8080/swagger-ui/index.html에서 POST API 동작 오류...
-> 요청은 들어오지만 JSON->DTO 바인딩이 실패하고 있음.. DTO 구조나 Jackson 설정이 꼬이면 발생할 수 있는 문제인 것이 확실... 컨트롤러 DTO: ProductDTO [name=null, price=0, stock=0]로 찍힘..
- 다음 글에 이어서 진행함