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

스프링부트 핵심 가이드(장정우 지음) - 예외 처리

개발학생 2025. 11. 26. 16:08
반응형

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

 

[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

 

 

  • 애플리케이션을 개발할 때는 불가피하게 많은 오류가 발생하게 됨
  • 자바에서는 try/catch, throw 구문을 활용해 예외 처리
  • 스프링 부트에서는 자바보다 더욱 편리하게 예외 처리를 할 수 있는 기능 제공

1. 예외와 에러

1) 예외(exception)

  • 입력 값 처리가 불가능하거나 참조된 값이 잘못된 경우 등, 애플리케이션이 정상적으로 동작하지 못하는 상황
  • 개발자가 미리 코드 설계를 통해 직접 처리할 수 있음

2) 에러(error)

  • 메모리 부족(OutOfMemory)과 스택 오버플로우(StackOverFlow) 등,
    미리 애플리케이션의 코드를 살펴보며 문제가 발생하지 않도록 예방해야 하는 문제
  • 자바의 가상머신에서 발생시키는 것으로, 애플리케이션 코드에서 처리할 수 있는 것이 거의 없음

2. 예외 클래스

자바 예외 클래스의 상속 구조

  • 자바의 모든 예외 클래스는 Throwable 클래스를 상속받음
  • 자바의 예외 클래스 중 Exception 클래스는 다양한 자식 클래스를 갖고 있으며,
    Cheched Exception과 Unchecked Exception으로 구분할 수 있음
  Checked Exception Unchecked Exception
예외 처리 여부 반드시 예외 처리 필요 명시적으로 예외 처리를 강제하지 않음
확인 가능 시점 컴파일 단계 실행 중 단계
대표적인 예외 클래스 IOException
SQLException
RuntimeException
NullPointerException
IllegalArgumentException
IndexOutOfBoundException
SystemException
RuntimeException 상속 여부 상속받지 않음 상속받음

 

3. 예외 처리 방법

1) 예외 복구

  • 예외 상황을 파악하여 문제를 해결하는 방식
  • try/catch 구문이 대표적인 방법:
    try 블록에는 예외가 발생할 수 있는 코드를 작성하고, catch 블록(여러 개 작성 가능)에는 try 블록에서 발생하는 예외 상황을 처리하는 내용 작성
    -> 예외 상황이 발생하면 애플리케이션에서 여러 개의 catch 블록을 순차적으로 거치며,
        예외 유형과 매칭되는 블록을 찾아 예외 처리 동작 수행
int a = 1;
String b = "a";

try {
  System.out.println(a + Integer.parseInt(b));
} catch (NumberFormatException e) {
  b = "2";
  System.out.println(a + Integer.parseInt(b));
}

2) 예외 처리 회피

  • 예외가 발생한 시점에서 바로 처리하는 것이 아니라, 예외가 발생한 메서드를 호출한 곳에서 에러 처리할 수 있게 전가
  • throw 키워드로 어떤 예외가 발생했는지 호출부에 내용을 전달
int a = 1;
String b = "a";

try {
  System.out.println(a + Integer.parseInt(b));
} catch (NumberFormatException e) {
  throw new NumberFormatException("숫자가 아닙니다.");
}

3) 예외 전환

  • 앞의 두 방식을 적절하게 섞은 것으로, 커스텀 예외를 만드는 과정에서 사용
  • try/catch 방식을 사용하면서 catch 블록에서 throw 키워드로 다른 예외 타입으로 전달

4. 스프링 부트의 예외 처리 방식 

  • 클라이언트에게 어떤 문제가 발생했는지 상황을 전달하는 방식
    -> 각 레이어에서 발생한 예외를 엔드포인트 레벨인 컨트롤러로 전달
  • @(Rest)ControllerAdvice와 @ExceptionHandler 어노테이션을 통해 모든 컨트롤러의 예외를 처리하거나,
    @ExceptionHandler를 통해 특정 컨트롤러의 예외만 처리

1) 예외 타입 레벨에 따른 예외 처리 우선순위

  • @ControllerAdvice 클래스 내에 동일하게 핸들러 메서드가 선언된 상태에서
    Exception 클래스와 그보다 좀 더 구체적인 NullPointerException.class)가 각각 선언된 경우에는,
    구체적인 클래스가 지정된 쪽이 우선순위를 가짐 

2) 핸들러 위치에 따른 예외 처리 우선순위

  • @ControllerAdvice의 글로벌 예외 처리와 @Controller 내의 컨트롤러 예외 처리에 동일한 타입의 예외 처리를 하면,
    범위가 좁은 컨트롤러의 핸들러 메서드가 우선순위를 갖게 됨 

