[WIL] GDG Spring 세션 5주차

Nakyeong Lee·2024년 11월 5일
1
post-thumbnail

Index

스프링 MVC의 개념 및 구조
View Resolver
RESTful 웹 서비스 설계
HATEOAS를 활용한 REST API 설계
🔗resources

스프링 MVC의 개념 및 구조

MVC 패턴

애플리케이션을 모델, 뷰, 컨트롤러로 분리하여 역할을 명확히 하고 유지보수(!!)를 용이하게 하는 디자인 패턴이다. 각 컴포넌트를 독립적으로 동작하게 하여 변경이 용이해진다.

구성역할설명
Model모델애플리케이션의 데이터와 비즈니스 로직을 담당
View출력사용자에게 데이터를 표시
Controller처리사용자 입력을 처리하고, 모델과 뷰를 연결

(부록) MVC를 지키며 코딩하는 방법

  1. Model은 Controller와 View에 의존하지 않아야 한다. (Model 내부에 Controller와 View와 관련된 코드가 없어야 한다.)
  2. View는 Model에만 의존하고 Controller에는 의존하면 안된다. (View 내부에 Model의 코드만 있어야 하고 Controller의 코드는 없어야 한다.)
  3. View가 Model로부터 데이터를 받을 때는 사용자마다 다르게 보여줘야 하는 데이터에 대해서만 받아야 한다.
  4. Controller는 Model과 View에 의존해도 된다.
  5. View가 Model로부터 데이터를 받을 때 반드시 Controller를 통해 받아야 한다.

Spring MVC란?

Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. The formal name, "Spring Web MVC," comes from the name of its source module (spring-webmvc), but it is more commonly known as "Spring MVC".
🔗 docs.spring.io

결국 MVC 패턴을 웹 애플리케이션 개발에 최적화한 형태로 구현한 프레임워크가 Spring Web MVC, 줄여서 Spring MVC인 것이다. 이는 스프링 프레임워크의 모듈 중 웹 계층에 서블릿(Servlet) API를 기반으로 클라이언트의 요청을 처리하는 모듈이다. (Spring boot에 기본적으로 서블릿들이 웹 애플리케이션으로 실행할 수 있도록 해주는 서블릿 컨테이너(Servlet Container) 중 하나인 Apache Tomcat이 내장되어 있다.)

Spring MVC 요청 처리 흐름

그렇다면 Spring MVC Framework에서 HTTP 요청이 들어왔을 때 응답이 반환되는 과정을 알아보자.

1. 들어오는 모든 요청을 전면 컨트롤러로 작동하는 Dispatcher Servlet이 가져간다.
2. 그런 다음 DispatcherServlet은 XML 파일에서 Handler Mapping 항목을 가져와 Controller에 전달한다.
3. ModelAndView의 object가 컨트롤러에서 반환된다.
4. DispatcherServlet은 XML 파일에서 View Resolver의 항목을 확인하고 적절한 View 구성 요소를 호출한다.

View Resolver

위에서 본 것처럼, MVC Framework의 HTTP 요청 처리 과정에서 View Resolver를 볼 수 있다. 이 View Resolver에 대해 더 알아보자

뷰 리졸버(View Resolver)

Spring MVC defines the ViewResolver and View interfaces that let you render models in a browser without tying you to a specific view technology. ViewResolver provides a mapping between view names and actual views. View addresses the preparation of data before handing over to a specific view technology.
🔗 docs.spring.io

View Resolver는 결국 MVC 패턴에서 Controller가 처리를 마친 후에 어떤 View로 응답을 생성할지 결정하는 역할을 한다. Controller가 반환한 View 이름을 실제 템플릿 파일로 매핑(ex. Controller가 "novel" 반환하면 View Resolver가 "novel.jsp"/"novel.html"과 같이 실제 파일로 변환)하며, 다양한 뷰 기술을 지원하기 위해 여러 View Resolver를 제공하여 JSP, Thymeleaf 등 다양한 템플릿 엔진을 사용할 수 있다.

View Resolver
AbstractCachingViewResolver
UrlBasedViewResolver
InternalResourceViewResolver
FreeMarkerViewResolver
ContentNegotiatingViewResolver
BeanNameViewResolver

