Spring 입문주차 + 개인과제
목표: 나만의 일정 관리 앱 서버 만들기
필수 구현 기능
나는 5단계까지 구현을 했다.
추가 구현 기능은 9단계까지 있다
entity
package com.sparta.nbcamp_java_5th_schedulemanagement.entity;
import com.sparta.nbcamp_java_5th_schedulemanagement.dto.ScheduleRequestDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class Schedule {
private Long id;
private String title;
private String contents;
private String manager;
private String password;
private String date;
public Schedule(ScheduleRequestDto scheduleRequestDto) {
this.title = scheduleRequestDto.getTitle();
this.contents = scheduleRequestDto.getContents();
this.manager = scheduleRequestDto.getManager();
this.password = scheduleRequestDto.getPassword();
this.date = scheduleRequestDto.getDate();
}
}
dto(request,response)
package com.sparta.nbcamp_java_5th_schedulemanagement.dto;
import lombok.Getter;
@Getter
public class ScheduleRequestDto {
private String title;
private String contents;
private String manager;
private String password;
private String date;
}
/////////////////////////////////////////////////////////
package com.sparta.nbcamp_java_5th_schedulemanagement.dto;
import com.sparta.nbcamp_java_5th_schedulemanagement.entity.Schedule;
import lombok.Getter;
@Getter
public class ScheduleResponseDto {
private Long id;
private String title;
private String contents;
private String manager;
private String date;
public ScheduleResponseDto(Schedule schedule) {
this.id = schedule.getId();
this.title = schedule.getTitle();
this.contents = schedule.getContents();
this.manager = schedule.getManager();
this.date = schedule.getDate();
}
public ScheduleResponseDto(Long id, String title, String contents, String manager, String date) {
this.id = id;
this.title = title;
this.contents = contents;
this.manager = manager;
this.date = date;
}
}
repository
package com.sparta.nbcamp_java_5th_schedulemanagement.repository;
import com.sparta.nbcamp_java_5th_schedulemanagement.dto.ScheduleRequestDto;
import com.sparta.nbcamp_java_5th_schedulemanagement.dto.ScheduleResponseDto;
import com.sparta.nbcamp_java_5th_schedulemanagement.entity.Schedule;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
@Repository
public class ScheduleRepository {
private final JdbcTemplate jdbcTemplate;
public ScheduleRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public Schedule saveSchedule(Schedule schedule) {
KeyHolder keyHolder = new GeneratedKeyHolder();
String sql = "INSERT INTO scheduleTable (title, contents, manager, password, date) VALUES (?,?,?,?,?)";
jdbcTemplate.update(con -> {
PreparedStatement preparedStatement = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, schedule.getTitle());
preparedStatement.setString(2, schedule.getContents());
preparedStatement.setString(3, schedule.getManager());
preparedStatement.setString(4, schedule.getPassword());
preparedStatement.setString(5, schedule.getDate());
return preparedStatement;
}, keyHolder);
Long id = keyHolder.getKey().longValue();
schedule.setId(id);
return schedule;
}
public List<ScheduleResponseDto> findAllSchedules() {
String sql = "SELECT * FROM scheduleTable ORDER BY date DESC";
return jdbcTemplate.query(sql, this::mapRowForSchedule);
}
public ScheduleResponseDto getSchedule(Long id) {
String sql = "SELECT * FROM scheduleTable WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{id}, this::mapRowForSchedule);
}
private ScheduleResponseDto mapRowForSchedule(ResultSet resultSet, int i) throws SQLException {
Long id = resultSet.getLong("id");
String title = resultSet.getString("title");
String contents = resultSet.getString("contents");
String manager = resultSet.getString("manager");
String date = resultSet.getString("date");
return new ScheduleResponseDto(id, title, contents, manager, date);
}
public void updateSchedule(Long id, ScheduleRequestDto scheduleRequestDto) {
String sql = "UPDATE scheduleTable SET title = ?, contents = ?, manager = ? WHERE id = ?";
jdbcTemplate.update(sql, scheduleRequestDto.getTitle(), scheduleRequestDto.getContents(), scheduleRequestDto.getManager(), id);
}
public Schedule findById(Long id) {
String sql = "SELECT * FROM scheduleTable WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{id}, (resultSet, i) -> {
Schedule schedule = new Schedule();
schedule.setId(resultSet.getLong("id"));
schedule.setTitle(resultSet.getString("title"));
schedule.setContents(resultSet.getString("contents"));
schedule.setManager(resultSet.getString("manager"));
schedule.setPassword(resultSet.getString("password"));
schedule.setDate(resultSet.getString("date"));
return schedule;
});
}
public void deleteSchedule(Long id) {
String sql = "DELETE FROM scheduleTable WHERE id = ?";
jdbcTemplate.update(sql, id);
}
}
controller
package com.sparta.nbcamp_java_5th_schedulemanagement.controller;
import com.sparta.nbcamp_java_5th_schedulemanagement.dto.ScheduleRequestDto;
import com.sparta.nbcamp_java_5th_schedulemanagement.dto.ScheduleResponseDto;
import com.sparta.nbcamp_java_5th_schedulemanagement.service.ScheduleService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api")
public class ScheduleController {
private final ScheduleService scheduleService;
public ScheduleController(ScheduleService scheduleService) {
this.scheduleService = scheduleService;
}
@PostMapping("/schedules")
public ScheduleResponseDto createSchedule(@RequestBody ScheduleRequestDto scheduleRequestDto) {
return scheduleService.createSchedule(scheduleRequestDto);
}
@GetMapping("/schedules")
public List<ScheduleResponseDto> getSchedules() {
return scheduleService.getAllSchedules();
}
@GetMapping("/schedules/{id}")
public ScheduleResponseDto getSchedule(@PathVariable Long id) {
return scheduleService.getSchedule(id);
}
@PutMapping("/schedules/{id}/{password}")
public Long updateSchedule(@PathVariable Long id, @PathVariable String password, @RequestBody ScheduleRequestDto scheduleRequestDto) {
return scheduleService.updateSchedule(id, password, scheduleRequestDto);
}
@DeleteMapping("/schedules/{id}/{password}")
public Long deleteSchedule(@PathVariable Long id, @PathVariable String password) {
return scheduleService.deleteSchedule(id, password);
}
}
service
package com.sparta.nbcamp_java_5th_schedulemanagement.service;
import com.sparta.nbcamp_java_5th_schedulemanagement.dto.ScheduleRequestDto;
import com.sparta.nbcamp_java_5th_schedulemanagement.dto.ScheduleResponseDto;
import com.sparta.nbcamp_java_5th_schedulemanagement.entity.Schedule;
import com.sparta.nbcamp_java_5th_schedulemanagement.repository.ScheduleRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ScheduleService {
private final ScheduleRepository scheduleRepository;
public ScheduleService(ScheduleRepository scheduleRepository) {
this.scheduleRepository = scheduleRepository;
}
public ScheduleResponseDto createSchedule(ScheduleRequestDto scheduleRequestDto) {
Schedule schedule = new Schedule(scheduleRequestDto);
Schedule savedSchedule = scheduleRepository.saveSchedule(schedule);
return new ScheduleResponseDto(savedSchedule);
}
public List<ScheduleResponseDto> getAllSchedules() {
return scheduleRepository.findAllSchedules();
}
public ScheduleResponseDto getSchedule(Long id) {
return scheduleRepository.getSchedule(id);
}
public Long updateSchedule(Long id, String inputPassword, ScheduleRequestDto scheduleRequestDto) {
Schedule schedule = scheduleRepository.findById(id);
if (schedule != null) {
if (inputPassword.equals(schedule.getPassword())) {
scheduleRepository.updateSchedule(id, scheduleRequestDto);
return id;
} else {
throw new IllegalArgumentException("비밀번호 불일치");
}
} else {
throw new IllegalArgumentException("일정이 존재하지 않습니다");
}
}
public Long deleteSchedule(Long id, String inputPassword) {
Schedule schedule = scheduleRepository.findById(id);
if (schedule != null) {
if (inputPassword.equals(schedule.getPassword())) {
scheduleRepository.deleteSchedule(id);
return id;
} else {
throw new IllegalArgumentException("비밀번호 불일치");
}
} else {
throw new IllegalArgumentException("일정이 존재하지 않습니다");
}
}
}
이렇게 5단계까지 구현을 했다. JdbcTemplate를 이용했다
JPA로 구현을 할까 고민을 했지만 아직 공부를 더 해야 되는 상태여서 그렇다
이렇게 코드를 작성하고 튜터님한테 제출했다.
피드백이 왔다. 약 4단계로 구분이 돼 있었고 나는 그 피드백에 맡게 코드를 리팩토링 했다.
피드백 1 : Controller에서 @RequestMapping("/api/schedules")를 이렇게 활용하여
아래 /schedules는 제거해 주시면 훨씬 깔금해지실 것 같아요
>> 내가 미처 발견을 하지 못했다.. 바로 코드를 바꿨다
[feedback 1] ScheduleController API 엔드 포인트 수정
ScheduleController
package com.sparta.nbcamp_java_5th_schedulemanagement.controller;
import com.sparta.nbcamp_java_5th_schedulemanagement.dto.ScheduleRequestDto;
import com.sparta.nbcamp_java_5th_schedulemanagement.dto.ScheduleResponseDto;
import com.sparta.nbcamp_java_5th_schedulemanagement.service.ScheduleService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/schedules")
public class ScheduleController {
private final ScheduleService scheduleService;
public ScheduleController(ScheduleService scheduleService) {
this.scheduleService = scheduleService;
}
@PostMapping
public ScheduleResponseDto createSchedule(@RequestBody ScheduleRequestDto scheduleRequestDto) {
return scheduleService.createSchedule(scheduleRequestDto);
}
@GetMapping
public List<ScheduleResponseDto> getAllSchedules() {
return scheduleService.getAllSchedules();
}
@GetMapping("/{id}")
public ScheduleResponseDto getSchedule(@PathVariable Long id) {
return scheduleService.getSchedule(id);
}
@PutMapping("/{id}/{password}")
public Long updateSchedule(@PathVariable Long id, @PathVariable String password, @RequestBody ScheduleRequestDto scheduleRequestDto) {
return scheduleService.updateSchedule(id, password, scheduleRequestDto);
}
@DeleteMapping("/{id}/{password}")
public Long deleteSchedule(@PathVariable Long id, @PathVariable String password) {
return scheduleService.deleteSchedule(id, password);
}
}
피드백 2 : 현재 수정 및 삭제 API 구성에서 password를 URI path에 추가하셨는데
패스워드 같은 민감한 정보들은 최대한 노출을 지양하는 것이 좋습니다!
HTTP 프로토콜 body 내부에 넣어주시면 좋아요!
(실제로 네이버에서는 로그인할 때 body에 비밀번호를 암호화해서 넣어 보냅니다!)
일단 우리는 암호화까지는 하지 않더라고 body 내부에라도 넣어주시면 좋을 것 같아요
>> 이 부분은 내가 구현을 하고 봐도 좀 아닌 거 같았다.. 그래서 바로 바꿨다
[feedback 2] Password URI path > HTTP 프로토콜 body
ScheduleController
@PutMapping("/{id}")
public Long updateSchedule(@PathVariable Long id, @RequestBody UpdateScheduleRequestDto updateScheduleRequestDto) {
return scheduleService.updateSchedule(id, updateScheduleRequestDto);
}
@DeleteMapping("/{id}")
public Long deleteSchedule(@PathVariable Long id, @RequestBody DeleteScheduleRequestDto deleteScheduleRequestDto) {
return scheduleService.deleteSchedule(id, deleteScheduleRequestDto);
}
dto 2개를 생성했다
package com.sparta.nbcamp_java_5th_schedulemanagement.dto;
import lombok.Getter;
@Getter
public class DeleteScheduleRequestDto {
private String password;
}
//////////////////////////////////
package com.sparta.nbcamp_java_5th_schedulemanagement.dto;
import lombok.Getter;
@Getter
public class UpdateScheduleRequestDto {
private String title;
private String contents;
private String manager;
private String password;
private String date;
}
ScheduleRepository
import com.sparta.nbcamp_java_5th_schedulemanagement.dto.*;
public void updateSchedule(Long id, UpdateScheduleRequestDto updateScheduleRequestDto) {
String sql = "UPDATE scheduleTable SET title = ?, contents = ?, manager = ?, date = ? WHERE id = ?";
jdbcTemplate.update(sql, updateScheduleRequestDto.getTitle(), updateScheduleRequestDto.getContents(), updateScheduleRequestDto.getManager(), updateScheduleRequestDto.getDate(), id);
}
ScheduleService
import com.sparta.nbcamp_java_5th_schedulemanagement.dto.*;
public Long updateSchedule(Long id, UpdateScheduleRequestDto updateScheduleRequestDto) {
Schedule schedule = scheduleRepository.findById(id);
if (schedule != null) {
if (updateScheduleRequestDto.getPassword().equals(schedule.getPassword())) {
scheduleRepository.updateSchedule(id, updateScheduleRequestDto);
return id;
} else {
throw new IllegalArgumentException("비밀번호 불일치");
}
} else {
throw new IllegalArgumentException("일정이 존재하지 않습니다");
}
}
public Long deleteSchedule(Long id, DeleteScheduleRequestDto deleteScheduleRequestDto) {
Schedule schedule = scheduleRepository.findById(id);
if (schedule != null) {
if (deleteScheduleRequestDto.getPassword().equals(schedule.getPassword())) {
scheduleRepository.deleteSchedule(id);
return id;
} else {
throw new IllegalArgumentException("비밀번호 불일치");
}
} else {
throw new IllegalArgumentException("일정이 존재하지 않습니다");
}
}
피드백 3 : inputPassword.equals 패스워드 비교에서는 혹시나 DTO로 들어온 password
데이터가 null인 경우 NPE가 발생할 수도 있기 때문에 문자열 비교는 최대한 Objects.equals 같은 것을
사용하는 것이 안전할 것 같습니다!! NPE 처리를 위해 탄생했다고 해도
무방한 Optional 도 학습 후에 적용해 보시면 좋을 것 같습니다
>> 피드백을 수용해 코드를 바꿔봤다!
[feedback 3] NPE 방지를 위해 패스워드 비교에 Objects.equals 사용
ScheduleService
import java.util.Objects;
public Long updateSchedule(Long id, UpdateScheduleRequestDto updateScheduleRequestDto) {
Schedule schedule = scheduleRepository.findById(id);
if (schedule != null) {
if (Objects.equals(updateScheduleRequestDto.getPassword(), schedule.getPassword())) {
scheduleRepository.updateSchedule(id, updateScheduleRequestDto);
return id;
} else {
throw new IllegalArgumentException("비밀번호 불일치");
}
} else {
throw new IllegalArgumentException("일정이 존재하지 않습니다");
}
}
public Long deleteSchedule(Long id, DeleteScheduleRequestDto deleteScheduleRequestDto) {
Schedule schedule = scheduleRepository.findById(id);
if (schedule != null) {
if (Objects.equals(deleteScheduleRequestDto.getPassword(), schedule.getPassword())) {
scheduleRepository.deleteSchedule(id);
return id;
} else {
throw new IllegalArgumentException("비밀번호 불일치");
}
} else {
throw new IllegalArgumentException("일정이 존재하지 않습니다");
}
}
피드백 4 : repository에서 resultSet 데이터를 사용해서 Schedule 객체를 생성할 때 Setter 가 아니라
생성자를 사용해서 생성했으면 더 좋았을 것 같습니다! setter를 사용하면 안 되는 것은 아니지만
setter를 무분별하게 열어두고 개발 하다 보면 해당 객체의 데이터 변경 책임이 많은 곳에 산파되어
나중에 문제를 야기할 가능성이 매우 커집니다! 최대한 객체 생성과 데이터 변경은
생성자와 내부 메서드를 사용해서 처리해 주시면 정말 좋을 것 같습니다
>> 피드백을 수용해 코드를 바꿔봤다!
[feedback 4] Schedule 엔티티 생성 시 setter 대신 생성자 사용
Schedule
public Schedule(Long id, String title, String contents, String manager, String password, String date) {
this.id = id;
this.title = title;
this.contents = contents;
this.manager = manager;
this.password = password;
this.date = date;
}
}
ScheduleRepository
public Schedule findById(Long id) {
String sql = "SELECT * FROM scheduleTable WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{id}, (resultSet, i) ->
new Schedule(
resultSet.getLong("id"),
resultSet.getString("title"),
resultSet.getString("contents"),
resultSet.getString("manager"),
resultSet.getString("password"),
resultSet.getString("date")
)
);
}
자체 피드백: 객체 생성자를 사용하고 중첩 조건문 if/else를 Early Return Pattern을 적용해 봤다.
refactor : Early return Pattern 적용 & 객체 생성자 사용
ScheduleRepository
public Schedule saveSchedule(Schedule schedule) {
KeyHolder keyHolder = new GeneratedKeyHolder();
String sql = "INSERT INTO scheduleTable (title, contents, manager, password, date) VALUES (?,?,?,?,?)";
jdbcTemplate.update(con -> {
PreparedStatement preparedStatement = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, schedule.getTitle());
preparedStatement.setString(2, schedule.getContents());
preparedStatement.setString(3, schedule.getManager());
preparedStatement.setString(4, schedule.getPassword());
preparedStatement.setString(5, schedule.getDate());
return preparedStatement;
}, keyHolder);
Long id = keyHolder.getKey().longValue();
return new Schedule(id, schedule.getTitle(), schedule.getContents(), schedule.getManager(), schedule.getPassword(), schedule.getDate());
}
ScheduleService
public Long updateSchedule(Long id, UpdateScheduleRequestDto updateScheduleRequestDto) {
Schedule schedule = scheduleRepository.findById(id);
if (schedule == null) {
throw new IllegalArgumentException("일정이 존재하지 않습니다");
}
if (!Objects.equals(updateScheduleRequestDto.getPassword(), schedule.getPassword())) {
throw new IllegalArgumentException("비밀번호 불일치");
}
scheduleRepository.updateSchedule(id, updateScheduleRequestDto);
return id;
}
public Long deleteSchedule(Long id, DeleteScheduleRequestDto deleteScheduleRequestDto) {
Schedule schedule = scheduleRepository.findById(id);
if (schedule == null) {
throw new IllegalArgumentException("일정이 존재하지 않습니다");
}
if (!Objects.equals(deleteScheduleRequestDto.getPassword(), schedule.getPassword())) {
throw new IllegalArgumentException("비밀번호 불일치");
}
scheduleRepository.deleteSchedule(id);
return id;
}
깃허브 전체 코드
https://github.com/kiseokkm/Nbcamp_java_5th_Schedule-Management
다음 학습이랑 과제도 파이팅 🔥
'TIL' 카테고리의 다른 글
TIL - 2024/05/22 (0) | 2024.05.22 |
---|---|
TIL - 2024/05/21 (0) | 2024.05.21 |
TIL - 2024/05/17 (0) | 2024.05.17 |
TIL - 2024/05/16 (0) | 2024.05.16 |
TIL - 2024/05/14 (4) | 2024.05.14 |