우리가 일반적으로 생각하는 대표적인 암호화의 대상은 크리덴셜(Credential)의 한 종류인 패스워드입니다. 패스워드는 암호화의 대상인 메시지를 조금 더 구체적으로 표현한 것이라고 볼 수 있을테구요.

이번 시간에는 암호화에 대한 아주 쉬운 이야기를 짧게 해보려고 합니다.

암호화란 무엇인지 암호화 유형에는 기본적으로 어떤것이 있는지 등을 아주 쉽게 이해할 수 있도록 짧게 정리해 보겠습니다.

 

암호화란?

  • 사용자의 민감한 정보를 보호하기 위한 기술

  • Crypto 또는 Cipher라는 비슷한 용어가 있음

  • Ciphertext: 암호화 된 텍스트

  • Cryptography
    • 암호학적 관점에서의 암호화 전체를 아우르는 말
  • Crypto Currency
    • 암호 화폐
  • Encryption / Decryption
    • 암호학 관점에서의 데이터 변환 과정
    • Encryption
      • 암호화
    • Decryption
      • 복호화
  • Encoding / Decoding
    • 암호학 관점은 아님
    • 데이터의 형식이나 패턴을 목적에 맞게 변환하는 과정
    • 예)
      • Encoding
        • encode(원본 데이터) → Base64 형식의 문자열로 인코딩
      • Decoding
        • decode(Base64 형식으로 인코딩된 문자열) → 원본 데이터


암호화 대상의 기준

웹 애플리케이션의 아이디/패스워드 로그인을 생각해보면 암호화의 대상 기준은 무엇이 되어야하는가를 잘 알 수 있습니다.

패스워드는 민감한(Sensitive) 정보이므로 타인에게 평문(Plain Text) 형태로 유출이 되면 안될테니까요.

따라서 애플리케이션 구현시 외부에 유출되면 문제가 발생할 수 있는 정보들은 암호화의 대상이 될 수 있으며, 아래에서 나열한 항목들은 암호화를 진지하게 고려해 보아야 하는 대상이라고 볼 수 있습니다.

  • 로그인 인증을 위한 패스워드
  • 구글, AWS 등의 외부 서비스를 이용하기 위한 계정 정보 또는 Secret 정보
  • JWT를 생성하고 검증하는데 필요한 Secret 정보
  • 데이터베이스 접속 정보
    • 데이터베이스 접속 정보에서 root 계정을 사용하는 분들이 많은데 root 계정이 아닌 별도의 사용자 계정을 만들어서 사용하길 권장합니다.
    • root는 말 그대로 최고 권한을 가진 계정이기 때문에 직접적으로 사용하지 않는 것이 좋습니다.

 

암/복호화 가능 여부에 따른 암호화 방식

  • 양방향 암호화
    • 암호화도 되고 복호화도 되는 암호화 방식
    • 암호화 된 메시지를 상대방이 볼 수 있어야 하는 암호화 방식이며, 복호화가 되어야 정상적인 메시지를 확인할 수 있습니다.

  • 단방향 암호화
    • 암호화는 되지만 복호화는 되지 않는 암호화 방식
    • 암호화된 메시지를 굳이 복호화 해서 확인할 필요가 없을 때 사용할 수 있는 암호화 방식입니다.
    • 단방향 암호화를 적용하는 대표적인 항목이 로그인 패스워드입니다. 로그인 패스워드의 경우 사용자가 입력한 패스워드를 암호화 한 데이터와 이미 암호화 되어 저장되어 있는 데이터가 일치한지 여부만 판단하면 되기 때문에 복호화가 필요없습니다.
    • 단방향 암호화는 일반적으로 해시 알고리즘을 사용하는데 엄밀하게 말하자면 해시 알고리즘 자체가 암호화라고 보기는 어렵지만 어쨌든 원본 메시지를 보호할 수 있다는 관점에서의 암호화라고 생각하면 좋을 것 같습니다.

 

 키 유형에 따른 암호화 방식

  • 대칭키 암호화 방식
    • 하나의 키를 서로 공유해서 해당 키로 암호화/복호화를 수행
    • 대칭키 암호화 방식은 하나의 키를 서로 공유해야 하기 때문에 공유된 키가 유출되면 심각한 문제가 발생할 수 있습니다.

  • 비대칭키 암호화 방식
    • 암호화를 수행하는 키와 복호화를 수행하는 키가 다릅니다.
    • Public Key와 Private Key
    • Public Key로 암호화 하는 것은 민감한 데이터 또는 메시지 보호를 위해서.
    • Private Key로 암호화 하는 것은 디지털 서명(Digital Signature)을 위해서.
      • 블록체인에서 이 방식을 사용합니다.

 