Spring boot에서의 ViewResolver 사용 예시

Thymeleaf ViewResolver를 사용해보자. 우선 build.gradle에 spring-boot-starter-thymeleaf 의존성을 추가한다. 이렇게 의존성만 추가해주면 Spring Boot가 자동으로 ThymeleafViewResolver를 설정해준다!

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

이후 템플릿 파일 경로를 설정해준다. Spring Boot는 기본적으로 src/main/resources/templates 폴더에서 Thymeleaf 템플릿 파일을 찾지만 설정이 따로 필요할 경우 application.yaml 파일에 아래와 같이 설정을 추가한다.

spring:
  thymeleaf:
    prefix: classpath:/custom-templates/
    suffix: .html

이후 아래와 같은 Controller에서 주석과 같이 동작하는 것을 볼 수 있다.

@Controller
public class HelloController {

    @GetMapping("hello")
    public String hello(Model model){
        model.addAttribute("data", "hello!!");
        return "hello"; // 설정한 path 안의 hello.html로 변환
    }
}

복잡한 xml 파일을 쓰지 않아도 됨에 springboot에게 무한한 감사,,

RESTful 웹 서비스 설계

REST?

Representational State Transfer
REST(Representational State Transfer)는 네트워크 아키텍처 원리 중 하나로, 자원(Resource)의 표현(Representation)에 의한 상태 전달을 의미한다. 클라이언트가 서버로부터 자원의 상태(정보)를 요청할 때, 서버는 해당 자원의 표현을 클라이언트에 전달하며, 클라이언트는 이 표현을 통해 자원의 상태를 알 수 있다.

REST 구성 요소

  • 자원(Resource) : HTTP URI
  • 자원에 대한 행위(Verb) : HTTP Method
  • 자원에 대한 행위의 내용 (Representations) : HTTP Message Payload

REST 설계 원칙

REST는 다음과 같은 6가지 설계 원칙을 지켜야 한다.
1. Client-Server Architecture(클라이언트-서버 구조)
클라이언트와 서버는 독립적으로 분리되어야 한다. 클라이언트는 서버와의 상호작용을 통해 자원을 요청하고, 서버는 해당 자원을 제공하는 역할을 한다. 이를 통해 클라이언트와 서버 간의 관심사를 분리하고 유지보수를 용이하게 한다.
2. Stateless(무상태성)
REST의 상호작용은 무상태적이어야 한다. 즉, 서버는 각 요청을 독립적으로 처리하며, 요청 간의 상태를 저장하지 않는다. 클라이언트는 매 요청마다 필요한 모든 정보를 포함하여 서버에 전달해야 한다. 이를 통해 서버 확장성과 복구 용이성이 향상된다.
3. Cacheability(캐시 가능성)
RESTful 시스템은 클라이언트가 요청한 자원의 응답을 캐시할 수 있어야 한다. 응답에는 캐싱이 가능한지 여부와 유효기간이 명확하게 표시되어야 하며, 캐싱을 통해 서버 부하를 줄이고 성능을 향상할 수 있다.
4. Layered System(계층화 시스템)
클라이언트와 서버 사이에 중개 계층을 둘 수 있다. 이 계층은 보안, 로드 밸런싱, 캐싱 등을 수행할 수 있으며, 이를 통해 전체 시스템의 확장성과 안정성이 높아진다. 클라이언트는 중개 계층의 존재를 인식하지 않고도 요청을 수행할 수 있어야 한다.
5. Uniform Interface(균일한 인터페이스)
REST의 핵심 원칙 중 하나로, 모든 리소스는 통일된 방식으로 액세스되어야 한다. 이를 통해 일관된 API 설계를 구현할 수 있으며, 재사용성과 단순성이 향상된다. 균일한 인터페이스를 위해 다음과 같은 제약이 필요합니다:

  • 리소스 식별: 각 리소스는 URI로 식별된다.
  • 표현을 통한 조작: 클라이언트는 리소스를 직접 조작하지 않고, 리소스에 대한 표현을 통해 조작해야 한다.
  • 자기 설명적 메시지: 각 요청은 스스로에 대한 모든 정보를 포함하여, 별도의 컨텍스트 없이도 이해할 수 있어야한다.
  • 하이퍼미디어 제약: 응답 내에 다음에 할 수 있는 작업에 대한 링크를 포함하여, 클라이언트가 서버와의 상호작용을 스스로 탐색할 수 있어야 한다.

