Post

이주헌님 포트폴리오 4주차 작업 내용 피드백 (2)

메가스터디IT아카데미 SpringBoot 백엔드 개발자 과정 주말반 (2025.02.22 ~ 2025.09.13). 이주헌님의 포트폴리오 4주차 작업 내용에 대한 피드백

이주헌님 포트폴리오 4주차 작업 내용 피드백 (2)

이주헌님 포트폴리오 4주차 작업 내용 피드백 (2)

작성일: 2025-09-13
주헌님의 요청에 따라 “예외 처리(Exception Handling) 중심 상세 분석 리포트”를 추가 작성했습니다. 전반적으로 수업 내용이 잘 반영된 코드 입니다. 개선사항은 3. 종합적인 개선 제안 (Overall Improvement Suggestions) 섹션을 참고하세요. 마지막 피드백이니만큼 개선사항에는 수업 내용보다 한 단계 더 나아간 제안도 포함되어 있습니다.

1. 종합 요약: 체계적인 예외 처리 시스템의 구축

최근 4일간 이루어진 가장 중요한 아키텍처 개선은 전역적이고 일관된 예외 처리 시스템을 구축한 것입니다. 이 변화는 애플리케이션의 안정성과 유지보수성을 비약적으로 향상시키는 핵심적인 단계입니다.

  • 변경 전 (Before):
    • 예외 처리가 각 Controller 메서드 내에 try-catch 블록으로 흩어져 있었습니다.
    • 데이터 조회 실패 시 null을 반환하거나, RuntimeException 또는 ResponseStatusException과 같은 일반적인 예외를 직접 던져 처리했습니다.
    • 이 방식은 코드 중복을 유발하고, API가 반환하는 오류 응답 형식이 일관되지 않을 수 있으며, 각 계층(Controller, Service)의 책임이 불분명해지는 문제를 야기합니다.
  • 변경 후 (After):
    1. @RestControllerAdvice를 사용한 전역 예외 핸들러(MyRestExceptionHandler)를 도입하여 애플리케이션 전역에서 발생하는 예외를 한 곳에서 중앙 관리합니다.
    2. DataNotFoundException과 같은 의미 있는 커스텀 예외(Semantic Custom Exception)를 정의하여, 비즈니스 로직 상의 특정 오류 상황을 명확하게 표현합니다.
    3. Service 계층은 데이터 조회 실패와 같은 비즈니스 규칙 위반 시 null 대신 커스텀 예외를 던지는 책임을 갖게 되었습니다.
    4. Controller 계층은 더 이상 예외 처리 로직을 직접 포함하지 않고, Service 계층 호출과 View 반환이라는 본연의 역할에만 집중하게 되어 코드가 매우 간결해졌습니다.

이러한 변화는 관심사의 분리(Separation of Concerns) 원칙을 성공적으로 적용한 사례로, 각 컴포넌트가 자신의 역할에만 충실하도록 만들어 전체 시스템의 복잡도를 낮추고 예측 가능성을 높였습니다.


2. 예외 처리 관련 상세 분석

주제 1: 전역 예외 처리기의 도입 (MyRestExceptionHandler.java)

