study/Spring boot

REST API 만들기

올스왑 2021. 3. 31. 20:21

간단히 설명

REST API는 정보를 주고받는 형식이다.
소프트웨어와 소프트웨어(소프트웨어가 아닌 것들도)가 지정된 형식으로 정보를 요청, 응답하는 수단을 API라고 한다.
요즘은 REST란 형식의 API를 사용한다. 과거의 soap를 대체한 것이다.
REST의 특성은 각 요청이 어떤 동작이나 정보를 위한 것인지 요청의 모습 자체로 추론이 가능하다.
REST API 요청할 때, HTTP의 메소드로 요청한다.

 

참고 자료: www.youtube.com/watch?v=iOueE9AXDQQ&t=126s


스프링 부트에서 JPA를 사용해 REST API를 만들 수 있다.

 

1. 요청, 응답에 사용하는 메시지 클래스를 만든다.

 

스프링에선 객체를 그냥 리턴해도 JSON으로 반환하기 때문에 주고받을 정보의 필드를 갖는 클래스를 만든다.

단순히 한종류의 데이터를 위한 api를 만든다면 상관없지만, 여러 종류의 데이터를 주고받기 위한 api를 만든다면 제네릭을 사용해 주고받을 데이터의 타입을 전달한다.

 

클래스의 필드로는 데이터 타입과 상관없는 정보(시간, 응답 코드 등)와 데이터 타입과 관련된 정보가 있을 것이다.

public class Message<T> {
	// 통신 시간
    private LocalDateTime transactionTime;
    // 응답 코드
    private String resultCode;
    // 주고 받을 데이터
    private T data;
}

 

api를 통해 주고받을 데이터들에 대해 요청과 응답에 대한 객체를 따로 만들어 준다.

-> 같은 데이터라도 요청 시 전달하는 정보의 범위, 응답 시 전달하는 정보의 범위가 다르기 때문이다.

ex) ItemRequest.java, ItemResponse.java

이렇게 작성한 요청, 응답 데이터 클래스는 메시지 클래스에서 사용된다.

 

그리고 메시지 클래스에 public static으로 여러 종류의 통신 메시지를 반환하는 코드를 작성한다. @Build 어노테이션을 적용해 빌드 생성자 패턴을 사용했다.

// OK
public static <T> Message<T> OK(){
	return (Message<T>) Message.builder()
    .transactionTime(LocalDateTime.now())
    .resultCode("OK")
    .build();
}

// DATA OK
public static <T> Message<T> OK(T data){
	return (Message<T>) Message.builder()
	.transactionTime(LocalDateTime.now())
	.resultCode("OK")
	.data(data)
	.build();
}

// ERROR
public static <T> Message<T> ERROR(){
	return (Message<T>) Message.builder()
	.transactionTime(LocalDateTime.now())
	.resultCode("ERROR")
	.build();
}

해당 메소드들이 호출되면, 전달받은 데이터를 사용해 상황에 맞는 메시지 객체를 만들어 반환한다. 이 메서드들은 사용자의 요청을 처리하는 컨트롤러에서 호출하게 된다.

 

 

2. CRUD 인터페이스, REST API 컨트롤러 만들기

 

데이터 타입마다 REST API 컨트롤러를 개발하게 되기 때문에, 공통적으로 구현하는 CRUD 메소드에 대해 따로 인터페이스를 만든다.

CRUD 인터페이스는 요청 데이터 클래스, 응답 데이터 클래스 총 두개의 타입을 제네릭으로 받는다. -> 요청 데이터, 응답 데이터 모두 처리하기 때문에

 

CRUD에 대해 메소드를 작성한다.

public interface CrudInterface<Requset, Response> {
    Message<Response> create(Message<Requset> request);
    Message<Response> read(Integer id);
    Message<Response> update(Header<Requset> request);
    Message delete(Integer id);
}

Create - 전달된 요청 데이터 타입을 갖는 메시지 객체를 전달받으면, 응답 데이터 타입을 갖는 메시지 객체를 반환한다.

Read - Id 값을 전달받고 응답 데이터 타입을 갖는 메시지 객체를 반환한다.

Update - Create 와 같다.

Delete - Read와 같다.

 

그리고 API에서 사용할 데이터마다 CRUD 인터페이스를 구현하는 컨트롤러를 만든다.

create는 Post 요청으로 Http의 body 값을 받고, read, delete는 Get 요청으로 쿼리 스트링을 받고, Update는 Put 요청으로 Http의 body 값을 받는다.

@RestController
@RequestMapping("/itemapi")
public class ItemApiController implements CrudInterface<ItemRequest, ItemResponse> {

    @Override
    @PostMapping("") // /itemapi
    public Message<ItemResponse> create(@RequestBody Message<ItemRequest> request) {
        return null;
    }

    @Override
    @GetMapping("{id}") // /itemapi/{id}
    public Message<ItemResponse> read(@PathVariable Long id) {
        return null;
    }

    @Override
    @PutMapping("") // /itemapi
    public Message<ItemResponse> update(@RequestBody Message<ItemRequest> request) {
        return null;
    }

    @Override
    @DeleteMapping("{id}") // /itemapi/{id}
    public Message delete(@PathVariable Long id) {
        return null;
    }
}

 

 

3, CRUD 로직을 처리하는 서비스 클래스를 만든다.

 

먼저 @Service에 대해 설명

@Service : 해당 클래스를 Bean 객체로 생성해주는 어노테이션. 로직을 처리하는 서비스 클래스가 된다.

 

