4번째 필수 강좌 Part 1. ch3. 12, 13강 요약
이번에는 PUT 맵핑을 통해 저장된 정보를 수정하는 기능을 구현해본다.
@PutMapping("/developer/{memberId}")
public DeveloperDetailDTO editDeveloper(
@PathVariable(name = "memberId") String memberId,
@RequestBody @Valid EditDeveloper.Request request
){
return dMakerService.editDeveloper(memberId, request);
}
GET 메서드로 DeveloperDetailDTO를 받아오던 것처럼 수정할 memberId를 Path variable로 받고, RequestBody로 수정할 내용 EditDeveloper.Request를 받아온다.
EditDeveloper는 이름이나 나이보다는 개발자 레벨, 개발 분야, 경력을 수정하도록 3가지 정보만 가지고 있다.
수정 사항을 따로 반환할 필요는 없으므로 fromEntity 메서드를 만들어주지는 않았다.
public class EditDeveloper {
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public static class Request {
@NotNull
private DeveloperLevel developerLevel;
@NotNull
private DeveloperSkillType developerSkillType;
@NotNull
@Min(0)
@Max(20)
private Integer experienceYears;
}
}
다음으로, 수정사항을 검증하는 메서드를 만든다. 개발자 레벨과 경력의 비즈니스 로직은 개발자 객체를 생성할 때와 동일하므로 그 부분만 따로 빼서 작성한다.
private void validateDeveloperLevel(DeveloperLevel developerLevel, Integer experienceYears) {
// business validation
if ((developerLevel == DeveloperLevel.SENIOR)
&& (experienceYears < 10)) {
// throw new RuntimeException("SENIOR needs 10 years of experience.");
throw new DMakerException(DMakerErrorCode.LEVEL_EXPERIENCE_YEARS_NOT_MATCHED);
}
if ((developerLevel == DeveloperLevel.JUNGNIOR) &&
(experienceYears < 4 || experienceYears > 10)) {
throw new DMakerException(DMakerErrorCode.LEVEL_EXPERIENCE_YEARS_NOT_MATCHED);
}
if ((developerLevel == DeveloperLevel.JUNIOR) &&
(experienceYears > 4)) {
throw new DMakerException(DMakerErrorCode.LEVEL_EXPERIENCE_YEARS_NOT_MATCHED);
}
}
따로 빼낸 메서드를 적용하여 수정사항 request를 검증하는 메서드를 만든다.
private void validateEditDeveloperRequest(EditDeveloper.Request request,
String memberId) {
validateDeveloperLevel(
request.getDeveloperLevel(),
request.getExperienceYears());
}
마지막으로, 개발자 정보를 수정하는 메서드를 만든다. memberId와 request 객체를 받아서 검증과정을 거친 후, 해당하는 memberId의 객체를 받아온다(없으면 예외처리). 그리고 수정사항 request 객체의 개발자 레벨, 개발 분야, 경력을 이 개발자 객체에 저장한 후 이를 DeveloperDetailDTO로 변환하여 반환한다.
@Transactional
public DeveloperDetailDTO editDeveloper(String memberId, EditDeveloper.Request request) {
validateEditDeveloperRequest(request, memberId);
Developer developer = developerRepository.findByMemberId(memberId)
.orElseThrow(() ->
new DMakerException(DMakerErrorCode.NO_DEVELOPER)
);
developer.setDeveloperLevel(request.getDeveloperLevel());
developer.setDeveloperSkillType(request.getDeveloperSkillType());
developer.setExperienceYears(request.getExperienceYears());
return DeveloperDetailDTO.fromEntity(developer);
}
이 때 DB에도 변경사항이 저장될 수 있도록 @Transactional 애너테이션을 붙여주어야 한다.
처음 testDeveloper 객체의 정보는 다음과 같다.
이를 JUNGNIOR로 레벨을 올리고, 경력을 5년으로 늘려보면 성공적으로 수정된 것을 확인할 수 있다.
GET 메서드로 다시 확인해봐도 같은 결과가 나오는 것을 통해 제대로 수정된 것을 확인할 수 있었다.
DeleteMapping을 사용해 개발자 정보를 삭제하는 기능을 만들었다. 삭제할 memberId를 받아 해당하는 개발자 정보를 삭제한다.
그런데 실제로 어떤 기록을 만들고 지우는 기능을 만든다고 할 때, 법률상의 문제나, sql 쿼리 효율 등으로 인해 실제로 정보를 DB에서 아예 삭제하지 않고 상태를 삭제됨으로 바꾸거나 하여 남겨둔다고 한다.
@DeleteMapping("/developer/{memberId}")
public DeveloperDetailDTO deleteDeveloper(
@PathVariable String memberId){
return dMakerService.deleteDeveloper(memberId);
}
DELETE 메서드로 요청이 들어올 경우 해당하는 Developer의 상태를 고용에서 퇴직 상태로 변경하도록 한다.
고용 상태 코드를 Enum으로 구현하였다.
@Getter
@AllArgsConstructor
public enum StatusCode {
EMPLOYED("고용"),
RETIRED("퇴직");
private final String description;
}
상태가 퇴직으로 변경된 Developer는 개발자 레벨, 개발 분야, 경력을 제외한 memberId와 이름만을 가진 RetiredDeveloper 객체로 저장한다.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@EntityListeners(AuditingEntityListener.class)
public class RetiredDeveloper {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String memberId;
private String name;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
RetiredDeveloper를 관리할 RetiredDeveloperRepository를 만들고
보통은 삭제(로 상태가 변경된)된 데이터도 그냥 한 테이블에서 관리하지 따로 테이블로 관리하지는 않는듯 하다.
@Repository
public interface RetiredDeveloperRepository
extends JpaRepository<RetiredDeveloper, Long> {
}
DELETE 요청이 들어오면, PathVariable로 전달받은 memberId를 가진 developer를 찾아 반환한다(없으면 NO_DEVELOPER 예외를 던진다). 이 developer의 statusCode를 RETIRED로 변경하고, 이 developer를 RetiredDeveloper 객체로 build하여 RetiredDeveloperRepository에 저장한다.
마지막으로 retiredDeveloper가 된 developer의 DeveloperDetailDTO를 반환한다.
@Transactional
public DeveloperDetailDTO deleteDeveloper(String memberId) {
// EMPLOYED -> RETIRED
Developer developer = developerRepository.findByMemberId(memberId)
.orElseThrow(() -> new DMakerException(DMakerErrorCode.NO_DEVELOPER));
developer.setStatusCode(StatusCode.RETIRED);
// save into RetiredDeveloper
RetiredDeveloper retiredDeveloper = RetiredDeveloper.builder()
.memberId(memberId)
.name(developer.getName())
.build();
retiredDeveloperRepository.save(retiredDeveloper);
return DeveloperDetailDTO.fromEntity(developer);
}
deleteDeveloper에서는
1) developer 객체의 StatusCode 변경
2) RetiredDeveloper 객체 저장
이라는 2가지 작업이 진행되고 있다. 이 때 @Transactional 애너테이션이 없다면, 중간에 예외가 발생하는 등 도중에 작업이 끊기는 경우 1번 작업만 실행되고 2번은 실행되지 않아 심각한 오류를 일으킬 수 있다.
Atomic한 특성을 지켜야 하기 때문에, 추후 언제 어떤 작업이 추가될지 모르는 만큼 작은 DB 작업이라고 해도 @Transactional 애너테이션을 아끼지 말고 사용해야 할 것 같다.