1. 개요

DoEatFit 서비스에서 심박수 계산 API(POST /api/hr-calculator/calculate_heartrate)를 호출할 때 간헐적으로 서버 오류(500 Internal Server Error)를 일으키던 문제를 극복해 냈습니다. 특히 비회원(게스트) 상태에서 기능을 사용할 때 에러가 지속해서 발생했고, 로그인 사용자도 결과 조회 시 예외가 격발될 위험이 도사리고 있었습니다. 겉보기에는 단순한 NullPointerException처럼 보였지만, 원인을 면밀히 분석해 보니 Hibernate DDL 자동화의 물리적 한계JPA 임베디드(Embedded) 타입의 초기화 생명 주기가 얽혀 있는 복합적인 문제였답니다. 원인을 면밀하게 규명하고 근본적으로 처치한 기술 기록을 정리해 드립니다.


2. 핵심 내용

2-1. 🚨 문제 현상 및 원인 분석

  • 문제 현상:
    • 게스트용 식단 및 운동 강도 분석 과정에서 심박수 계산기를 작동할 때, 500 에러가 점등되며 연산 처리가 멈췄습니다.
    • 로그인 유저 또한 특정 입력값에 따라 매퍼 레이어에서 NullPointerException이 무정하게 노출되는 상태였습니다.
  • 원인 분석:
    1. Hibernate ddl-auto: update 옵션의 제약조건 수정 불능: 백엔드 엔티티 코드에서는 비회원을 위해 user_id 연관관계 필드에 nullable = true를 선언해 두었습니다. 그러나 기존 데이터베이스 스키마 테이블에는 여전히 NOT NULL 제약이 단단히 굳어 있었습니다. Hibernate의 자동화 엔진은 기존 테이블의 컬럼 null 허용 제약을 직관적으로 완화 변경하지 못했고, 이로 인해 게스트 인입 시 DB 제약 위반 에러가 발생한 것이었습니다.
    2. JPA Embedded 프록시 초기화 지연: HrCalculatorEntity 내부에서 고유 식별자 처리를 위해 @Embedded 타입으로 선언한 uuidEntity 필드가 있었습니다. 데이터베이스 조회 시 JPA는 기본 생성자를 호출해 객체를 빈 껍데기로 만들고 프록시를 씌워 반환하는데, 필드 선언 레벨에서 초기화(new UuidEntity())가 안 되어 있어 uuidEntity 자체가 null로 담겼습니다. 이 상태에서 DTO로 가공하기 위해 entity.getUuidEntity().getUuid()를 두드리는 순간 NPE 폭탄이 터진 것이었습니다.
    3. 운동 강도 데이터의 유효성 검사 누출: 클라이언트 측에서 보내온 운동 강도 문자열 파라미터(intensity)에 대소문자나 공백 등이 가미되었을 때, 가벼운 Trim 처리나 정합성 튜닝이 없어 조건부 분기를 이탈하며 연산 예외로 빠져들고 있었습니다.

2-2. 💡 단계별 에러 극복 및 솔루션 구현

  • 데이터베이스 컬럼 직접 개편 (LXC Console):
    • DDL 자동화 스펙에 무모하게 위임하는 대신, 호스트 데이터베이스 터미널에 직접 서명 진입하여 물리적인 컬럼 널 허용 제약조건 변경 쿼리를 전격 수행했습니다.
    ALTER TABLE HEART_RATE_CALCINFO MODIFY user_id BIGINT NULL;
  • 엔티티 임베디드 필드 초기화 습관 정비:
    • 영속성 프록시 라이프사이클 속에서도 객체 인스턴스가 절대 널(Null)로 회귀하지 못하게 선언 즉시 기본 생성 인스턴스를 주입했습니다.
    @Entity
    public class HrCalculatorEntity {
        @Embedded
        private UuidEntity uuidEntity = new UuidEntity();
    }
  • 입력 문자열 정규화 및 방어 코드 수립:
    • 인입되는 운동 강도 텍스트를 무조건 대문자 및 트림 처리하고 필수 파라미터 널 체크를 전면 실시했습니다. DTO 파라미터에 @NotBlank를 부착하여 입구 차단력을 대폭 보강했습니다.
    String normalizedIntensity = intensity.toUpperCase().trim();
    Objects.requireNonNull(age, "나이 정보는 필수 계산 값입니다.");
sequenceDiagram
    participant Client as 클라이언트 (브라우저/PWA)
    participant Controller as 심박수 컨트롤러
    participant Service as 심박수 서비스
    participant Entity as 심박수 엔티티
    participant DB as MySQL 데이터베이스

    Client->>Controller: POST /api/hr-calculator/calculate_heartrate (게스트)
    alt DTO 데이터 검증 실패 (@NotBlank 등 위반)
        Controller-->>Client: 400 Bad Request
    else 정상 진입
        Controller->>Service: calculateHeartRate(Dto)
    end
    Note over Service: 강도 대문자화 및 공백 제거, 필수 널 검증 수행
    Service->>Entity: 엔티티 빌드 (new UuidEntity 자동 주입 보장)
    Service->>DB: INSERT INTO HEART_RATE_CALCINFO (user_id = NULL)
    Note over DB: 컬럼 속성 직접 수정 완료로 NULL 정상 영속화!
    DB-->>Service: 저장 완료
    Service-->>Controller: 연산 및 매핑 완료 (NPE 완벽 보호)
    Controller-->>Client: 200 OK (정제된 심박수 연산 결과 응답)

2-3. ✅ 결과 검증 및 모던 오픈소스 라이브러리 검토

  • 결과 검증:
    • 게스트 및 로그인 사용자 계정으로 모바일 PWA 환경에서 심박수 연산 모듈을 다각도로 호출하며 스트레스 테스트를 집행했습니다.
    • 디버그 콘솔 결과, 비회원 데이터 주입 시 user_id에 정상적으로 NULL 값이 영속화되며 에러가 소거되었고, 임베디드 DTO 가공 시 발생하던 NPE 현상 역시 깔끔하게 원천 제거됨을 검증했습니다. 유효하지 않은 입력 데이터에 대해 500 에러 대신 정갈하게 400 Bad Request 에러를 회신하는 견고함도 확보했습니다.
  • 추천 오픈소스 라이브러리 검토:
    1. MapStruct
      • 평가: 엔티티와 DTO 매핑을 수동 Getter/Setter에 기댈 시, 찰나의 순간 참조 에러나 빌더 누락이 나기 마련입니다. 컴파일 타임에 타입 안전한 소스 자동 매퍼를 구성하는 MapStruct를 적극 활용해 보실 것을 제안합니다. 자동 생성 매핑 메서드는 필드 자체의 널 보호 가드 클로즈를 기본 탑재해 줍니다.
    2. Jakarta Bean Validation (Hibernate Validator)
      • 평가: 단순히 @NotBlank에 국한하지 않고, 심박수 수치 범위를 제어하는 @Min(30)@Max(250) 등을 운동 강도와 나이 필드에 장착하여 백엔드 비즈니스 로직에 인입될 수 있는 불량 데이터 침투 리스크를 사전 봉쇄하기에 적합한 최상의 보안 수단입니다.