컨트롤러는 요청을 받으면 해당 로직을 구현하는 서비스 메소드에게 전달한다. 따라서 컨트롤러가 원래 갖고 있는 메소드들을 구현하기 때문에 똑같이 CRUD 인터페이스를 구현한다. 그리고 데이터를 직접 처리하기 때문에, 데이터에 대한 JpaRepository에 대한 의존성을 주입받는다.

 

Create:
1. 요청 메시지를 가져오고
2. 그 데이터로 객체를 만들고
3. 만든 객체를 DB에 저장하고, 응답 메시지를 만들어 리턴한다
--
Read:
1. id를 통해 DB에서 객체를 가져온다.
2. 존재한다면 응답 메시지를 만들어 반환하고
3. 없다면 에러 메시지를 반환한다.
--
Update:
1.  요청 메시지를 가져오고
2. id값으로 DB에서 객체를 찾는다.
3. 존재한다면, 값을 업데이트해서 DB에 저장하고 응답 메시지를 만들어 반환한다.
4. 없다면, 에러 메시지를 반환한다.
--
Delete:
1. id를 통해 DB에서 객체를 가져온다.
2. 존재한다면 데이터를 삭제하고 응답 메시지를 만들어 반환한다.
3. 없다면 에러 메시지를 반환한다.

 

CRUD 마다 메시지를 만들어 반환하기 때문에, 따로 response 메소드를 만들었다.

 

@Service
public class ItemService implements CrudInterface<ItemRequest, ItemResponse> {

    @Autowired
    private ItemRepository itemRepository;

    @Override
    public Message<ItemResponse> create(Message<ItemRequest> request) {
        // 1. 요청 메시지를 가져오고
        ItemRequest i = request.getData();
        // 2. 그 데이터로 객체를 만들고
        Item item = Item.builder()
                .name(i.getName())
                .price(i.getPrice())
                .build();
		// 3. 만든 객체를 DB에 저장하고, 응답 메시지를 만들어 리턴한다
        Item newItem = itemRepository.save(item);
        return response(newItem);

    }

    @Override
    public Message<ItemApiResponse> read(Long id) {
        // 1. id를 통해 DB에서 객체를 가져온다.
        // 2. 존재한다면 응답 메시지를 만들어 반환하고
        return itemRepository.findById(id)
            .map(item -> response(item))
            .orElseGet(() -> Message.ERROR()); // 3. 없다면 에러 메시지를 반환한다.
    }

    @Override
    public Message<ItemApiResponse> update(Message<ItemApiRequest> request) {
        // 1.  요청 메시지를 가져오고
        ItemApiRequest item = request.getData();
        // 2. id값으로 DB에서 객체를 찾는다.
        Optional<Item> optional = itemRepository.findById(item.getId());        
        // 3. 존재한다면, 값을 업데이트해서 DB에 저장하고 응답 메시지를 만들어 반환한다.
        return optional.map(updateItem -> {
            updateItem.setName(item.getName())
                    .setPrice(item.getPrice());
            return updateItem;
        })
        .map(newItem -> itemRepository.save(newItem))
        .map(item -> response(item))
        .orElseGet(() -> Message.ERROR()); // 4. 없다면, 에러 메시지를 반환한다.
    }

    @Override
    public Header delete(Long id) {
        // 1. id를 통해 DB에서 객체를 가져온다.
        // 2. 존재한다면 데이터를 삭제하고 응답 메시지를 만들어 반환한다.
        return itemRepository.findById(id)
            .map(item -> {
                itemRepository.delete(item);
                return Header.OK();
        })
        .orElseGet(() -> Header.ERROR()); // 3. 없다면 에러 메시지를 반환한다.
    }

    private Message<ItemResponse> response(Item item){
        // item으로 응답 메시지를 만들어서 리턴
        ItemResponse body = ItemResponse.builder()
                .id(item.getId())
                .name(item.getName())
                .price(item.getPrice())
                .build();
        return Message.OK(body);
    }
}

 

4. 서비스 클래스를 사용해 API 컨트롤러의 메소드를 구현한다.

 

@Autowired로 서비스 클래스를 선언하고, 각 CRUD 메소드는 클라이언트로부터 받은 값을 서비스 클래스에 전달하고 반환받는 메시지를 클라이언트에게 전달한다.

@RestController
@RequestMapping("/itemapi")
public class ItemApiController implements CrudInterface<ItemRequest, ItemResponse> {
	
    @Autowired
    private ItemService itemService;
    
    @Override
    @PostMapping("") // /itemapi
    public Message<ItemResponse> create(@RequestBody Message<ItemRequest> request) {
        return itemService.create(request);
    }

    @Override
    @GetMapping("{id}") // /itemapi/{id}
    public Message<ItemResponse> read(@PathVariable Long id) {
        return itemService.read(id);
    }

    @Override
    @PutMapping("") // /itemapi
    public Message<ItemResponse> update(@RequestBody Message<ItemRequest> request) {
        return itemService.update(request);
    }

    @Override
    @DeleteMapping("{id}") // /itemapi/{id}
    public Message delete(@PathVariable Long id) {
        return itemService(id);
    }
}

 

지금까지의 과정을 마치면 rest api를 만들게 된다!!

 

추가적으로 application.properties에서 JSON 반환 값 형태를 설정할 수 있다.
spring.jackson.property-naming-strategy=SNAKE_CASE -> 스네이크 케이스로 반환한다.

-> 코드 작성은 camel case로 JSON 반환은 snake case로 바꿀 수 있다.