애플리케이션의 모든 Exception을 가로채 일관된 응답을 생성하는 “관제탑” 역할을 하는 컴포넌트가 도입되었습니다.

  • 파일명: src/main/java/com/example/adv/MyRestExceptionHandler.java
  • 역할 및 목적: @RestControllerAdvice 어노테이션을 통해 모든 @RestController에서 발생하는 예외를 감지하고 처리합니다. 이를 통해 API 사용자(프론트엔드)는 항상 일관된 형식의 오류 메시지를 받게 되어 안정적인 연동이 가능해집니다.
  • 핵심 구현 상세:
    1. 예외-HTTP 상태 코드 매핑: EXCEPTION_STATUS_MAP은 다양한 표준 예외 클래스 이름을 키로, 대응하는 HttpStatus를 값으로 가집니다. 예를 들어, 잘못된 인자(IllegalArgumentException)가 들어오면 400 Bad Request를 반환하도록 미리 정의해두었습니다. 이는 매우 효율적이고 확장 가능한 패턴입니다.

      • 소스코드 인용 (Line 19-21):
        1
        2
        3
        4
        5
        
        private static final Map<String, HttpStatus> EXCEPTION_STATUS_MAP = new HashMap<>();
        static {
            EXCEPTION_STATUS_MAP.put("IllegalArgumentException", HttpStatus.BAD_REQUEST);
            // ... (기타 예외 매핑)
        }
        
    2. 중앙 핸들러 메서드: myExceptionHandler 메서드는 @ExceptionHandler(Exception.class)를 통해 모든 예외의 최상위 클래스를 처리 대상으로 지정합니다. 발생한 예외가 우리가 직접 만든 MyException 타입이면 해당 예외에 정의된 상태 코드를 사용하고, 그렇지 않으면 EXCEPTION_STATUS_MAP에서 찾아 사용하며, 둘 다 아니면 기본값인 500 Internal Server Error를 사용합니다.

      • 소스코드 인용 (Line 70-76):
        1
        2
        3
        4
        5
        6
        7
        8
        
        if (e instanceof MyException myException) {
            status = myException.getStatus().value();
        } else {
            status = EXCEPTION_STATUS_MAP.getOrDefault(
                    e.getClass().getSimpleName(),
                    HttpStatus.INTERNAL_SERVER_ERROR
            ).value();
        }
        
  • 잘된 부분:
    • AOP(관점 지향 프로그래밍)의 훌륭한 활용: @RestControllerAdvice는 AOP의 일종으로, 핵심 비즈니스 로직(Controller)과 부가 기능(예외 처리)을 분리하여 코드의 모듈성을 높였습니다.
    • 상세한 오류 로깅: log.error(e.getMessage(), e);를 통해 예외 메시지뿐만 아니라 스택 트레이스 전체를 기록하여, 문제 발생 시 원인 분석을 용이하게 했습니다.
  • 개선할 부분:
    • 로깅 정보 구체화: 현재 예외 발생 시 요청 path를 로깅하고 있습니다. 여기에 더해 어떤 요청 파라미터나 HTTP 메서드로 요청이 들어왔을 때 오류가 발생했는지 로깅에 추가하면 디버깅에 큰 도움이 될 것입니다. (단, 비밀번호나 개인정보와 같은 민감한 정보는 마스킹 처리해야 합니다.)

주제 2: 의미를 가지는 커스텀 예외 클래스 정의

“데이터가 없음”이라는 비즈니스 상황을 명확히 표현하기 위한 커스텀 예외 클래스들이 추가되었습니다.

  • 파일명: src/main/java/com/example/adv/exceptions/MyException.java (추상 클래스)
  • 파일명: src/main/java/com/example/adv/exceptions/DataNotFoundException.java
  • 역할 및 목적:
    • MyException은 모든 커스텀 비즈니스 예외의 부모 역할을 하는 추상 클래스입니다. 내부에 HttpStatus를 멤버로 가져, 예외가 곧 HTTP 응답 상태를 결정하도록 설계되었습니다.
    • DataNotFoundExceptionMyException을 상속하며, 생성 시 HttpStatus.NOT_FOUND (404)를 상태로 지정합니다. 이제 코드에서 throw new DataNotFoundException(...)을 호출하는 것만으로 “데이터를 찾을 수 없어 404 오류를 반환해야 한다”는 의미를 명확하게 전달할 수 있습니다.
  • 소스코드 인용 (DataNotFoundException.java, Line 6-8):
    1
    2
    3
    4
    5
    
    public class DataNotFoundException extends MyException {
        public DataNotFoundException(String message) {
            super(HttpStatus.NOT_FOUND, message);
        }
    }
    
  • 잘된 부분:
    • 가독성 및 의도 명확화: throw new RuntimeException("not found") 보다 throw new DataNotFoundException("시설 정보를 찾을 수 없습니다")가 코드의 의도를 훨씬 명확하게 보여줍니다.
    • 계층적 설계: MyException이라는 공통 부모를 둠으로써, 앞으로 InvalidInputException, AuthenticationFailedException 등 다양한 비즈니스 예외를 추가하더라도 일관된 구조를 유지할 수 있습니다.