6. Code on Demand(선택적 원칙)
필요할 경우 서버가 클라이언트에 코드(JavaScript 등)를 전송하여 클라이언트가 이를 실행할 수 있도록 한다. 이를 통해 클라이언트 기능 확장이 가능하지만, 이 원칙은 필수는 아니다.

RESTful

RESTful 서비스는 REST 아키텍처 스타일을 완전히 준수하는 웹 서비스를 의미한다. 이는 클라이언트와 서버 간의 통신에서 특정 규약을 따른다는 것이다.
RESTful 서비스는 HATEOAS(Hypermedia As The Engine Of Application State)를 포함한 REST의 여섯 가지 제약 조건을 모두 만족시켜야 한다. 이때 HATEOAS는 클라이언트가 서버로부터 어떤 요청을 받았을 때, 그 다음에 어떤 행동을 할 수 있는지에 대한 정보를 포함하여 응답을 제공하는 것을 의미한다. 이를 통해 클라이언트는 서버로부터 받은 응답만으로도 어떤 행동을 해야 할지를 알 수 있게 되어, 클라이언트와 서버 간의 통신이 더욱 유연해진다. RESTful 서비스의 이러한 특징은 클라이언트와 서버가 각각 독립적으로 발전할 수 있게 하여 웹 서비스의 확장성과 유지보수성을 크게 향상시킨다.

RESTful API 예시

  • 피드 조회 API: GET /feeds/{feedId}
  • 피드 생성 API: POST /feeds
  • 피드 수정 API: PUT /feeds/{feedId}
  • 피드 삭제 API: DELETE /feeds/{feedId}

Spring에서의 RESTful 웹 서비스 구현

그렇다면 Spring에서는 RESTful을 어떻게 구현할까? 우선 @RestController가 있다.

1. @RestController

@RestController@Controller@ResponseBody의 기능을 모두 하는 어노테이션이다. 따라서 @RestController가 붙은 클래스의 모든 메서드는 기본적으로 @ResponseBody가 적용되어, 반환되는 객체가 JSON 또는 XML 형식으로 자동 변환된다.

그렇다면 RESTful API의 예시와 같은 URL을 매칭하는 방식은 무엇일까. @RequestMapping, 그리고 이와 함께 쓰이는 @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping이 있다.

2. @RequestMapping

@RequestMapping은 요청 URL을 매핑하는 데 사용된다. 클래스나 메서드 레벨에 선언할 수 있으며, HTTP 메서드를 지정하지 않으면 기본적으로 모든 HTTP 메서드를 허용한다.

3. @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping

@RequestMapping은 클래스, 메서드 레벨 모두에 쓰일 수 있지만 메서드 레벨에서의 경우 위와 같은 어노테이션들이 주로 쓰인다. @RequestMapping같은 역할을 하지만 HTTP 메서드가 명확하게 지정되어 있어 코드의 가독성을 높여준다.

위와 같은 어노테이션들을 적용하면 다음과 같이 코드가 작성된다.

@RequestMapping("/feeds")
@RestController
@RequiredArgsConstructor
public class FeedController {

    @GetMapping("/{feedId}")
    public ResponseEntity<FeedGetResponse> getFeed(Principal principal,
                                                   @PathVariable("feedId") Long feedId) {
        // 피드 조회
    }
    
    @PostMapping
    public ResponseEntity<Void> createFeed(Principal principal,
                                           @Valid @RequestBody FeedCreateRequest request) {
        // 피드 생성
    }

    @PutMapping("/{feedId}")
    public ResponseEntity<Void> updateFeed(Principal principal,
                                           @PathVariable("feedId") Long feedId,
                                           @Valid @RequestBody FeedUpdateRequest request) {
        // 피드 수정
    }

    @DeleteMapping("/{feedId}")
    public ResponseEntity<Void> deleteFeed(Principal principal,
                                           @PathVariable("feedId") Long feedId) {
        // 피드 삭제
    }
}

