카테고리 없음

[REST] RESTful Service 구현방법 #3(RESTFUL Service 기능 확장)

오늘도출근하는다람쥐 2023. 10. 22. 22:11

RESTFul Service 구현방법 #2Permalink

1장. 유효성 체크를 위한 Validation API 사용Permalink

Validation API 사용을 위해서는 pom.xml 아래에 아래 dependency를 추가해주고 Maven Reload 수행

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

User 도메인 클래스

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Past;
import javax.validation.constraints.Size;

import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;

    @Size(min=2) //이 부분 추가
    private String name;
    @Past //이 부분 추가
    private Date joinDate;
}

UserController 클래스

    @PostMapping("/users")
    public ResponseEntity<User> createUser(@Valid @RequestBody User user){
        //@Valid 부분 추가(유효성 검사를 원하는 곳에)
        User savedUser = service.save(user);
        ...

하지만 여기까지만 추가해주면 POST MAN에서 테스트했을때 @Valid가 동작하지 않음을 확인할 수 있다.

검색해보니 아래와 같이 해결법이 나와 있다

spring boot 2.3 version 이상부터는 spring-boot-starter-web 의존성 내부에 있던 validation이 사라졌습니다. 때문에 사용하시는 spring boot version이 2.3 이상이라면 validation 의존성을 따로 추가해주셔야 사용할 수 있습니다.

그래서 pom.xml(dependency쪽에 아래와 같이 추가해주면 된다)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>2.5.2</version>
</dependency>

이렇게 하고 다시한번 Maven Reload 진행하고 테스트하면 정상적으로 결과를 확인 할 수 있다

결과값을 보면 Bad Request 400이라고 확인은 가능하지만 아무것도 Return 되는것이 없다.

우리는 에러를 리턴해주고 싶다

기존에 만들었던 CustomizedResponseEntityExceptionHandler 클래스로 이동

 /*
 ResponseEntityExceptionHandler 안에 159라인에 있는 
 handleMethodArgumentNotValid 라는 메소드를 여기서 Override 해서 재정의
 */
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,HttpHeaders headers,HttpStatus status,WebRequest request) {
        ExceptionResponse exceptionResponse
             = new ExceptionResponse(new Date(), "Validation Failed 발생하였습니다", ex.getBindingResult().toString());
             //여기서 2번째 파라미터("Validation Failed ~) 내용은 원하는걸로 변경 가능
        
        return new ResponseEntity(exceptionResponse, HttpStatus.BAD_REQUEST);
    }

User 클래스로 이동

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;

    @Size(min=2, message = "Name은 2글자 이상 입력해 주세요.") //여기에 Message 부분에 내용 추가
    private String name;
    @Past
    private Date joinDate;
}

이렇게 해주면 자동으로 Valid 에러가 나는 부분을 캐치해서 위와 같이 에러를 반환해줄 수 있다

 

2장. 다국어 처리를 위한 Internationalization 구현 방법Permalink

만약 웹브라우저 기본 설정이 영어로 되어 있으면 영어로 표시, 한국어로 되어 있으면 한국어로 표시

다국어 처리가 특정 컨트롤러에만 사용되는 것이 아니라 프로젝트 전반적으로 모두 사용될 수 있게 하겠음

다국어 처리에 필요한 빈을 스프링부트어플리케이션에 등록해둬서 초기화 될 때 사용될 수 있게 동작

RestfulWebServiceApplication 클래스로 이동(@SpringBootApplication)

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

import java.util.Locale;

@SpringBootApplication
public class RestfulWebServiceApplication {

	public static void main(String[] args) {

		SpringApplication.run(RestfulWebServiceApplication.class, args);
	}

	@Bean //이 부분 추가(다국어 처리 - 기본 언어 : Korea)
	public LocaleResolver localeResolver(){
		SessionLocaleResolver localeResolver = new SessionLocaleResolver();
		localeResolver.setDefaultLocale(Locale.KOREA);
		return localeResolver;
	}
    /*
    SpringBootApplication 이라는 곳에 Bean을 등록하게 되면 스프링 부트가 초기화 될 때 이 정보가 같이 메모리에 
    올라가서 다른쪽에 있는곳도 사용 가능해진다
    */
}

두번째로 다국어 처리를 위해서는 다국어 파일을 저장해야 하는데 다국어 파일명을 먼저 application.yml 파일에 먼저 저장해야한다

server:
  port: 8088

logging:
  level:
    org.springframework: DEBUG

spring:
  message:
    basename: messages

