๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐ŸŒฟ Spring/โœ… RESTful Web Service

4. Spring Boot API ์‚ฌ์šฉ

by nitronium102 2023. 6. 20.

REST API Level 3๋ฅผ ์œ„ํ•œ HATEOAS ์„ค์ •

HATEOAS

Hypermedia As the Engine Of Application State

ํ˜„์žฌ ๋ฆฌ์†Œ์Šค์™€ ์—ฐ๊ด€๋œ(ํ˜ธ์ถœ ๊ฐ€๋Šฅํ•œ) ์ž์› ์ƒํƒœ ์ •๋ณด๋ฅผ ์ œ๊ณต

REST API ์„ค๊ณ„ ๋‹จ๊ณ„

  1. The Swamp of POX : ํŠน์ •ํ•œ ์›น ํ”„๋กœํ† ์ฝœ์„ ํ†ตํ•ด์„œ ์ปดํ“จํ„ฐ๊ฐ€ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์ž์›์„ ์˜๋ฏธ ์—†์ด ์ „๋‹ฌ
  2. Resources
  3. HTTP Verbs : http ์ƒํƒœ ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉ
  4. Hypermedia controls : ๋ฆฌ์†Œ์Šค์˜ ์ •๋ณด๋ฅผ ์ถ”๊ฐ€์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฆฌ์†Œ์Šค์™€ ํ•จ๊ป˜ ์ „๋‹ฌ
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

IntelliJ์—์„œ ๊ฒ€์ƒ‰

shift key 2๋ฒˆ ๋ˆ„๋ฅด๊ธฐ → pom.xml ์„ ํƒ

๋ฒ„์ „๋ณ„ HATEOAS implementation

pom.xml ์ œ์ผ ์ƒ๋‹จ์„ ๋ณด๋ฉด ์Šคํ”„๋ง๋ถ€ํŠธ ๋ฒ„์ „ ํ™•์ธ ๊ฐ€๋Šฅ

<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.3</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
  1. spring 2.1.8 RELEASE
  • Resource : ๋งŒ๋“ค๊ณ ์ž ํ•˜๋Š” ์ถ”๊ฐ€ ์ž์›์„ Resouce ๊ฐ์ฒด๋กœ ๋„ฃ์Œ
  • ControllerLinkBuilder : ๋งํฌํ•ด์คŒ
@GetMapping("/users/{id}")
public Resource<User> retrieveUser(@PathVariable int id){
// HATEOAS
// "all-users", SERVER_PATH + "/users"
// retrieveAllUsers
	Resource<User> resource = new Resource<>(user);
	ControllerLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllUsers());
	resource.add(linkTo.withRel("all-users");

	return resource;
}
  1. spring 2.2
  • Resource → EntityModel
  • ControllerLinkBuilder → WebMvcLinkBuilder
@GetMapping("/users/{id}")
public Resource<User> retrieveUser(@PathVariable int id){
// HATEOAS
// "all-users", SERVER_PATH + "/users"
// retrieveAllUsers
	EntityModel<User> model= new EntityModel<>(user);
	WebMvcLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllUsers());
	model.add(linkTo.withRel("all-users"); // ๋ฆฌ์†Œ์Šค ์ด๋ฆ„ : all-users

	return model;
}

retrieveAllUsers()์™€ all-users ์—ฐ๋™ → /users์™€ all-users mapping

REST API Documentation์„ ์œ„ํ•œ Swagger ์„ค์ •

Swagger

API ์„ค๊ณ„, ๋นŒ๋“œ, ๋ฌธ์„œํ™”, ์‚ฌ์šฉ์— ๋„์›€์„ ์ฃผ๋Š” ์˜คํ”ˆ์†Œ์Šค ํ”„๋ ˆ์ž„์›Œํฌ

<dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-boot-starter</artifactId>
      <version>3.0.0</version>
</dependency>

<dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger-ui</artifactId>
      <version>3.0.0</version>
</dependency>

spring boot 2.6 ๋ฒ„์ „ ์ดํ›„์— spring.mvc.pathmatch.matching-strategy ๊ฐ’์ด ant_path_matcher์—์„œ path_pattern_parser๋กœ ๋ณ€๊ฒฝ๋˜๋ฉด์„œ ๋ช‡๋ช‡ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(swaggerํฌํ•จ)์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ์•„๋ž˜ ๊ฐ’์„ application.yml์— ์ถ”๊ฐ€ํ•ด์ค˜์•ผ ํ•จ.

Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException: Cannot invoke "org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getPatterns()" because "this.condition" is null
spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher

⇒ ํ•˜์ง€๋งŒ ์–˜๋Š” actuator๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š”๋‹ค(actuator๋Š” pathpattern-based parsing์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ)

@Configuration
@EnableSwagger2
public class SwaggerConfig {
	@Bean
	public Docket api(){
		return new Docket(DocumentationType.SWAGGER_2);
	}
	// SWAGGER 2
	// All the paths
	// All the apis
}

@Data
@AllArgsConstructor
@NoArgsConstructor // ๋””ํดํŠธ ์ƒ์„ฑ์ž ์ƒ์„ฑ
@ApiModel(description = "์‚ฌ์šฉ์ž ์ƒ์„ธ ์ •๋ณด๋ฅผ ์œ„ํ•œ ๋„๋ฉ”์ธ ๊ฐ์ฒด")
public class User {
	private Integer id;

	@Size(min=2, message = "Name์€ 2๊ธ€์ž ์ด์ƒ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
	@ApiModelProperty(notes = "์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
	private String name;

	@Past // ๊ณผ๊ฑฐ ๋‚ ์งœ๋งŒ ๊ฐ€๋Šฅํ•œ ์ œ์•ฝ ์กฐ๊ฑด
	@ApiModelProperty(notes = "์‚ฌ์šฉ์ž์˜ ๋“ฑ๋ก์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
	private Date joinDate;

	@ApiModelProperty(notes = "์‚ฌ์šฉ์ž์˜ ํŒจ์Šค์›Œ๋“œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
	private String password;

	@ApiModelProperty(notes = "์‚ฌ์šฉ์ž์˜ ์ฃผ๋ฏผ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
	private String ssn; // ์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธ
}

 

๋ญ”๊ฐ€ ๋‚˜์ค‘์— ๋„์›€์ด ๋  ๊ฒƒ ๊ฐ™์€ ์งˆ๋ฌธ

Swagger์™€ Jackson Filter ์‚ฌ์šฉ ์‹œ Swagger-ui์˜ example value - ์ธํ”„๋Ÿฐ | ์งˆ๋ฌธ & ๋‹ต๋ณ€

REST API Monitoring์„ ์œ„ํ•œ Actuator ์„ค์ •

ํ˜„์žฌ ๊ธฐ๋™ํ•˜๊ณ  ์žˆ๋Š” ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ƒํƒœ๋ฅผ ์‰ฝ๊ฒŒ monitoringํ•  ์ˆ˜ ์žˆ๋‹ค. ํ˜„์žฌ ์„œ๋ฒ„๊ฐ€ ๊ตฌ๋™ ์ค‘์ธ์ง€๋„ ํ™•์ธ ๊ฐ€๋Šฅ

์„œ๋ฒ„์˜ ๋ชจ๋‹ˆํ„ฐ๋ง ๋„๊ตฌ๋กœ ์‚ฌ์šฉ → ๋ณ„๋„๋กœ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•ด์„œ ์‚ฌ์šฉํ•˜๋ฉด ๋” ์ข‹๋‹ค.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

์—๋Ÿฌ

org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper';

https://github.com/springfox/springfox/issues/3462

SwaggerConfig.java ์•ˆ์— bean ์„ค์ •ํ•ด์ฃผ๊ธฐ
@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));
	    }

http://localhost:8088/actuator

http://localhost:8088/actuator/health

→ ์„œ๋ฒ„๊ฐ€ ์ž‘๋™ ์ค‘์ธ์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค

application.yml ์„ค์ •

์กฐ๊ธˆ ๋” ๋‹ค์–‘ํ•œ ํŒŒ์ผ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

# actuator ์„ค์ •
management:
  endpoints:
    web:
      exposure:
        include: "*"

HAL Browser

  • Hypertext Application Language
  • “HAL is a simple format that gives a consistent and easy way to hyperlink between resources in your API”
  • API ๋ฆฌ์†Œ์Šค ์‚ฌ์ด์—์„œ ์ผ๊ด€์ ์ธ ํ•˜์ดํผ๋งํฌ๋ฅผ ์ œ๊ณต
  • API ์„ค๊ณ„์—์„œ HAL์„ ๋„์ž…ํ•˜๊ฒŒ ๋˜๋ฉด API ๊ฐ„ ์‰ฌ์šด ๊ฒ€์ƒ‰์ด ๊ฐ€๋Šฅํ•ด์ง„๋‹ค. → ๋” ๋‚˜์€ ๊ฐœ๋ฐœํ™˜๊ฒฝ ์ œ๊ณต
  • HAL์„ response message์— ๋„์ž…ํ•˜๊ฒŒ ๋˜๋ฉด ๊ทธ ๋ฉ”์‹œ์ง€์˜ ํฌ๋งท๊ณผ ์ƒ๊ด€์—†์ด API๋ฅผ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ •๋ณด(๋ฉ”ํƒ€์ •๋ณด)๋ฅผ ํ•˜์ดํผ๋งํฌ ํ˜•์‹์œผ๋กœ ์ œ๊ณต
<dependency>
	<groupId>org.springframework.data</groupId>
	<artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>

HAL Browser๊ฐ€ deprecated ๋˜์–ด์„œ ์ตœ์‹  spring boot ๋ฒ„์ „์—์„œ๋Š” HAL explorer๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค. (๋ฒ„์ „ ์ถ”๊ฐ€ ์•ˆ ํ•˜๋ฉด ์—๋Ÿฌ๋‚จ)

<dependency>
   <groupId>org.springframework.data</groupId>
   <artifactId>spring-data-rest-hal-explorer</artifactId>
	<version>3.5.1</version>
</dependency>

์žฅ์ 

response๊ฐ์ฒด์— ๋ถ€๊ฐ€์ ์ธ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ด์ค€๋‹ค

๋งํฌ ์ •๋ณด๋ฅผ jsonํ˜•ํƒœ๋กœ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ํ‘œ์ค€

์žฅ์  : rest์ž์›์„ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•œ ์ž๋ฃŒ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค์ง€ ์•Š์•„๋„ HATEOAS๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค

Spring Security

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • dependency๋ฅผ ์„ค์ •ํ•œ ํ›„, ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•˜์—ฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค. (์›น์„œ๋ฒ„๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”)
  • 5bea3312-3c01-4d40-8ab3-03e86f56627f

  • ์ธ์ฆ์ด ๋˜์–ด ์žˆ์ง€ ์•Š๋‹ค๋ฉด 401 Unauthorized๊ฐ€ ๋œฌ๋‹ค.

  • ์—ฌ๊ธฐ์—์„œ username ๊ฐ’์„ user๋กœ ํ•ด๋„ ๋ถˆ๋Ÿฌ์™€์ง€๋Š” ์ด์œ ๋Š” spring security์˜ ๊ธฐ๋ณธ ์„ค์ • ๋•Œ๋ฌธ์ด๋‹ค.

spring.security.user.name / spring.security.user.password๋ฅผ ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ธฐ๋ณธ ์œ ์ €๋ช…๊ณผ ๋žœ๋ค ํŒจ์Šค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

→ ๊ธฐ๋ณธ ์œ ์ €๋ช…์€ user์ด๋ฉฐ password์˜ ๊ฒฝ์šฐ ์Šคํ”„๋ง๋ถ€ํŠธ๊ฐ€ ๊ธฐ๋™๋  ๋•Œ ํ‘œ์‹œ๋œ๋‹ค

  • ์Šคํ”„๋ง๋ถ€ํŠธ์—์„œ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋Š” ํ•ญ๋ชฉ

Common Application Properties

Configuration ํด๋ž˜์Šค๋ฅผ ์ด์šฉํ•œ ์‚ฌ์šฉ์ž ์ธ์ฆ ์ฒ˜๋ฆฌ

[application.yml]
spring:
security:
    user:
      name: nitro
      password: helloworld

์ถ”๊ฐ€ : spring cloud bus → yml ํŒŒ์ผ ๋ณ€๊ฒฝ ์‹œ ์„œ๋ฒ„์˜ ์žฌ๋ถ€ํŒ… ์—†์ด ์—…๋ฐ์ดํŠธ ๊ฐ€๋Šฅ

Spring Cloud Bus

@Configuration // Spring boot ๊ธฐ๋™ ์‹œ ๋ฉ”๋ชจ๋ฆฌ์— ์„ค์ • ์ •๋ณด๋ฅผ ๊ฐ™์ด ๋กœ๋”ฉํ•˜๊ฒŒ ๋œ๋‹ค

๋Œ“๊ธ€