※ 이 글은 아끼는 저의 학생이 공유한 글을 기반으로 재편집한 글입니다.

기존의 Spring MVC 환경에서는 인메모리 DB인 h2의 web console을 이용하기 수월했었는데, Spring WebFlux + R2DBC 환경에서는 Spring MVC 환경에서 사용하던 방법 그대로 h2의 web console을 이용할 수 없기 때문에 별도의 설정을 해주어야 합니다.

Spring WebFlux + R2DBC 환경에서 h2 web console을 사용하기 위한 설정 방법은 다음과 같습니다.

Gradle 설정

== build.gradle ==

dependencies {
	...
	implementation 'io.r2dbc:r2dbc-h2'
}

build.gradle의 dependencies에 implementation 'io.r2dbc:r2dbc-h2'를 추가합니다.

 

application.yml 설정

spring:
  sql:
    init:
      schema-locations: classpath*:db/h2/schema.sql
      data-locations: classpath*:db/h2/data.sql
  r2dbc:         # (1)                                   
    url: r2dbc:h2:mem:///test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    username: sa
    password:
h2:       # (2)
  console:
    port: 8090
logging:
  level:
    org:
      springframework:
  • (1)과 같이 R2DBC용 H2 Connection 정보를 설정합니다.
  • (2)와 같이 h2 콘솔 포트를 8090으로 설정합니다. 포트 번호는 편한 번호로 설정하면 됩니다.

 

h2 console 서버 설정


==== H2ServerConfig.java ====

@Slf4j
@Component
public class H2ServerConfig {
    @Value("${h2.console.port}")
    private Integer port;
    private Server webServer;

    @EventListener(ContextRefreshedEvent.class)
    public void start() throws java.sql.SQLException {
        log.info("started h2 console at port {}.", port);
        this.webServer = Server.createWebServer("-webPort", port.toString()).start();
    }

    @EventListener(ContextClosedEvent.class)
    public void stop() {
        log.info("stopped h2 console at port {}.", port); this.webServer.stop();
    }
}

application.yml에서 추가한 포트 번호를 H2 Server를 start 하기 전에 지정해 줍니다.

 

애플리케이션 실행

애플리케이션을 실행한 후, http://localhost:8090으로 접속해서 h2 web console로 접속이 잘 되는지 확인합니다.

HTTPS의 동작 원리와 CA를 그림으로 표현한다면 한마디로 아래와 같습니다.

HTTPS의 동작 원리

HTTPS의 동작 원리

 

CA의 역할

CA의 역할

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로 변환하기 위해 아래와 같이 PojoInstantiatorinstantiate()에서 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 패턴 등을 사용할 경우에는 항상 디폴트 생성자를 기본으로 가질 수 있도록 해야한다는 사실을 기억하면 좋을 것 같습니다.

 

 

NIO의 구성 요소

Channel

네트워크 소켓에 대한 채널을 의미하며 NetworkChannel 인터페이스로 정의된다. NetworkChannel 인터페이스의 구현 클래스로는 ServerSocketChannel, SocketChannel, DatagramChannel 등이 있다.

※ 소켓이란?

