강의는 Spring Boot 2.1.13.RELEASE 버전을 사용합니다.
저는 2.2.7.RELEASE 버전을 사용했습니다.
0. Web Service & Web Application
웹서버
웹 어플리케이션
XML 보다 요즘은 JSON 을 사용
SOAP (Simple Object Access Protocol): XML 메시지 요청하고 응답받는 서비스
RESTful (REpresentational State Transfer)
- Resource 의 Representation 에 의한 상태 전달
- HTTP Method 를 통해 Resource 를 처리하기 위한 아키텍쳐
RESTful
- REST API 를 제공하는 웹 서비스
Resource
- URI (Uniform Resource Identifier), 인터넷 자원을 나타내는 유일한 주소
- XML, HTML, JSON
1. Spring Boot로 개발하는 RESTful Service
Spring Boot
- WAS (Tomcat) 가 내장되어 있어 별도로 설치할 필요가 없다.
- XML 환경설정이 필요없다.
프로젝트 생성 사이트
템플릿을 설정할 수 있다.
개발도구에서 스프링 부트 프로젝트를 사이트방문 없이 생성할 수 있다.
메인클래스를 생성한다.
@SpringBootApplication
프로젝트 생성
- 홈페이지 접속
- **Dependencies 에 라이브러리 추가
- Spring Web
- Spring Data JPA**
RESTful Services
OpenJDP 다운로드 받기
- Temurin 11 (LTS) 선택 후 설치
Spring Tools 4 다운로드 받기
- Projects - DEVELOPMENT TOOLS 에 Spring Tools 4 - Spring Tools 4 for Eclipse 에 Windows 버전으로 다운로드
- JAR 파일을 아래와 같이 지정 후 실행
C:\Program Files\Eclipse Adoptium\jdk-11.0.14.101-hotspot\bin\java.exe
- cmd 실행 후 아래코드로 IDE 를 설치한다.
java -jar spring-tool-suite-4-4.13.1.RELEASE-e4.22.0-win32.win32.x86_64.self-extracting.jar
테스트
- Create new Spring Starter Project - Next
- Developer Tools 모두 선택, Web - Spring Web 선택 후 Finish
- 프로젝트 우클릭 - Run As - 5 Spring Boot App
- 포트번호를 변경하려면 src/main/resources/application.properties 에
server.port = 80
과 같이 입력한 후 저장한다.
Prerequisite
- IntelliJ IDEA Ultimate
- Postman or curl
IntelliJ IDEA: The Capable & Ergonomic Java IDE by JetBrains
Download Postman | Get Started for Free
POSTMAN: GET POST 이외의 메서드 호출에 필요하다.
프로젝트 생성
인텔리제이: Spring Initializer
이클립스: Spring Starter
- 인텔리제이가 Community 버전이라면 사이트에서 생성한 후 압축을 풀어 프로젝트를 연다.
- Java 버전 8 로 생성
- Dependencies 5개를 추가
- Developer Tools: Spring Boot DevTools, Lombok
- Web: Spring Web
- SQL: Spring Data JAP, H2 Database
- 버전 2.5.10
C:\Users\Connor\IdeaProjects
폴더에 생성
C:\Users\Connor\.m2\repository
라이브러리를 보관하는 폴더
.파일명
시스템폴더로 숨겨져있다.
오른쪽 탭 - Maven - Lifecycle 에는 메이븐에서 실행할 수 있는 명령어들이 있다.
- application.properties → application.yml 로 변경
- yml 이 요즘 더 많이 사용된다.
- 들여쓰기가 중요하다.
server:
port: 8088
File - Settings - Editor - Font
Q. endpoint 란 ?
A.
@RestController
public class HelloWorldController {
// GET
// /hello-world (endpoint)
// @Requestmapping() 옛날
@GetMapping("/hello-world")
public String helloWorld() {
return "Hello World";
}
}
Postman - Client tool
Postman 에서도 응답을 주고받을 수 있다.
이클립스 F2 = Alt Enter
@AllArgsConstructor
@NoArgsConstructor
디폴트 생성자
annotation 검색 (Build, Execution, Deployment - Compiler - Annotation Processors) - Enable annotation processing 체크
Settings 단축키: - Ctrl Alt S
Plugins - lombok 검색 후 설치
Structure 를 누르면 구조를 확인할 수 있다.
Spring Boot 동작 원리
application.yml
logging:
level:
org.springframework: DEBUG
DispatcherServlet
dispatch
보내다. 발송.
RestController
- Spring4 부터
@RestController
지원 @Controller
+@ResponseBody
- View 를 갖지 않는 REST Data(JSON/XML) 를 반환
Path Variable
@GetMapping(path="/helllo-world-bean/path-variable/{name}")
public HelloWorldBean helloWorldBean(@PathVariable String name) {
return new HelloWorldBean(String.format("Hello World, %s ", name));
}
@GetMapping("/hello-world-bean/path-variable/{name}")
public HelloWorldBean helloWorldBean(@PathVariable String name) {
return new HelloWorldBean(String.format("Hello World, %s", name));
}
크롬 플러그인
JSON Viewer 추가
2. User Service API 구현
User
@Data
@AllArgsConstructor
public class User {
private Integer id;
private String name;
private Date joinDate;
}
UserDaoService
@Service
public class UserDaoService {
private static List<User> users = new ArrayList<>();
private static int usersCount = 3;
static {
users.add(new User(1, "Kenneth", new Date()));
users.add(new User(2, "Alice", new Date()));
users.add(new User(3, "Elena", new Date()));
}
public List<User> findAll() {
return users;
}
public User save(User user) {
if (user.getId() == null) {
user.setId(++usersCount);
}
users.add(user);
return user;
}
public User findOne(int id) {
for (User user : users) {
if (user.getId() == id) {
return user;
}
}
return null;
}
}
DI
@Controller
@Service
어노테이션 추가 후
생성자 추가하거나 @Autowired
사용
UserController
@RestController
public class UserController {
private UserDaoService service;
public UserController(UserDaoService service) {
this.service = service;
}
@GetMapping("/users")
public List<User> retrieveAllUsers() {
return service.findAll();
}
// GET /users/1 or /users/10
@GetMapping("/users/{id}")
public User retrieveUser(@PathVariable int id) {
return service.findOne(id);
}
}
Postman 에서 POST 호출하는 법
- Post 선택 후 주소입력
- Body 탭의 raw 와 JSON 선택
HTTP Status Code 제어
- 201 Created
UserController
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
User savedUser = service.save(user);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(savedUser.getId())
.toUri();
return ResponseEntity.created(location).build();
}
Status 를 다르게 표현하는 것이 REST API
HTTP Status Code 제어를 위한 Exception Handling
두 줄로 나눌 수 있는 단축키: 인텔리제이에서 메서드 우클릭 - Refactor - Introduce Variable (Ctrl Alt V)
UserNotFoundException
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
500 Internal Server Error → 코드가 노출되므로 좋은 방법은 아니다.
// 2XX -> OK
// 4XX -> Client
// 5XX -> Server
다음 코드로 변경한다.
@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
500 → 404Not Found 로 바뀐다.
Spring의 AOP를 이용한 Exception Handling
@ControllerAdvice
모든 컨트롤러 실행 전 실행된다.
{
"timestamp": "2022-03-09T13:27:22.628+00:00",
"message": "ID[100] not found",
"details": "uri=/users/100"
}
@RestController
@ControllerAdvice
public class CustomizeResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(Exception.class)
public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
ExceptionResponse exceptionResponse =
new ExceptionResponse(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@ExceptionHandler(Exception.class)
예외가 발생하면 메서드 실행
@ExceptionHandler(UserNotFoundException.class)
public final ResponseEntity<Object> handleUserNotFoundExceptions(Exception ex, WebRequest request) {
ExceptionResponse exceptionResponse =
new ExceptionResponse(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(exceptionResponse, HttpStatus.NOT_FOUND);
}
500 → 404
사용자 삭제를 위한 API 구현 - DELETE HTTP Method
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable int id) {
User user = service.deleteById(id);
if (user == null) {
throw new UserNotFoundException(String.format("ID[%s] not found", id));
}
}
REST API 메서드
- HTTP 가 가지고 있는 모든 메서드타입 가지고 있지 않다.
3. RESTful Service 기능 확장
1) 유효성 체크를 위한 Validation API 사용
하이버네이트 라이브러리에 포함된 Validation 을 사용한다.
하이버네이트는 자바의 객체와 데이터베이스의 엔티티와 매핑을 하기위한 프레임워크를 제공한다.
@Size
javax.validation.constraints
되지 않는다면 스프링부트 버전을 확인합니다.
User
@Data
@AllArgsConstructor
public class User {
private Integer id;
@Size(min = 2)
private String name;
@Past
private Date joinDate;
}
유효성검사 어노테이션
@Size
@Past
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
User savedUser = service.save(user);
@Valid
유효성검사에 어긋날 경우 400 Bad Request Status 를 반환한다.
{
"timestamp": "2022-03-10T12:21:26.280+0000",
"message": "Validation Failed",
"details": "org.springframework.validation.BeanPropertyBindingResult: 1 errors\nField error in object 'user' on field 'name': rejected value [N]; codes [Size.user.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; arguments []; default message [name],2147483647,2]; default message [반드시 최소값 2과(와) 최대값 2147483647 사이의 크기이어야 합니다.]"
}
CustomizeResponseEntityExceptionHandler
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex
, HttpHeaders headers
, HttpStatus status
, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date()
, "Validation Failed", ex.getBindingResult().toString());
return new ResponseEntity<>(exceptionResponse, HttpStatus.BAD_REQUEST);
}
{
"timestamp": "2022-03-10T12:22:49.829+0000",
"message": "Validation Failed",
"details": "org.springframework.validation.BeanPropertyBindingResult: 1 errors\nField error in object 'user' on field 'name': rejected value [N]; codes [Size.user.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; arguments []; default message [name],2147483647,2]; default message [Name 은 2글자 이상 입력해 주세요.]"
}
@Size(min = 2, message = "Name 은 2글자 이상 입력해 주세요.")
2) 다국어 처리를 위한 Internationalization 구현 방법
yml
spring:
messages:
basename: messages
다국어 파일명을 messages 로 하겠다.
properties 파일을 만든다.
HelloWorldController
@GetMapping("/hello-wrold-internationalized")
public String helloWorldInternationalized(@RequestHeader(name="Accept-Language", required=false) Locale locale) {
return messageSource.getMessage("", null, locale);
}
'안녕하세요' 대신 '?????' 로 나온다면 Settings 에서 File Encodings 를 UTF-8 로 바꾼다.
3) Response 데이터 형식 변환 - XML format
REST API 의 반환값을 스프링부트의 기본설정인 JSON 포맷이 아니라 XML 포맷으로 변경하여 전달하는 방법이다.
Postman 에서 Headers 에 Accept - application/xml 를 GET 방식으로 호출한다면 결과가 나오지 않는다.
Maven 을 사용한다면 pom.xml
Gradle 을 사용한다면 build.gradle 을 사용한다.
<List>
<item>
<id>1</id>
<name>Kenneth</name>
<joinDate>2022-03-10T12:54:23.109+0000</joinDate>
</item>
<item>
<id>2</id>
<name>Alice</name>
<joinDate>2022-03-10T12:54:23.109+0000</joinDate>
</item>
<item>
<id>3</id>
<name>Elena</name>
<joinDate>2022-03-10T12:54:23.109+0000</joinDate>
</item>
</List>
잘 반환된다.
pom.xml
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.10.2</version>
</dependency>
4) Response 데이터 제어를 위한 Filtering
User
private String password;
private String ssn;
[
{
"id": 1,
"name": "Kenneth",
"joinDate": "2022-03-10T13:01:05.750+0000",
"password": "pass1",
"ssn": "701010-1111111"
},
{
"id": 2,
"name": "Alice",
"joinDate": "2022-03-10T13:01:05.750+0000",
"password": "pass2",
"ssn": "801010-2222222"
},
{
"id": 3,
"name": "Elena",
"joinDate": "2022-03-10T13:01:05.750+0000",
"password": "pass2",
"ssn": "901010-1111111"
}
]
중요한 정보가 보여지고 있다.
스프링부트에서 JSON 데이터를 처리하기 위해 잭슨 라이브러리를 사용하면 노출시키고 싶지 않은 데이터를 제어할 수 있다.
User
@JsonIgnore
private String password;
@JsonIgnore
private String ssn;
[
{
"id": 1,
"name": "Kenneth",
"joinDate": "2022-03-10T13:07:11.371+0000"
},
{
"id": 2,
"name": "Alice",
"joinDate": "2022-03-10T13:07:11.371+0000"
},
{
"id": 3,
"name": "Elena",
"joinDate": "2022-03-10T13:07:11.371+0000"
}
]
비밀번호와 주민등록번호를 이와같이 숨길 수 있다.
@JsonIgnoreProperties(value={"password", "ssn"})
public class User {
같은 코드이다.
5) 프로그래밍으로 제어하는 Filtering 방법 - 개별 사용자 조회
{
"id": 1,
"name": "Kenneth",
"password": "pass1",
"ssn": "701010-1111111"
}
필터대로 데이터를 가져온다.
AdminUserController
@GetMapping("/users/{id}")
public MappingJacksonValue retrieveUser(@PathVariable int id) {
User user = service.findOne(id);
if (user == null) {
throw new UserNotFoundException(String.format("ID[%s] not found", id));
}
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter
.filterOutAllExcept("id", "name", "password", "ssn");
FilterProvider filters = new SimpleFilterProvider().addFilter("UserInfo", filter);
MappingJacksonValue mapping = new MappingJacksonValue(user);
mapping.setFilters(filters);
return mapping;
}
}
6) 프로그래밍으로 제어하는 Filtering 방법 - 전체 사용자 조회
데이터를 필터링하여 반환
[
{
"id": 1,
"name": "Kenneth",
"joinDate": "2022-03-10T13:47:23.338+0000",
"password": "pass1"
},
{
"id": 2,
"name": "Alice",
"joinDate": "2022-03-10T13:47:23.338+0000",
"password": "pass2"
},
{
"id": 3,
"name": "Elena",
"joinDate": "2022-03-10T13:47:23.338+0000",
"password": "pass2"
}
]
AdminUserController
@GetMapping("/users")
public MappingJacksonValue retrieveAllUsers() {
List<User> users = service.findAll();
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter
.filterOutAllExcept("id", "name", "joinDate", "password");
FilterProvider filters = new SimpleFilterProvider().addFilter("UserInfo", filter);
MappingJacksonValue mapping = new MappingJacksonValue(users);
mapping.setFilters(filters);
return mapping;
}
7) URI를 이용한 REST API Version 관리
https://developers.facebook.com/docs/graph-api/guides/versioning
curl: 포스트맨과 같이 터미널에서 클라이언트에서 서버로 Request 보낼 때 사용할 수 있는 테스트 툴이다.
BeanUtils.
copyProperties(source, target)
속성이 같은 것을 복사한다.
URI 를 통한 버전관리
@GetMapping("/v2/users/{id}")
public MappingJacksonValue retrieveUserV2(@PathVariable int id) {
User user = service.findOne(id);
if (user == null) {
throw new UserNotFoundException(String.format("ID[%s] not found", id));
}
// User -> User2
UserV2 userV2 = new UserV2();
BeanUtils.copyProperties(user, userV2); // id, name, joinDate, password, ssn
userV2.setGrade("VIP");
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter
.filterOutAllExcept("id", "name", "joinDate", "grade");
FilterProvider filters = new SimpleFilterProvider().addFilter("UserInfoV2", filter);
MappingJacksonValue mapping = new MappingJacksonValue(userV2);
mapping.setFilters(filters);
return mapping;
}
8) Request Parameter와 Header를 이용한 API Version 관리
Request Parameter 와 Header 정보로 버전관리
ㄱ. Params 로 버전관리
@GetMapping(value = "/users/{id}", params = "version=1")
public MappingJacksonValue retrieveUserV1(@PathVariable int id) {
User user = service.findOne(id);
if (user == null) {
throw new UserNotFoundException(String.format("ID[%s] not found", id));
}
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter
.filterOutAllExcept("id", "name", "password", "ssn");
FilterProvider filters = new SimpleFilterProvider().addFilter("UserInfo", filter);
MappingJacksonValue mapping = new MappingJacksonValue(user);
mapping.setFilters(filters);
return mapping;
}
ㄴ. Headers 로 버전관리
@GetMapping(value = "/users/{id}", headers="X-API-VERSION=1")
public MappingJacksonValue retrieveUserV1(@PathVariable int id) {
User user = service.findOne(id);
if (user == null) {
throw new UserNotFoundException(String.format("ID[%s] not found", id));
}
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter
.filterOutAllExcept("id", "name", "password", "ssn");
FilterProvider filters = new SimpleFilterProvider().addFilter("UserInfo", filter);
MappingJacksonValue mapping = new MappingJacksonValue(user);
mapping.setFilters(filters);
return mapping;
}
ㄷ. MIME 로 버전관리
MIME이란?
Multipurpose Internet Mail Extensions의 약자
@GetMapping(value = "/users/{id}", produces = "application/vnd.company.appv1+json")
public MappingJacksonValue retrieveUserV1(@PathVariable int id) {
User user = service.findOne(id);
if (user == null) {
throw new UserNotFoundException(String.format("ID[%s] not found", id));
}
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter
.filterOutAllExcept("id", "name", "password", "ssn");
FilterProvider filters = new SimpleFilterProvider().addFilter("UserInfo", filter);
MappingJacksonValue mapping = new MappingJacksonValue(user);
mapping.setFilters(filters);
return mapping;
}
URI, Requeset 는 일반 브라우저에서 실행 가능
Headers 와 MIME 타입은 일반 브라우저에서 실행 불가
개발자는 포스트맨으로 테스트할 수 있다.
4. Spring Boot API 사용
1) Level3 단계의 REST API 구현을 위한 HATEOAS 적용
Hateoas = Hypermedia As the Engine Of Application State
Shift x 2: 프로젝트 검색
pom.xml
<!-- Hateoas -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
ㄱ. spring 2.1.8.RELEASE
Resource
ControllerLinkBuilder
Resouce<User> resource = new Resource<>(user);
ControllerLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllUsers());
resource.add(linkTo.withRel("all-users"));
return resource;
ㄴ. spring 2.2 일 경우
Resource -> EntityModel
ControllderLinkBuilder -> WebMvcLinkBuilder
EntityModel<User> model = new EntityModel<>(user);
WebMvcLinkBuilder linkTo = linkTo(methodOn(this.getClass())retrieveAllUsers());
model.add(linkTo.withRel("all-users"));
return model;
동작하기 위해 JSON filter 는 주석처리한다.
{
"id": 1,
"name": "Kenneth",
"joinDate": "2022-03-11T03:43:50.144+0000",
"password": "pass1",
"ssn": "701010-1111111",
"links": [
{
"rel": "all-users",
"href": "http://localhost:8088/users"
}
]
}
상세보기, 삭제, 수정을 같은 패턴으로 추가할 수 있다.
본인은 2.2.7 버전이므로
UserController
@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 = new EntityModel<>(user);
WebMvcLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllUsers());
model.add(linkTo.withRel("all-users"));
return model;
}
linkTo 와 methodOn 메서드는 import static 으로 선언한다.
2) REST API Documentation을 위한 Swagger 사용
사용자 혹은 개발자를 위한 Documentation 만들기
- Dependency 추가
@Configuration
pom.xml
<!-- Swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
이 코드는 스프링부트의 버전이 2.1 일 때 사용할 수 있다.
이 코드를 추가 후 서버를 실행했을 때 아래와 같은 에러가 발생한다면
에러
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
<!-- Swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
이 코드를 대신 사용하자.
본인은 이 코드를 사용하였다.
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2);
}
}
구버전에서는 http://localhost:8088/swagger-ui.html 로 확인한다.
최신버전에서는 http://localhost:8088/swagger-ui/index.html 로 확인한다.
Swagger 가 컨트롤러 등을 분석해서 페이지를 만들어준다.
JSON 데이터
http://localhost:8088/v2/api-docs
3) Swagger Documentation 구현 방법
Swagger 커스터마이징을 해보자.
SwaggerConfig
import io.swagger.models.Contact;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
private static final Contact DEFAULT_CONTACT = new Contact();
private static final ApiInfo DEFAULT_API_INFO = new ApiInfo("Awesome API Title"
, "My User management REST API service", "1.0", "urn:tos"
, null, "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);
}
}
이제 도메인 객체도 커스터마이징 해보자.
definitions - User 에 있는 데이터를 확인해보면
"User": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
"joinDate": {
"type": "string",
"format": "date-time",
"description": "사용자 등록일을 입력해 주세요."
},
"name": {
"type": "string",
"description": "사용자 이름을 입력해 주세요.",
"minLength": 2,
"maxLength": 2147483647
},
"password": {
"type": "string",
"description": "사용자 패스워드를 입력해 주세요."
},
"ssn": {
"type": "string",
"description": "사용자 주민번호를 입력해 주세요."
}
},
"title": "User",
"description": "사용자 상세 정보를 위한 도메인 객체"
},
@Data
@AllArgsConstructor
// @JsonIgnoreProperties(value={"password", "ssn"})
@NoArgsConstructor
//@JsonFilter("UserInfo")
@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;
}
4) REST API Monitoring을 위한 Actuator 설정
스프링 프로젝트에 모니터링 추가
pom.xml
<!-- 모니터링 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
http://localhost:8088/actuator
모니터링의 목록을 확인할 수 있다.
http://localhost:8088/actuator/health
서버상태를 확인할 수 있다.
management:
endpoints:
web:
exposure:
include: "*"
이 코드를 추가하면 더 많은 정보를 볼 수 있다.
5) HAL Browser를 이용한 HATEOAS 기능 구현
HAL Browser
Hypertext Application Language 의 약자
HAL 은 API 리소스들 사이에서 쉽게 일관적인 하이퍼 링크를 제공하는 방식이다.
pom.xml
<!-- HAL 브라우저 -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>
http://localhost:8088 로 접속하면
HAL 브라우저로 연결된다.
Explorer 에 /actuator
를 검색하고
Links 에서 metrics 로 들어간다.
Response Body 에서 jvm.memory.max
를 복사하여 Explorer 에 붙여넣기한다.
http://localhost:8088/actuator/metrics/jvm.memory.max
6) Spring Security를 이용한 인증 처리
REST API 인증을 처리하기 위한 다양한 방법이 있다.
- OAuth 를 이용한 방식
- JWT 토큰을 이용하는 방식
- ID/Password 를 이용하는 방식
Spring Security 를 이용한 방식으로 인증을 처리해보자.
pom.xml
<!-- 인증 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
콘솔에 password 를 검색하면
Using generated security password: bed895a5-40b8-4cef-b6c3-9edcd6278f8c
포스트맨에서 Authorization 의 TYPE 을 Basic Auth 로 변경해서 아이디에 user 비밀번호에 콘솔에 출력된 비밀번호를 입력한 후 호출하면
데이터가 잘 나오는 것을 확인할 수 있다.
7) Configuration 클래스를 이용한 사용자 인증 처리
개발자가 지정한 아이디 비밀번호로 인증처리를 해보자.
application.yml
spring:
security:
user:
name: username
password: passw0rd
아이디와 비밀번호를 위와같이 임의로 줄 수 있다.
@Configuration
이 어노테이션이 붙은 클래스는 스프링부트가 기동하면서 메모리에 설정정보를 같이 로딩하게 됩니다.
SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication()
.withUser("kenneth")
.password("{noop}test1234")
.roles("USERS");
}
}
5. Java Persistence API 사용
JPA
Java Persistence API 라고 한다. 자바에서 정의한 Object 와 데이터베이스에서 사용하는 Entity 모델과 Mapping 하는 방식으로 Programming 하도록 한다.
1) Java Persistence API의 개요
JPA
- Java Persistence API
- 자바 ORM 기술에 대한 API 표준 명세
- 자바 어플리케이션에서 RDBMS 를 사용하는 방식을 정의한 인터페이스
- Entity Manager 를 통해 CRUD 처리
Hibernate
- JPA 의 구현체, 인터페이스를 직접 구현한 라이브러리
- 생산성, 유지보수, 비종속성
Spring Data JPA
- Spring Module
- JPA 를 추상화한 Repository 인터페이스 제공
Hibernate 대신 이클립스 링크 등을 사용해도 된다.
하지만 성능이나 안정성이 좋으므로 생산성, 유지보수, 종속성에 대해 좋다.
2) JPA를 사용을 위한 Dependency 추가와 설정
pom.xml
<!-- JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
콘솔에 h2.consol
을 검색하면 spring.h2.console.enabled=true
를 확인할 수 있다.
http://localhost:8088/h2-console 로 접속한다.
JDBC URL 을 jdbc:h2:mem:testdb
로 바꾼다.
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 추가
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/h2-console/**").permitAll();
http.csrf().disable();
http.headers().frameOptions().disable();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication()
.withUser("kenneth")
.password("{noop}test1234")
.roles("USERS");
}
}
이 코드로 인증처리를 막는다.
Test Connection 을 누르면 Test successful 이 뜨는 것을 확인할 수 있다.
3) Spring Data JPA를 이용한 Entity 설정과 초기 데이터 생성
어노테이션
@Entity
@id
@GeneratedValue
어노테이션만으로 Project 실행 시 데이터베이스를 자동으로 생성합니다.
application.yml
spring:
jpa:
show-sql: true
User
@Entity
public class User {
@Id
@GeneratedValue
private Integer id;
콘솔에 create table
을 검색하면 테이블이 생성되는 것을 알 수 있다.
resources 에 data.sql 파일을 생성한다.
스프링부트 처음 로딩할 때 data.sql 파일로 초기데이터 생성 등의 작업을 한다.
data.sql
INSERT INTO user VALUES (1, sysdate(), 'User1', 'test1111', '701010-1111111');
INSERT INTO user VALUES (2, sysdate(), 'User2', 'test2222', '801010-2222222');
INSERT INTO user VALUES (3, sysdate(), 'User3', 'test3333', '901010-1111111');
콘솔에 insert into
라고 검색하면 데이터가 들어간 것을 볼 수 있다.
4) JPA Service 구현을 위한 Controller, Repository 생성
UserRepository
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
}
UserJpaController
@RestController
@RequestMapping("/jpa")
public class UserJpaController {
@Autowired
private UserRepository userRepository;
@GetMapping("/users")
public List<User> retrieveAllUsers() {
return userRepository.findAll();
}
}
5) JPA를 이용한 사용자 목록 조회 - GET HTTP Method
UserJpaController
@RestController
@RequestMapping("/jpa")
public class UserJpaController {
@Autowired
private UserRepository userRepository;
@GetMapping("/users")
public List<User> retrieveAllUsers() {
return userRepository.findAll();
}
@GetMapping("/users/{id}")
public EntityModel<User> retrieveUser(@PathVariable int id) {
Optional<User> user = userRepository.findById(id);
if (!user.isPresent()) {
throw new UserNotFoundException(String.format("ID[%s] not found", id));
}
// HATEOAS
EntityModel<User> model = new EntityModel<>(user.get());
WebMvcLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllUsers());
model.add(linkTo.withRel("all-users"));
return model;
}
}
6) JPA를 이용한 사용자 추가와 삭제 - POST/DELETE HTTP Method
유저 삭제
@DeleteMapping("/users/{id}")
private void deleteUser(@PathVariable int id) {
userRepository.deleteById(id);
}
유저 생성
import java.net.URI;
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
User savedUser = userRepository.save(user);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(savedUser.getId())
.toUri();
return ResponseEntity.created(location).build();
}
id 의 시퀀스가 1부터 시작하므로 초기데이터의 id 는 중복되지 않게 수정해야 한다.
7) 게시물 관리를 위한 Post Entity 추가와 초기 데이터 생성
@ManyToOne(fetch = FetchType.LAZY)
게시글과 유저의 관계가 다 대 1 이다.
Post
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Post {
@Id
@GeneratedValue
private Integer id;
private String description;
// User : Post -> 1 : (0~N), Main : Sub -> Parent : Child
@ManyToOne(fetch = FetchType.LAZY)
@JsonIgnore
private User user;
}
User
@OneToMany(mappedBy = "user")
private List<Post> posts;
특정 부모테이블을 OneToMany 로 설정하면 자식 테이블은 ManyToOne 이 됩니다.
자식테이블은 하나의 부모테이블을 가집니다.
data.sql
INSERT INTO post VALUES (10001, 'My first post', 90001);
INSERT INTO post VALUES (10002, 'My second post', 90001);
User
public User(int id, String name, Date joinDate, String password, String ssn) {
this.id = id;
this.name = name;
this.joinDate = joinDate;
this.password = password;
this.ssn = ssn;
}
Q. 왜 이 코드를 추가해야할까?
A.
8) 게시물 조회를 위한 Post Entity와 User Entity와의 관계 설정
http://localhost:8088/jpa/users/90001/posts 을 호출합니다.
[
{
"id": 10001,
"description": "My first post"
},
{
"id": 10002,
"description": "My second post"
}
]
UserJpaController
@GetMapping("/users/{id}/posts")
public List<Post> retrieveAllPostsByUser(@PathVariable int id) {
Optional<User> user = userRepository.findById(id);
if (!user.isPresent()) {
throw new UserNotFoundException(String.format("ID[%s] not found", id));
}
return user.get().getPosts();
}
ID 가 90001 인 유저의 모든 게시글을 불러옵니다.
[
{
"id": 90001,
"name": "User1",
"joinDate": "2022-03-10T15:00:00.000+0000",
"password": "test1111",
"ssn": "701010-1111111",
"posts": [
{
"id": 10001,
"description": "My first post"
},
{
"id": 10002,
"description": "My second post"
}
]
},
{
"id": 90002,
"name": "User2",
"joinDate": "2022-03-10T15:00:00.000+0000",
"password": "test2222",
"ssn": "801010-2222222",
"posts": []
},
{
"id": 90003,
"name": "User3",
"joinDate": "2022-03-10T15:00:00.000+0000",
"password": "test3333",
"ssn": "901010-1111111",
"posts": []
}
]
9) JPA를 이용한 새 게시물 추가 - POST HTTP Method
http://localhost:8088/jpa/users/90001/posts
UserJpaController
@PostMapping("/users/{id}/posts")
public ResponseEntity<Post> createPost(@PathVariable int id, @RequestBody Post post) {
Optional<User> user = userRepository.findById(id);
if (!user.isPresent()) {
throw new UserNotFoundException(String.format("ID[%s] not found", id));
}
post.setUser(user.get());
Post savedPost = postRepository.save(post);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(savedPost.getId())
.toUri();
return ResponseEntity.created(location).build();
}
{
"id": 90001,
"name": "User1",
"joinDate": "2022-03-10T15:00:00.000+0000",
"password": "test1111",
"ssn": "701010-1111111",
"posts": [
{
"id": 1,
"description": "My third post"
},
{
"id": 10001,
"description": "My first post"
},
{
"id": 10002,
"description": "My second post"
}
],
"_links": {
"all-users": {
"href": "http://localhost:8088/jpa/users"
}
}
}
글 작성이 잘 된 것을 볼 수 있다.
6. RESTful API 설계 가이드
1) Richardson Maturity Model 소개
REST 설계한 Leonard Richardson
Level 0
GET 과 POST 방식으로 모두 표현. URI 에 GET POST 의 동작값을 같이 표시한다.
Level 1
Level 2
Level 1 단계에 HTTP Methods
Level 3
Level 2 단계에 Hateoas 를 추가한 단계
2) REST API 설계 시 고려해야 할 사항
Request methods
- GET
- POST
- PUT
- DELETE
Response Status
- 200
- 404
- 400
- 201
- 401
Use plurals
- prefer/users to/user X
- prefer/users/
User nouns for resources
동사 대신 명사를 사용할 것
For exceptions
- define a consistent approach
- /search
- PUT /gists/{id}/star
- DELETE /gists/{id}/star
'Backend > 노트' 카테고리의 다른 글
Tucker 의 Go 언어 프로그래밍 (0) | 2022.08.14 |
---|---|
한 번에 끝내는 Node.js 웹 프로그래밍 초격차 패키지 Online (0) | 2022.07.06 |
🍀스프링부트 (0) | 2022.03.29 |
DB 연결 (0) | 2022.03.19 |
따라하며 배우는 도커와 CI환경 (0) | 2022.03.11 |