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

1단계: "@NoArgsConstructor가 굴린 대형 눈덩이"

writingforever162 2024. 12. 8. 01:48

[문제]

처음에는 전부 다 빨간 줄이 쳐져서 얼마나 심장이 벌렁벌렁 뛰었는지 모른다. 분명 강의를 들으며 코드를 따라 쳤는데, 왜 내가 쓴 코드에만 문제가 생기는지 영 감을 못 잡았다. 챗GPT에도 물었으나, 분명한 답을 얻지는 못했다.

 

[원인]

package com.spring.weekthree.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.time.LocalDate;
import java.time.LocalDateTime;

/*
entity에 해당하는 Plan 클래스
[오답] NoArgsConstructor
[정답] AllArgsConstructor
*/
@NoArgsConstructor
@Getter
public class Plan {
    // 속성
    private Long id;
    private String name;
    private String password;
    private LocalDate plannedDate;
    private String title;
    private String task;
    private LocalDateTime createdDateTime;
    private LocalDateTime updatedDateTime;

    /**
     * 생성자
     * @param name        : 사용자 이름
     * @param password    : 사용자 비밀번호
     * @param plannedDate : 사용자가 입력한 일정 날짜
     * @param title       : 사용자가 입력한 일정의 제목
     * @param task        : 사용자가 입력한 일정의 상세 정보
     */
    public Plan(
            String name,
            String password,
            LocalDate plannedDate,
            String title,
            String task
    ) {
        this.name = name;
        this.password = password;
        this.plannedDate = plannedDate;
        this.title = title;
        this.task = task;
        this.createdDateTime = LocalDateTime.now();
        this.updatedDateTime = LocalDateTime.now();
    }
    
    // 기능
}

원인은 강의 자료를 읽은 끝에 찾아낼 수 있었다. 엔티티(entity)에 해당하는 Plan 클래스(class)에서 나는 기본 생성자를 만드는 @NoArgsConstructor 어노테이션(annotation)을 썼는데, 강의에서는 클래스의 모든 속성을 매개변수로 받는 @AllArgsConstructor 어노테이션을 썼다. 그러니까, 받아야 하는 매개변수가 맞지 않아 문제가 생기고 말았다.

 

[해결]

package com.spring.weekthree.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.time.LocalDate;
import java.time.LocalDateTime;

/*
[1/2] entity에 해당하는 Plan 클래스 수정하기 
[수정 전] @NoArgsConstructor
[수정 후] @AllArgsConstructor
 */
@AllArgsConstructor
@Getter
public class Plan {
    // 속성
    private Long id;
    private String name;
    private String password;
    private LocalDate plannedDate;
    private String title;
    private String task;
    private LocalDateTime createdDateTime;
    private LocalDateTime updatedDateTime;

    // 생성자
    public Plan(
            String name,
            String password,
            LocalDate plannedDate,
            String title,
            String task
    ) {
        this.name = name;
        this.password = password;
        this.plannedDate = plannedDate;
        this.title = title;
        this.task = task;

        this.createdDateTime = LocalDateTime.now();
        this.updatedDateTime = LocalDateTime.now();
    }

    // 기능
}
package com.spring.weekthree.repository;

import com.spring.weekthree.dto.PlanResponseDto;
import com.spring.weekthree.entity.Plan;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;

import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

// [2/2] Data Access Layer(Repository Layer) 수정하기
@Repository
public class JdbcTemplatePlanRepository implements PlanRepository {
    private final JdbcTemplate jdbcTemplate;

    public JdbcTemplatePlanRepository(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public PlanResponseDto save(Plan plan) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        jdbcInsert.withTableName("planner").usingGeneratedKeyColumns("id");

        Map<String, Object> parameters = new HashMap<>();

        parameters.put("name", plan.getName());
        parameters.put("password", plan.getPassword());
        parameters.put("plannedDate", plan.getPlannedDate());
        parameters.put("title", plan.getTitle());
        parameters.put("task", plan.getTask());
        parameters.put("createdDateTime", plan.getCreatedDateTime());
        parameters.put("updatedDateTime", plan.getUpdatedDateTime());

        Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));

        return new PlanResponseDto(
                key.longValue(),
                plan.getName(),
                plan.getPlannedDate(),
                plan.getTitle(),
                plan.getTask(),
                plan.getCreatedDateTime(),
                plan.getUpdatedDateTime());
    }

    @Override
    public List<PlanResponseDto> fetchAllPlans(String name, LocalDate updatedDate) {
        return jdbcTemplate.query("SELECT * FROM planner", plannerRowMapper());
    }

    @Override
    public Optional<Plan> fetchPlanById(Long id) {
        List<Plan> result = jdbcTemplate.query("SELECT * FROM planner WHERE id = ?", plannerRowMapperEach(), id);
        return result.stream().findAny();
    }

    private RowMapper<PlanResponseDto> plannerRowMapper() {
        return new RowMapper<PlanResponseDto>() {
            @Override
            public PlanResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
                return new PlanResponseDto(
                        rs.getLong("id"),
                        rs.getString("name"),
                        rs.getDate("plannedDate").toLocalDate(),
                        rs.getString("title"),
                        rs.getString("task"),
                        rs.getTimestamp("createdDateTime").toLocalDateTime(),
                        rs.getTimestamp("updatedDateTime").toLocalDateTime()
                );
            }
        };
    }

    private RowMapper<Plan> plannerRowMapperEach() {
        return new RowMapper<Plan>() {
            @Override
            public Plan mapRow(ResultSet rs, int rowNum) throws SQLException {
                return new Plan(
                        rs.getLong("id"),
                        rs.getString("name"),
                        rs.getString("password"),
                        /*
                        [수정 전] 비밀번호 부분 누락 
                        [수정 후] 추가: rs.getString("password"),
                        [주의 사항] 쉼표(,) 누락하지 말자!
                         */
                        rs.getDate("plannedDate").toLocalDate(),
                        rs.getString("title"),
                        rs.getString("task"),
                        rs.getTimestamp("createdDateTime").toLocalDateTime(),
                        rs.getTimestamp("updatedDateTime").toLocalDateTime()
                );
            }
        };
    }
}

이번 문제는 적절한 어노테이션(annotation)을 사용하지 않고, 엔티티와 DTO(Data Transfer Object)를 헷갈려서 생겼다. 일정 목록을 조회할 때 반환하는 값은 PlanResponseDto이며 클라이언트(client)에게 엔티티가 아니라 이 PlanResponseDto를 전달하는데, 이 사실을 순간 잊었다.

 

'맞다, 생각해 보니까 데이터를 전달할 때 Plan이 아니라 DTO를 주잖아?' 

 

그러니 일정 목록을 조회할 때 비밀번호는 전달하지 말아야 한다는 생각에 사로잡혀 rs.getString("password") 부분 또한 적어야 한다는 점을 금방 떠올리지 못했다. 앞으로는 좀 더 차근차근 코드를 써야겠다.

 

[결과 수치화]

[수정 전] 자바(Java) 인텔리제이(IntelliJ) 빨간 줄 경고 표시 3개 발생 

[수정 후] 자바(Java) 인텔리제이(IntelliJ) 빨간 줄 경고 표시 0개 발생