Back-End/React.js, 스프링 부트, AWS로 배우는 웹 개발 101

[React.js, 스프링 부트, AWS로 배우는 웹 개발 101][백엔드 개발] - 서비스 개발

얄루몬 2022. 6. 11. 11:37

본 포스팅은 'React.js, 스프링 부트, AWS로 배우는 웹 개발 101 - 김다정'님의 책을 보고 작성되었습니다.


목차
1. Logger 설정
2. HTTP POST를 이용하는 Create REST API 개발
3. HTTP GET를 이용하는 Get REST API 개발
4. HTTP UPDATE를 이용하는 Update REST API 개발
5. HTTP DELETE를 이용하는 DeleteREST API 개발

1. Logger 설정

  • 기존의 출력문을 사용해서 로그를 남길 수 있지만 이 방법은 제한적이다. 그렇기에 우리는 @Slf4j 애노테이션을 사용해 용도에 따라 로그 정보를 남겨주는 방식을 사용할 것이다.
  • 로깅은 웹 서비스에 반드시 필요하며 로깅 없이 디버깅하는 것은 코와 입을 막고 숨을 쉬는 것과 똑같다고 한다.

로그 레벨

  1. info
  2. debug
  3. warn
  4. error

2. HTTP POST를 이용하는 Create REST API 개발

Todo 아이템을 생성하는 레포지토리, 서비스, 컨트롤러 등을 알아보고 구현한다.

퍼시스턴스 구현

TodoRepository를 사용하고 JpaRepository 상속을 하고 있으므로 JpaRepository가 제공하는 메서드를 사용할 수 있다. 엔티티 저장에선 save( )를 새 Todo 리스트 반환에는 우리가 만든 findByUserId( )를 사용한다.

서비스 구현

  • 검증: 넘어온 엔티티가 유효한지를 검사하는 로직으로 이 부분은 코드가 더 커지면 따로 클래스 분리도 가능하다.
  • save( ): 엔티티를 데이터베이스에 저장하고 로그를 남긴다.
  • findByUserId( ): 저장된 엔티티를 포함하는 새 리스트를 리턴한다.
package com.example.demo.service;

import com.example.demo.model.TodoEntity;
import com.example.demo.persistence.TodoRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j
@Service
public class TodoService {

    @Autowired
    private TodoRepository repository;


    public String testService(){

        //TodoEntity 생성
        TodoEntity entity = new TodoEntity().builder().title("My first todo item").build();

        //TodoEntity 저장
        repository.save(entity);

        //TodoEntity 검색
        TodoEntity savedEntity = repository.findById(entity.getId()).get();


        return savedEntity.getTitle();
    }

    public List<TodoEntity> create(final TodoEntity entity){
        validate(entity);
        repository.save(entity);

        log.info("Entity Id: {} is saved.",entity.getId());

        return repository.findByUserId(entity.getUserId());

    }
	
    private void validate(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");
        }
    }

}
  • 검증부분 메서드는 create 메서드 안에 넣어서 진행할 수도 있지만 계속 사용할 예정이라면 따로 private method로 리팩토링 해서 사용하는 것을 권장한다. (인텔리제이 기준 리팩토링 단축키 =  리팩토링할 부분 드래그 후 + ctrl + alt + n)

컨트롤러 구현

  • HTTP 응답을 반환할 때 비지니스 로직을 캡슐화하거나 추가 정보를 함께 반환하기 위해서는 Model이나 Entity가 아닌 DTO를 사용한다. 따라서 컨트롤러는 사용자에게 TodoDTO를 요청 바디로 넘겨 받고 이를 TodoEntity로 변환해서 저장해주어야 한다.
  • TodoService의 create( )가 리턴하는 TodoEntity는 TodoDTO로 변환해 리턴해주어야 한다.
package com.example.demo.dto;

import com.example.demo.model.TodoEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@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();
    }
	
    //DTO를 TodoEntity로 변환
    public static TodoEntity toEntity(final TodoDTO dto){
        return TodoEntity.builder()
                .id(dto.getId())
                .title(dto.getTitle())
                .done(dto.isDone())
                .build();
    }
}

toEntity를 이용한 컨트롤러

package com.example.demo.controller;


import com.example.demo.dto.ResponseDTO;
import com.example.demo.dto.TodoDTO;
import com.example.demo.model.TodoEntity;
import com.example.demo.service.TodoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("todo")
public class TodoController {

    //빈을 알아서 찾고 그 빈을 이 인스턴스 멤버 변수로 연결하라는 뜻이다.
    @Autowired
    private TodoService service;

    @GetMapping("/test")
    public ResponseEntity<?> testTodo(){
        String str = service.testService();//테스트 서비스 사용
        List<String> list = new ArrayList<>();
        list.add(str);

        ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
        return ResponseEntity.ok().body(response);

    }
    @PostMapping
    public ResponseEntity<?> createTodo(@RequestBody TodoDTO dto){
        try {
            String temporaryUserId = "temporary-user"; // 임시 유저 아이디

            //1 TOdoEntity 변환
            TodoEntity entity = TodoDTO.toEntity(dto);
            //2. id를 null로 초기화 해준다. 생성 당시에 id가 없어야 하기 때문이다.
            entity.setId(null);
            //3.임시 사용자 아이디를 설정해 준다. 지금은 당장 인가가 필요 없는 부분이기에 추후에 수정해줄 예정
            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 errorMessage = e.getMessage();
            ResponseDTO<TodoDTO> response =  ResponseDTO.<TodoDTO>builder().error(errorMessage).build();
            return ResponseEntity.badRequest().body(response);
        }
    }


}

