[Spring] API 계층 - Controller
개요
Spring에서 지원하는 모든 기능들을 포함해서 Spring Framework이라고 부른다.
Spring의 모듈 중에는 웹 계층을 담당하는 몇 가지 모듈이 있는데 특히 서블릿(Servket) API를 기반으로 클라이언트의 요청을 처리하는 모듈이 있다. 이 모듈 이름은 spring-webmvc라고 한다(Spring MVC혹은 Spring MVC 프레임워크라고 부른다)
- 클라이언트의 요청을 처리하는 모듈인 서블릿 API를 기반으로 동작하는 모듈을 Spring MVC라고 부른다.
- Spring MVC는 클라이언트의 요청을 편리하게 처리해 주는 프레임워크이다.
서블릿이란?
클라이언트의 요청을 처리하도록 특정 규약에 맞추어서 Java 코드로 작성하는 클래스 파일이다. 아파치 톰캣은 이러한 서블릿들이 웹 애플리케이션으로 실행되도록 해주는 서블릿 컨테이너 중 하나이다!
Spring MVC
Model
MVC중 M에 해당한다.
Spring MVC 기반 웹 애플리케이션이 클라이언트의 요청을 전달받으면 요청 사항을 처리하기 위한 작업을 한다.
Model은 이렇게 처리한 작업의 결과를 클라이언트에게 응답으로 돌려주는 결과 데이터를 뜻한다.
클라이언트의 요청 사항을 구체적으로 처리하는 영역을 서비스 계층(Service Layer)이라 한다.
요청 사항을 처리하기 위해 java코드로 구현한 것을 비즈니스 로직이라고 한다.
- Model은 웹 애플리케이션의 작업 처리 결과
View
View는 MVC중에서 V에 해당한다.
Model 데이터를 이용해서 웹브라우저 같은 클라이언트 애플리케이션의 화면에 보이는 리소스를 제공하는 역할을 한다.
우리가 실질적으로 학습하게 되는 View는 JSON 포맷의 데이터를 생성한다.
우리가 사용하게 될 View의 형태
Model 데이터를 XML, JSON등 특정 형식의 포맷으로의 변환하는 것
Model 데이터를 특정 프로토콜 형태로 변환해서 변환된 데이터를 클라이언트 측에 전송하는 방식
서버는 특정 형식의 데이터만 전송하고, 프런트엔드 측에서 이 데이터를 기반으로 HTML 페이지를 만드는 방식이다.
JSON이란?
Spring MVC에서 클라이언트 애플리케이션과 서버 애플리케이션이 주고받는 데이터 형식이다.
과거는 XML ➡ 현재는 JSON(상대적으로 가볍고 복잡하지 않음)
기본 포맷 : { "속성" : "값" }
Controller
Spring MVC에서 C에 해당합니다.
클라이언트 측의 요청을 직접적으로 전달 받는 엔드포인트로써
Model과 View의 중간에서 상호 작용을 해주는 역할을 합니다.
즉, 컨트롤러의 요청을 받아서 Controller 내부의 비즈니스 로직을 거친 후에 Model 데이터가 만들어지면,
이 Model데이터를 View로 전달하는 역할을 합니다.
- CoffeeController 코드 예
@RestController
@RequestMapping(path = "/v1/coffee")
public class CoffeeController {
private final CoffeeService coffeeService;
CoffeeController(CoffeeService coffeeService) {
this.coffeeService = coffeeService;
}
@GetMapping("/{coffee-id}") // (1)
public Coffee getCoffee(@PathVariable("coffee-id") long coffeeId) {
return coffeeService.findCoffee(coffeeId); // (2)
}
}
컨트롤러가 클라이언트의 요청을 전달받아서 비즈니스 로직을 거친다.
CoffeeController의 getCoffee 메서드가 반환하는 Coffee 객체가 Model 데이터가 된다. 컨트롤러는 이 Model 데이터를 View로 전달하는 역할을 한다.
Spring MVC 동작 방식과 구성 요소
Spring MVC에서 클라이언트의 요청이 어떤 과정을 거쳐서 Controller까지 전달되는가?
- 클라이언트가 요청을 전송하면, DispatcherServlet이라는 클래스에 요청이 전달된다.
- DispatcherServlet은 요청을 처리할 Controller에 대한 검색을 HandlerMapping 인터페이스에게 요청한다.
- HandlerMapping은 클라이언트 요청과 매핑되는 핸들러 객체를 다시 DispatcherServlet에게 리턴한다.
- 핸들러 객체는 해당 핸들러의 Handler 메서드를 포함하고 있다. 핸들러 메서드는 Controller 클래스 안에 구현된 요청 처리 메서드이다.
- 요청을 처리할 Controller 클래스를 알았으니, 실제로 클라이언트 요청을 처리할 Handler 메서드를 찾아서 호출해야 한다. HandlerAdapter에게 Handler 메서드 호출을 위임한다.
- HandlerAdapter는 HandlerMapping이 DispatcherServlet에게 전달해 줘서 전달받은 Controller 정보를 기반으로 해당 Controller의 Handler 메서드를 호출한다.
- Controller의 Handler 메서드는 비즈니스 로직 처리 후 전달받은 Model 데이터를 HandlerAdapter에게 전달한다.
- HandlerAdapter는 전달받은 Model 데이터와 View 정보를 다시 DispatcherServlet에게 전달한다.
- DispatcherServlet은 ViewResolver(뷰 검색 위임), View(Model 데이터 전달)를 통해 전달받은 응답 데이터를 최종적으로 클라이언트에게 전달한다.
DispatcherServlet : 클라이언트의 요청을 제일 먼저 받는 구성요소.
애플리케이션의 가장 앞단에 배치되어 다른 구성요소들과 상호작용하면서 클라이언트의 요청을 처리하는 패턴을
Front Controller Pattern이라 한다.
HandlerMapping : Controller 검색
HandlerAdapter : Controller 클래스 안에 있는 핸들러 메서드 호출
ViewResolver : HandlerAdapter가 전달해 준 View정보를 검색하기. 해당하는 View 리턴
View : 전달받은 Model 데이터와 View를 통해 응답 데이터 생성하여 전달
DispatcherServlet : 최종 응답 데이터를 클라이언트에게 전달
Controller 클래스 개요
클라이언트의 HTTP 요청을 직접적으로 전달받는 Controller에 대해서 살펴보고 직접 구현해 본다.
API 계층은 클라이언트의 요청을 직접적으로 전달받는 계층이다. 클라이언트 요청 흐름의 끝에는 Controller가 있다.
Controller 클래스가 Spring MVC에서 클라이언트의 최종 목적지가 된다.
- 클라이언트의 요청이 결국 컨트롤러 클래스(Handler) 내의 Handeler 메서드(@GetMapping, @PostMapping)까지 가야 한다는 의미에서 나온 말인가?
- 자바의 패키지 구조 : 기능 기반 패키지 구조 ✅ VS 계층 기반 패키지 구조
Spring boot기반 애플리케이션이 정상적으로 실행되기 위해선 main( ) 메서드가 포함된 엔트리 포인트가 필요하다.
- 'Spring Initializr'을 통해 생성한 프로젝트에는 엔트리포인트 클래스가 이미 작성되어 있다!
- @SpringBootApplication ⬅ Spring Bean으로 등록하는 기능을 활성화한다. 클래스 파일 위치에 주의할 것!
💬 API(응용 프로그래밍 인터페이스, Application Programming Interface)란?
API는 정의 및 프로토콜 집합을 사용하여 두 소프트웨어 구성요소가 서로 통신할 수 있게 하는 메커니즘입니다. API 맥락에서 애플리케이션이란 단어는 고유한 기능을 가진 모든 소프트웨어를 나타냅니다. 인터페이스는 두 애플리케이션 간의 서비스 계약이라고 할 수 있습니다. 이 계약은 요청과 응답을 사용하여 두 애플리케이션이 서로 통신하는 방법을 정의합니다. API 문서에는 개발자가 이러한 요청과 응답을 구성하는 방법에 대한 정보가 들어있답니다! (예. 카카오 api)
API 아키텍처는 클라이언트와 서버 측면에서 설명됩니다. 요청을 보내는 애플리케이션을 클라이언트, 응답을 보내는 애플리케이션을 서버라고 합니다. 따라서 기상청의 소프트웨어 시스템과 날씨 모바일 앱의 관계에서는 전자가 서버 후자가 클라이언트가 됩니다. API는 네 가지 방식으로 작동할 수 있답니다.
SOAP API, RPC API, Websocket API, REST API
오늘날 웹에서 가장 많이 사용하는 것은 REST API입니다.
REST는 클라이언트가 서버 데이터에 액세스 하는 데 사용할 수 있는 GET, PUT, DELETE 등의 함수 집합을 의미합니다. 클라이언트와 서버는 HTTP 네트워크 상의 리소스를 정의하고 해당 리소스를 URI라는 고유한 주소를 사용하여 데이터를 교환합니다. REST API의 주된 특징은 무상태입니다. 무상태는 서버가 요청 간에 클라이언트 데이터를 저장하지 않음을 의미합니다. 서버에 대한 클라이언트 요청은 웹 사이트를 방문하기 위해 브라우저에 입력하는 URL과 유사합니다. 서버의 응답은 웹 페이지의 일반적인 그래픽 렌더링이 없는 일반 데이터입니다.
REST API에서 의미하는 리소스는 데이터베이스에 저장된 데이터, 문서, 이미지, 동영상 등 HTTP와 통신을 통해 주고받을 수 있는 모든 것을 의미합니다.
REST API 기반의 애플리케이션에서는 일반적으로 애플리케이션이 제공해야 될 기능을 리소스로 분류합니다.
💬 URL vs URI
통합 리소스 식별자 vs 통합 자원 식별자
리소스를 가리키는 URL, URI는 그 리소스를 식별하는 식별자 역할을 합니다.
💬 HTTP란?
Hyper Text Transfer Protocol의 두문자어로 인터넷에서 데이터를 주고받을 수 있는 프로토콜이다. 프로토콜은 규칙이란 뜻으로 이러한 규칙을 정해놓았기 때문에 모든 프로그램은 이 규칙에 맞추어 개발되며 서로 정보를 교환할 수 있게 된 것이다. 통신이 오고 갈 때 정보가 담긴 메시지를 HTTP 메시지라고 하며 이는 시작줄, 헤더, 본문으로 이루어져 있다. 헤더에서 한 줄 띄운 뒤 본문이 시작된다.
(내용 채우기 - 웹 애플리케이션 작동원리 공부)
https://www.cloudflare.com/ko-kr/learning/security/api/what-is-api-endpoint/
다양한 애너테이션(클래스 레벨)
@RestController
- pring MVC에서는 특정 클래스에 @RestController를 추가하면 해당 클래스가 REST API의 리소스(자원, Resource)를 처리하기 위한 API 엔드포인트로 동작함을 정의합니다.
- 또한 @RestController 가 추가된 클래스는 애플리케이션 로딩 시, Spring Bean으로 등록해 줍니다.
@RequestMapping
- @RequestMapping 은 클라이언트의 요청과 클라이언트 요청을 처리하는 핸들러 메서드(Handler Method)를 매핑해 주는 역할을 합니다.
- 클래스 레벨에 추가하여 클래스 전체에 사용되는 공통 URL(Base URL)을 설정합니다.
다양한 애너테이션(메서드 레벨)
@RequestMapping 애너테이션은 HTTP Method에 해당하는 단축 표현들을 주로 사용합니다.
- @GetMapping: HTTP Get Method에 해당하는 단축 표현으로 서버의 리소스를 조회할 때 사용
- @PostMapping: HTTP Post Method에 해당하는 단축 표현으로 서버에 리소스를 등록(저장)할 때 사용
- @PutMapping: HTTP Put Method에 해당하는 단축 표현으로 서버의 리소스를 수정할 때 사용. 리소스의 모든 정보를 수정할 때 사용한다.
- @PatchMapping: HTTP Put Method에 해당하는 단축 표현으로 서버의 리소스를 수정할 때 사용. 리소스의 일부 정보만 수정할 때 사용한다.
- @DeleteMapping: HTTP Delete Method에 해당하는 단축 표현으로 서버의 리소스를 삭제할 때 사용.
@PathVariable
- @PathVariable의 괄호 안에 입력한 문자열 값은 @GetMapping("/{member-id}")처럼 중괄호({ }) 안의 문자열과 동일해야 합니다.
- 만약 두 문자열이 다르다면 MissingPathVariableException이 발생합니다.
- 이 애너테이션을 사용하면 클라이언트 요청 URI에 패턴 형식으로 지정된 변수의 값을 파라미터로 전달받을 수 있다.
@RequestParam
- 클라이언트 쪽에서 전송하는 요청 데이터를 쿼리 파라미터, 폼 데이터, x-www-form-urlencoded 형식으로 전송하면 이를 서버 쪽에서 전달받을 때 사용하는 애너테이션이다.
- PostMapping 애너테이션과 사용!
💬 레거시 코드란?
Legacy. 유산이라는 뜻의 영단어
더 이상 쓰기 힘들거나 화나게 만드는 코드로 유산이 되어버린, 개선이 필요한 코드를 말한다!
쉽게 개선이 가능하고 의존성이 낮은 코드를 짜는 개발자야말로 진정한 실력자이다.
Controller 클래스에서 작성한 핸들러 메서드
레거시 코드 개선하기!
ResponseEntity 클래스를 사용함으로써 개선한다.
이전에는 핸들러 메서드의 반환타입이 String으로 직접 JSON 형식의 문자열을 작성함으로써 반환해 주었는데, 여간 귀찮은 일이 아니었다. 오류 발생 가능성도 많았고 파라미터가 점점 늘어나는 순간 수정에 수정을 하는 좋지 않은 코드였다.
// 레거시 코드
@PostMapping
public String postOrder(@RequestParam("memberId") long memberId,
@RequestParam("coffeeId") long coffeeId){
String response = "{\"" +
"memberId\":\""+memberId+"\"," +
"\"coffeeId\":\""+coffeeId+"\"" +
"}";
return response;
}
// 개선된 코드
@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);
}
개선 1.
Map 객체를 리턴하게 되면 내부적으로 JSON 형식의 응답 데이터로 변환해 준다. 따라서 클래스 레벨의 RequestMapping의 애트리뷰트였던 produces={MediaType...}을 없애줘도 된다!
@RequestMapping(value="/v1/members", produces={MediaType.APPLICATION_JSON_VALUE})
@RequestMapping("v1/members")
개선 2.
String 타입의 JSON을 ResponseEntity 클래스의 객체를 리턴하는 것으로 바뀌었다.
return 문을 보면 return new ResponseEntity(map, HttpStatus.CREATED)로 생성자 파라미터로 map과
HTTP응답상태(서버가 클라이언트의 요청을 어떻게 처리하였음)를 함께 전달하고 있다. Map객체를 리턴해도 알아서 JSON으로 바꿔주지만 응답 데이터를 객체로 래핑함으로써 조금 더 세련된 방식이라고 합니다.
또한 잊지 말고 핸들러 메서드의 반환타입을 String에서 ResponseEntity로 바꾸자!
실습과제 100% 이해해 보기
처음 학습했을 땐 몰라서 그냥 넘어간 실습이다. 실습 내용은 MemberController의 핸들러 메서드를 구현해 보는 것이다.
클라이언트의 요청으로 어떤 것이 들어오겠는가? 했을 때 ⬅ 컨트롤러 핸들러 메서드 설계의 시작점
대표적으로 수정, 삭제, 조회 등이 있겠다. 이때 수정, 삭제하는 핸들러 메서드를 직접 구현해 보는 것이 목표이다.
문제 1. 휴대폰 번호를 수정해 주세요!
@PatchMapping 애너테이션을 사용했다. 아직 DB 연동을 하지 않았기 때문에, 아래와 같은 코드로 애플리케이션 로딩 시에 init() 메서드를 통해 memberId가 1인 회원정보가 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);
}
members Map 안에 <Long, Map <String, Object>> 를 저장하였다. 오~ 신기한데~
// 특정 회원 수정 메서드
@PatchMapping("/{member-id}")
public ResponseEntity patchMember(@PathVariable("member-id") long memberId){
Map<String, Object> map = members.get(memberId);
map.put("phone", "010-1111-2222"); // 회원정보 put으로 갱신
return new ResponseEntity(map, HttpStatus.OK);
}
이미 정보가 저장되어 있는 members Map에서 memberId가 1인 회원 정보를 갖고 와 수정하길 원하는 전화번호로 업데이트하고 다시 클라이언트로 응답으로 전송한다.
- member-id가 URI 경로에 포함되어 있어야 함
- @PathVariable : 클라이언트 요청 URI에 패턴 형식으로 지정된 변수의 값을 파라미터로 전달받을 수 있다.
- HashMap의 put 메서드는 이미 저장된 Key가 있으면 value값을 새로 갱신한다.
- member1에 바로 접근할 수 없다. 메서드 안에 쓰인 지역변수이기 때문이다. 따라서 인스턴스 변수인 members를 통해 Map 객체를 새로 만들어 값을 저장해 주었다.
- 난 Map 객체를 갖고 있단다.. Map 타입 참조변수로 받아주겠니?
문제 2. 회원 정보를 삭제해 주세요!
// 특정 회원 삭제 메서드
@DeleteMapping("/{member-id}")
public ResponseEntity deleteMember(@PathVariable("member-id") long memberId){
Map<String, Object> map = members.get(memberId);
map.clear();
return new ResponseEntity(map, HttpStatus.NO_CONTENT);
}
위와 똑같다. HashMap의 clear 메서드를 사용하였다. 이 메서드는 HashMap에 저장된 모든 객체를 제거한다.
HTTP 상태 메시지는 NO_CONTENT로 하였다.
근데 저렇게 map객체를 저장할 참조변수를 따로 만들지 않아도..! 쉽게 풀 수 있는 방법이 있었다.
remove 메서드는 키를 입력받는데, 입력받은 키에 저장된 객체를 삭제한다.
// 특정 회원 삭제 메서드
@DeleteMapping("/{member-id}")
public ResponseEntity deleteMember(@PathVariable("member-id") long memberId){
members.remove(memberId);
return new ResponseEntity(members, HttpStatus.NO_CONTENT);
}
이렇게 해도 된다...?
처음에 무척 헤맸는데 다시 해보니 쉽게 풀 수 있어서 기쁘다!
잘하는 페어분을 만나 그분이 문제를 푸는 방식을 보고 이게 실습문제를 푸는 방법이구나! 를 깨달았다.
와우,,, 더 붙잡고 싶다,,, 저를 더 알려주세요... 🏃♀️ Don't go!
DTO
데이터를 전송하기 위한 용도의 객체
요청 데이터를 하나의 객체로 전달받는 역할
데이터 유효성(Validation) 검정의 단순화