'React.js, 스프링 부트, AWS로 배우는 웹 개발 101'을 읽고 정리한 글입니다.
실습 내용
- Logger 설정
- HTTP POST를 이용하는 Create REST API 개발
- HTTP GET을 이용하는 Retrieve REST API 개발
- HTTP UPDATE를 이용하는 Update REST API 개발
- HTTP DELETE를 이용하는 Delete REST API 개발
스프링과 JPA 어노테이션을 기반으로 생성, 검색, 수정, 삭제 API를 작성해보자.
구현 과정은 퍼시스턴스 => 서비스 => 컨트롤러 순으로 한다.
로그 어노테이션
서비스 구현에 앞서 디버깅을 위한 로그 설정을 하자. 가장 간단하게 출력하는 방법이 있지만 기능이 제한적이다.
어떤 로그인 정보를 위한 것이고 어떤 로그는 디버깅을 위한 자세한 정보일 수 있다.
용도에 따라 로그를 크게 info, debug, warn, error로 나누고 이를 로그 레벨이라고 부른다.
로깅은 웹 서비스에서 반드시 필요하다. 그중 Slf4j는 로그계의 JPA 정도로 생각하면 된다. Slf4j를 사용하려면 구현부를 연결해 줘야 한다. 이 연결 작업은 스프링이 알아서 해준다. 스프링은 기본적으로 Logback 로그 라이브러리를 사용한다.
@Slf4j
@Service
public class TodoService {
로깅할 클래스에 @Slf4j 어노테이션을 추가하자.
2.3.1 Create Todo 구현
Todo 아이템을 생성하는 리포지터리, 서비스, 컨트롤러 등을 알아보고 구현한다.
퍼시스턴스 구현
- TodoRepository는 JpaRepository를 상속하므로 JpaRepository가 제공하는 메서드를 사용할 수 있다.
- 엔티티 저장에는 save 메서드를, 새 Todo 리스트 반환에는 findByUerId() 메서드를 사용한다.
서비스 구현
서비스 추가를 위해 TodoService에 create 메서드를 작성하자.
public List<TodoEntity> create(final TodoEntity entity) {
// Validations
if (entity == null) {
log.warn("Entity cannot be null.");
throw new RuntimeException("Entity cannot be null");
}
if (entity.getUserId() == null) {
log.warn("Unknown user.");
throw new RuntimeException("Unknown user.");
}
repository.save(entity);
log.info("Entity Id : {} is saved", entity.getId());
return repository.findByUserId(entity.getUserId());
}
메서드는 크게 세 단계로 구성돼 있다.
- 검증 Validation: 넘어온 엔티티가 유효한지 검사하는 로직으로 이부분은 코드가 더 커지면 분리시킬 수 있다.
- save(): 엔티티를 데이터베이스에 저장하고 로그를 남긴다.
- findByUserId(): 저장된 엔티티를 포함하는 새 리스트를 리턴한다.
코드 리팩토링
검증 부분은 다른 메서드에서도 계속 쓰일 예정이므로 private method로 리팩토링한다.
public List<TodoEntity> create(final TodoEntity entity) {
// Validations
validate(entity);
repository.save(entity);
log.info("Entity Id : {} is saved", entity.getId());
return repository.findByUserId(entity.getUserId());
}
// 리팩토링한 메서드
private void validate(final TodoEntity entity) {
if (entity == null) {
log.warn("Entity cannot be null.");
throw new RuntimeException("Entity cannot be null");
}
if (entity.getUserId() == null) {
log.warn("Unknown user.");
throw new RuntimeException("Unknown user.");
}
}
컨트롤러 구현
HTTP 응답을 반환할 때 비즈니스 로직을 캡슐화하거나 추가적인 정보를 함께 반환하려고 DTO를 사용한다고 했다.
따라서 컨트롤러는 사용자에게서 TodoDTO를 요청 바디로 넘겨받고 이를 TodoEntity로 변환해 저장해야 한다.
또 TodoService의 create()가 리턴하는 TodoEntity를 TodoDTO로 변환해 리턴해야 한다.
TodoDTO에 DTO를 Entity로 변환하려면 아래와 같이 toEntity 메서드를 작성하자.
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class TodoDTO {
private String id;
private String title;
private boolean done;
public TodoDTO(final TodoEntity entity) {
this.id = entity.getId();
this.title = entity.getTitle();
this.done = entity.isDone();
}
public static TodoEntity toEntity(final TodoDTO dto) {
return TodoEntity.builder()
.id(dto.getId())
.title(dto.getTitle())
.done(dto.isDone())
.build();
}
}
그 다음 TodoController를 구현하자.
@PostMapping
public ResponseEntity<?> createTodo(@RequestBody TodoDTO dto) {
try {
String temporaryUserId = "temporary-user"; // temporary user id.
/* (1) TodoEntity로 변환한다. */
TodoEntity entity = TodoDTO.toEntity(dto);
/* (2) id를 null로 초기화한다. 생성 당시에는 id가 없어야 하기 때문이다. */
entity.setId(null);
/* (3) 임시 사용자 아이디를 설정해 준다. 이 부분은 4장 인증과 인가에서 수정할 예정이다.
지금은 인증과 인가 기능이 없으므로 한 사용자만 로그인 없이 사용할 수 있는 애플리케이션인 셈이다. */
entity.setUserId(temporaryUserId);
/* (4) 서비스를 이용해 Todo 엔티티를 생성한다. */
List<TodoEntity> entities = service.create(entity);
/* (5) 자바 스트림을 이용해 리턴된 엔티티 리스트를 TodoDTO 리스트로 변환한다. */
List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
/* (6) 변환된 TodoDTO 리스트를 이용해 ResponseDTO를 초기화한다. */
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
/* (7) ResponseDTO를 리턴한다. */
return ResponseEntity.ok().body(response);
} catch (Exception e) {
/* (8) 예외가 있는 경우 dto 대신 error에 메시지를 넣어 리턴한다. */
String error = e.getMessage();
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().error(error).build();
return ResponseEntity.badRequest().body(response);
}
}
테스팅
포스트맨을 이용해 Body에 아래와 같이 작성하며 실행하면 정상적으로 동작하는 것을 알 수 있다.
2.3.2 Retrieve Todo 구현
Todo 리스트를 검색하는 리포지터리, 서비스, 컨트롤러를 구현한다.
퍼시스턴스 구현
- 퍼시스턴스로 TodoRepository를 사용
- 새 Todo 리스트 반환을 위해 findByUerId() 메서드 사용
서비스 구현
TodoService에 아래와 같이 retrieve 메서드를 구현한다.
public List<TodoEntity> retrieve(final String userId) {
return repository.findByUserId(userId);
}
컨트롤러 구현
TodoController에 새 GET 메서드를 만들어 작성한다.
@GetMapping
public ResponseEntity<?> retrieveTodoList() {
String temporaryUserId = "temporary-user"; // termporary user id.
/* (1) 서비스 메서드의 retrieve() 메서드를 사용해 Todo 리스트를 가져온다.*/
List<TodoEntity> entities = service.retrieve(temporaryUserId);
/* (2) 자바 스트림을 이용해 리턴된 엔티티 리스트를 TodoDTO 리스트로 변환한다. */
List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
/* (3) 변환된 todoDTO 리스트를 이용해 ResponseDTO를 초기화한다. */
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
/* (4) ResponseDTO를 리턴한다. */
return ResponseEntity.ok().body(response);
}
테스팅
이전에 테스팅 했던 Todo 아이템은 없어졌을 것이다.
HTTP POST 메서드로 새 Todo 아이템을 create한 후 HTTP GET 메서드로 리스트를 받아보자.
2.3.3 Update Todo 구현
Todo를 업데이트하는 리포지터리, 서비스, 컨트롤러를 구현한다.
퍼시스턴스 구현
- 퍼시스턴스로 TodoRepository 사용
- save(), findByUserId() 메서드 사용
서비스 구현
Update 메서드 작성
public List<TodoEntity> update(final TodoEntity entity) {
/* (1) 저장할 엔티티 유효한지 확인 */
validate(entity);
/* (2) 넘겨받은 엔티티 id를 이용해 TodoEntity를 가져온다. 존재하지 않는 엔티티는 업데이트 할 수 없다. */
final Optional<TodoEntity> original = repository.findById(entity.getId());
/* (3) 반환된 TodoEntity가 존재하면 값을 새 entity 값으로 덮어 씌운다. */
original.ifPresent(todo -> {
todo.setTitle(entity.getTitle());
todo.setDone(entity.isDone());
/* (4) 데이터베이스에 새 값을 저장한다. */
repository.save(todo);
});
return retrieve(entity.getUserId());
}
컨트롤러 구현
TodoController에 PUT 메서드를 만들고 update 메서드를 이용해 작성한다.
@PutMapping
public ResponseEntity<?> updateTodo(@RequestBody TodoDTO dto) {
String temporaryUserId = "temporary-user";
/* (1) dto를 entity로 변환한다.*/
TodoEntity entity = TodoDTO.toEntity(dto);
/* (2) id를 temporaryUserId로 초기화한다. */
entity.setUserId(temporaryUserId);
/* (3) 서비스를 이용해 entity를 업데이트한다. */
List<TodoEntity> entities = service.update(entity);
/* (4) 자바 스트림을 이용해 리턴된 엔티티 리스트를 TodoDTO 리스트로 변환한다. */
List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
/* (5) 변환된 TodoDTO 리스트를 이용해 ResponseDTO를 초기화한다. */
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
/* (6) ResponseDTO를 리턴한다. */
return ResponseEntity.ok().body(response);
}
테스팅
Todo 아이템 생성한 후 HTTP PUT 메서드로 수정 테스트
2.3.4 Delete Todo 구현
Todo를 삭제하는 리포지터리, 서비스, 컨트롤러를 구현한다.
퍼시스턴스 구현
- 퍼시스턴스로 TodoRepository를 사용
- delete(), findByUserId() 메서드 사용
서비스 구현
TodoService에 delete() 메서드 구현
public List<TodoEntity> delete(final TodoEntity entity) {
/* (1) 삭제할 엔티티티가 유효한지 확인한다. */
validate(entity);
try {
/* (2) 엔티티 삭제 */
repository.delete(entity);
} catch(Exception e) {
/* (3) exception 발생 시 id와 exception을 로깅 */
log.error("error deleting entity ", entity.getId(), e);
/* (4) 컨트롤러로 exception을 보낸다.
데이터베이스 내부 로직을 캡슐화하려면 e를 리턴하지 않고 새 exception 오브젝트를 리턴한다. */
throw new RuntimeException("error deleting entity " + entity.getId());
}
/* (5) 새 Todo 리스트를 가져와 리턴한다. */
return retrieve(entity.getUserId());
}
컨트롤러 구현
TodoController에 DELETE 메서드 생성하고 서비스 코드를 이용해 구현한다.
@DeleteMapping
public ResponseEntity<?> deleteTodo(@RequestBody TodoDTO dto) {
try {
String temporaryUserId = "temporary-user";
/* (1) TodoEntity로 변환한다. */
TodoEntity entity = TodoDTO.toEntity(dto);
/* (2) 임시 사용자 아이디 설정 */
entity.setUserId(temporaryUserId);
/* (3) 서비스를 이용해 entity를 삭제 */
List<TodoEntity> entities = service.delete(entity);
/* (4) 자바 스트림을 이용해 리턴된 엔티티 리스트를 TodoDTO 리스트로 변환한다. */
List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
/* (5) 변환된 TodoDTO 리스트를 이용해 ResponseDTO를 초기화한다. */
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
/* (6) ResponseDTO를 리턴한다. */
return ResponseEntity.ok().body(response);
} catch (Exception e) {
/* (7) 예외가 있는 경우 dto 대신 error에 메시지를 넣어 리턴한다. */
String error = e.getMessage();
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().error(error).build();
return ResponseEntity.badRequest().body(response);
}
}
테스팅
Todo 아이템 생성한 후 HTTP DELETE 메서드로 삭제 테스트
- 삭제할 때 요청 바디로 id만 명시하는 것에 주의하자.
다른 필드는 삭제를 위해 사용하지 않을 것이기 때문이다. - DELETE 요청을 실행한 후 Todo 아이템이 삭제돼 빈 리스트가 리턴되는 것을 확인할 수 있다.