테스트

HTTP POST를 이용한 Create Todo 테스트

3. HTTP GET를 이용하는 Retrieve REST API 개발

Todo 리스트를 검색하는 레포지토리, 서비스, 컨트롤러 구현한다.

퍼시스턴스 구현

새 Todo 리스트 반환을 위해 findByUserId()를 사용한다.

서비스 구현

레포지토리의 findById() 메서드를 이용해서 TodoService 코드와 같이 retrieve라는 메서드를 만들어 사용한다.

//TodoService의 retrieve 메서드
public List<TodoEntity> retrieve(final String userId){
    return repository.findByUserId(userId);
}

컨트롤러 구현

TodoController에 Get 메서드를 만들어준 뒤 메서드 내부는 서비스 코드를 사용해서 작성한다.

@GetMapping
public ResponseEntity<?> retrieveTodoList(){
    String temporaryUserId = "temporary-user"; // 임시 유저 아이디

    //1. 서비스 메서드의 retrieve() 메서드를 사용해 Todo 리스트를 가져온다.
    List<TodoEntity> entities = service.retrieve(temporaryUserId);

    //2. 자바 스트림을 사용해 리턴된 엔티티 리스트를 TodoDTD 리스트로 변환한다.
    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);
}

HTTP GET을 이용한 Retrieve Todo 테스트

JSON형태로 응답이 리턴되는 것을 확인할 수 있다.

4. HTTP UPDATE를 이용하는 Update REST API 개발

Todo를 업데이트 하는 레포지토리, 서비스, 컨트롤러를 구현한다.

퍼시스턴스 구현

업데이트를 위해 save( ), findByUserId( )를 사용한다.

서비스 구현

Update 기능을 구현하기 위해서 update( ) 메소드를 작성한다.

람다 X

    public List<TodoEntity> update(final TodoEntity entity){
        //1. 저장할 엔티티가 유효한지 확인해준다.
        validate(entity);

        //2. 넘겨받은 엔티티 id를 사용해서 TodoEntity를 가져온다. 존재하지 않는 경우라면 업데이트 할수 없기 때문이다.
        final Optional<TodoEntity> original = repository.findById(entity.getId());

        if(original.isPresent()){
            //3. 반환된 TodoEntity가 존재하면 값을 새 entity 값으로 덮어 씌운다.
            final TodoEntity todo = original.get();
            todo.setTitle(entity.getTitle());
            todo.setDone(entity.isDone());

            repository.save(todo);
        }
        return retrieve(entity.getUserId());
    }

람다 O

    public List<TodoEntity> update(final TodoEntity entity){
        //1. 저장할 엔티티가 유효한지 확인해준다.
        validate(entity);

        //2. 넘겨받은 엔티티 id를 사용해서 TodoEntity를 가져온다. 존재하지 않는 경우라면 업데이트 할수 없기 때문이다.
        final Optional<TodoEntity> original = repository.findById(entity.getId());

        original.isPresent(todo -> {
            todo.setTitle(entity.getTitle());
            todo.setDone(entity.isDone());
            
            repository.save(todo);
        });

        return retrieve(entity.getUserId());
    }
}

컨트롤러 구현

PUT 메서드를 사용해서 만들어주고 메서드 내부는 서비스 코드를 이용해 작성한다.

@PutMapping
    public ResponseEntity<?> updateTodo(@RequestBody TodoDTO dto){
        String temporaryUserId = "temporary-user"; // 임시 유저 아이디
        //1. dto를 entity로 변환
        TodoEntity entity = TodoDTO.toEntity(dto);

        //2. id를 임시 유저 아이디로 초기화한다. 여기 역시 수정할 예정
        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();
        
        return ResponseEntity.ok().body(response);
    }

HTTP PUT을 이용해 Todo 아이템 업데이트

기존 POST로 저장된 아이템 정보를 PUT을 사용해서 수정한 결과 성공적으로 수정 되었다.

5. HTTP DELETE를 이용하는 DeleteREST API 개발

퍼시스턴스 구현

업데이트를 위해 delete( ), findByUserId( )를 사용한다.

서비스 구현

삭제 시 업데이트를 위해서 delete() 메서드를 구현한다.

public List<TodoEntity> delete(final TodoEntity entity) {
        //1. 저장할 엔티티 유효성 검사를 한다
        validate(entity);

        try {
            //2. 엔티티를 삭제한다.
            repository.delete(entity);
        } catch (Exception e){
            //3. 예외 발생 시 id와 exception을 로깅한다.
            log.error("error deleting entity", entity.getId(), e);

            //4. 컨트롤러로 예외를 보낸다. 데이터베이스 내부 로직을 캡슐화 하기 위해서는 e를 리턴하지 않고 새 예외 오브젝트를 리턴해준다.
            throw new RuntimeException("error deleting entity " + entity.getId());
        }
        //5. 새 Todo 리스트를 가져온 뒤 리턴해준다.
        return retrieve(entity.getUserId());
    }

컨트롤러 구현

    @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){
            String errorMessage = e.getMessage();
            ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().error(errorMessage).build();
            return ResponseEntity.badRequest().body(response);
        }
    }

 

기본적인 CURD를 구현했으니 다음은 프론엔드 개발을 진행하겠다.