Spring MVC - API 계층 - Controller

김소희·2023년 4월 11일
1

📃 목차

  • Controller 요청처리
  • Controller 실습코드

계층형 아키텍처에서 API 계층의 모습

API 계층은 클라이언트로부터 요청을 받아 비즈니스 로직을 수행하는 서비스 계층을 호출하고, 그 결과를 클라이언트에게 반환하는 역할을 수행합니다.

API 계층을 구성하는 방법은 크게 2가지로 REST 컨트롤러와 MVC 컨트롤러가 있습니다.

  • REST 컨트롤러 : @RestController 애너테이션을 사용
  • MVC 컨트롤러: @Controller 애너테이션을 사용

Controller 요청 처리

엔드포인트는 애플리케이션의 외부에서 들어오는 요청을 처리하는 곳을 가리키는 용어로, 스프링에서는 일반적으로 컨트롤러가 해당 역할을 수행합니다.

컨트롤러의 역할을 담당하는 클래스를 @Controller 또는 @RestController 어노테이션을 이용해서 정의합니다. 또한, 요청과 매핑되는 메서드는 @RequestMapping 어노테이션을 이용해서 지정합니다.


커피 주문 애플리케이션의 Controller 설계

커피 주문 어플리케이션을 가정하고 예제 실습을 해보았습니다. (1)~(5)

(1) 애플리케이션을 제작하기 위해서 실질적으로 제일 먼저 해야되는 일은 애플리케이션의 경계를 설정하는 것과 애플리케이션 기능 구현을 위한 요구 사항을 수집하는 일입니다.

  • 주인이 커피 정보를 관리하는 기능
  • 고객이 커피 정보를 조회하는 기능
  • 고객이 커피를 주문하는 기능
  • 고객이 주문한 커피를 주인이 조회하는 기능

(2) 두번째는 생성한 프로젝트에 Java 패키지 구조를 잡는 것 입니다.
Java 패키지 구조에는 '기능 기반 패키지 구조'와 '계층 기반 패키지 구조'가 있습니다.
Spring Boot 팀에서는 테스트와 리팩토링이 용이하고, 향후에 마이크로 서비스 시스템으로의 분리가 상대적으로 용이한 기능 기반 패키지 구조 사용을 권장하고 있고, 이에 따라 coffee, member, order 패키지를 생성했습니다.

(3) 엔트리포인트 클래스는 Spring Initializr를 통해 프로젝트를 생성하면서 자동으로 생성되어 있기 때문에 생략합니다.

(4) Controller 구조 작성

  • @RestController
    : 해당 클래스가 REST API의 리소스를 처리하기 위한 API 엔드포인트로 동작함을 정의합니다.
    : 또한 애플리케이션 로딩 시, Spring Bean으로 등록해줍니다.

  • @RequestMapping("/v1/members")
    : 클라이언트의 요청과 클라이언트 요청을 처리하는 핸들러 메서드(Handler Method)를 매핑해주는 역할을 합니다. 뒤에(괄호로 요청 파라미터 URL을 쓰면 해당 요청 파라미터 값을 컨트롤러 메서드의 파라미터로 받아올 수 있습니다.)

package com.codestates.member;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController   
@RequestMapping("/v1/members")  
public class MemberController {
}

(5) MemberController의 핸들러 메서드 작성
@PostMapping, @GetMapping를 이용해서 postMember, getMember, getMembers 3개의 핸들러 메서드를 작성했지만 구현은 되어 있지 않아서 HttpStatus.OK를 반환합니다.

@PostMapping
    public ResponseEntity postMember(@RequestParam("email") String email,
                                     @RequestParam("name") String name,
                                     @RequestParam("phone") String phone) {
        
        Map<String, String> map = new HashMap<>();
        map.put("email", email);
        map.put("name", name);
        map.put("phone", phone);

        return new ResponseEntity<>(map, HttpStatus.CREATED);
    }

    @GetMapping("/{member-id}")
    public ResponseEntity getMember(@PathVariable("member-id") long memberId) {
        System.out.println("# memberId: " + memberId);
        // not implementation
        return new ResponseEntity<>(HttpStatus.OK);
    }

    @GetMapping
    public ResponseEntity getMembers() {
        System.out.println("# get Members");
        // not implementation
        return new ResponseEntity<>(HttpStatus.OK);
    }

이번에는 Postman을 이용해서 핸들러 메서드에게 API 요청을 보내고 응답 결과도 받아보았습니다.
커피 정보가 데이터베이스에 저장되어 있지 않고, Map에 저장되어 있습니다.
각각 수정과 삭제 요청을 수행하는 메서드를 작성했습니다.

package com.codestates.member;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/v1/members")
public class MemberController {
    private final Map<Long, Map<String, Object>> members = new HashMap<>();

    @PostConstruct //객체가 생성된 후 자동으로 호출되는 메서드, 객체의 초기화
    public void init() {
        Map<String, Object> member1 = new HashMap<>();
        long memberId = 1L;
        member1.put("memberId", memberId);
        member1.put("email", "hgd@gmail.com");
        member1.put("name", "홍길동");
        member1.put("phone", "010-1234-5678");

        members.put(memberId, member1);
    }

    // 1. 회원 정보 수정을 위한 핸들러 메서드 구현
    @PutMapping("update/{member-id}")
    public ResponseEntity updateMember(@PathVariable("member-id") long memberId) {
        members.get(memberId).replace("phone","010-1111-2222");

        return new ResponseEntity<>(members.get(memberId), HttpStatus.OK);
    }