주제 3: Service 계층의 역할 재정의 - 예외 발생의 책임

Service 계층이 비즈니스 규칙을 검증하고, 위반 시 명시적인 예외를 발생시키는 역할을 담당하게 되었습니다.

  • 파일명: src/main/java/com/example/adv/services/impl/FacilityServiceImpl.java
  • 변경 전후 비교 (getById 메서드):
    • Before: return facilityMapper.selectById(id); - DB 조회 결과가 없으면 null을 그대로 반환했습니다.
    • After: 조회 결과를 item 변수에 받고, itemnull인지 검사하여 null일 경우 DataNotFoundException을 던집니다.
  • 소스코드 인용 (Line 29-34):
    1
    2
    3
    4
    5
    6
    7
    8
    
    public Facilities getById(Long id) throws MyException {
        log.debug("getById(id={})", id);
        Facilities item = facilityMapper.selectById(id);
        if (item == null){
            throw new DataNotFoundException("해당 시설 정보를 찾을 수 없습니다. id=" + id);
        }
        return item;
    }
    
  • 잘된 부분:
    • Fail-Fast 원칙 적용: 문제가 발생할 수 있는 지점(데이터 없음)에서 즉시 실행을 중단하고 예외를 던짐으로써, null 값이 애플리케이션의 다른 부분으로 전파되어 NullPointerException을 유발하는 것을 원천적으로 차단합니다.
    • Service의 계약 강화: getById 메서드는 이제 “반드시 Facilities 객체를 반환하며, 찾을 수 없는 경우는 예외로 처리한다”는 명확한 계약을 갖게 되었습니다.

주제 4: Controller 계층의 간소화 - 예외 처리 로직 제거

전역 예외 처리 도입으로 인해 Controller 코드가 눈에 띄게 깔끔해졌습니다.

  • 파일명: src/main/java/com/example/adv/controllers/EnjoyController.java
  • 변경 전후 비교 (detail 메서드):
    • Before: try-catch 블록으로 facilityService.getById(id)를 감싸고, 반환된 값이 null인지 별도로 확인하여 ResponseStatusException을 던지는 등 복잡한 로직이 포함되어 있었습니다.
    • After: try-catchnull 체크가 모두 사라지고, facilityService.getById(id)를 직접 호출하는 한 줄로 간소화되었습니다. 메서드 시그니처에 throws MyException을 추가하여, 이 메서드에서 발생할 수 있는 예외를 명시적으로 선언했습니다.
  • 소스코드 인용 (diff): ```diff
    • public String detail(@RequestParam Long id, Model model) {
    • Facilities item = null;
    • try {
    • item = facilityService.getById(id);
    • } catch (Exception e) {
    • throw new RuntimeException(e);
    • }
    • if (item == null) throw new org.springframework.web.server.ResponseStatusException(
    • org.springframework.http.HttpStatus.NOT_FOUND);
    • public String detail(@RequestParam Long id, @RequestParam(required=false) String ym, Model model) throws MyException {
    • Facilities item = facilityService.getById(id); ```
  • 잘된 부분:
    • 가독성 및 유지보수성 향상: Controller가 오직 HTTP 요청을 받아 Service에 전달하고, 그 결과를 Model에 담아 View를 반환하는 핵심 로직에만 집중하게 되어 코드를 이해하고 수정하기가 매우 쉬워졌습니다.
    • 계층 간 책임 분리: 예외 처리는 MyRestExceptionHandler로, 비즈니스 규칙 검증은 FacilityService로 책임이 명확하게 분리되었습니다.

이상으로 예외 처리 관점에서 최근 4일간의 변경 사항을 심층 분석한 리포트를 마칩니다. 이 기간 동안의 작업은 애플리케이션을 더욱 견고하고 확장 가능하게 만드는 중요한 기반을 다졌다고 평가할 수 있습니다.


