Troubleshooting: 무엇이 문제였는가?/본캠프 3주 차: 일정 관리 앱 Develop

1단계: "not-null property references a null or transient value"

writingforever162 2024. 12. 13. 14:55

[문제]

'일정 관리 앱 Develop' 과제는 JPA를 활용해야 했다. 필수 과제 1단계에서는 일정을 생성, 조회, 수정, 삭제할 수 있도록 CRUD를 구현해야 했는데, 생성(Create)에 해당하는 C를 구현한 다음 프로그램을 실행하자, 바로 500 Internal Server Error 메시지가 떴다. 우선 null이 들어가면 안 되는 곳에 null이 들어가서 생긴 문제라 짐작했다.

 

[원인]

package com.example.plan;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

/*
[오답] 어노테이션(annotation) 누락
[정답] 추가: @EnableJpaAuditing
 */
@EnableJpaAuditing
@SpringBootApplication
public class PlanApplication {

    public static void main(String[] args) {
       SpringApplication.run(PlanApplication.class, args);
    }
}

원인은 필요한 어노테이션(annotation)을 빠뜨린 데에 있었다. 과제의 요구 사항 중 하나가 '작성 날짜와 수정 날짜는 JPA Auditing 활용하기'였는데, @EnableJpaAuditing을 쓰지 않아 오류가 발생했다.

 

[해결]

@EnableJpaAuditing 어노테이션(annotation)을 추가하고 다시 프로그램을 실행한 결과, 일정 생성에 성공했다는 201 Created 메시지가 나왔으나, 생성 날짜와 수정 날짜가 데이터베이스(database)에만 저장되었을 뿐, 반환되지는 않았다. 추가로 응답할 때 두 정보가 전달되도록 아래와 같이 수정했다.

더보기
package com.example.plan.plan.repository;

// [1/8] Data Access Layer(Repository Layer)
import com.example.plan.plan.entity.Plan;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PlanRepository extends JpaRepository<Plan, Long> {
}
package com.example.plan.plan.service;

import com.example.plan.plan.dto.response.PlanResponseDto;

// [2/8] Service Layer 구현에 필요한 인터페이스(interface)
public interface PlanService {
    public PlanResponseDto save(
            String name,
            String title,
            String task
    );
}
package com.example.plan.plan.controller;

import com.example.plan.plan.dto.request.CreatePlanRequestDto;
import com.example.plan.plan.dto.response.PlanResponseDto;
import com.example.plan.plan.service.PlanServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

// [3/8] Presentation Layer
@RestController
@RequestMapping("/plans")
@RequiredArgsConstructor
public class PlanController {
    private final PlanServiceImpl planService;

    @PostMapping
    public ResponseEntity<PlanResponseDto> save(
            @RequestBody CreatePlanRequestDto requestDto
    ) {
        PlanResponseDto savedPlan = planService.save(
                requestDto.getUsername(),
                requestDto.getTitle(),
                requestDto.getTask()
        );
        return new ResponseEntity<>(savedPlan, HttpStatus.CREATED);
    }
}
package com.example.plan.plan.entity;

import jakarta.persistence.*;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

/*
[4/8] BaseEntity 클래스(class)
용도: 엔티티(entity)에 작성 날짜와 수정 날짜 반영 
 */
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class PlanBaseEntity {

    @CreatedDate
    @Column (nullable = false, updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime createdAt; // 작성 날짜

    @LastModifiedDate
    @Column (nullable = false)
    private LocalDateTime updatedAt; // 수정 날짜
}
package com.example.plan.plan.entity;

import jakarta.persistence.*;
import lombok.Getter;

// [5/8] BaseEntity를 상속한 Plan 엔티티(entity) 클래스
@Getter
@Entity
@Table(name = "plans")
public class Plan extends PlanBaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long planId;

    @Column(nullable = false)
    private String username; // 작성자 이름

    @Column(nullable = false)
    private String title; // 일정 제목

    @Column(nullable = true, columnDefinition = "longtext")
    private String task; // 일정 내용

    public Plan() {
    }

    public Plan(
            String username,
            String title,
            String task
    ) {
        this.username = username;
        this.title = title;
        this.task = task;
    }
}
package com.example.plan.plan.dto.request;

import lombok.Getter;

// [6/8] 일정 생성 요청에 해당하는 request DTO
@Getter
public class CreatePlanRequestDto {
    private final String username; // 작성자 이름
    private final String title; // 일정 제목
    private final String task; // 일정 내용

    public CreatePlanRequestDto(
            String username,
            String title,
            String task
    ) {
        this.username = username;
        this.title = title;
        this.task = task;
    }
}
더보기
package com.example.plan.plan.service;

import com.example.plan.plan.dto.response.PlanResponseDto;
import com.example.plan.plan.entity.Plan;
import com.example.plan.plan.repository.PlanRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

/*
[7/8] Service Layer 수정하기
PlanService 인터페이스를 오버라이딩한 클래스이다. 
 */
@Service
@RequiredArgsConstructor
public class PlanServiceImpl implements PlanService {
    private final PlanRepository planRepository;

    @Override
    public PlanResponseDto save(
            String username,
            String title,
            String task
    ) {
        Plan planToSave = new Plan(
                username,
                title,
                task
        );
        Plan savedPlan = planRepository.save(planToSave);

        return new PlanResponseDto(
                savedPlan.getPlanId(),
                savedPlan.getUsername(),
                savedPlan.getTitle(),
                savedPlan.getTask(), 
                savedPlan.getCreatedAt(), // [수정] 추가
                savedPlan.getUpdatedAt() // [수정] 추가
                // [주의] 추가할 때 쉼표(,)를 누락하지 말자.
        );
    }
}
package com.example.plan.plan.dto.response;

import lombok.Getter;

import java.time.LocalDateTime;

// [8/8] 응답에 해당하는 response DTO
@Getter
public class PlanResponseDto {
    private final Long planId; // 일정 식별자
    private final String username; // 작성자 이름
    private final String title; // 일정 제목
    private final String task; // 일정 내용
    private final LocalDateTime createdAt; // [수정] 추가: 작성 날짜
    private final LocalDateTime updatedAt; // [수정] 추가: 수정 날짜 

    /* 
    [수정] 두 가지 매개변수 추가 
    [주의] 추가할 때 쉼표(,)를 누락하지 말자.
    (1) LocalDateTime createdAt
    (2) LocalDateTime updatedAt
     */
    public PlanResponseDto(Long planId,
                           String username,
                           String title,
                           String task, 
                           LocalDateTime createdAt,
                           LocalDateTime updatedAt
    ) {
        this.planId = planId;
        this.username = username;
        this.title = title;
        this.task = task;
        this.createdAt = createdAt; // [수정] 추가
        this.updatedAt = updatedAt; // [수정] 추가
    }
}

역시 처음 배운 내용을 바로 완벽하게 적용하기란 불가능했다. 그래도 로직(logic)을, 코드의 흐름을 뜯어고칠 정도로 심각한 문제가 아니라서 안도감이 살짝 들었다.

 

[결과 수치화]

[수정 전] 500 Internal Server Error 1건 발생
[수정 후] 500 Internal Server Error 0건 발생, 201 Created 메시지 생성 및 데이터베이스 연동 성공