서버 프로그램과 클라이언트 프로그램의 양방향 통신을 위한 양방향 소프트웨어 엔드포인트이다. 엔드포인트는 IP 주소와 포트 번호로 구성된다.

Java의 스트림은 read/write 둘 중 하나만 수행할 수 있는 단방향이지만 Channel은 read/write 모두 수행할 수 있는 양방향 입출력 클래스이다.

  • ServerSocketChannel
    • ServerSocketChannel은 클라이언트의 연결 요청이 들어오면 [open] - [bind] - [accept] 단계를 거친 후, SocketChannel을 생성한다.
  • SocketChannel
    • 클라이언트가 전달한 데이터를 read하고, 클라이언트에게 데이터를 write하기 위한 채널이다.
    • ServerSocketChannel이 클라이언트의 연결을 accept하면 SocketChannel이 생성된다.

Selector

  • Selector는 채널에 I/O 이벤트가 있는지 없는지를 알려주는 일종의 이벤트 리스너(Event Listener)의 역할을 한다.
  • ServerSocketChannelSocketChannelregister() 메서드를 통해 Selector로 등록된다.
  • Selector에 등록된 채널은 I/O 이벤트 발생 시, 이벤트 리스닝의 대상이 된다.
  • 즉, Selector.select()를 호출하면 I/O 이벤트가 있는 채널 set을 리턴한다.

SelectionKey

  • SelectionKey는 Selector에 등록된 채널이 어떤 I/O 이벤트를 처리할지를 지정하는 객체이다.
  • SelectionKey 유형
    • SelectionKey.OP_ACCEPT
    • SelectionKey.OP_CONNECT
    • SelectionKey.OP_READ
    • SelectionKey.OP_WRITE
  • SelectionKey는 채널의 register()를 호출하면 리턴된다.
  • SelectionKey.attach()를 통해 SelectionKey 유형의 이벤트를 처리할 이벤트 핸들러를 등록할 수 있다.
  • SelectionKey의 유형은 SelectionKey.interestOps()로 지정할 수 있다.

 

ByteBuffer

  • 데이터를 read/write 할 수 있는 버퍼로 Java의 NIO에서 사용된다.
  • byte buffer가 사용하는 메모리의 위치에 따라서 non-direct buffer와 direct buffer로 나눌 수 있다.
  • non-direct buffer는 우리가 흔히 알고 있는 JVM의 heap memory를 사용하지만 direct buffer는 OS(Operatoion System)의 memory를 사용한다.

Charset

  • character(문자)를 byte로 인코딩하거나 byte를 charter(문자)로 디코딩할 때 사용하는 클래스이다.

NIO의 동작 방식

ServerSocketChannel의 바인딩 흐름

ServerSocketChannel의 바인딩 흐름

  • ServerSocketChannel과 Selector의 생성
    • 애플리케이션이 실행되면 ServerSocketChannel.open()을 통해 ServerSocketChannel을 생성한다.
    • 애플리케이션이 실행되면 Selector.open()을 통해 Selector를 생성한다. 생성된 Selector는 ServerSocketChannel 또는 SocketChannel 등을 Selector에 등록하는데 사용된다.
  • ServerSocketChannel.bind()를 통해 ServerSocketChannel을 바인딩한다.
    • 여기서의 바인딩은  IP 주소와 로컬 포트 번호를 서버 소켓과 연결함을 의미한다.
  • ServerSocketChannel.register()를 통해서 Selector에 ServerSocketChannel을 등록한다.
    • SelectionKey.OP_ACCEPT를 지정함으로써 클라이언트의 connect 요청 이벤트가 발생하면 클라이언트의 connect 요청을 수락하기 위한 로직을 처리할 수 있다.

 

ServerSocketChannel과 Selector를 통한 SocketChannel 연결 흐름

