이주헌님 포트폴리오 4주차 작업 내용 피드백 (2)
메가스터디IT아카데미 SpringBoot 백엔드 개발자 과정 주말반 (2025.02.22 ~ 2025.09.13). 이주헌님의 포트폴리오 4주차 작업 내용에 대한 피드백
이주헌님 포트폴리오 4주차 작업 내용 피드백 (2)
작성일: 2025-09-13
주헌님의 요청에 따라 “예외 처리(Exception Handling) 중심 상세 분석 리포트”를 추가 작성했습니다. 전반적으로 수업 내용이 잘 반영된 코드 입니다. 개선사항은 3. 종합적인 개선 제안 (Overall Improvement Suggestions) 섹션을 참고하세요. 마지막 피드백이니만큼 개선사항에는 수업 내용보다 한 단계 더 나아간 제안도 포함되어 있습니다.
1. 종합 요약: 체계적인 예외 처리 시스템의 구축
최근 4일간 이루어진 가장 중요한 아키텍처 개선은 전역적이고 일관된 예외 처리 시스템을 구축한 것입니다. 이 변화는 애플리케이션의 안정성과 유지보수성을 비약적으로 향상시키는 핵심적인 단계입니다.
- 변경 전 (Before):
- 예외 처리가 각 Controller 메서드 내에
try-catch
블록으로 흩어져 있었습니다. - 데이터 조회 실패 시
null
을 반환하거나,RuntimeException
또는ResponseStatusException
과 같은 일반적인 예외를 직접 던져 처리했습니다. - 이 방식은 코드 중복을 유발하고, API가 반환하는 오류 응답 형식이 일관되지 않을 수 있으며, 각 계층(Controller, Service)의 책임이 불분명해지는 문제를 야기합니다.
- 예외 처리가 각 Controller 메서드 내에
- 변경 후 (After):
@RestControllerAdvice
를 사용한 전역 예외 핸들러(MyRestExceptionHandler
)를 도입하여 애플리케이션 전역에서 발생하는 예외를 한 곳에서 중앙 관리합니다.DataNotFoundException
과 같은 의미 있는 커스텀 예외(Semantic Custom Exception)를 정의하여, 비즈니스 로직 상의 특정 오류 상황을 명확하게 표현합니다.- Service 계층은 데이터 조회 실패와 같은 비즈니스 규칙 위반 시
null
대신 커스텀 예외를 던지는 책임을 갖게 되었습니다. - Controller 계층은 더 이상 예외 처리 로직을 직접 포함하지 않고, Service 계층 호출과 View 반환이라는 본연의 역할에만 집중하게 되어 코드가 매우 간결해졌습니다.
이러한 변화는 관심사의 분리(Separation of Concerns) 원칙을 성공적으로 적용한 사례로, 각 컴포넌트가 자신의 역할에만 충실하도록 만들어 전체 시스템의 복잡도를 낮추고 예측 가능성을 높였습니다.
2. 예외 처리 관련 상세 분석
주제 1: 전역 예외 처리기의 도입 (MyRestExceptionHandler.java
)
애플리케이션의 모든 Exception
을 가로채 일관된 응답을 생성하는 “관제탑” 역할을 하는 컴포넌트가 도입되었습니다.
- 파일명:
src/main/java/com/example/adv/MyRestExceptionHandler.java
- 역할 및 목적:
@RestControllerAdvice
어노테이션을 통해 모든@RestController
에서 발생하는 예외를 감지하고 처리합니다. 이를 통해 API 사용자(프론트엔드)는 항상 일관된 형식의 오류 메시지를 받게 되어 안정적인 연동이 가능해집니다. - 핵심 구현 상세:
예외-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); // ... (기타 예외 매핑) }
- 소스코드 인용 (Line 19-21):
중앙 핸들러 메서드:
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(); }
- 소스코드 인용 (Line 70-76):
- 잘된 부분:
- AOP(관점 지향 프로그래밍)의 훌륭한 활용:
@RestControllerAdvice
는 AOP의 일종으로, 핵심 비즈니스 로직(Controller)과 부가 기능(예외 처리)을 분리하여 코드의 모듈성을 높였습니다. - 상세한 오류 로깅:
log.error(e.getMessage(), e);
를 통해 예외 메시지뿐만 아니라 스택 트레이스 전체를 기록하여, 문제 발생 시 원인 분석을 용이하게 했습니다.
- AOP(관점 지향 프로그래밍)의 훌륭한 활용:
- 개선할 부분:
- 로깅 정보 구체화: 현재 예외 발생 시 요청
path
를 로깅하고 있습니다. 여기에 더해 어떤 요청 파라미터나 HTTP 메서드로 요청이 들어왔을 때 오류가 발생했는지 로깅에 추가하면 디버깅에 큰 도움이 될 것입니다. (단, 비밀번호나 개인정보와 같은 민감한 정보는 마스킹 처리해야 합니다.)
- 로깅 정보 구체화: 현재 예외 발생 시 요청
주제 2: 의미를 가지는 커스텀 예외 클래스 정의
“데이터가 없음”이라는 비즈니스 상황을 명확히 표현하기 위한 커스텀 예외 클래스들이 추가되었습니다.
- 파일명:
src/main/java/com/example/adv/exceptions/MyException.java
(추상 클래스) - 파일명:
src/main/java/com/example/adv/exceptions/DataNotFoundException.java
- 역할 및 목적:
MyException
은 모든 커스텀 비즈니스 예외의 부모 역할을 하는 추상 클래스입니다. 내부에HttpStatus
를 멤버로 가져, 예외가 곧 HTTP 응답 상태를 결정하도록 설계되었습니다.DataNotFoundException
은MyException
을 상속하며, 생성 시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
변수에 받고,item
이null
인지 검사하여null
일 경우DataNotFoundException
을 던집니다.
- Before:
- 소스코드 인용 (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
객체를 반환하며, 찾을 수 없는 경우는 예외로 처리한다”는 명확한 계약을 갖게 되었습니다.
- Fail-Fast 원칙 적용: 문제가 발생할 수 있는 지점(데이터 없음)에서 즉시 실행을 중단하고 예외를 던짐으로써,
주제 4: Controller 계층의 간소화 - 예외 처리 로직 제거
전역 예외 처리 도입으로 인해 Controller 코드가 눈에 띄게 깔끔해졌습니다.
- 파일명:
src/main/java/com/example/adv/controllers/EnjoyController.java
- 변경 전후 비교 (
detail
메서드):- Before:
try-catch
블록으로facilityService.getById(id)
를 감싸고, 반환된 값이null
인지 별도로 확인하여ResponseStatusException
을 던지는 등 복잡한 로직이 포함되어 있었습니다. - After:
try-catch
와null
체크가 모두 사라지고,facilityService.getById(id)
를 직접 호출하는 한 줄로 간소화되었습니다. 메서드 시그니처에throws MyException
을 추가하여, 이 메서드에서 발생할 수 있는 예외를 명시적으로 선언했습니다.
- Before:
- 소스코드 인용 (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) 예외 처리 구체화
현재 MyRestExceptionHandler
는 ConstraintViolationException
(Bean Validation 실패 시 발생)을 422 Unprocessable Entity
로 잘 매핑하고 있지만, 어떤 필드가 왜 유효성 검사에 실패했는지에 대한 상세 정보는 응답에 포함되지 않습니다. 프론트엔드 개발자가 어떤 입력값을 수정해야 하는지 명확히 알 수 있도록 상세 오류 메시지를 제공하는 것이 좋습니다.
개선 방안:
MyRestExceptionHandler
에ConstraintViolationException
을 위한 별도의 핸들러를 추가합니다.개선 예시 코드:
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.js
의 catch
블록은 콘솔에만 에러를 기록하고 목록을 비워두기 때문에, 사용자는 무엇이 잘못되었는지 알기 어렵습니다.
개선 방-안: 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>'; } } }