여기에 spring: message: messages 부분에 파일명을 지정해두고 resources 폴더에 messages.properties라는 파일을 만들어 준다

  1. messages.properties
  2. messages_fr.properties
  3. messages_en.properties

이렇게 위에 3개를 만들어 준다(한국어, 프랑스어, 영어)

//messages.properties 파일
greeting.message=안녕하세요

//messages_fr.properties 파일
greeting.message=bonjur

//messages_en.properties 파일
greeting.message=Hello

이렇게 만들고 나서 이 파일들을 사용할 수 있는 컨트롤러를 하나 수정 하겠음(여기서는 단순하게 HelloWorldController사용)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

import java.util.Locale;

@RestController //자동으로 JSON 타입으로 반환
public class HelloWorldController {

    @Autowired //이 부분 추가 필요
    private MessageSource messageSource;

    ...

    @GetMapping(path = "/hello-world-internationalized")
    public String helloWorldInternationalized(@RequestHeader(name="Accept-Language", required = false) Locale locale){ 
        //locale값은 RequestHeader에 의해 값이 주어질것이라서 RequestHeader 사용
        //앞에 설명드렸던 Accept-Language 헤더값이 포함되지 않았을때는 Defaul t 값(Korea)가 반환
        return messageSource.getMessage("greeting.message", null, locale);
    }
}

이렇게 작성하고 실행한다음 Post Man으로 테스트 해보면 아래와 같이 다국어 처리가 가능함을 알 수 있따

PostMan Header 부분에 Accept-Language 부분에 fr(프랑스어), en(영어) 나오며 아무것도 잆력 하지 않으면 default값인 한국어가 나온다

만약에 여기서 한글이 깨지고 ???로 나온다면 Intellij 설정을 바꿔주면 된다

preferences > file encoding > proferties files에 값을 UTF-8로 변경해준다

이렇게 하면 기존에 한글이 ???로 되어있을수 있는데 바꿔주고 서버 재실행하면 정상적으로 한글이 표시되는것을 확인 가능

3장. Response 데이터 형식 변환 - XML FormatPermalink

매번 JSON 타입을 반환받았지만 이번에는 XML 타입으로 요청 및 반환 진행

POST MAN 에서 request를 보낼때 Headers 부분에 KEY : Accept , VALUE : application/xml 입력하고 보내기

이렇게 하면 아직 서버에서는 XML 반환이 준비되어 있지 않아 406 에러가 표시된다

해결하기 위해서는 먼저 XML 처리할 수 있는 라이브러리 추가

pom.xml에 아래 depency 추가해주고 maven reload 실행

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.10.2</version>
</dependency>

이렇게 depency만 추가해주고 서버 재기동 해주는것만으로 xml 반환 처리 완료

Header 부분에 KEY : Accept , VALUE : application/json 을 하게 되면 처음에 나왔던 것 처럼 json 타입으로 반환해준다

4장. Response 데이터 제어를 위한 FilteringPermalink

사용자 정보관리 데이터중 클라이언트에 전달하는 데이터를 제어하는 방법에 대해서 학습

기존에 있던 User 도메인 클래스에 새로운 중요한 정보 필드를 추가(비밀번호 등)

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;

    @Size(min=2, message = "Name은 2글자 이상 입력해 주세요.")
    private String name;
    @Past
    private Date joinDate;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;

    @Size(min=2, message = "Name은 2글자 이상 입력해 주세요.")
    private String name;
    @Past
    private Date joinDate;
    private String password; //여기 추가
    private String ssn; //여기 추가
}

여기에 위에 필드를 두개 추가해주면 기존 DAO 클래스에 에러가 발생하는데 가서 아래처럼 필드 추가해주어야 한다

@Service
public class UserDaoService {
    //비즈니스 로직 = service
    private static List<User> users = new ArrayList<>();

    private static int usersCount = 3;

    static {
        users.add(new User(1, "Gildong", new Date(), "pass1", "701010-111111")); //여기 변경
        users.add(new User(2, "Gildong", new Date(), "pass2", "701010-111111")); //여기 변경
        users.add(new User(3, "Gildong", new Date(), "pass3", "701010-111111")); //여기 변경
        System.out.println("=============USER DAO SERVICE=================");
    }
    ...

이렇게 하면 중요한 정보인 password, ssn등 모두 아래처럼 표시되는것을 확인 할 수 있음

여기서 우리가 하고싶은것

도메인 클래스가 가지고 있던 정보중에 외부에 노출시키고 싶지 않은 경우

ex) 패스워드는 xxxxxx로 표시 , ssn은 701010-xxxxxx 이럲게 보여주는것