Client와 SocketChannel 연결 흐름

  • 애플리케이션이 실행되고, ServerSocketChannel이 바인딩 된 이후에 Selector는 무한 루프 상태에서 SelectionKey 유형에 해당하는 I/O 이벤트를  처리하기 위해 대기 모드를 유지한다.
  • Selector.select()를 통해 발생하는 이벤트를 기다린다.
    • 이벤트가 발생하는지에 대한 일종의 이벤트 리스닝이라고 볼 수 있다.
  • Selector.selectedKeys()를 통해 이벤트가 발생된 key를 의미하는 SelectionKey set을 얻는다.
    • 여기서는 클라이언트의 connect 요청 흐름에 대해서만 설명하므로 SelectionKey.OP_ACCEPT에 해당하는 이벤트만 처리한다.
  • SelectionKey.channel()을 통해 ServerSocketChannel을 얻는다.
  • ServerSocketChannel.accept()을 통해 클라이언트의 connect 요청을 기다리고, 클라이언트의 connect 요청이 들어오면 SocketChannel을 생성한다.
    • ServerSocketChannel에서 사용하는 포트 번호는 클라이언트의 연결 요청을 수락하기 위해서 계속해서 사용되어야 하므로 SocketChannel은 별도의 새 로컬 포트 번호를 할당 받아서 바인딩된다.

Client와 SocketChannel의 데이터 read/write 흐름

Client와 SocketChannel의 데이터 read/write 흐름

 

  • Client와 SocketChannel이 바인딩 되면 Selector.select()를 통해 I/O 이벤트가 발생할 때까지 대기한다.
  • I/O 이벤트가 발생하면 Selector.selectedKeys()를 이벤트가 발생한 SelectioinKey set을 얻는다.
  • SelectionKey.channel()을 통해 이벤트가 발생한 SocketChannel을 얻을 수 있으며, 이 SocketChannel은 해당 이벤트를 처리할 이벤트 핸들러에게 전달된다.
  • SelectionKey.attachment()를 통해 이벤트를 처리할 이벤트 핸들러를 얻을 수 있으며, 이벤트 핸들러가 SocketChannel을 이용해 이벤트를 처리한다.

Mac OS의 기본 셸 확인하기

Mac OS Catalina 이 전 버전까지는 기본 셸이 Bash shell(bash)이었으나, Catalina부터 Z shell(zsh)이 기본 셸로 변경되었습니다. 
따라서 Mac OS에서 시스템 환경 변수를 등록하기 위해서는 현재 사용하고 있는 Mac OS의 기본 셸이 무엇인지부터 확인을 해야 합니다.

Mac OS에서 기본 셸은 아래의 터미널 명령으로 손쉽게 확인할 수 있습니다.

kevin@hwang ~ % echo $SHELL
/bin/zsh
or
/bin/bash

 

Z shell에서 시스템 환경 변수 등록 방법

Z shell의 경우 다음과 같은 절차를 따릅니다.

  • 아래와 같이 vi 편집기로 .zshrc 파일을 생성 또는 오픈합니다.
kevin@hwang ~ % vi ~/.zshrc

 

  • .zshrc 파일에 아래와 같이 등록하고자 하는 환경 변수명과 값을 입력합니다.
export JWT_SECRET_KEY=kevin111111111111111111111111111111111111111111

 

  • 아래와 같이 source 명령어를 이용해 환경 변수를 등록합니다.
kevin@hwang ~ % source ~/.zshrc

 

Bash shell에서 시스템 환경 변수 등록 방법 

Bash shell의 경우 다음과 같은 절차를 따릅니다.

  • 아래와 같이 vi 편집기로 .bashrc 파일을 생성 또는 오픈합니다.
kevin@hwang ~ % vi ~/.bashrc

 

  • .bashrc 파일에 아래와 같이 등록하고자 하는 환경 변수명과 값을 입력합니다.
export JWT_SECRET_KEY=kevin111111111111111111111111111111111111111111

 

  • 아래와 같이 source 명령어를 이용해 환경 변수를 등록합니다.