5. 커스텀 예외 클래스 생성 및 활용 

  • 표준 예외에서 제공하는 클래스는 해당 예외 타입의 이름만으로 이해하기 어려운 경우가 많기에,
    사용 시 반드시 예외 메시지를 상세히 작성해야 함
  • 표준 예외를 상속받은 커스텀 예외를 사용하면,
    애플리케이션에서 발생하는 예외를 개발자가 직접 코드로 관리하기 때문에 책임 소재를 애플리케이션으로 가져와,
    동일한 예외 상황이 발생할 경우 한 곳에서 처리하며 특정 상황에 맞는 예외 코드를 적용할 수 있음
  • 이 예제에서는 클래스의 구조적인 설계를 통한 예외 클래스 생성 방법을 알아보는 것이 목적이므로, 클래스명도 Exception으로 함

*커스텀 예외 클래스 구조

1) Throwable을 상속받는 Exception 클래스 생성

package com.example.demo; 

// 사용자 정의 Exception 클래스 (주의: java.lang.Exception과 이름이 같아서 혼동될 수 있음)
public class Exception extends Throwable {
    
    // 직렬화(serialization)를 위한 고유 ID 값
    // 객체를 직렬화/역직렬화할 때 클래스 버전이 맞는지 확인하는 용도
    static final long serialVersionUID = -3387516993124229948L;
    
    // 기본 생성자: 메시지나 원인 없이 예외 객체 생성
    public Exception() {
        super(); // 부모 클래스(Throwable)의 기본 생성자 호출
    }
    
    // 메시지를 받아서 예외 객체 생성
    public Exception(String message) {
        super(message); // 부모 클래스에 메시지 전달
    }
    
    // 메시지와 원인(cause)을 함께 받아서 예외 객체 생성
    public Exception(String message, Throwable cause) {
        super(message, cause); // 부모 클래스에 메시지와 원인 전달
    }
    
    // 원인(cause)만 받아서 예외 객체 생성
    public Exception(Throwable cause) {
        super(cause); // 부모 클래스에 원인 전달
    }
    
    // 고급 생성자: 메시지, 원인, suppression 여부, stack trace 기록 여부까지 설정 가능
    // suppression: 예외를 억제할 수 있는지 여부
    // writableStackTrace: 스택 트레이스를 기록할지 여부
    protected Exception(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

 

2)  Constants 클래스 생성 - 도메인 레벨 표현을 위한 열거형을 생성

package com.example.demo;

public class Constants {
    
    // enum(열거형) 정의: 예외 클래스 이름을 관리하기 위한 열거형
    public enum ExceptionClass {
        // 열거형 상수 선언: PRODUCT 라는 예외 클래스 타입을 정의
        // 괄호 안의 "Product"는 생성자에 전달되는 값
        PRODUCT("Product");
        
        // 열거형이 내부적으로 가질 문자열 값
        private String exceptionClass;
        
        // enum 생성자: 각 상수에 문자열 값을 연결
        // 예: PRODUCT → "Product"
        ExceptionClass(String exceptionClass) {
            this.exceptionClass = exceptionClass;
        }
        
        // getter 메서드: enum에 저장된 문자열 값을 반환
        public String getExceptionClass() {
            return exceptionClass;
        }
        
        // toString() 메서드 오버라이드: enum을 문자열로 표현할 때의 형식 지정
        // 예: PRODUCT → "ProductException."
        @Override
        public String toString() {
            return getExceptionClass() + "Exception.";
        }
    }
}

 

3) 커스텀 예외 클래스 생성

package com.example.demo;

//Spring Framework에서 제공하는 HttpStatus 열거형을 사용하기 위해 import
//(예: 404 NOT_FOUND, 500 INTERNAL_SERVER_ERROR 등)
import org.springframework.http.HttpStatus;


public class CustomException extends Exception {
    // Constants 클래스 안에 정의된 ExceptionClass enum을 참조
    // 예외의 종류(예: ProductException)를 나타냄
    private Constants.ExceptionClass exceptionClass;
    
    // HTTP 상태 코드 (예: 404, 500 등)를 저장
    private HttpStatus httpStatus;
    
    // 생성자: 예외 클래스, HTTP 상태, 메시지를 받아서 예외 객체 생성
    public CustomException(Constants.ExceptionClass exceptionClass, HttpStatus httpStatus, String message) {
        // 부모 클래스(Exception)의 생성자를 호출하면서
        // "ProductException. + message" 형태로 메시지를 전달
        super(exceptionClass.toString() + message);
        
        // 전달받은 예외 클래스와 HTTP 상태를 필드에 저장
        this.exceptionClass = exceptionClass;
        this.httpStatus = httpStatus;
    }
    
    // 예외 클래스(enum)를 반환하는 getter
    public Constants.ExceptionClass getExceptionClass() {
        return exceptionClass;
    }
    
    // HTTP 상태 코드 숫자값을 반환 (예: 404, 500)
    public int getHttpStatusCode() {
        return httpStatus.value();
    }
    
    // HTTP 상태 코드의 설명 문자열을 반환 (예: "Not Found", "Internal Server Error")
    public String getHttpStatusType() {
        return httpStatus.getReasonPhrase();
    }
    