그런데 이렇게만 하면 진짜 RESTful한 코딩을 한걸까? REST API를 잘 적용하기 위해 Leonard Richardson이 정의한 총 4단계(0~3단계) 모델이 있다.

Richardson Maturity Model for REST APIs

  • 0단계: 단일 URI를 가지며 단일 HTTP 메소드를 사용하는 단계 (ex. /feeds)
  • 1단계: 단일 서비스 endpoint로 보내는 것이 아니라, 개별 리소스로 통신하는 것 (ex. /feeds/create)
  • 2단계: 4가지 HTTP Method를 사용해서 CRUD를 표현하고 StatusCode도 활용하여 반환한다. (ex. POST /feeds)
  • 3단계: HATEOAS 까지 적용

아까도 본 HATEOAS! 까지 적용해야 완벽한 REST API 설계라고 할 수 있다고 한다. HATEOAS에 대해 더 알아보자.

HATEOAS를 활용한 REST API 설계

HATEOAS?

Hypermedia as the Engine of Application State

Spring HATEOAS provides some APIs to ease creating REST representations that follow the HATEOAS principle when working with Spring and especially Spring MVC. The core problem it tries to address is link creation and representation assembly.
🔗 spring.io

우선 알기쉽게 간단히! 말하자면 응답과 관련된 링크들을 함께 주게 되는 것이다.
더 자세히 알아보자면, HATEOAS란 REST API의 주요 개념 중 하나로, 클라이언트와 서버 간의 상호작용을 더욱 유연하고 독립적으로 만들기 위해 설계된 원칙이다. HATEOAS를 적용하면, 클라이언트는 특정 요청의 응답으로 필요한 정보를 링크(hyperlinks) 형태로 전달받고, 이러한 링크를 통해 서버가 제공하는 다른 리소스와 상호작용할 수 있다. 즉, 클라이언트가 현재의 리소스 상태와 연결된 링크를 통해 추가적인 리소스에 접근하게 해준다. 이는 웹 브라우징과 유사한 방식으로, 클라이언트가 서버의 API 문서에 의존하지 않고도 제공된 링크만으로 탐색과 리소스 조작이 가능하게 한다.

예를 들어, 사용자 정보 조회 API에서 원래 다음과 같은 결과가 나온다고 하자.

{
    "id": 1,
    "name": "나경"
}

여기서 HATEOAS를 사용하면 아래와 같이 연결된 링크를 추가로 제공한다.

{
    "id": 1,
    "name": "나경",
    "_links": {
        "self": {
            "href": "http://localhost:8080/members/1"
        },
        "allMembers": {
            "href": "http://localhost:8080/members"
        }
    }
}

여기서 rel 속성은 링크의 관계, relationship을 나타내며, href는 실제 링크를 제공하는 부분이다.

HATEOAS를 적용한 API 설계 방법

1. Gradle dependency 추가

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-hateoas'
}

2. 리소스에 Hypermedia support 추가
RepresentationModel<>을 extend 해준다.

public class Member extends RepresentationModel<Member> {
    private Long id;
    private String name;
 
    // standard getters and setters
}

3. 링크 추가
Link를 하드코딩하는 방법도 있지만 아래와 같은 방법이 권장된다.

    @GetMapping("/{memberId}")
    public EntityModel<MemberGetResponse> getMember(@PathVariable Long memberId) {

        return EntityModel.of(
                memberService.getMember(),
                linkTo(methodOn(MemberController.class).getMember(memberId)).withSelfRel(),
                linkTo(methodOn(MemberController.class).getAllMembers()).withRel("allMembers")
        );
    }

HATEOAS 적용의 장점 및 사례

이로써 HATEOAS의 사용은 RESTful API에서 클라이언트와 서버 간의 의존성을 줄이고, 클라이언트가 리소스를 자유롭게 탐색하도록 하여 RESTful 아키텍처의 유연성과 확장성을 극대화하는 데 기여한다. 하지만 실무에서는 swagger 등의 api 명세서를 사용하고 HATEOAS는 많이 사용하지 않는다고도 한다. 그치만 완벽한 RESTful 서비스를 만들기 위해서는 필수이니 공부 굳럭~키비키~^^

🔗resources

더 읽어보면 좋을만한 글

profile
Web Backend Developer

0개의 댓글