kevin@hwang ~ % source ~/.bashrc

 

터미널 사용이 익숙하지 않을 경우 방법

만약 터미널을 사용해서 Shell을 편집하는 것이 익숙하지 않다면 아래와 같은 방법으로 시스템 환경 변수를 등록할 수 있습니다.

  • ⌘⇧. (Command + Shfit + . )을 눌러 숨김 파일을 표시합니다.
  • 아래와 같이 사용자 계정의 디렉토리에서 .zprofile을 찾아 더블 클릭한 후 파일을 엽니다.

 

  • 아래와 같이 등록하고자 하는 환경 변수명과 값을 입력한 후, 창을 닫습니다.

 

  • 열려진 터미널과 IntelliJ IDE가 있다면 창을 닫고 다시 시작합니다.

 

  • 아래와 같이 터미널과 IntelliJ IDE를 다시 오픈해서 환경 변수가 잘 등록 되었는지 확인합니다.

 

★ 시스템 환경 변수를 Spring Boot 애플리케이션에서 정상적으로 로드하기 위해서는 IntelliJ 같은 IDE를 반드시 Restart 하도록합니다.

HttpMessageConverter란?

웹 브라우저 같은 클라이언트에서 보여지는 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를 찾으면 HandlerAdapterController로 넘겨줄 파라미터를 결정하기 위해 이 작업을 HandlerMethodArgumentResolver에게 위임합니다.
  • HandlerMethodArgumentResolverHttpMessageConverter에게 HTTP Request Body를 특정 타입의 객체로 변환해주기를 요청합니다.
  • HttpMessageConverter는 HTTP Request Body를 특정 타입의 객체로 변환합니다.
  • HandlerMethodArgumentResolver는 변환된 데이터를 전달 받아서 이 데이터를 다시 HandlerAdapter에게 전달합니다.
  • HandlerAdapterHandlerMethodArgumentResolver로부터 전달 받은 데이터를 핸들러 메서드의 파라미터로 포함 시킨 후, 핸들러 메서드를 호출합니다.

 

응답 처리 시 HandlerAdapter의 동작 과정

  • 핸들러 메서드가 응답으로 전달할 데이터를 리턴합니다.
  • HandlerMethodReturnValueHandler는 핸들러 메서드로부터 전달 받은 응답 데이터를 HttpMessageConverter에게 전달합니다.
  • HttpMessageConverterHandlerMethodReturnValueHandler로부터 전달 받은 데이터를 HTTP Response Body에 포함되는 형식의 데이터로 변환합니다.
  • HandlerMethodReturnValueHandlerHttpMessageConverter로부터 전달 받은 데이터를 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의 대략적인 동작 흐름입니다. 참고하세요.

JSON -> DTO로 deserialization

HandlerMethodArgumentResolverComposite.resolveArgument() 

RequestResponseBodyMethodProcessor.resolveArgument()

RequestResponseBodyMethodProcessor.readWithMessageConverters() > converter.read()

objectReader.readValue()

 

DTO -> JSON serialization

HandlerMethodReturnValueHandlerComposite.handleReturnValue() 
→ HttpEntityMethodProcessor.handleReturnValue() 
→ AbstractMessageConverterMethodProcessor.writeWithMessageConverters() > genericConverter.write() 
→ AbstractGenericHttpMessageConverter.write() 
→ AbstractJackson2HttpMessageConverter.writeInternal() > objectWriter.writeValue(generator, value)
→ write() and flush() 과정을 거친다.
→ ...
→ ...
CoyoteAdapter.service() > response.finishResponse() // response가 close되면서 클라이언트 쪽에 출력

HttpMessageConverter 참고 자료

 

Spring Framework 5.3.19 API

 

docs.spring.io

 

ResponseEntity란?