3. 종합적인 개선 제안 (Overall Improvement Suggestions)

현재의 예외 처리 구조는 매우 훌륭하지만, 아래 제안들을 통해 한 단계 더 발전시킬 수 있습니다.

1. 유효성 검사(Validation) 예외 처리 구체화

현재 MyRestExceptionHandlerConstraintViolationException (Bean Validation 실패 시 발생)을 422 Unprocessable Entity로 잘 매핑하고 있지만, 어떤 필드가 왜 유효성 검사에 실패했는지에 대한 상세 정보는 응답에 포함되지 않습니다. 프론트엔드 개발자가 어떤 입력값을 수정해야 하는지 명확히 알 수 있도록 상세 오류 메시지를 제공하는 것이 좋습니다.

  • 개선 방안: MyRestExceptionHandlerConstraintViolationException을 위한 별도의 핸들러를 추가합니다.

  • 개선 예시 코드:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    // MyRestExceptionHandler.java 내부에 추가
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<Map<String, Object>> handleConstraintViolation(
        ConstraintViolationException e, WebRequest request) {
    
        // 어떤 필드가(key) 어떤 이유로(value) 실패했는지 맵으로 생성
        Map<String, String> fieldErrors = new HashMap<>();
        for (ConstraintViolation<?> violation : e.getConstraintViolations()) {
            String fieldName = violation.getPropertyPath().toString();
            String message = violation.getMessage();
            fieldErrors.put(fieldName, message);
        }
    
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("status", HttpStatus.UNPROCESSABLE_ENTITY.value());
        result.put("error", "ConstraintViolation");
        result.put("message", "입력 값의 유효성 검사에 실패했습니다.");
        result.put("fieldErrors", fieldErrors); // 필드별 상세 오류 정보 추가
        result.put("timestamp", LocalDateTime.now().toString());
        result.put("path", ((ServletWebRequest) request).getRequest().getRequestURI());
    
        return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(result);
    }
    

2. 프론트엔드 예외 처리 및 사용자 경험(UX) 개선

백엔드에서 API 오류가 발생했을 때, 프론트엔드(JavaScript)에서 이를 적절히 처리하여 사용자에게 상황을 알려주는 것이 중요합니다. 현재 dailyinfo.jscatch 블록은 콘솔에만 에러를 기록하고 목록을 비워두기 때문에, 사용자는 무엇이 잘못되었는지 알기 어렵습니다.

  • 개선 방-안: API 호출 실패 시, 사용자에게 보여줄 오류 메시지를 UI에 렌더링합니다.

  • 대상 파일: src/main/resources/static/assets/js/dailyinfo.js

  • 개선 예시 코드 (loadForISO 함수):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
    // 개선 전
    async function loadForISO(iso){
      syncUrl(iso);
      try{
        const data = await fetchDailyInfo(iso);
        renderFacilities(data.facilities);
        renderShows(data.shows);
      }catch(e){
        console.error('[dailyinfo] load failed', e);
        renderFacilities([]); // 그냥 비우기만 함
        renderShows([]);    // 그냥 비우기만 함
      }
    }
    
    // 개선 후
    async function loadForISO(iso){
      syncUrl(iso);
      try{
        const data = await fetchDailyInfo(iso);
        renderFacilities(data.facilities);
        renderShows(data.shows);
      }catch(e){
        console.error('[dailyinfo] load failed', e);
        // 사용자에게 오류 메시지를 보여주는 UI 로직 추가
        const facilityUl = document.querySelector('#facilityUl');
        if (facilityUl) {
            facilityUl.innerHTML = '<li class="no_data error">정보를 불러오는 데 실패했습니다. 잠시 후 다시 시도해주세요.</li>';
        }
        const showUl = document.querySelector('#showUl');
        if (showUl) {
            showUl.innerHTML = '<li class="no_data error">정보를 불러오는 데 실패했습니다. 잠시 후 다시 시도해주세요.</li>';
        }
      }
    }
    
This post is licensed under CC BY 4.0 by the author.