스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초

스프링 부트 핵심 가이드(장정우 지음) - 데이터베이스 연동 3: DAO 설계, 서비스와 컨트롤러 설계

개발학생 2025. 7. 23. 17:41
반응형

****수많은 문제들이 발생해서 업로드가 매우 늦어졌습니다.... 마지막 문제는 아직도 해결못했기때문에 다음 글로 이어집니다... 

*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성  

 

[Java] 차근차근 Java 설치하기 (JDK17, Window 11)

자바 개발 도구 설치 방법에 대해서 알아보겠습니다. Java17은 LTS(Long Term Support : 장기 지원) 릴리즈로 1년 후까지 기술 지원 및 버그를 개선한 서비스를 제공받을 수 있습니다. 업데이트 버전을 꾸

yungenie.tistory.com

 

[Windows] Spring Tool Suite 4(STS 4) 다운로드 및 설치

STS란?Spring Tool Suite(STS)는 스프링 프로젝트를 생성하고, 개발할 수 있게 해주는 도구입니다. STS 설치 과정에 대해 설명드리겠습니다. 설치 파일 다운로드STS 공식 사이트에서 설치 파일을 다운로

priming.tistory.com

 

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 프로젝트 생성한 과정 

*** 함께 보면 좋은 글

2025.07.10 - [스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초] - 스프링 부트 핵심 가이드(장정우 지음) - 스프링 부트 개요

 

스프링 부트 핵심 가이드(장정우 지음) - 스프링 부트 개요

1. 스프링 프레임워크자바(Java) 기반 애플리케이션 프레임워크로, 엔터프라이즈급(기업 환경 대상 개발) 애플리케이션을 위한 다양한 기능 제공-> 오픈소스 경량급 애플리케이션 프레임워크로

keep-programming-study.tistory.com

2025.07.11 - [스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초] - 스프링 부트 핵심 가이드(장정우 지음) - 개발에 앞서 알면 좋은 기초 지식

 

스프링 부트 핵심 가이드(장정우 지음) - 개발에 앞서 알면 좋은 기초 지식

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

2023.06.27 - [자바(JAVA)/JSP 웹 프로그래밍 공부 (성낙현의 JSP 자바 웹 프로그래밍 참고)] - JAVA/JSP 14. 데이터베이스 - 특징, 오라클 설치(Oracle Database 11gR2 Express Edition), 사용자 계정 생성 및 권한 설정

 

JAVA/JSP 14. 데이터베이스 - 특징, 오라클 설치(Oracle Database 11gR2 Express Edition), 사용자 계정 생성 및

1. 데이터베이스의 특징우리가 매일 PC나 스마트폰을 통해 접하는 거의 모든 웹 애플리케이션에서 사용함매일 업데이트되는 뉴스나 날씨 등의 정보는 데이터베이스가 없다면 클라이언트에 전달

keep-programming-study.tistory.com

2025.07.18 - [스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초] - 스프링 부트 핵심 가이드(장정우 지음) - 데이터베이스 연동 1: ORM(Object Relational Mapping) 특징, JPA(Java Persistence API)

 

스프링 부트 핵심 가이드(장정우 지음) - 데이터베이스 연동 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

2025.07.21 - [스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초] - 스프링 부트 핵심 가이드(장정우 지음) - 데이터베이스 연동 2: 프로젝트에 MySQL Community Server 연동, 엔티티 설계 & 리포지토리 인터페이스 설계

 

스프링 부트 핵심 가이드(장정우 지음) - 데이터베이스 연동 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나 엔티티를 사용하지 않기 함

출처-https://djcho.github.io/springboot/spring-boot-chapter6-10/

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 추가 방법 

2025.07.16 - [스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초] - 스프링 부트 핵심 가이드(장정우 지음) - REST API 명세를 문서화하는 방법(Swagger), 로깅 라이브러리(Logback)

 

스프링 부트 핵심 가이드(장정우 지음) - 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을 우클릭해서 '시작'을 누르면 해결됨 

서비스에 들어가서 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.

ProductServiceImpl에 @Service 어노테이션 추가

  • 서버 재시작후 드디어 성공!!

MySQL의 로컬 데이터베이스에 product 테이블 생성됨
localhost8080/swagger-ui/index.html

(3) 이번에는 localhost8080/swagger-ui/index.html에서 POST API 동작 오류...

POST API 500 오류(Internal Server Error)

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에 기본 생성자를 추가해야 함 

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를 컨트롤러에 추가

  •  여전히 상품 이름이 찍히지 않는 문제 발생...
    -> @Valid를 통해 ProductDTO를 검증했으나, name 필드가 null이라서 @NotBlank 조건을 만족하지 못해 검증 실패
    • System.out.println()을 ProductServiceImpl.java의 saveProduct 메서드에 넣고, 
      ProductDTO와 ProductResponseDTO에 필드의 값을 문자열로 조합해서 리턴하는 toString()을 추가하여
      콘솔창에 찍히는지 확인하면서 원인 파악 
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 [상품 이름은 필수입니다.]] ]

System.out.println()을 ProductServiceImpl.java의 saveProduct 메서드에 넣기
STS에서 Source->Generate toString()을 클릭
ProductDTO에 toString()추가
ProductResponseDTO에 toString() 추가

  •  콘솔창에 아무것도 안찍힘..! 
    -> createProduct() 메서드 호출 전에 Spring이 productDTO.namenull임을 감지하고, 예외를 던져 메서드 실행을 막은 것
    • 예외 메시지를 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 옵션이 필요함

Swagger 페이지에서 입력한 값이 인식이 안 되는 모습

  •  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"을 해당 파라미터에 매핑

ProductDTO 코드 수정
콘솔창엔 아예 아무것도 안찍히는 모습

  •  진짜진짜 마지막으로... 컨트롤러 코드를 테스트용 최소로만 구성해보기!! 
    -> 성공! 컨트롤러 호출 자체는 잘 됨

(4) 여전히 localhost8080/swagger-ui/index.html에서 POST API 동작 오류...
-> 요청은 들어오지만 JSON->DTO 바인딩이 실패하고 있음.. DTO 구조나 Jackson 설정이 꼬이면 발생할 수 있는 문제인 것이 확실... 컨트롤러 DTO: ProductDTO [name=null, price=0, stock=0]로 찍힘.. 

  • 다음 글에 이어서 진행함 

 

 

반응형