ResponseEntityHttpEntity의 확장 클래스로써 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 객체를 생성하는 방식입니다.

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/v1/coffees")
public class ResponseEntityExample01 {
    @PostMapping
    public ResponseEntity postCoffee(Coffee coffee) {
 
        // coffee 정보 저장
        
        return new ResponseEntity<>(coffee, HttpStatus.CREATED);
    }
}
[코드 1-1]

 

[코드 1-1]은 ResponseEntity 객체를 생성하면서 응답의 body 데이터와 HttpStatus의 상태를 전달하고 있습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/v1/coffees")
public class ResponseEntityExample01 {
    @GetMapping("/{coffee-id}")
    public ResponseEntity getCoffee(@PathVariable("coffee-id"long coffeeId) {
 
        if (coffeeId < 0) {
            return new ResponseEntity<>(
                    "coffeeId should be greater than 0",
                    HttpStatus.BAD_REQUEST);
        }
 
        return new ResponseEntity<>(new Coffee(), HttpStatus.OK);
    }
}
[코드 1-2]

 

코드 1-2는 coffeeId에 따라서 HttpStatus 상태를 동적으로 지정하는 예입니다.

 

코드 1-2는 요청 데이터에 대한 유효성 검사가 API 계층에 포함에 직접 포함이 되어 있는데, 이는 좋은 개발 방식은 아닙니다.

Handler Method는 단순히 클라이언트의 요청을 전달 받고, 처리된 응답을 다시 클라이언트에게 전달하는 역할만 하도록 로직을 단순화 하는 것이 좋으며, 유효성 검사나 비즈니스 처리 등의 로직은 별도의 메서드 또는 클래스로 분리를 하는 것이 좋습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@RequestMapping("/v1/coffees")
public class ResponseEntityExample01 {
    @GetMapping("/{coffee-id}")
    public ResponseEntity getCoffee(@PathVariable("coffee-id"long coffeeId) {
 
        if (coffeeId < 0) {
            return new ResponseEntity<>(
                    "coffeeId should be greater than 0",
                    HttpStatus.BAD_REQUEST);
        }
 
        HttpHeaders headers = new HttpHeaders();
        headers.add("Custom-Header""bar");
        return new ResponseEntity<>(new Coffee(), headers, HttpStatus.OK);
    }
}
[코드 1-3]

만일 ResponseEntity에 Custom header를 포함하고 싶다면 코드 1-3과 같이 HttpHeaders에 원하는 header를 추가하고 ResponseEntity의 생성자로 headers 객체를 전달하면 됩니다.

 

BodyBuilder를 이용한 메서드 체인 방식

ResponseEntity 클래스의 생성자로 body, header, HttpStatus 등을 추가하는 방식과 달리 BodyBuilder 클래스를 이용하면 각각의 항목들을  메서드 체인 방식으로 전달할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/v1/coffees")
public class ResponseEntityExample02 {
    @PostMapping
    public ResponseEntity<Coffee> postCoffee(Coffee coffee) {
 
        // coffee 정보 저장
        long savedCoffeeId = 1L;
        return ResponseEntity.created(URI.create("/members/" + savedCoffeeId)).body(coffee);
    }
}
[코드 1-4]

코드 1-4는 코드 1-1을 ResponseEntity.BodyBuilder를 이용해서 메서드 체인 방식으로 ResponseEntity 객체를 리턴해주고 있습니다.

created() 메서드의 경우 URI를 지정할 수 있는데, 이는 새롭게 생성된 리소스에 대한 접근 URI를 Location 헤더 값으로 포함시킴으로써 클라이언트 쪽에서 이 정보를 이용해 해당 리소스에 접근할 수 있도록 해줍니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/v1/coffees")
public class ResponseEntityExample02 {
    @GetMapping("/{coffee-id}")
    public ResponseEntity getCoffee(@PathVariable("coffee-id"long coffeeId) {
 
        if (coffeeId < 0) {
            return ResponseEntity.badRequest().body("coffeeId should be greater than 0");
        }
 
        HttpHeaders headers = new HttpHeaders();
        headers.add("Custom-Header""bar");
        return ResponseEntity.ok().headers(headers).body(new Coffee());
    }
}
[코드 1-5]

코드 1-5는 코드 1-3을 BodyBuilder를 이용해서 메서드 체인 방식으로 변경한 코드입니다.

 

ResponseEntity 관련 자료

Controller의 핸들러 메서드는 다양한 유형의 Argument(인수)를 지원합니다.

그 중에서 REST API 애플리케이션에서 자주 사용되는 유형의 Argument를 간단히 살펴보도록 하겠습니다.

Method Argument 설명
@RequestParam 쿼리 파라미터, form-data 등의 Servlet request Parameter를 바인딩 해야 할 경우 사용합니다.
@RequestHeader request header를 바인딩해서 header의 key/value에 접근할 수 있습니다.
@RequestBody request body를 읽어서 지정한 Java 객체로 deserialization 해줍니다.
@RequestPart 'multipart/form-data' 형식의 request 데이터를 part 별로 바인딩할 수 있도록 해줍니다.
@PathVariable @RequestMapping 에 패턴 형식으로 정의된 URL의 변수에 바인딩할 수 있도록 해줍니다.
@MatrixVariable URL 경로 세그먼트 부분에 key/value 쌍으로 된 데이터에 바인딩할 수 있도록 해줍니다.
HttpEntity request header와 body에 접근할 수 있는 컨테이너 객체를 사용할 수 있습니다.
javax.servlet.ServletRequest, 
javax.servlet.ServletResponse
로우 레벨의 ServeletRequest와 ServletResponse의 정보가 필요할 때 사용할 수 있습니다.

 

@RequestParam

@RequestParam은 클라이언트 쪽에서 쿼리 파라미터, form data, x-www-form-urlencoded 등의 형식으로 전달되는 요청 데이터를 바인딩해서 사용할 수 있도록 해줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
@RequestMapping("/coffees")
public class CoffeeController {
 