User 도메인 클래스에 정보를 노출시키고 싶지 않은 필드에 @JsonIgnore를 추가

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;

    @Size(min=2, message = "Name은 2글자 이상 입력해 주세요.")
    private String name;
    @Past
    private Date joinDate;
    
    @JsonIgnore //여기 추가
    private String password;

    @JsonIgnore //여기 추가
    private String ssn;
}

이렇게 하면 정보가 출력되지 않는것을 확인 할 수 있음

아래처럼 동일하게 사용도 가능(@JsonIgnore로 하나씩 하기보다는 한번에 @JsonIgnoreProperties 사용)

@JsonIgnoreProperties(value = {"password", "ssn"})
public class User {
    ...
    private String password;
    private String ssn;
}

User 도메인 클래스에 숨길 필드값을 지정해두면 User 도메인 클래스를 사용하는 모든 곳에 똑같이 적용

@JsonIgnore, @JsonIgnoreProperties 사용

5장. 프로그래밍으로 제어하는 Filtering 방법 - 개별 사용자 조회Permalink

User 도메인 클래스에 @JsonFilter(“원하는 이름”) 추가

@Data
@AllArgsConstructor
@NoArgsConstructor
//@JsonIgnoreProperties(value = {"password","ssn"})
@JsonFilter("UserInfo")
public class User {
    private Integer id;

    @Size(min=2, message = "Name은 2글자 이상 입력해 주세요.")
    private String name;
    @Past
    private Date joinDate;
    private String password;
    private String ssn;
}

이렇게 하고 기존에 있던 UserController를 복사해서 AdminUserController 클래스를 만들어 준다

@RestController
public class AdminUserController {
    private UserDaoService service;

    public AdminUserController(UserDaoService service){
        this.service = service;
    }

    @GetMapping("/admin/users")
    public List<User> retriveAllUsers(){
        return service.findAll();
    }

    // GET /users/1 or /users/10 -> String : 기본적으로 String이 들어오지만 (아래와 같이 int id)를 해주면 자동으로 int로 변환
    @GetMapping("/admin/users_temp/{id}")
    public User retrieveUser_temp(@PathVariable int id){
        return service.findOne(id);
    }

    // GET /users/1 or /users/10 -> String : 기본적으로 String이 들어오지만 (아래와 같이 int id)를 해주면 자동으로 int로 변환
    @GetMapping("/admin/users/{id}")
    public User retrieveUser(@PathVariable int id){
        User user = service.findOne(id);
        if(user == null){
            throw new UserNotFoundException(String.format("[ID[%s] not found]", id));
             //이렇게만 하면 500번 에러로 message에 첨부되서 넘어간다. 하지만 알아보기 너무 어렵다
        }
        return user;
    }
}

위에처럼 모든 메소드 @GetMapping 뒤에 “/admin/~”을 붙여도 되지만 아래처럼 @RequestMapping을 이용하여 한번만 붙여도 괜찮다

@RestController
@RequestMapping("/admin")
public class AdminUserController {
    ...
    @GetMapping("/users")
    public List<User> retriveAllUsers(){
    }

    @GetMapping("/users_temp/{id}")
    public User retrieveUser_temp(@PathVariable int id){
    }

    @GetMapping("/users/{id}")
    public User retrieveUser(@PathVariable int id){
    }
}

호출 URL : /admin/users , /admin/users/{id}

이번에는 Admin 쪽만 아까 숨겼던 비밀번호, SSN 같은 모든 정보가 보이게 하고싶음

클라이언트 쪽은 여전히 안보이도록 사용하고 싶으면 아래처럼 적용 해줄수 있음

@JsonFilter("UserInfo") //여기에 있는 ID 값을 결국 밑에 Filter 파라미터로 추가해준다
public class User {
    ...
}

@GetMapping("/users/{id}")//리턴값을 User => MappingJacksonValue로 변경
public MappingJacksonValue retrieveUser(@PathVariable int id){
    User user = service.findOne(id);

    if(user == null){
        throw new UserNotFoundException(String.format("[ID[%s] not found]", id)); //이렇게만 하면 500번 에러로 message에 첨부되서 넘어간다. 하지만 알아보기 너무 어렵다
    }

    //여기서 보여주고 싶은 필드값을 선택
    SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept("id", "name", "joinDate", "ssn"); 

    //파라미터 값에 어떤 도메인 클래스를 지정할지 이름 지정("UserInfo") + 위에서 만든 필터 추가
    FilterProvider filters = new SimpleFilterProvider().addFilter("UserInfo", filter);

    MappingJacksonValue mapping = new MappingJacksonValue(user); //리턴하고싶은 객체(user) 담기
    mapping.setFilters(filters); //user객체에서 어떤것들만 보여줄지(Filter) 처리

    return mapping;
}