    // HttpStatus 객체 자체를 반환하는 getter
    public HttpStatus getHttpStatus() {
        return httpStatus;
    }
}

 

4) GlobalExceptionHandler 클래스에 CustomException에 대한 예외 처리 코드 추가

package com.example.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
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 jakarta.servlet.http.HttpServletRequest;

import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 애플리케이션 전역에서 발생하는 예외를 처리하는 클래스
 * 특히 유효성 검사 실패(MethodArgumentNotValidException)에 대한 응답을 커스터마이징하여
 * 클라이언트에게 명확한 오류 메시지를 전달합
 */
@RestControllerAdvice // 모든 컨트롤러에 적용되는 전역 예외 처리기
public class GlobalExceptionHandler {


	// 로거(Logger) 선언: 로그를 남기기 위해 SLF4J Logger 사용
	private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
	
    ... 생략 ...
    
    /**
     * CustomException을 처리
     * @param e CustomException: 사용자 정의 예외
     * @param request HttpServletRequest: 요청 정보(URI 등)를 가져오기 위해 사용
     * @return ResponseEntity<Map<String, String>>: 예외 정보를 담은 JSON 응답
     */
    @ExceptionHandler(value=CustomException.class)
    public ResponseEntity<Map<String, String>> handleException(CustomException e, HttpServletRequest request) {
        // 응답 헤더 객체 생성
        HttpHeaders responseHeaders = new HttpHeaders();
        
        // 예외 발생 시 로그 기록 (URI와 메시지 출력)
        LOGGER.error("Advice 내 handleException 호출, {}, {}", request.getRequestURI(), e.getMessage());
        
        // 응답 본문에 담을 Map 생성
        Map<String, String> map = new HashMap<>();
        map.put("error type", e.getHttpStatusType()); // 예외 타입 (예: "Bad Request")
        map.put("code", Integer.toString(e.getHttpStatusCode())); // HTTP 상태 코드 숫자 (예: 400)
        map.put("message", e.getMessage()); // 예외 메시지
        
        // ResponseEntity로 JSON 응답 반환 (헤더 + 본문 + 상태코드)
        return new ResponseEntity<>(map, responseHeaders, e.getHttpStatus());
    }
}

5) CustomException을 발생시키는 컨트롤러 메서드 추가

package com.example.demo.controller;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.Constants.ExceptionClass;
import com.example.demo.CustomException;

/**
 * 예외 발생을 테스트하기 위한 컨트롤러 클래스
 * - @RestController: REST API 응답을 반환하는 컨트롤러임을 나타냄
 */
@RestController
public class ExceptionController {
    
    /**
     * /custom 엔드포인트를 GET 요청으로 호출했을 때 실행되는 메서드
     * - 항상 CustomException을 강제로 발생시킴
     * - 예외는 GlobalExceptionHandler에서 처리되어 클라이언트에게 JSON 응답으로 반환됨
     *
     * @throws CustomException 사용자 정의 예외
     */
    @GetMapping("/custom") // GET 요청을 "/custom" 경로에 매핑
    public void getCustomException() throws CustomException {
        // CustomException을 강제로 발생시킴
        // ExceptionClass.PRODUCT → "ProductException."
        // HttpStatus.BAD_REQUEST → 400 상태 코드
        // 메시지 → "getCustomException 메서드 호출"
        throw new CustomException(ExceptionClass.PRODUCT, HttpStatus.BAD_REQUEST, "getCustomException 메서드 호출");
    }
 }

 

6) http://localhost:8080/swagger-ui/index.html에 접속하여 결과 확인

  • 서버 재시작 후 엄청난 오류가 발생하여 다음 글에서 해결하겠습니다...

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.

2025-11-26 16:06:50.724 [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]: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1826)

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:607)

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)

at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)

at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)

at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)

at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:207)

at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:970)

at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:627)

at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)

at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752)

at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439)

at org.springframework.boot.SpringApplication.run(SpringApplication.java:318)

at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361)

at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350)

at com.example.demo.StudyApplication.main(StudyApplication.java:11)

Caused by: org.hibernate.service.spi.ServiceException: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)

at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:276)

at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:238)

at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:215)

at org.hibernate.boot.model.relational.Database.<init>(Database.java:45)

at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.getDatabase(InFlightMetadataCollectorImpl.java:226)

at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.<init>(InFlightMetadataCollectorImpl.java:194)

at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:171)

at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1442)

at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1513)

at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:66)

at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:390)

at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:419)

at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:400)

at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:366)

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1873)

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1822)

... 15 common frames omitted

Caused by: org.hibernate.HibernateException: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)

at org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl.determineDialect(DialectFactoryImpl.java:191)

at org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl.buildDialect(DialectFactoryImpl.java:87)

at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentWithDefaults(JdbcEnvironmentInitiator.java:186)

at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentUsingJdbcMetadata(JdbcEnvironmentInitiator.java:408)

at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:129)

at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:81)

at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:130)

at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:263)

... 30 common frames omitted

 

 

반응형