    // ...
 
    @GetMapping
    public Coffee getCoffee(@RequestParam("coffeeId"int coffeeId) {
       Coffee coffee = coffeeService.getCoffee(coffeeId);
        return coffee;
    }
 
    // ...
 
}
[코드 1-1]
cs

 

@RequestHeader

@RequestHeader는 HTTP request header의 key/value 쌍의 데이터에 접근할 수 있도록 해줍니다.

1
2
3
4
5
6
@GetMapping("/coffee")
public void getCoffee(
        @RequestHeader("Content-Type"String contentType, 
        @RequestHeader("Content-Length"long contentLength) { 
    //...
}
[코드 1-2]
cs

 

@RequestBody

@RequestBody는 HTTP request body를 읽어서 지정한 Java 객체로 변환(deserialization) 해줍니다.

1
2
3
4
5
@PostMapping("/coffees")
public void handle(@RequestBody Coffee coffee) {
    // ...
    coffeeService.save(coffee)
}
[코드 1-3]
cs

@RequestBody는 특히 리소스를 등록하는 @PostMapping에서 주로 사용됩니다.

 

@RequestPart

'multipart/form-data' 형식의 request 데이터를 part 별로 바인딩할 수 있도록 해줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
public class CoffeeController {
    @PostMapping("/coffees")
    public String postCoffee(@RequestPart("coffee"Coffee coffee,
           @RequestPart("file") MultipartFile photoFile) {
 
        if (!photoFile.isEmpty()) {
            byte[] bytes = file.getBytes();
            // TODO 파일 저장
            return "success";
        }
        return "failed";
    }
}
[코드 1-4]
cs

예를 들어, 클라이언트 쪽에서 커피 사진이 포함된 multpart/form-data 타입의 커피 정보를 전송한다면,  [코드 1-4]와 같이  form data를 part별로 나누어서 전달 받을 수 있습니다.

 

@PathVariable

@PathVariable@RequestMapping 에 패턴 형식으로 정의된 URL의 변수에 바인딩할 수 있도록 해줍니다.

1
2
3
4
@GetMapping("/members/{member-id}/coffees/{coffee-id}")
public Pet getCoffee(@PathVariable("member-id") Long memberId,
@PathVariable("coffee-id") Long coffeeId) {
    // ...
}
[코드 1-5]
cs

[코드 1-5]와 같이 '{variable name}'과 같은 형태의 URL 변수가 여러개 있을 경우, @PathVariable을 핸들러 메서드에 순차적으로 추가해서 URL 변수의 값을 받을 수 있습니다.

 

@MatrixVariable

@MatrixVariable은 URL 경로 세그먼트 부분에 key/value 쌍으로 된 데이터에 바인딩할 수 있도록 해줍니다.

@MatrixVariable의 사용은 예제 코드를 보는게 가장 이해가 빠릅니다.

1
2
3
4
5
6
7
8
9
10
11
12
// 클라이언트 요청 URL
// GET /coffees/42;q=11;r=22
 
@GetMapping("/coffees/{coffeeId}")
public void getCoffee(@PathVariable("coffee-id") coffeeId, 
                      @MatrixVariable int q,
                      @MatrixVariable int r) {
 
    // coffeeId == 42
    // q == 11
    // r == 22
}
[코드 1-6]
cs

만약에 클라이언트 쪽 요청 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")
public class HttpEntityExample {
    @PostMapping
    public Coffee postCoffee(HttpEntity<Coffee> entity) {
        Coffee coffee = entity.getBody();
        HttpHeaders headers = entity.getHeaders();
        
        // coffee 정보 저장
        return coffee;
    }
}
[코드 1-7]
cs

 

​@RequestMapping 애너테이션은 어디에 사용할 수 있나요?

@RequestMapping 애너테이션은 클라이언트의 요청과 Controller의 핸들러 메서드를 매핑하기 위해서 사용하는 애너테이션입니다. 즉, @RequestMapping 애너테이션에 정의된 URI에 매치되는 요청을 처리할 수 있도록 해줍니다.

@RequestMapping 애너테이션은 클래스 레벨에 사용할 수 있고, 메서드 레벨에 사용할 수 있습니다.
일반적으로 공통 URI는 클래스 레벨에 정의하고, 핸들러 메서드별로 달라지는 URI는 각각의 핸들러 메서드에 정의합니다.

 

@RequestMapping 애너테이션 단축 표현

@RequestMapping 애너테이션은 HTTP Method 에 해당하는 단축 표현들을 주로 사용합니다.

  • @GetMapping: HTTP Get Method에 해당하는 단축 표현으로 서버의 리소스를 조회할 때 사용
  • @PostMapping: HTTP Post Method에 해당하는 단축 표현으로 서버에 리소스를 등록(저장)할 때 사용
  • @PutMapping: HTTP Put Method에 해당하는 단축 표현으로 서버의 리소스를 수정할 때 사용. 리소스의 모든 정보를 수정할 때 사용한다.
  • @PatchMapping: HTTP Put Method에 해당하는 단축 표현으로 서버의 리소스를 수정할 때 사용. 리소스의 일부 정보만 수정할 때 사용한다.
  • @DeleteMapping: HTTP Delete Method에 해당하는 단축 표현으로 서버의 리소스를 삭제할 때 사용.

일반적으로 클래스 레벨에는 @RequestMapping 애너테이션을 사용하고, 메서드 레벨에서는 단축 표현을 사용하는 것을 권장하고 있습니다.

 

@RequestMapping 애너테이션에 사용되는 Attribute

@RequestMapping 애너테이션에 사용되는 Attribute는 생각보다 많이 사용되지 않습니다. 하지만 필요할 경우 그때 그때 설정해서 사용할 수 있으므로 아래 링크를 확인하시고 어떤 Attribute가 있는 살펴보기 바랍니다.

 

+ Recent posts

출처: http://large.tistory.com/23 [Large]