Spring Boot를 이용한 RESTful Web Services 개발
Level3 단계의 REST API 구현을 위한 HATEOAS 적용
HATEOAS란?
- Hypermedia As the Engine Of Application State의 약자
- 현재 리소스와 연관된(호출 가능한) 자원 상태 정보를 제공
- Level 0
- REST API라고는 볼 수 없고 단순히 웹이나 특정한 네트워크를 통해서 컴퓨터 자원을 의미없이 전달해주는 단계
- HTTP를 사용하지만 웹의 매커니즘을 사용하진 않음
- 모든 요청을 단일 엔드 포인트에 보낸다.
- Level 1
- 요청이 개별 리소스와 통신
- 약속 리소스(ex: uri)를 확보하고 해당 리소스에 응답함
- Level 2
- 리소스와 HTTP 요청 메서드를 사용함
- 일반적인 프로젝트에서 구현하는 수준
- Level3
- Level2에 link를 추가할 수 있다. 이를 통해 네비게이션이 가능하다.
- Response에 HAL이라는 추가적인 링크가 붙는다.
HATEOAS 라이브러리 추가
- build.gradle
implementation 'org.springframework.boot:spring-boot-starter-hateoas'
HATEOAS 적용
- UserController.java
@GetMapping("/users/{id}")
public EntityModel<User> retrieveUser(@PathVariable int id) {
User user = service.findOne(id);
if(user == null) {
throw new UserNotFoundException(String.format("ID[%s] not found", id));
}
// HATEOAS
EntityModel<User> model = EntityModel.of(user);
WebMvcLinkBuilder linkTo = WebMvcLinkBuilder
.linkTo(methodOn(this.getClass()).retrieveAllUsers());
model.add(linkTo.withRel("all-users"));
return model;
}
스프링 버전에 따라(2.1.X, 2.2.x) 헤테오스 사용 방법이 다르므로 주의하자. 강의에서는 2.1.X 버전을 사용 중이고 나는 2.2.X버전을 사용중이다. 그리고 강의에서는 EntityModel<User> model = new EntityModel<>(user);라고 적혀있지만 스프링 헤테오스 1.X가 추가되면서 EntityModel과 CollectionModel의 생성자가 Deprecated 되어 of()를 사용하도록 권장하고 있다. 그래서 EntityModel<User> model = EntityMOdel.of(user);로 사용하면 된다.
- 서버 실행 후 포스트맨 확인
500 에러가 발생했다. 에러 메세지를 확인해보면 PropertyFilter 'UserInfo'를 사용할 수 없다고 한다.
- User.java
//@JsonFilter("UserInfo")
public class User { ... }
UserController의 retrieveUser() 메서드에 저 필터를 적용하는 관련 코드가 없어서 발생하는 에러이다. @JsonFilter를 잠시 주석처리 하자.
- 서버 재실행 후 포스트맨 확인
;; 잘 나오긴하는데 강의에서와는 다르게 joinDate가 배열 형태로 나온다. 내가 Date를 안쓰고 LocalDateTime을 사용해서 그런가보다.
- User.java
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime joinDate;
@JsonFormat 적용 후 서버 재실행, 포스트맨을 확인해보면 제대로 출력되는 걸 확인할 수 있다.
@JsonFormat 말고 @EnableWebMvc를 없애는 방법도 있는데 나는 숨어있는 저녀석을 찾아보다가.. 시간이 너무 흘러 간단하게 @JsonFormat으로 처리했다. @EnableWebMvc 방법은 아래 블로그를 참고하면 된다.
REST API Documentation을 위한 Swagger 사용
Swagger란?
Swagger는 REST 웹 서비스를 설계, 빌드, 문서화, 소비하는 일을 도와주는 대형 도구 생태계의 지원을 받는 오픈 소스 소프트웨어 프레임워크이다.
- 출처 : 위키백과
Swagger 라이브러리 추가
- build.gradle
implementation 'io.springfox:springfox-boot-starter:3.0.0'
Swagger 사용
- SwaggerConfig.java 생성
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2);
}
}
서버 재실행 후 테스트 해보려했으나 런타임 에러가 발생했다.
org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
찾아보니 Spring boot 2.6 버전 이후에 spring.mvc.pathmatch.matcing-strategy 값이 ant_apth_matcher에서 path_pattern_parser로 변경되면서 몇몇 라이브러리에 오류가 발생한다고 한다. 이를 해결하려면 application.yml 파일에 아래 코드를 추가하면 된다.
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
- 서버 재실행 후 swagger 페이지 접속
- http://localhost:8088/v2/api-docs : swagger 파일에 의해 만들어진 내용이 JSON 타입으로 보여짐
- http://localhost:8088/swagger-ui/index.html : swagger 위 주소 JSON 데이터 기반으로 만들어진 페이지
Swagger Documentation 꾸미기
위 부분을 커스터마이징 해보자.
- SwaggerConfig.java
private static final Contact DEFAULT_CONTACT = new Contact("LEO",
"http://www.joneconsulting.co.kr", "devhan9117@gmail.com");
private static final ApiInfo DEFAULT_API_INFO = new ApiInfo("Awesome API Title",
"My User management REST API Service", "1.0", "urn:tos",
DEFAULT_CONTACT, "Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0", new ArrayList<>());
private static final Set<String> DEFAULT_PRODUCES_AND_CONSUMES = new HashSet<>(
Arrays.asList("application/json", "application/xml"));
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(DEFAULT_API_INFO)
.produces(DEFAULT_PRODUCES_AND_CONSUMES)
.consumes(DEFAULT_PRODUCES_AND_CONSUMES);
}
- 서버 재실행 후 http://localhost:8088/v2/api-docs 접속
내용이 변경된 걸 확인할 수 있다.
이번엔 User 도메인 클래스에 대한 설명을 꾸며보자. 현재 상태는 아래와 같다.
- User.java
@ApiModel(description = "사용자 상세 정보를 위한 도메인 객체")
public class User {
private Integer id;
@Size(min = 2, message = "Name은 2글자 이상 입력해 주세요.")
@ApiModelProperty(notes = "사용자 이름을 입력해 주세요.")
private String name;
@Past // 현재 시간보다 과거의 날짜 및 시간이 저장되어야 한다는 제약
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
@ApiModelProperty(notes = "사용자 등록일을 입력해 주세요.")
private LocalDateTime joinDate;
@ApiModelProperty(notes = "사용자 패스워드를 입력해 주세요.")
private String password;
@ApiModelProperty(notes = "사용자 주민번호를 입력해 주세요.")
private String ssn;
}
요로코롬 꾸민 후 서버 재실행 후 페이지에서 확인한다.
된 줄 알았더니 User.java에서 클래스 블록에 입력했던 @ApiModel(description = "~~")이 보이지 않고있다.. 뭘 잘못쳤나 했는데 스크롤을 밑으로 더 내려보니 조신하게 자리를 지키고 있었다.
swagger ui 페이지에서도 도메인 객체 설명이 잘 나온다.
REST API Monitoring을 위한 Actuator 설정
Actuator 라이브러리 추가
- build.gradle
implementation 'org.springframework.boot:spring-boot-starter-actuator'
강의에선 라이브러리만 추가하고 서버 재실행하면 페이지 접속이 되는데 나는 404가 떴다. 해결 방법을 찾아보니 SwaggerConfig.java에 아래 코드를 추가하면 정상적으로 페이지가 보인다.
@Bean
public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier,
ServletEndpointsSupplier servletEndpointsSupplier,
ControllerEndpointsSupplier controllerEndpointsSupplier,
EndpointMediaTypes endpointMediaTypes,
CorsEndpointProperties corsProperties,
WebEndpointProperties webEndpointProperties,
Environment environment) {
List<ExposableEndpoint<?>> allEndpoints = new ArrayList();
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
allEndpoints.addAll(webEndpoints);
allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
String basePath = webEndpointProperties.getBasePath();
EndpointMapping endpointMapping = new EndpointMapping(basePath);
boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment, basePath);
return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes, corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath), shouldRegisterLinksMapping, null);
}
private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, String basePath) {
return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
}
webEndpointServletHandlerMapping() 메서드의 매개변수에 빨간 줄이 그어져있어도 일단 서버 재실행 후에 아래 주소로 들어가면 페이지가 제대로 보인다. http://localhost:8088/actuator
더 많은 정보를 보기 위해 application.yml 파일에 아래 코드를 추가하고 서버 재실행해보자.
management:
endpoints:
web:
exposure:
include: "*"
HAL Browser를 이용한 HATEOAS 기능 구현
HAL Browser란?
- Hypertext Application Language의 약자
- 하이퍼텍스트로 어플리케이션의 부가적인 기능을 추가한다.
- API에 HAL 도입 시 API 간 검색이 쉽다는 장점이 있다.
- API Reponse 메세지에 적용하게 되면 어떤 포맷이든 API의 메타정보를 하이퍼링크 형식으로 간단하게 포함할 수 있다.
HAL Browser 라이브러리 추가
- build.gradle
implementation 'org.springframework.data:spring-data-rest-hal-explorer'
라이브러리 추가 후 서버 재실행, http://localhost:8088/에 접속하면 주소창의 url이 변경되면서 http://localhost:8088/explorer/index.html#uri=/ 여기로 접속된다.
actuator를 검색해보면 actuator와 관련된 여러가지 기능 정보들을 볼 수 있다.
여기서 조금 더 깊게 들어가보자. metrics의 초록색 '<' 버튼 클릭
RequestBody 부분의 'jvm.memory.max'의 정보를 봐보자.
입력되어 있는 검색창 맨 뒤에 /jvm.memory.max를 붙여주고 검색하면 아래와 같은 정보가 나온다.
이렇게 HAL Browser를 이용하면 REST 자원을 표시하기 위한 자료구조를 별도로 생성하지 않아도 확인할 수 있다는 장점이 있다.
Spring Security를 이용한 인증 처리
Security 라이브러리 추가
- build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
라이브러리 추가 후 서버를 재실행한다. 서버를 실행하면 콘솔 창에서 자동으로 발급해주는 비밀번호를 찾아야한다. 컨트롤+f를 누르고 password를 검색한다.
복사하자.
포스트맨에서 request를 한번 날려보자.
잘 작동하던 api였는데 security를 적용하고 나서 이상한 문구들이 나타난다. HTTP Status code는 200인데; 강의에선 401로 나오고 reponse 메세지도 좀 다르다. 업데이트 됐다보다. 그래도 200 코드는 좀..; (이 문제를 해결해보려고 찾아봤는데 시간이 너무 오래걸릴 거 같아 일단 스킵)
포스트맨의 Authorization을 클릭 후 아래와 같이 세팅한다. 비밀번호는 콘솔에서 복사했던 비밀번호를 입력하면 된다.
send하면 정상적으로 호출되는 걸 확인할 수 있다.
Configuration 클래스를 이용한 사용자 인증 처리
Security 아이디, 비밀번호 설정하기
- application.yml
spring:
security:
user:
name: username
password: test1
서버 재실행 후 아이디 비밀번호를 입력하고 send 클릭
근데 이렇게 application.yml에 아이디, 비밀번호를 생성하면 보안상의 문제도 있지만 아이디 비밀번호가 변경될 때마다 들어가서 변경해줘야하는 번거로움이 있다. 그래서 대부분은 DB를 연동해서 사용한다고 한다. application.yml말고 따로 설정 파일을 만들어서 등록해보자.
- SecurityConfig.java
@Configuration
public class SecurityConfig {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("test")
.password("{noop}test1234") //{noop}은 아무런 인코딩 없이 그냥 값으로 처리하기 위함
// 실무에선 {noop} 보다는 적절한 인코딩 알고리즘을 사용해야 함
.roles("USER");
}
}
원래 강의에서는 SecurityConfig 클래스가 WebSecurityConfigurerAdapter를 상속받았었는데 WebSecurityConfigurerAdapter 클래스가 Deprecated 되었다. 찾아보니 일단 WebSecurityConfigurerAdapter의 메서드를 직접적으로 쓰진 않아 일단 상속 관련 코드를 지워버렸다.
서버 재실행 후 아이디, 비밀번호를 SecurityConfig에서 설정한 값으로 입력 후 send
- 출처 : 인프런 Spring Boot를 이용한 RESTful Web Services 개발 강의