    // 2. 회원 정보 삭제를 위한 핸들러 메서드 구현
    @DeleteMapping("delete/{member-id}")
    public ResponseEntity deleteMember(@PathVariable("member-id") long memberId) {
        members.remove(memberId);

        return  new ResponseEntity<>(members.remove(memberId), HttpStatus.NO_CONTENT);
    }


}


△ 전화번호 수정 요청, 결과 200 OK

△ 회원 정보 삭제 요청, 결과 204 No Content

package com.codestates.coffee;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/v1/coffees")
public class CoffeeController {
    private final Map<Long, Map<String, Object>> coffees = new HashMap<>();

    @PostConstruct
    public void init() {
        Map<String, Object> coffee1 = new HashMap<>();
        long coffeeId = 1L;
        coffee1.put("coffeeId", coffeeId);
        coffee1.put("korName", "바닐라 라떼");
        coffee1.put("engName", "Vanilla Latte");
        coffee1.put("price", 4500);

        coffees.put(coffeeId, coffee1);
    }

    // 1. 커피 정보 수정을 위한 핸들러 메서드 구현
    @PutMapping("update/{coffee-id}")
    public ResponseEntity updateCoffee (@PathVariable("coffee-id") long coffeeId) {
        coffees.get(coffeeId).replace("korName","바닐라 빈 라뗴");
        coffees.get(coffeeId).replace("price", 5000);

        return new ResponseEntity<>(coffees.get(coffeeId), HttpStatus.OK);
    }


    // 2. 커피 정보 삭제를 위한 핸들러 서드 구현
    @DeleteMapping("delete/{coffee-id}")
    public ResponseEntity deleteCoffee (@PathVariable("coffee-id") long coffeeId) {
        coffees.remove(coffeeId);

        return  new ResponseEntity<>(coffees.remove(coffeeId), HttpStatus.NO_CONTENT);
    }
}


△ 이름과 가격 변경 요청, 결과 200 OK

△ 커피 정보 삭제 요청, 결과 204 No Content

현재 요청 매개변수의 개수가 증가할수록 @RequestParam도 여러번 사용되는데, 따로따로 받지않고 요청 데이터를 하나의 객체로 전달받기위해 DTO클래스를 사용할 수 있습니다.
그럴경우 @RequestBody를 사용합니다. (@RequestBody CoffeePostDto coffeePostDto)
@RequestBody는 Json형태의 RequestBody를 DTO객체로 변환하는 역할로 만약 Json가 아닐시에는 Unsupported Media Type가 포함된 오류메세지를 받게 됩니다.

수정된 최종 컨트롤러 코드

package com.codestates.coffee.controller;

import com.codestates.coffee.dto.CoffeePatchDto;
import com.codestates.coffee.dto.CoffeePostDto;
import com.codestates.coffee.dto.CoffeeResponseDto;
import com.codestates.coffee.entity.Coffee;
import com.codestates.coffee.mapper.CoffeeMapper;
import com.codestates.coffee.service.CoffeeService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Positive;
import java.util.List;

@RestController
@RequestMapping("/v10/coffees")
@Validated
public class CoffeeController {
    private CoffeeService coffeeService;
    private CoffeeMapper mapper;

    public CoffeeController(CoffeeService coffeeService, CoffeeMapper coffeeMapper) {
        this.coffeeService = coffeeService;
        this.mapper = coffeeMapper;
    }

    @PostMapping
    public ResponseEntity postCoffee(@Valid @RequestBody CoffeePostDto coffeePostDto) {
        Coffee coffee = coffeeService.createCoffee(mapper.coffeePostDtoToCoffee(coffeePostDto));

        return new ResponseEntity<>(mapper.coffeeToCoffeeResponseDto(coffee), HttpStatus.CREATED);
    }

    @PatchMapping("/{coffee-id}")
    public ResponseEntity patchCoffee(@PathVariable("coffee-id") @Positive long coffeeId,
                                      @Valid @RequestBody CoffeePatchDto coffeePatchDto) {
        coffeePatchDto.setCoffeeId(coffeeId);
        Coffee coffee = coffeeService.updateCoffee(mapper.coffeePatchDtoToCoffee(coffeePatchDto));

        return new ResponseEntity<>(mapper.coffeeToCoffeeResponseDto(coffee), HttpStatus.OK);
    }

    @GetMapping("/{coffee-id}")
    public ResponseEntity getCoffee(@PathVariable("coffee-id") long coffeeId) {
        Coffee coffee = coffeeService.findCoffee(coffeeId);

        return new ResponseEntity<>(mapper.coffeeToCoffeeResponseDto(coffee), HttpStatus.OK);
    }

    @GetMapping
    public ResponseEntity getCoffees() {
        List<Coffee> coffees = coffeeService.findCoffees();
        List<CoffeeResponseDto> response = mapper.coffeesToCoffeeResponseDtos(coffees);

        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @DeleteMapping("/{coffee-id}")
    public ResponseEntity deleteCoffee(@PathVariable("coffee-id") long coffeeId) {
        coffeeService.deleteCoffee(coffeeId);

        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}

안보고도 코드를 작성할 수 있을 때까지 여러번 반복해봐야 겠다는 생각이 들었습니다.

profile
백엔드 자바 개발자 소희의 노트

0개의 댓글