여기까지 진행하고 서버 재실행하면 아래처럼 기존에 잘 되었던 UserController에도 문제가 발생하는것을 확인 가능

왜냐하면 지금 User도메인 클래스에 @JsonFilter 값이 걸려있기 때문에 다른곳에서 에러가 발생

@JsonFilter("UserInfo") //여기에 있는 ID 값을 결국 밑에 Filter 파라미터로 추가해준다
public class User {
    ...
}

이 부분은 추후에 진행하면서 해결, 지금은 호출할때 /admin/users/1 호출

http://localhost:8088/admin/users/1

위에 처럼 정상적으로 호출이 되는것을 볼 수 있으며, 필터에서 선택한 값들만 [id, name, joinDate, ssn]만 보여지는것을 확인할 수 있다.

6장. 프로그래밍으로 제어하는 Filtering 방법 - 전체 사용자 조회Permalink

이번에는 /admin/users를 호출했을때 나오는 결과값에 Filter를 처리하는 부분(기존에 했던것과 동일한 방식)

    @GetMapping("/users")
    public List<User> retriveAllUsers(){
        return service.findAll();
    }

기존 위의 코드에서 아래처럼 변경해주면 된다

    @GetMapping("/users")
    public MappingJacksonValue retriveAllUsers(){
        List<User> users = service.findAll();

        SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept("id", "name", "joinDate", "ssn");

        FilterProvider filters = new SimpleFilterProvider().addFilter("UserInfo", filter);

        MappingJacksonValue mapping = new MappingJacksonValue(users);
        mapping.setFilters(filters);

        return mapping;
    }

이렇게 해주면 아래처럼 정상적으로 모든 값이 나오는게 아니라 Filter 처리해서 나오는것을 확인 할 수 있다

6장. URI를 이용한 REST API Version 관리Permalink

사용자관리 API의 버전관리 하는 방법

  1. 페이스북 개발자 API(https://developers.facebook.com/docs/apps/versions)

위 사이트에 접속하면 아래처럼 예제가 나와있는데 중간에 v2.10 처럼 버전을 관리하는것을 확인 가능

curl -i -X “https://graph.facebook.com/v2.10/{my-user-id}&access_token={access-token}”

etc) curl 명령어 : 터미널에서 서버쪽으로 REQUEST 보내는 TOOL 이라고 생각

[RESTAPI에서 버전을 관리] URI에서 버전을 관리

추후에 Header에서 관리하는 방법 설명

URI에 버전 추가하는 방법(아주 EASY)

    // GET /admin/users/1 -> /admin/v1/users/1 버전 변경
    //@GetMapping("/users/{id}")
    @GetMapping("/v1/users/{id}") //여기에 앞에 /v1 추가만 해주면 된다
    public MappingJacksonValue retrieveUserV1(@PathVariable int id){
        ...
    }

그 다음 v1 메소드를 카피해서 아래에 v2를 만들어주고 URI에 /V2 넣어주기

추가로 기존에 사용했던 User 도메인 클래스를 사용하는게 아니라 좀 더 확장된 UserV2 도메인 클래스 사용

아래처럼 새로운 UserV2 클래스를 만들어준다

@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonFilter("UserInfoV2")
public class UserV2 extends User{
    /*
    etc) 상속을 할 때 부모클래스에 꼭 Default 생성자가 존재해야지 상속받을때 에러가 발생하지 않는다
    User 클래스에 @NoArgsConstructor 추가 필요
     */
    private String grade;
}
    @GetMapping("/v2/users/{id}")
    public MappingJacksonValue retrieveUserV2(@PathVariable int id){
        ...
    }

URI 에 /v2추가해주고 아래처럼 코드 수정

    @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)); //이렇게만 하면 500번 에러로 message에 첨부되서 넘어간다. 하지만 알아보기 너무 어렵다
        }

        // User -> UserV2 변환
        UserV2 userV2 = new UserV2();
        BeanUtils.copyProperties(user, userV2);//BeanUtils = 스프링에서 제공하는 클래스 말 그대로 빈과 관련된 작업들을 도와주는 클래스(여러가지 중에 있는데 지금은 카피 기능 사용)

        // id, name, joinDate, password, ssn 필드값을 User로부터 상속받고 추가로 UserV2에는 grade라는것이 존재함

         userV2.setGrade("VIP"); //UserV2는 사용자의 등급을 관리할 수 있음

        SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept("id", "name", "joinDate", "grade", "ssn");

        FilterProvider filters = new SimpleFilterProvider().addFilter("UserInfoV2", filter);

        MappingJacksonValue mapping = new MappingJacksonValue(userV2);
        mapping.setFilters(filters);

        return mapping;
    }

