기존의 Spring MVC 환경에서는 인메모리 DB인 h2의 web console을 이용하기 수월했었는데, Spring WebFlux + R2DBC 환경에서는 Spring MVC 환경에서 사용하던 방법 그대로 h2의 web console을 이용할 수 없기 때문에 별도의 설정을 해주어야 합니다.
Spring WebFlux + R2DBC 환경에서 h2 web console을 사용하기 위한 설정 방법은 다음과 같습니다.
Spring Data JPA에서 findById() 같은 API를 사용해 데이터를 조회할 때, No default constructor for entity 라는 에러가 발생하는 이유가 무엇인지 조금 더 디테일하게 분석해보았습니다.
Spring Data JPA에서는 개발자가 JpaRepository 등의 인터페이스를 상속하는 Repository 인터페이스를 구현하는 구현 클래스의 인스턴스를 생성하기 위해 내부적으로 Dynamic Proxy라는 기술과 Reflection을 이용하는데 RepositoryFactorySupport에서 해당 인스턴스(Repository의 Proxy)를 생성합니다.
그런데, 여기까지는 Spring에서 지원하는 Repository 인터페이스(개발자가 JpaRepository 같은 인터페이스를 상속한 Custom Repository)의 Proxy를 생성하는 역할을 할 뿐이고, 이 Proxy에서 타겟 클래스인 SimpleJpaRepository의 쿼리 메서드를 이용한 과정까지입니다.
제가 원한건 No default constructor for entity라는 에러가 발생하는 이유를 조금 더 디테일하게 알고 싶은것이었기 때문에 조금 더 분석을 해보았습니다.
분석 결과 Spring Data에서 제공하는 RepositoryComposition이 RepositoryMethodInvoker를 이용해 앞에서 얻은 Proxy의 타겟 클래스인 SimpleJpaRepository에 구현된 쿼리 메서드(findById() 같은)를 호출합니다.
그 다음부터는 Hibernate ORM의 영역입니다. Hibernate ORM에서 EntityManager의 역할을 하는 Session 인터페이스의 구현 클래스인 SessionImpl에서 데이터베이스에 접속해 데이터를 조회합니다.
SessionImpl로부터 데이터베이스에 데이터를 조회하는 복잡한 로직이 시작되고 결국 조회한 데이터를 개발자가 정의 Entity 클래스의 object로 변환하기 위해 아래와 같이 PojoInstantiator의 instantiate()에서 Reflection을 통해 얻은 Constructor 인스턴스로 newInstance()를 호출해 Entity 클래스의 object를 생성합니다.
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.tuple;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import org.hibernate.InstantiationException;
import org.hibernate.PropertyNotFoundException;
import org.hibernate.bytecode.spi.ReflectionOptimizer;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.ReflectHelper;
import org.hibernate.mapping.Component;
/**
* Defines a POJO-based instantiator for use from the tuplizers.
*/
public class PojoInstantiator implements Instantiator, Serializable {
private static final CoreMessageLogger LOG = CoreLogging.messageLogger( PojoInstantiator.class.getName() );
private transient Constructor constructor;
private final Class mappedClass;
private final transient ReflectionOptimizer.InstantiationOptimizer optimizer;
private final boolean embeddedIdentifier;
private final boolean isAbstract;
...
...
public Object instantiate() {
if ( isAbstract ) {
throw new InstantiationException( "Cannot instantiate abstract class or interface: ", mappedClass );
}
else if ( optimizer != null ) {
return optimizer.newInstance();
}
else if ( constructor == null ) {
throw new InstantiationException( "No default constructor for entity: ", mappedClass );
}
else {
try {
return applyInterception( constructor.newInstance( (Object[]) null ) );
}
catch ( Exception e ) {
throw new InstantiationException( "Could not instantiate entity: ", mappedClass, e );
}
}
}
...
...
}
그런데, 코드를 보면 constructor가 null이라면 InstantiationException을 throw하도록 구현된 것을 볼 수 있습니다. 이유는 Java의 Reflection에서 object를 생성하기 위해서는 파라미터가 없는 디폴트 생성자(default constructor)가 필요하기 때문입니다.
이러한 이유때문에 Spring Data JPA에서 사용되는 Entity 클래스에는 디폴트 생성자가 반드시 있어야한다는 사실을 기억하면 좋을 것 같습니다.
Java의 클래스에 파라미터를 가지는 별도의 생성자를 추가하지 않으면 내부적으로 디폴트 생성자가 있다고 가정하지만 오버로딩된 생성자가 추가되는 순간 디폴트 생성자는 개발자가 직접 지정을 해야한다는 사실은 잘 알고 있을거라 생각합니다.
그리고 lombok에서 지원하는 @Builder 패턴 등을 사용할 경우에는 항상 디폴트 생성자를 기본으로 가질 수 있도록 해야한다는 사실을 기억하면 좋을 것 같습니다.
Mac OS Catalina 이 전 버전까지는 기본 셸이 Bash shell(bash)이었으나, Catalina부터 Z shell(zsh)이 기본 셸로 변경되었습니다. 따라서 Mac OS에서 시스템 환경 변수를 등록하기 위해서는 현재 사용하고 있는 Mac OS의 기본 셸이 무엇인지부터 확인을 해야 합니다.
웹 브라우저 같은 클라이언트에서 보여지는 HTML 컨텐츠가 렌더링(Rendering)되는 방식은 크게 두 가지입니다.
하나는 웹 애플리케이션 서버에서 동적으로 변하는 데이터를 포함하는 HTML을 만들어서 HTML 자체를 한번에 클라이언트 쪽으로 내려주는 방식입니다. 이 방식은 JSP나 타임리프(Thymeleaf) 같은 기술을 사용해서 HTML을 템플릿화 한 다음에 Controller의 핸들러 메서드에서 리턴하는 모델 데이터를 템플릿에 동적으로 채워 넣은 후, 최종적으로 완성된 HTML을 클라이언트 쪽으로 내려주는 방식입니다. 이 방식을 바로 서버 사이드 렌더링(Server Side Rendering)이라고 합니다.
또 하나는 클라이언트 쪽을 담당하는 Apache나 NginX 같은 웹 서버에 HTML을 올려 놓은 후, 자바스크립트의 Ajax 기술을 이용해서 웹 애플리케이션 서버에 데이터를 요청 합니다. 그리고 응답으로 전달받은 데이터를 웹 서버에 올려둔 HTML에 동적으로 채워 넣은 후, 최종적으로 완성된 HTML을 클라이언트 쪽으로 내려주는 방식입니다. 이 방식을 바로 클라이언트 사이드 렌더링(Client Side Rendering)이라고 합니다.
위 두가지 방식 중에서 HttpMessageConverter는 두 번째 방식인 클라이언트 사이드 렌더링과 관련이 있습니다. 즉, 클라이언트 사이드 렌더링을 위해 필요한 데이터를 특정 형식으로 변환해 주는 것이 바로 HttpMessageConverter입니다.
HttpMessageConverter의 사용 목적
HttpMessageConverter는 크게 두 가지 목적으로 사용됩니다.
하나는 웹 서버에서 전송된 Reqeust Body를 DTO 같은 클래스의 객체로 변환해서 웹 애플리케이션 서버 쪽에서 사용할 수 있도록 해주는 것이고,
다른 하나는 웹 서버 쪽으로 전달할 응답 데이터를 Response Body로 변환해주는 것입니다.
HttpMessageConverter의 동작 과정
[그림 1-1] Spring MVC 요청/응답 동작 과정
[그림 1-1]은 클라이언트의 요청을 처리하는 Spring MVC의 기본 동작 과정입니다. 이 Spring MVC의 동작 과정에서 HandlerAdapter의 동작 과정을 조금 더 구체적으로 설명하겠습니다.
요청 처리 시 HandlerAdapter의 동작 과정
HandlerMapping을 통해 적절한 HandlerAdapter를 찾으면 HandlerAdapter는 Controller로 넘겨줄 파라미터를 결정하기 위해 이 작업을 HandlerMethodArgumentResolver에게 위임합니다.
HandlerMethodArgumentResolver는 HttpMessageConverter에게 HTTP Request Body를 특정 타입의 객체로 변환해주기를 요청합니다.
HttpMessageConverter는 HTTP Request Body를 특정 타입의 객체로 변환합니다.
HandlerMethodArgumentResolver는 변환된 데이터를 전달 받아서 이 데이터를 다시 HandlerAdapter에게 전달합니다.
HandlerAdapter는 HandlerMethodArgumentResolver로부터 전달 받은 데이터를 핸들러 메서드의 파라미터로 포함 시킨 후, 핸들러 메서드를 호출합니다.
응답 처리 시 HandlerAdapter의 동작 과정
핸들러 메서드가 응답으로 전달할 데이터를 리턴합니다.
HandlerMethodReturnValueHandler는 핸들러 메서드로부터 전달 받은 응답 데이터를 HttpMessageConverter에게 전달합니다.
HttpMessageConverter는 HandlerMethodReturnValueHandler로부터 전달 받은 데이터를 HTTP Response Body에 포함되는 형식의 데이터로 변환합니다.
HandlerMethodReturnValueHandler는 HttpMessageConverter로부터 전달 받은 데이터를 HandlerAdapter에게 전달합니다.
REST API에서의 HttpMessageConverter
Spring MVC에서는 기본적으로 아래의 HttpMessageConverter 구현체가 활성화 되어 있습니다.
ByteArrayHttpMessageConverter
StringHttpMessageConverter
ResourceHttpMessageConverter
SourceHttpMessageConverter
FormHttpMessageConverter
Jaxb2RootElementHttpMessageConverter
MappingJackson2HttpMessageConverter
MappingJacksonHttpMessageConverter
AtomFeedHttpMessageConverter
RssChannelHttpMessageConverter
만약 우리가 HTTP Request 또는 Response를 위한 통신 프로토콜로 JSON을 사용하는 REST API Controller를 구현했다고 가정해 봅시다.
특정 Controller의 핸들러 메서드 파라미터에@RequestBody애너테이션이 추가되거나 파라미터의 타입이 HttpEntity라면 위에서 나열한 HttpMessageConverter가 순서대로 HTTP Request Body를 체크해서 핸들러 파라미터에 지정한 클래스로 변환이 되는지, HTTP Message의 Content Type을 지원하는지 여부를 확인하게 됩니다.
이 두가지 조건을 모두 만족한다면 아마도 MappingJackson2HttpMessageConverter가 Request Body를 해당 클래스의 객체로 변환할 것이고, 조건에 부합하는 HttpMessageConverter가 존재하지 않는다면 예외가 발생할 것입니다.
이제 응답 데이터를 JSON 형식으로 전달하는 상황을 생각해 봅시다. 핸들러 메서드에 @ResponseBody 애너테이션을 추가하거나 핸들러 메서드의 리턴 타입이 HttpEntity라면 역시 위에서 나열한 HttpMessageConverter가 순서대로 핸들러 메서드의 리턴 값을 체크해서 해당 클래스 타입을 지원하는지, Accept Header에 명시된 MediaType으로 변환이 되는지 여부를 확인하게 됩니다.
HttpMessageConverter가 동작하는 시점
HttpMessageConverter가 동작하는 시점은 JSON을 DTO로 deserialization 할 때와 DTO를 JSON으로 serialization 할 때가 다릅니다. 아래는 HttpMessageConverter의 대략적인 동작 흐름입니다. 참고하세요.
ResponseEntity는 HttpEntity의 확장 클래스로써 HttpStatus 상태 코드를 추가한 전체 HTTP 응답(상태 코드, 헤더 및 본문)을 표현하는 클래스입니다.
ResponseEntity를 어디에 사용할 수 있나요?
ResponseEntity 클래스는 주로 @Controller 또는 @RestController 애너테이션이 붙은 Controller 클래스의 핸들러 메서드(Handler Method)에서 요청 처리에 대한 응답을 구성하는데 사용됩니다. Used in RestTemplate as well as in @Controller methods.
그리고 RestTemplate으로 외부의 API 통신에 대한 응답을 전달 받아서 처리할 경우, 역시 ResponseEntity를 사용합니다.
ResponseEntity를 어떻게 사용해야 할까요?
가장 일반적인 방식
가장 일반적인 방식은 [코드 1-1]과 같이 new로 ResponseEntity 객체를 생성하는 방식입니다.
만약에 클라이언트 쪽 요청 URL이 '/coffees/42;q=11;r=22'와 같다면 세미콜론(;) 뒤에 오는 q, r을 변수로 보고 각 변수의 값을 [코드 1-6]과 같이 핸들러 메서드에서 전달 받을 수 있습니다.
HttpEntity
클라이언트 요청 body와 header 정보를 HttpEntity 객체를 통해 한번에 전달 받을 수 있습니다. 물론 HttpServletRequest 객체를 통해서도 요청 body와 header에 접근할 수 있지만 HttpEntity 객체를 통해 조금 더 간단하게 두 정보에 접근할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/v1/coffees")
publicclass HttpEntityExample {
@PostMapping
public Coffee postCoffee(HttpEntity<Coffee> entity) {