Closed binchoo closed 2 years ago
https://oasis-cabin-8bb.notion.site/a118eea9f9bb4e26b7a591e0c9acd130
@caffeine-library/readers-pro-spring-5
서비스는 비즈니스 요구사항이 잘 정의되고 잘 분화된 독립적인 컴포넌트입니다. 그러므로 재사용과 컴포지션에 유용한 단위이고, 큰 서비스는 작은 서비스를 조합하여 사용합니다.
클라이언트는 서비스 디렉토리를 조회하여 적합한 서비스를 찾아 호출할 수 있습니다. (일종의 메타 정보 제공처가 필요함을 의미합니다.)
클라이언트의 코드는 마치 함수를 호출하는 것처럼 원격지 서비스를 호출합니다. 이를 위해 원격지 서비스에 대한 메타 정보를 활용하여 RPC 지원 라이브러리가 추상화를 제공해야 합니다.
XML 기반의 메타 정보를 주고 받습니다. (WDSL, Envelop)
WSDL 파일 예제 https://github.com/sensepost/wikto/blob/master/Web%20References/com.google.api/GoogleSearch.wsdl
클라이언트 코드 예제
클라이언트 측 HTTP 헤더에 적재된 SOAP Envelop
SOAP는 HTTP 프로토콜 위에 Envelop이라는 계층을 하나 더 얹은 구조입니다. Envelop 내용은 원격 프로시저의 시그니쳐 정보에 의존하기 때문에 수시로 바뀌지만 HTTP 패킷 구조는 변하지 않습니다. (요청 URL & HTTP Method는 POST로 일정함)
따라서 HTTP 요청의 단순함을 활용하지 못합니다.
SOAP Envelop 대신에 HTTP 요청 URL에 프로시저 시그니처를 담았습니다. REST의 이론에 어긋나는 설계이나, 실서비스 곳곳에서 발견되는 API입니다.
해당 API가 REST 이론에 맞추어
http://flickr.com/photos/tags/penguin
에 대한 GET 요청으로 바뀐다면 어떨까요?
SOAP 클라이언트 원격 프로시저의 시그니쳐에 의존하여 머신 친화적인 아키텍처로 인식됩니다. 하지만 ROA는 클라이언트 서버 간 교환 대상이 되는 리소스에 집중하여 인간 친화적인 API를 제공받도록 합니다.
리소스는 URI를 통해 고유하게 식별됩니다. 리소스에 대한 연산(조회, 생성, 수정, 삭제)는 더 이상 Envelop이 아닌 HTTP의 기본 메서드들을 이용합니다.
덕분에 RESTful 웹 서비스의 요청 포맷은 매우 단순해졌습니다. 클라이언트와 서버는 리소스 객체를 직렬화한 JSON 또는 마샬링한 XML을 교환합니다.
따라서 각 엔드포인트는 JSON 컨버터 및 마샬러를 활용해야 하며 12장에서 관련 빈들이 모두 설명되었습니다.
@ResponseBody
어노테이션스프링MVC에서 컨트롤러의 메서드는 "논리적인 뷰"를 반환하는 것으로 상정한다.
하지만 REST 서비스는 리소스를 반환해야하기 때문에 상성이 맞지 않다.
그럴 땐 컨트롤러 메서드에@ResponseBody
붙여준다.
@ResponseBody
를 붙인 메서드의 반환은, 뷰가 되는 것이 아니며
즉시 HTTP 응답 스트림으로 사용된다.
반환된 값은 디스패쳐 서블릿에 등록된 메시지 컨버터 빈을 거쳐 JSON 및 XML로 변환되고 HTTP 응답 스트림에 쓰인다.
@Controller
@ResquestMapping(value="/singer")
public class SingerController {
final Logger logger = LoggerFactory.getLogger(SingerController.class);
@Autowired private SingerService singerService;
@ResponseStatus(HttpStatus.OK)
@RequestMapping(value = "/listdata", method = RequestMethod.GET)
@ResponseBody
public Singers listData() {
return new Singers(singerService.findAll());
}
}
@RestController
@RestController
클래스 수준 어노테이션을 사용하면,
@RequestMapping
이 붙은 메서드에 자동으로 @ResponseBody
가 적용된다.
@GetMapping
, @PostMapping
등은 @RequestMapping
을 래핑한 버전이므로 역시 적용된다.
RestTemplate을 사용하는 서비스 레이어 구조 및 구현
서비스가 비즈니스 객체를 다른 곳에서 획득해 오는 경우가 있다. 그 출처는 DB가 될 수도 있고, 다른 웹 서비스가 되기도 한다. DB와 퍼시스턴스 레이어를 거쳐 객체를 뽑아오든, 웹 API를 호출하여 객체를 얻어오든 두 방식을 구현하는 코드 패턴은 스프링에서 거의 유사하다. 3티어 아키텍쳐로 책임을 나누게 되며. 티어 간 객체 변환이 공통적으로 요구되는 사항이다.
서비스는 레포지토리 레이어를 통해 DB 데이터에 접근한다. 레포지토리 단은 JPA 구현체 Hibernate 기술 등을 채용하여 ORM(객체-관계형 매핑)을 제공한다. ResultSet에 담긴 관계형 데이터를 자바 엔터티 객체로 변경하는 것은 중요한 요구사항이다.
가장 익숙하고 유명한 리모팅 프로토콜인 HTTP(S)를 사용하여 웹 서비스 API가 제공 되고 있다고 가정하자. 서비스 단은 웹 클라이언트 단에 부탁하여 해당 원격 서비스의 API를 호출토록 한다. 그 결과로 돌아오는 HTTP(S) 응답 패킷의 바디에는 JSON 혹은 XML 포맷을 갖는 객체가 담겨있다. 따라서 JSON, XML 혹은 형식 없는 스트링을 자바 엔터티 객체로 변경하는 것은 중대한 요구사항이다.
비즈니스에 중요한 정보와 로직을 캡슐화한 객체
@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;
}
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 요청 패킷은 직관적으로 작성 가능하다.
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
는 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);
}
자바 객체 - 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 라이브러리를 사용할 수 있다.
@JsonNaming(PropertyNamingStrategy.SankeCaseStrategy.class)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonProperty("name")
@JsonFormat("yyyy-MM-dd")
@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
...
}
단말에서 송수신하는 HTTP(S) 패킷을 조회하는 데스크톱 앱. 브라우저 개발자 모드의 네트워크 탭의 기능과 거의 흡사합니다.
차이점은 해당 데스크탑을 프록시 서버로 만들 수 있다는 점.
FIddler4를 켜면 기본적으로 8888번 포트로 프록시 서버가 실행됩니다.
피들러를 켠 후 자기 스마트폰의 네트워크 설정에서 본인 데스크탑을 프록시로 지정하면, 내 스마트폰을 오고가는 HTTP(S) 패킷을 디버깅 할 수 있습니다. 이것이 제가 FIddler4를 쓰는 주 사용처.
한 편, 오고 가는 패킷을 인터셉트하여 커스터마이징 하는 기능도 제공하고 있습니다. 엄청 재밌는 기능이에요.
웹 페이지의 i18n을 테스트할 때 요청의 로케일을 `ko-KR`에서 `ja-JP`로 덮어 씌우는 등... 활용처가 있습니다.
이렇듯 Fidder4 프록시 서버에 JVM 발 요청이 지나다닐 수 있다면, 자바로 구현한 웹 API 클라이언트가 송수신하는 패킷을 들여다 볼 수 있습니다.
(링크 글의 내용을 옮겨왔습니다. )
피들러는 프로그램을 켜면 자동으로 8888번 포트에 프록시 서버를 엽니다. 그럼 바로 HTTP 패킷을 엿볼 수 있게 되는데요.
하지만 HTTPS 패킷을 보려면 인증서가 필요하죠?
그럼 일단 인증서를 발행해야하며, 대응되는 키는 클라이언트 쪽에서 보유해야합니다. 이를 위한 절차가 아래에 소개됩니다.
keytool.exe -import -file C:\Users\\\Desktop\\FiddlerRoot.cer -keystore FiddlerKeystore -alias Fiddler
JVM 실행 시에 실행 옵션을 주어 프록시 서버와의 연결을 설정을 할 수 있습니다. 보통 IDE의 실행 환경 설정에서 아래 옵션들을 입력해 주면 됩니다.
-DproxySet=true
-DproxyHost=127.0.0.1
-DproxyPort=8888
-Djavax.net.ssl.trustStore="path\to\java_home\bin\FiddlerKeyStore"
-Djavax.net.ssl.trustStorePassword="password_used_during_keystore_creation"
자바 코드로 프록시 서버를 설정해 보겠습니다.
실행 옵션을 부여하는 것 대신, 자바 코드에서 System.setProperty()
를 호출하여도 됩니다.
스프링 프레임워크를 사용하는 앱에서 프록시를 설정 빈을 만들어 보겠습니다.
[ProxyEnabler.java] 파일
@Profile("proxy")
@Component
public class ProxyEnabler {
@PostConstruct
void enableProxy() {
Properties props = new Properties();
try {
props.load(new ClassPathResource("proxy-config.properties").getInputStream());
System.setProperties(props);
} catch (IOException e) {
e.printStackTrace();
}
}
}
스프링 컨텍스트가 부트스트랩 하면서 빈 ProxyEnabler
를 생성하게 되면
프로퍼티 파일을 읽어와서 시스템 프로퍼티로 옮기게 됩니다.
그럼 JVM을 오가는 패킷을 Fiddler4가 볼 수 있습니다!
[proxy-config.properties] 파일
이 때, 프로퍼티 파일의 내용은 JVM 실행 옵션과 동일한 것들입니다.
http.proxyHost=127.0.0.1
https.proxyHost=127.0.0.1
http.proxyPort=8888
https.proxyPort=8888
javax.net.ssl.trustStore=C:\\Users\\wnwoq\\.jdks\\azul-1.8.0_275\\bin\\FiddlerKeystore
javax.net.ssl.trustStorePassword=password
이제 JVM 측이 생성하고 수신하는 HTTP 패킷은 Fiddler4를 거쳐갑니다.
한 번 RestTemplate로 웹 API 요청을 날려 보았습니다.
해당 요청들이 피들러 프록시에 캡쳐되는 것을 확인할 수 있습니다.
주제
12장 스프링 리모팅 사용하기를 읽고 중요✨ 하다고 생각하는 키워드와 선택한 이유에 대해서 코멘트로 달아주세요.
연관챕터
80
@caffeine-library/readers-pro-spring-5