이렇게 해주면 아래와 같이 정상적으로 v1, v2 버전이 관리되며 결과값을 리턴해주는것을 확인 할 수 있다

 

7장. Request Parameter와 Header를 이용한 API Version 관리Permalink

기존에 작성했던 @GetMapping(“/v1/users/{id}”) => @GetMapping(value=”/users/{id}”, params=”version=1”)로 변경해준다

    //@GetMapping("/v1/users/{id}")
    @GetMapping(value = "/users/{id}/", params = "version=1")
    public MappingJacksonValue retrieveUserV1(@PathVariable int id){
        ...
    }

    //@GetMapping("/v2/users/{id}")
    @GetMapping(value = "/users/{id}/", params = "version=2")
    public MappingJacksonValue retrieveUserV2(@PathVariable int id){
        ...
    }

위 방식은 Request Parameter를 이용하는 방법(뒤에 파라미터를 이용하기 때문에 {id}뒤에 /를 꼭 붙여줘야한다)

요청 URI : http://localhost:8088/admin/users/1/?version=1

아래처럼 정상적으로 동작 확인 가능

다음으로 Header를 이용한 버전 관리

기존에 작성했던 @GetMapping(value = “/users/{id}/”, params = “version=1”) => @GetMapping(value = “/users/{id}”, headers = “X-API-VERSION=1”)로 변경해준다

    //@GetMapping("/v1/users/{id}")
    //@GetMapping(value = "/users/{id}/", params = "version=1")
    @GetMapping(value = "/users/{id}", headers = "X-API-VERSION=1") //headers에는 임의값(본인설정)
    public MappingJacksonValue retrieveUserV1(@PathVariable int id){
        ...
    }

    //@GetMapping("/v2/users/{id}")
    //@GetMapping(value = "/users/{id}/", params = "version=2")
    @GetMapping(value = "/users/{id}", headers = "X-API-VERSION=2") //headers에는 임의값(본인설정)
    public MappingJacksonValue retrieveUserV2(@PathVariable int id){
        ...
    }

요청 URI : http://localhost:8088/admin/users/1
Headers에 KEY : X-API-VERSION , VALUE : 1 추가해주기

이번에는 MINE 타입을 이용해서 버전 관리

@GetMapping(value = “/v1/users/{id}”, produces = “application/vnd.company.appv1+json”)

    //@GetMapping("/v1/users/{id}")
    //@GetMapping(value = "/users/{id}/", params = "version=1")
    //@GetMapping(value = "/users/{id}", headers = "X-API-VERSION=1") 
    @GetMapping(value = "/v1/users/{id}", produces = "application/vnd.company.appv1+json")
    public MappingJacksonValue retrieveUserV1(@PathVariable int id){
        ...
    }

    //@GetMapping("/v2/users/{id}")
    //@GetMapping(value = "/users/{id}/", params = "version=2")
    //@GetMapping(value = "/users/{id}", headers = "X-API-VERSION=2") 
    @GetMapping(value = "/v2/users/{id}", produces = "application/vnd.company.appv2+json")
    public MappingJacksonValue retrieveUserV2(@PathVariable int id){
        ...
    }

요청 URI : http://localhost:8088/admin/users/1
Headers에 KEY : Accept , VALUE : application/vnd.company.appv1+json 추가해주기

그런데 여기서 에러가 나옴.. 추후 확인해서 수정 해두자

버전 방법 정리

  1. URI Versioning - Twitter(일반브라우저 실행 O)
  2. Request Parameter versioning - Amazon(일반브라우저 실행 O)
  3. Media Type Versioning - Github(일반브라우저 실행 X)
  4. (Custom) headers versioning - Microsoft(일반브라우저 실행 X)

버전관리를 위해 주의해야할 점

  1. URI가 지저분하거나 너무 과도한 정보 포함
  2. Headers의 잘못된 사용
  3. 인터넷 웹브라우저 캐싱에 의해서 우리 버전이 제대로 반영되지 않으면 적절하게 브라우저에서 삭제해줘야 한다
  4. 웹브라우저에서도 적절하게 사용될 수 있어야 한다
  5. API 문서가 있어야 한다

-끝-

참고

  1. 인프런 - SpringBoot를 이용한 RESTful 개발
  2. SpringBoot Validation 적용하는 법, @Valid 적용 안되는 이유