RestTemplate을 사용하는 서비스 레이어 구조 및 구현
요약
서비스가 비즈니스 객체를 다른 곳에서 획득해 오는 경우가 있다. 그 출처는 DB가 될 수도 있고, 다른 웹 서비스가 되기도 한다. DB와 퍼시스턴스 레이어를 거쳐 객체를 뽑아오든, 웹 API를 호출하여 객체를 얻어오든 두 방식을 구현하는 코드 패턴은 스프링에서 거의 유사하다. 3티어 아키텍쳐로 책임을 나누게 되며. 티어 간 객체 변환이 공통적으로 요구되는 사항이다.
3티어 아키텍처 비교
DB에서 객체를 얻을 때
서비스는 레포지토리 레이어를 통해 DB 데이터에 접근한다. 레포지토리 단은 JPA 구현체 Hibernate 기술 등을 채용하여 ORM(객체-관계형 매핑)을 제공한다. ResultSet에 담긴 관계형 데이터를 자바 엔터티 객체로 변경하는 것은 중요한 요구사항이다.
웹 서비스 호출로 객체를 얻을 때
가장 익숙하고 유명한 리모팅 프로토콜인 HTTP(S)를 사용하여 웹 서비스 API가 제공 되고 있다고 가정하자. 서비스 단은 웹 클라이언트 단에 부탁하여 해당 원격 서비스의 API를 호출토록 한다. 그 결과로 돌아오는 HTTP(S) 응답 패킷의 바디에는 JSON 혹은 XML 포맷을 갖는 객체가 담겨있다. 따라서 JSON, XML 혹은 형식 없는 스트링을 자바 엔터티 객체로 변경하는 것은 중대한 요구사항이다.
REST 클라이언트의 요구사항
- 서비스는 클라이언트 레이어를 거쳐 비즈니스 엔터티를 획득한다.
- 엔터티의 출처는 웹 서비스가 제공한 API이다. 엔터티를 DB에서 읽거나, 웹 서비스 API로 가져오거나 어찌됐건 서비스 단의 관심 사항이 아니다.
- 클라이언트는 웹 API 스펙이 요구하는대로 HTTP 요청을 작성하고 이를 타겟 서비스에 전송해야 한다. 요청 완료 후 타겟 서비스로부터 HTTP 응답을 받아야한다.
- HTTP 요청 패킷에 대응하여 추상화된 객체가 요청 엔터티이다.
- HTTP 응답 패킷에 대응하여 추상화된 객체는 응답 엔터티이다.
엔터티 식별하기
비즈니스 엔터티
비즈니스에 중요한 정보와 로직을 캡슐화한 객체
@ToString
@EqualsAndHashCode
@Setter
@Getter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class Movie {
Long id;
String title;
String titleEng;
@JsonFormat(pattern = "yyyy-MM-dd")
DateTime date;
Float userRating;
Float audienceRating;
Float reviewerRating;
Float reservationRate;
Long reservationGrade;
Long grade;
String thumb;
String image;
String photos;
String videos;
String outlinks;
String genre;
Long duration;
Long audience;
String synopsis;
String director;
String actor;
Long like;
Long dislike;
}
HTTP 엔터티
HttpEntity
HttpHeaders + 비즈니스 엔터티(HTTP Body가 됨)
HTTP 요청 패킷과 HTTP 응답 패킷의 추상화.
RequestEntity extends HttpEntity
HTTP Method + HttpHeaders + 비즈니스 엔터티(HTTP Body)
요청 메서드도 함께 캡슐화 함.
스태틱 메서드들은 빌더 패턴으로 쉽게 HTTP 헤더 및 바디를 작성하는 편의 기능 제공.
RestTemplate API 중
exchange
의 인자로 사용 가능.
RequestEntity requestEntity = RequestEntity.post(URI.create(URL_READ_MOVIE_LIST))
.header(HttpHeaders.COOKIE, "PHPSESSIONID=123456", "token=8855")
.header(HttpHeaders.AUTHORIZATION, "Basic YWxhZGRpbjpvcGVuc2VzYW1l")
.body(new Movie());
ResponseEntity<Movie> response = restTemplate.exchange(requestEntity, Movie.class);
ResponseEntity extends HttpEntity
HTTP Status Code + HttpHeaders + 비즈니스 엔터티(HTTP Body)
응답 상태 코드도 함께 캡슐화 함.
스태틱 메서드들은 빌더 패턴으로 각 응답 코드에 대응하는 HTTP 헤더 및 바디를 작성하도록 편의 기능 제공.
HTTP 요청하고 응답하기
요청 엔터티 작성 & 전송 & 응답 엔터티 수신 & 비즈니스 객체로 매핑
헬퍼 클래스들의 도움으로 HTTP 요청 패킷은 직관적으로 작성 가능하다.
RestTemplate API를 사용하면 요청 패킷 전송 & 응답 수신 & 비즈니스 객체 매핑이 진행된다.
- exchange 예시
RequestEntity requestEntity = RequestEntity.post(URI.create(URL_READ_MOVIE_LIST))
.header(HttpHeaders.COOKIE, "PHPSESSIONID=123456", "token=8855")
.header(HttpHeaders.AUTHORIZATION, "Basic YWxhZGRpbjpvcGVuc2VzYW1l")
.body(new Movie());
ResponseEntity<Movie> response = restTemplate.exchange(requestEntity, Movie.class);
- getForObject + 요청 파라미터 예시
@Override
public List<Movie> readMovie(Long id, String name) {
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(URL_READ_MOVIE);
Optional.ofNullable(id).ifPresent(it-> uriBuilder.queryParam("id", it));
Optional.ofNullable(name).ifPresent(it-> uriBuilder.queryParam("name", name));
String url = uriBuilder.build().toUriString(); // build string without encoding
MovieResponse response = restTemplate.getForObject(url, MovieResponse.class); // because restTemplate will encodes the url
return Optional.of(response.getResult())
.orElseThrow(InternalError::new);
}
UriComponentsBuilder로 요청 파라미터 삽입 시 주의사항
UriComponentsBuilder
는 RestTemplate
과 같이 사용할 때 인코딩 쪽에 주의가 필요하다.
restTemplate의 API는 String URL 혹은 URI을 요구한다.
문자열 URL을 전달하게 되면, restTemplate의 내부에서 UTF-8로 인코딩을 진행하므로, 굳이 UriComponentsBuilder
로 URL 생성시 인코딩을 적용할 필요가 없다.
인코딩이 2번 진행되면 한글 표현 등 문제가 발생하니 유의.
String url = uriBuilder.toUriString(); // bad - 인코딩 진행함
String url = uriBuilder.build().encode().toUriString(); // bad - 인코딩 진행함
String url = uriBuilder.build().toUriString(); // good - 인코딩 진행 안 함
엔터티 간 매핑하기
HTTP 패킷은 문자열이다.
고로 자바 객체 - 문자열 포맷간 직렬화/역직렬화가 꼭 필요하지만, 엔터티 마다 이런 로직을 구현하기는 매우 까다롭다. 다행히도 RestTemplate
에겐 MessageConverter
가 존재하여 그런 역할을 수행해 주고 있다.
비즈니스 엔터티 ➡️ 요청 엔터티
비즈니스 엔터티
Movie
객체는 HTTP 요청의 바디에 들어갈 때 적절한 문자열 형태가 되어야 한다. RestTemplate을 사용하는 개발자는 직렬화 로직을 작성하지 않고 RestTemplate의 MessageConverter에게 맡기면 된다.RequestEntity requestEntity = RequestEntity.post(URI.create(URL_READ_MOVIE_LIST)) .header(HttpHeaders.COOKIE, "PHPSESSIONID=123456", "token=8855") .header(HttpHeaders.AUTHORIZATION, "Basic YWxhZGRpbjpvcGVuc2VzYW1l") .body(new Movie());
응답 엔터티 ➡️ 비즈니스 엔터티
HTTP 응답의 바디에서 문자열로 표현된 객체는 비즈니스 객체
MovieResponse
로 매핑이 되어야 한다. RestTemplate을 사용하는 개발자는 역직렬화 로직을 작성하지 않고 RestTemplate의 MessageConverter에게 맡기면 된다.@Override public List<Movie> readMovie(Long id, String name) { UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(URL_READ_MOVIE); if (id != null) uriBuilder.queryParam("id", id); if (name != null) uriBuilder.queryParam("name", name); MovieResponse response = restTemplate.getForObject(uriBuilder.toUriString(), MovieResponse.class); return Optional.of(response.getResult()) .orElseThrow(InternalError::new); }
[jaskson-databind] Object Mapper 구성 예시
자바 객체 - JSON 간 매핑
MappingJackson2HttpMessageConverter
빈을 구성하고ObjectMapper
빈을 주입하여 사용하는 것이 다수이다.@ComponentScan(basePackages = "org.binchoo.study.spring.resttemplate.movieservice") @Configuration public class ClientConfig { @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); restTemplate.setMessageConverters(Arrays.asList(messageConverter())); return restTemplate; } @Bean public MappingJackson2HttpMessageConverter messageConverter() { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setObjectMapper(objectMapper()); return converter; } @Bean public ObjectMapper objectMapper() { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.registerModule(jodaModule()); return objectMapper; } @Bean public JodaModule jodaModule() { return new JodaModule(); } }
자바 객체 - XML 간 매핑
pro spring 5 예제에서는 자바 객체와 XML간 매핑도 다루고 있다.
이 경우
MarshallingHttpMessageConverter
빈을 구성하고 마샬/언마샬러로 Castor XML 라이브러리를 사용할 수 있다.
[jaskson-databind] 참고할 어노테이션
@JsonNaming(PropertyNamingStrategy.SankeCaseStrategy.class)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonProperty("name")
@JsonFormat("yyyy-MM-dd")
[jaskon-datatype-joda] 간편하게 JodaTime 매핑 설정하기
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(jodaModule());
return objectMapper;
}
@Bean
public JodaModule jodaModule() {
return new JodaModule();
}
---
@ToString
@EqualsAndHashCode
@Setter
@Getter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class Movie {
Long id;
@JsonFormat(pattern = "yyyy-MM-dd")
DateTime date; //joda의 DateTime
...
}
'웹 개발 > 스프링 프레임워크' 카테고리의 다른 글
[스프링MVC] MultipartFile말고 Part로 파일 업로드 - 주의사항 있음 (0) | 2021.12.15 |
---|---|
[스프링 Web] RestTemplate이 쏘는 패킷을 보고싶다 - Fiddler4로 JVM 패킷 디버깅하기 (2) | 2021.12.14 |
[스프링MVC] 어노테이션 이야기 - REST와 @ResponseBody (0) | 2021.12.14 |
[스프링] 다양한 작업 실행 전략 이야기 - 스케줄링 및 비동기 실행 (0) | 2021.12.14 |
[스프링 Security] CSRF 토큰 이야기 - csrf() 켰더니 로그아웃이 안 되네? (0) | 2021.12.14 |