제가 학습했던, 추천받거나 추천드리고 싶은 자료들을 정리한 Github Repo입니다.

 

링크

github.com/Lob-dev/Junior-Back-end-Developer-Concepts

 

개정 이력

- 2021.04.29 14:01 : 공통, 자바, 객체 지향, 스프링 추가 (서적, 인강, 참고로 구분)

- 2021.04.29 14:47 : 내용 보강 (기타)

- 2021.04.30 16:07 : 내용 보강 (자바 서적 추가, 스프링 질문 5개 추가)

- 2021.05.03 01:58 : 개인적으로 추천하는 학습 흐름 추가

- 2021.05.03 11:30 : 서적 관련 설명 추가, 문서 포맷 변경, Java 면접 질문 강화. SQL, 설계 항목 템플릿 추가 

- 2021.05.03 11:30 : 서적 관련 설명 추가, 문서 포맷 변경, Java 면접 질문 강화. SQL 과 설계 항목 템플릿 추가

- 2021.05.05 00:14 : 각각의 설명 수정, 내용 보강

- 2021.05.06 23:01 : SQL 항목 업데이트, 참고 영상 추가

- 2021.05.23 02:09 : Job Interview.md에 채용 공고 링크와 이동욱님 Repo 링크 추가

- 2021.05.23 21:01 : Architecture Cleancode md 내용 업데이트

- 2021.06.03 20:14 : DB 면접 질문 업데이트

- 2021.06.09 10:46 : 스프링, 면접 관련 참고 링크 추가 및 위치 수정 (Common에 있던 Spring 관련 링크 이동)

- 2021.06.30 13:45 : 스프링 입문 부분 내용 보강 ( 개인 프로젝트 진행 전 etc docker, aws 추가 )

 

 

1월 13일에 Notion에 정리했던 내용을 옮겨 적은 것입니다.

잘못된 내용은 댓글 부탁드립니다.

 

한동안 회사 일로 공부한 걸 게시할 시간이 없네요.. ㅠ

 

 

CGI? (Common Gateway Interface)

  • HTTP 프로토콜 기반의 웹 서버와 다양한 언어로 구현된 프로그램 간의 데이터를 교환하는 표준 스펙이자 Interface이다.
  • 일종의 Interface, 스펙이기 때문에 여러 언어로 구현이 가능하다

장점

  • 여러 작업에 대한 많은 템플릿 코드들이 많이 존재한다.
  • 표준 스펙을 준수하는 한 모든 언어와 플랫폼에서 작성될 수 있다.

단점

  • 매 요청이 들어올 때마다 프로세스가 생성되고 각각의 CGI 구현체를 통해 동작한다.
    • 메모리를 공유하는 쓰레드에 비해서 프로세스는 각자의 공간을 지니기 때문에 상대적으로 무겁고 생성되는데 시간이 상대적으로 오래 걸린다.
  • 상대적으로 많은 처리 시간을 소모한다.

기존 방식의 문제점을 해결하기 위해 요청마다 스레드를 생성하게끔 변경하였지만, 결국 CGI 구현체가 계속해서 생성되는 것을 개선하지 못하였다.

 

 

Servlet

이전에 사용되던 기술인 CGI의 여러 문제점을 해결하기 위해 등장하였다.

  • 웹 애플리케이션을 개발하기 위한 표준 스펙이자 Interface이다.
    • 서블릿도 CGI 규약을 따르지만 이를 Servlet Container에게 위임하였고 Container와 Servlet 간의 규칙이 존재한다.
  • Servlet 인스턴스는 Servlet Container를 통해 등록되고 Cycle이 관리된다.
    • Servlet은 모든 요청을 받을 수 있으며, 각 요청마다 스레드가 생성되거나 기존에 생성된 스레드를 꺼내와 (Thread Pool) 동작한다.
    • Servlet 구현체는 싱글톤 패턴을 적용하였기에 하나의 구현체를 통해 동작할 수 있다.

 

 

Servlet Flow

  1. Web Container는 요청을 받으면 Class loader를 통해 Servlet Class를 로드한다. (1번만)
  2. Servlet Class가 로드되면 인스턴스를 생성한다. ( 1번만 생성하고 싱글톤으로 관리한다.)
  3. 인스턴스를 생성한 뒤 init()을 호출하여 Servlet을 초기화한다. ( 1번만 초기화한다. )
  4. 이후 Web Container는 요청이 들어올 때마다 스레드를 통해 Service()를 호출한다.
  5. Web Container가 종료되기 전 혹은 특정 Servlet 인스턴스에 대한 자원 반납을 진행한다.
  6. → Destroy() 호출

위에서 나온 init, service, destroy 메서드들은 Web Container가 호출하며, 스레드는 각각의 지역변수 값을 가지고 공유 자원들을 이용한다.

 

 

Servlet을 Web Container 동작시 Load 하는 방법?

위에 설명된 Flow를 살펴보면, 요청 시 Servlet Class를 load 한다고 정리되어 있는데 이 방식은 첫 번째 요청이 다른 동일 요청보다 더 많은 시간을 사용한다는 것을 의미한다.

 

Web.xml의 Mapping Servlet 설정 시 load-on-startup을 이용하면, 서버가 실행될 때 Servlet Class를 Load 할 수 있게 변경할 수 있다.

 

<servlet>
        <servlet-name>post<servlet-name>
        <servlet-class>example.servlets.PostServlet</servlet-class>

        // 이렇게 작성하고 0 ~ N의 정수를 넘겨주면 그 순서대로 Servlet.Class가 로드된다.
        <load-on-startup>0</load-on-startup> 
</servlet>

 

 

 

Servlet URI Mapping

Web Container는 각각의 요청에 대한 URI 정보를 가지고 Servlet 구현체를 매핑하여 준다.

→ 이와 관련된 설정은 Web.xml 에 진행하게 된다.

 

즉 URL에 따라 여러 Servlet 구현체가 존재하고 Web.xml 파일을 통해 관리된다.

 

web.xml 예시

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
         http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

        //context-param....

        //listener....

        <servlet>
                <servlet-name>post</servlet-name>
                <servlet-class>example.servlets.PostServlet</servlet-class>
        </servlet>
        <servlet-mapping>
                <servlet-name>post</servlet-name>
                <url-pattern>/api/posts</url-pattern>
        </servlet-mapping>

        <servlet>
                <servlet-name>user</servlet-name>
                <servlet-class>example.servlets.UserServlet</servlet-class>
        </servlet>
        <servlet-mapping>
                <servlet-name>user</servlet-name>
                <url-pattern>/api/users</url-pattern>
        </servlet-mapping>
        <servlet-mapping>
                <servlet-name>user</servlet-name>
                <url-pattern>/api/sign-up</url-pattern>
        </servlet-mapping>

        <servlet>
                <servlet-name>comment</servlet-name>
                <servlet-class>example.servlets.CommentServlet</servlet-class>
        </servlet>
        <servlet-mapping>
                <servlet-name>comment</servlet-name>
                <url-pattern>/api/comments</url-pattern>
        </servlet-mapping>

</web-app>

Web Container (Servlet Container)

Servlet의 Life Cycle을 관리하며, Network 통신, Thread 기반의 병렬 처리를 대행한다.

  • Servlet 스펙을 구현하고 HTTP 요청을 자바 API로 변환하는 등의 행위를 한다.
  • Client의 웹 요청을 해석하여 적정한 Servlet 구현체의 메서드ServletRequet, ServletResponse 매개변수와 함께 호출한다.
  • Web Server나 WAS에서 동작한다.
  • → ( ex) Tomcat : Tomcat은 Web Server의 일반적인 몇몇 기능들을 제공하지 않는다.)

 

Web Container Flow

  1. Web.xml 설정 정보를 통해 요청과 Servlet을 Mapping 한다.
  2. 이 요청에 대한 req, res 객체를 생성한다.
  3. 생성하였거나, 스레드 풀에서 꺼내온 스레드를 이용하여 서비스 메서드를 호출한다.
  4. 호출된(HttpServlet) 서비스 메서드는 Servlet 구현체의 서비스 메서드를 호출한다.
  5. → 해당 메서드에서는 Servlet 응답, 요청 객체를 HttpServlet 응답, 요청으로 Casting 한다.
  6. 서비스 메서드에서 요청 객체의 메서드 유형(HTTP Method)을 파악하고 해당하는 메서드를 호출한다. (doGet(), doPost()....)
  7. 호출된 유형별 메서드는 해당 요청을 처리하고 응답 객체에 저장한 뒤 반환한다.
  8. Servlet Container는 Client에게 응답을 보내고 요청, 응답 객체를 삭제한다.

 

 

Servlet Context

하나의 Servlet이 Servlet Container와 통신하기 위해 사용하는 메서드들을 관리하는 인터페이스이며, Web Container에 의해 생성된다. 컨테이너가 객체를 관리하기 위한 인터페이스

  • Servlet Container에서 생성되는 Application 단위의 Servlet 요청들을 관리한다. (Tomcat)
  • Servlet에 대한 정보 접근을 도와준다. (Servlet API 지원)
    • 파일의 MIME 유형 가져오기, 요청 전달, 로그 파일 작성 등에 사용된다.
  • web.xml에서 구성 정보를 가져오는 데 사용된다.
    <context-param>
        <param-name>driverName</param-name>
        <param-value>....XxxDriver</param-value>
    </context-param>
    
    
    PrintWriter out = response.getWriter();
    // Context context = getServletConfig().getServletContext();
    ServletContext context = getServletContext();
    out.println(sc.getInitParameter("driverName"));

 

 

 

 

Servlet의 구현체들

각각의 구현체들은 GenericServlet이나 HttpServlet을 상속받아 구현한다.

WAS, Servlet Container


-   Servlet Interface
-   GenericServlet
-   HttpServlet 등

Spring Framework

-   HttpServletBean
-   FrameworkServlet
-   DispatcherServlet

 


GenericServlet?


클라이언트 요청을 처리하는 메서드인 service()를 재정의하여야 하는 일반적인 인터페이스.

HTTP를 제외한 별도의 독립적인 프로토콜을 이용하는 서블릿을 만들 때 사용한다.

 

 

public class ExampleServlet extend GenericServlet{

	// 재정의
    @Override
    public void service(ServletRequest req, SetvletResponse res) 
                                      throws IOException, ServletException {
            System.out.println("Service");
    }
    
}

 

 

HttpServlet?


HTTP 프로토콜에 대한 요청을 처리하는 Service() 메서드들을 제공하는 구현체이다.

-   ServletRequest, Response를 HttpServlet 형식으로 Casting 하는 Service()
-   요청 Method 타입에 따라 별도의 로직을 실행하는 service()가 존재한다.

 

// 해당 메서드는 요청을 casting하는 service()에서 호출한다.
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {

	// 해당 요청의 메서드 값
    String method = req.getMethod();

    if (method.equals(METHOD_GET)) {
        long lastModified = getLastModified(req);
        if (lastModified == -1) {
            // servlet doesn't support if-modified-since, no reason
            // to go through further expensive logic
            doGet(req, resp);
        } else {
            long ifModifiedSince;
            try {
                ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
            } catch (IllegalArgumentException iae) {
                // Invalid date header - proceed as if none was set
                ifModifiedSince = -1;
            }
            if (ifModifiedSince < (lastModified / 1000 * 1000)) {
                // If the servlet mod time is later, call doGet()
                // Round down to the nearest second for a proper compare
                // A ifModifiedSince of -1 will always be less
                maybeSetLastModified(resp, lastModified);
                doGet(req, resp);
            } else {
                resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            }
        }

    } else if (method.equals(METHOD_HEAD)) {
        long lastModified = getLastModified(req);
        maybeSetLastModified(resp, lastModified);
        doHead(req, resp);

    } else if (method.equals(METHOD_POST)) {
        doPost(req, resp);

    } else if (method.equals(METHOD_PUT)) {
        doPut(req, resp);

    } else if (method.equals(METHOD_DELETE)) {
        doDelete(req, resp);

    } else if (method.equals(METHOD_OPTIONS)) {
        doOptions(req,resp);

    } else if (method.equals(METHOD_TRACE)) {
        doTrace(req,resp);

    } else {
        //
        // Note that this means NO servlet supports whatever
        // method was requested, anywhere on this server.
        //

        String errMsg = lStrings.getString("http.method_not_implemented");
        Object[] errArgs = new Object[1];
        errArgs[0] = method;
        errMsg = MessageFormat.format(errMsg, errArgs);

        resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
    }
}

 

 

 

HttpServletBean?

Servlet init()과 관련하여 초기에 설정되는 값들을 Bean 속성으로 매핑하는 확장 클래스이다.

Spring Framework에서 사용되는 모든 Servlet에 대해 적용된다.

// 구성 매개 변수를 매핑한 뒤 초기화를 진행한다.
@Override
public final void init() throws ServletException {

    // Set bean properties from init parameters.
    PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
    if (!pvs.isEmpty()) {
        try {
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
            initBeanWrapper(bw);
            bw.setPropertyValues(pvs, true);
        }
        catch (BeansException ex) {
            if (logger.isErrorEnabled()) {
                logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
            }
            throw ex;
        }
    }

    // Let subclasses do whatever initialization they like.
    initServletBean();
}

 

HttpServlet의 기본 동작을 상속받지만, 모든 요청 처리에 대해서 Sub Class에게 위임한다.

(FrameworkServlet, DispatcherServlet은 해당 구현체를 상속받는다.)

 

FrameworkServlet?

Spring Framework에서 사용되는 Servlet 기능을 위한 구현체이다.


WebApplicationContext를 통해 Servlet 인스턴스가 관리되게끔 지원한다.

해당 구현체로는 doService()를 처리할 수 없으며, 해당 메서드 처리를 위해서는 하위 구현체를 구현하여야 한다. (DispatcherServlet은 해당 메서드를 구현하고 있다.)

 

protected abstract void doService(HttpServletRequest request, HttpServletResponse response)
        throws Exception;
        
/**
* Delegate GET requests to processRequest/doService.
*

Will also be invoked by HttpServlet's default implementation of {@code doHead},
* with a {@code NoBodyResponse} that just captures the content length.
* @see #doService
* @see #doHead
*/
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
	throws ServletException, IOException {
    
    processRequest(request, response);
}

/**
 * Delegate POST requests to {@link #processRequest}.
 * @see #doService
 */
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

    processRequest(request, response);
}

코드를 보게 되면 doService()는 추상 메서드로, doPost, doGet 등은 별도 처리를 거치고 위임을 하는 형식을 지닌다.

Default Context는 XMLApplicationContext이다.

(HttpServletBean이 xml기반이기 때문일까?)

 

 

 

DispatcherServlet?

Spring MVC는 하나의 Servlet을 통해 요청을 처리하고 매핑하는 형식의 구조를 지닌다.

비즈니스 로직에 대하여서는 Adapter를 통해 Mapping 된 컨트롤러에게 위임하며, 설정 값에 따라 Resolver를 선택하거나 컨버터를 통해 랜더링을 거치고 결과를 반환한다.

해당 글은 Notion에 정리되어 있던 Somaeja 프로젝트 관련 정리 글 중 하나입니다.

 

 

이번 프로젝트를 진행할 때 Post Service에 Transcation Read only 설정을 적용하게 되었었다. 

 

단순히 read only를 사용하면 read-write 보다 성능이 더 좋다고 들었기 때문이다. 그러지 마...

 

하지만 해당 설정에 대하여서 DB마다 동작 방식이 다르다고 하여, 많이 사용되는 DB들을 기준으로 조사해보았다.

 

 

 

Oracle

Read Only 트랜잭션을 이용할 경우 이 트랜잭션이 시작되기 이전에 커밋된 데이터만 접근할 수 있으며, 트랜잭션 실행되는 동안 커밋되는 데이터는 결과에 반영되지 않는다.

 

해당 트랜잭션 시에 지원하는 DML은 SELECT(조회) 구문뿐이다.

→ 해당 트랜잭션 내에서 일관적인 데이터를 얻도록(데이터 일관성) 보장한다. 즉 성능 이점만을 위함이 아니다.

 

 

 

PostgreSQL

해당 트랜잭션을 이용할 경우 SELECT를 제외한 DDL, DML, DCL은 동작하지 않는다.

 

Postgresql에서는 읽기 동작을 가정할 경우, Read/Write 속성과 성능 차이를 가지지 않으며 (해당 부분의 최적화 X) Deffered 속성(행 단위가 아닌 트랜잭션 단위의 제약조건 검증 처리)을 적용하였을 때, SERIALIZABLE 이거나 READ ONLY를 사용하게 되어 내부의 튜플이 수정되지 않는 안전한 동작을 하게끔 지원하는 용도이다.

 

→ Postgre의 Read Only 설정도 성능 이점이 아닌 동시성 제어를 위함이다.

→ Postgre는 Read Only 설정 시 트랜잭션 ID를 일반적인 ID가 아닌 가상 ID로 제공하기에 실제로 제공되는 트랜잭션 ID가 수가 줄어들어 성능이 개선될 수도 있다.

 

 

Postgresql의 튜플(Tuple)?

MVCC를 지원하기 위해 사용되는 기능으로 PostgreSQL은 트랜잭션 처리 시 기존에 가지고 있는 데이터 로우를 복제하여 기존 로우를 사용자들이 조회하는 용도로, 복제한 row를 트랜잭션 처리 용도 (데이터)로 사용하게 되는데 이러한 원본, 복제본에 버전을 명시할 때 사용되는 것이 튜플이다.

 

삭제, 롤백, 업데이트 시에는 생성된 튜플들에 대하여서 Dead라는 마킹이 적용되며 (실제 데이터가 삭제되지 않는다.) VACUUM FULL를 통하여 해당 데이터들을 파기하게 된다.

 

 

 

MySQL

해당 트랜잭션을 이용할 경우 SELECT 문에 대해서만 기능을 지원하며, Transaction ID 설정에 대한 오버헤드를 해결할 수 있다. Read Only 트랜잭션에 대해서는 ID가 부여되지 않는다. 

 

추가적으로 세션 별로 임시 테이블을 변경하거나 Lock 쿼리를 실행할 수 있는데, 이는 다른 트랜잭션(Read Write 혹은 다른 Read Only) 들이 가시 할 수 없는 범위이기 때문이다.

 

Oracle과 마찬가지로 별도의 스냅샷을 통해 데이터를 조회하기 때문에, 데이터 일관성을 보장할 수 있다.

→ 트랜잭션 ID 설정에 대한 오버헤드를 해결하고, 스냅샷을 통해 데이터 일관성을 보장한다.

 

 

 

추가) Spring의 Read Only 설정 방식?

Oracle은 9i, 10g 버전에서 Connection.setReadOnly를 통해 해당 트랜잭션을 지원하였다.

 

하지만 이후 버전에서는 11, 12c 드라이버를 사용하는 경우에 Connection.setReadOnly(true)를 설정하더라도 Read only 트랜잭션이 설정, 동작하지 않았었다.

  • 이것을 해결하기 위해 DataSourceTransactionManager에 repareTransactionConnection Template method가 추가되었으며, setEnforceReadOnly 설정을 true로 설정하면 prepareTransactionalConnection를 통한 DB와 커넥션 연결 시에 SET TRANSACTION READ ONLY 구문을 보냄으로써 Read Only Transaction을 사용할 수 있게 되었다.해당 설정은 스프링 4.3.7 이후에 추가되었다. (Oracle, MySQL, Postgre에 적용 가능)
// DataSourceTransactionManager.java 

private boolean enforceReadOnly = false; 

/** 
* Specify whether to enforce the read-only nature of a transaction 
* (as indicated by {@link TransactionDefinition#isReadOnly()} 
* through an explicit statement on the transactional connection: 
* "SET TRANSACTION READ ONLY" as understood by Oracle, MySQL and Postgres. 
* <p>The exact treatment, including any SQL statement executed on the connection, 
* can be customized through {@link #prepareTransactionalConnection}. 
* <p>This mode of read-only handling goes beyond the {@link Connection#setReadOnly} 
* hint that Spring applies by default. In contrast to that standard JDBC hint, 
* "SET TRANSACTION READ ONLY" enforces an isolation-level-like connection mode 
* where data manipulation statements are strictly disallowed. Also, on Oracle, 
* this read-only mode provides read consistency for the entire transaction. 
* <p>Note that older Oracle JDBC drivers (9i, 10g) used to enforce this read-only 
* mode even for {@code Connection.setReadOnly(true}. However, with recent drivers, 
* this strong enforcement needs to be applied explicitly, e.g. through this flag. 
* @since 4.3.7 * @see #prepareTransactionalConnection 
*/ 

public void setEnforceReadOnly(boolean enforceReadOnly) { 
    this.enforceReadOnly = enforceReadOnly; 
} 

/** 
* Return whether to enforce the read-only nature of a transaction 
* through an explicit statement on the transactional connection. 
* @since 4.3.7 
* @see #setEnforceReadOnly 
*/ 
public boolean isEnforceReadOnly() { 
    return this.enforceReadOnly; 
} 

/** 
* Prepare the transactional {@code Connection} right after transaction begin. 
* <p>The default implementation executes a "SET TRANSACTION READ ONLY" statement 
* if the {@link #setEnforceReadOnly "enforceReadOnly"} flag is set to {@code true} 
* and the transaction definition indicates a read-only transaction. 
* <p>The "SET TRANSACTION READ ONLY" is understood by Oracle, MySQL and Postgres 
* and may work with other databases as well. If you'd like to adapt this treatment, 
* override this method accordingly. 
* @param con the transactional JDBC Connection 
* @param definition the current transaction definition 
* @throws SQLException if thrown by JDBC API 
* @since 4.3.7 
* @see #setEnforceReadOnly 
*/ 
protected void prepareTransactionalConnection(Connection con, TransactionDefinition definition) throws SQLException { 
    if (isEnforceReadOnly() && definition.isReadOnly()) { 
        try (Statement stmt = con.createStatement()) { 
            stmt.executeUpdate("SET TRANSACTION READ ONLY"); 
        } 
    } 
}

 

 

 

Read only Transaction을 사용하는 일반적인 이유

  • 해당 설정을 사용한 트랜잭션은 데이터(테이블, 칼럼)에 대하여 Lock을 적용할 필요가 없고 접근할 수 있는 데이터가 (스냅샷, 튜플 등) 변경되지 않기 때문에, 일관적인 데이터를 읽어오고 제공할 수 있다.
  • 해당 속성의 경우 일반적으로 트랜잭션 ID를 부여하지 않아도 되기에 불필요한 ID 설정에 대한 오버헤드가 발생하지 않기 때문에 성능의 이점을 볼 수 있다.
  • → 추가적으로 읽기-쓰기 트랜잭션을 위해 구성되는 별도 스냅샷의 총 수가 줄어든다.

 

 

 

참고 자료

 

틀린 내용은.. 댓글로...!! 남겨주시면 수정하도록 하겠습니다..!

 

해당 글은

02-RESTful-API-예제-Spring-Framework

에서 작성된 프로젝트를 좀 더 REST 하게 변경하는 과정을 간략하게 담은 글입니다.

 

이러한 과정을 통해 리처드슨 성숙도 모델의 3 level에 가까운 혹은 만족하는 API를 구현할 수 있습니다.

 

 

ETag란 무엇인가요? (Entity Tag)


HTTP Spec의 일부로써 웹 캐시 유효성 검사를 위해 제공하는 메커니즘 중 하나입니다.

이 방식의 특징으로는 클라이언트가 조건에 따른 요청을 할 수 있게끔 한다는 것입니다.

 

리소스에 대한 특정 버전 혹은 결과에 대해 ETag를 생성하여 요청을 검증하고, 서버 리소스에서 변경이 발생한다면 ETag를 변경하여 검증을 실패하게 합니다.

 

이는 새로운 리소스 결과를 클라이언트에 전달하며 ETag 값은 요청, 응답 헤더로 제공되게 됩니다.

 

 

이를 통한 장점

  • 불필요한 요청 트래픽 ( 네트워크 트래픽 )을 감소시키고, 요청에 대한 빠른 응답을 지원하게 됩니다. 서버에 영향을 주지 않는 HTTP 메서드 혹은 로직을 지원합니다. get, head..
  • 이러한 요청 트래픽 감소는 부가적으로 DB의 트랜잭션을 감소시킬 수 있습니다.

 

ETag를 사용하지 말아야할 때

  • 결과 응답 방식 포맷 등에 따라서 ETag 값이 효용성을 잃거나 계속 생성될 수 있습니다. gzip의 Timestamp..
  • 서버의 리소스를 변경시키는 로직, 자주 결과가 바뀌는 API에 대해 ETag를 적용한 경우 이는 오히려 서버의 부하를 제공할 수 있습니다.
  • ETag Spec에 맞게 구현되었더라도 다른 구현 방식, 구형 브라우저에 의해 해당 기능이 사용되지 않을 수 있습니다.

 

 

자바 진영은 ETag를 어떻게 제공하나요?


자바 진영에서는 ETag 기능을 Serlvet Filter를 통해 지원합니다. ShallowEtagHeaderFilter

 

그렇기에 다른 프레임워크에서도 재사용 가능합니다. Spring에서는 기본적으로 MD5 알고리즘을 이용해 응답의 Body를 byteStream으로 가져와 해싱하여, ETag 값으로 제공합니다.

protected String generateETagHeaderValue(InputStream inputStream, boolean isWeak) throws IOException {
        // length of W/ + " + 0 + 32bits md5 hash + "
        StringBuilder builder = new StringBuilder(37);
        if (isWeak) {
            builder.append("W/");
        }
        builder.append("\"0");
        DigestUtils.appendMd5DigestAsHex(inputStream, builder);
        builder.append('"');
        return builder.toString();
    }

 

 

 

Etag를 위한 Filter 추가하기


@Configuration
public class ETagHeaderFilter {

    @Bean
    public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
        FilterRegistrationBean<ShallowEtagHeaderFilter> filterRegistrationBean    = new FilterRegistrationBean<>( new ShallowEtagHeaderFilter());
        filterRegistrationBean.addUrlPatterns("/api/notices");
        return filterRegistrationBean;
    }
}

@Bean 메서드는 스프링 컨테이너에 의해 관리되는 Configuration Proxy Bean을 통해 호출되는 일종의 펙토리 메서드입니다. 일반적으로 위와 같이 FilterRegistrationBean과 같은 특정 객체에 값과 설정을 제공하고, 그 결과 해당 Bean 혹은 빌더의 결과를 전달하게 됩니다.

 

 

 

ETag가 설정 되었는지 확인해보기


이후 사용될 MockMvc를 언급하자면, 이 객체는 MVC 테스트에 필요한 객체로 실제 웹 애플리케이션을 구동시키지 않고 클라이언트의 역할을 하여 MockHttpServletRequest, Response를 보내고 받아 검증할 수 있습니다.

 

이를 통해 작성된 테스트 코드를 통해 동작 여부를 확인할 수 있습니다.

 

 

기본적인 작성 방식은

mockMvc.perform({MockHttpServletRequestBuilder.get, delete, post...}({URI 정보})
                .content({요청 바디에 담을 정보})
                .contentType({요청 바디의 타입}))
                .andDo(print()) // 요청, 응답 결과를 콘솔에 출력
                .andExpect(status().isCreated()) // Http 상태, 바디 값, 헤더 등 존재 여부 등 응답 결과를 검증
        ; //.andExpect(jsonPath()); JSON 데이터 검증

이렇게 됩니다.

 

 

ETag FIlter가 적용된 곳에 요청 보내기

String etag = mockMvc.perform(get("/api/notices")
                .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(header().exists("ETag"))
                .andReturn().getResponse().getHeader("Etag");
MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json", ETag:""0e3344bf1e63fc52fbe7ce711a2f5014f"", Content-Length:"295"]
     Content type = application/json
             Body = [{"id":2,"author":"author","title":"title","content":"content","createDate":"2021-03-07T17:27:19.504702","modifyDate":"2021-03-07T17:27:19.504702"},{"id":1,"author":"author","title":"title","content":"content","createDate":"2021-03-07T17:27:19.423703","modifyDate":"2021-03-07T17:27:19.423703"}]
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

 

최초 다음 요청부터 ETag 정보를 추가하여 전송하기

mockMvc.perform(get("/api/notices")
                .header("If-None-Match", etag)
                .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isNotModified());
MockHttpServletResponse:
           Status = 304
    Error message = null
          Headers = [Content-Type:"application/json", ETag:""0e3344bf1e63fc52fbe7ce711a2f5014f""]
     Content type = application/json
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

 

서버의 리소스를 업데이트 한 뒤 요청을 한다면?

mockMvc.perform(post("/api/notices")
              .content(objectMapper.writeValueAsString(createNoticeDto))
              .contentType(MediaType.APPLICATION_JSON))
              .andDo(print())
              .andExpect(status().isCreated());

// 이전 요청에서 보낸 ETag 요청을 전송
mockMvc.perform(get("/api/notices")
              .header("If-None-Match", etag)
              .contentType(MediaType.APPLICATION_JSON))
              .andDo(print())
              .andExpect(status().isOk());
MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json", ETag:""0649d2b6caf906a380376fe9258f0a7ae"", Content-Length:"442"]
     Content type = application/json
             Body = [{"id":3,"author":"author","title":"title","content":"content","createDate":"2021-03-07T17:27:19.557702","modifyDate":"2021-03-07T17:27:19.557702"},{"id":2,"author":"author","title":"title","content":"content","createDate":"2021-03-07T17:27:19.504702","modifyDate":"2021-03-07T17:27:19.504702"},{"id":1,"author":"author","title":"title","content":"content","createDate":"2021-03-07T17:27:19.423703","modifyDate":"2021-03-07T17:27:19.423703"}]
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

 

 

응답 정보에 URI를 포함하기


build.gradle

implementation 'org.springframework.boot:spring-boot-starter-hateoas'

 

 

Notice Controller의 create 변경하기

@PostMapping("/notices")
public ResponseEntity<NoticeInfo> createNotice(@Valid @RequestBody CreateNoticeDto noticeDto) {
        int result = noticeService.save(noticeDto);
        return ResponseEntity.created(linkTo(NoticeController.class).slash("notices").slash(result).toUri())
                .body(new NoticeInfo(result, "notice created"));
}

----------

mockMvc.perform(post("/api/notices")
                .content(objectMapper.writeValueAsString(createNoticeDto))
                .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isCreated());
MockHttpServletResponse:
           Status = 201
    Error message = null
          Headers = [Location:"http://localhost/api/notices/1", Content-Type:"application/json", Content-Length:"41"]
     Content type = application/json
             Body = {"message":"notice created","noticeId":1}
    Forwarded URL = null
   Redirected URL = http://localhost/api/notices/1
          Cookies = []

 

 

Notice Controller의 update 변경하기

@PutMapping("/notices/{noticeId}")
public ResponseEntity updateById(@Valid @RequestBody UpdateNoticeDto noticeDto,
                                             @PathVariable Long noticeId) {

        noticeService.updateById(noticeDto, noticeId);
        return ResponseEntity.ok(linkTo(NoticeController.class).slash("notices").slash(noticeId).toUri());
}

----------

mockMvc.perform(put("/api/notices/{noticeId}", 1L)
                .content(objectMapper.writeValueAsString(updateNoticeDto))
                .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isOk());
MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = "http://localhost/api/notices/1"
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

 

Notice Controller의 delete 변경하기

@DeleteMapping("/notices/{noticeId}")
public ResponseEntity deleteById(@PathVariable Long noticeId) {

        noticeService.deleteById(noticeId);
        return ResponseEntity.ok(linkTo(NoticeController.class).slash("notices").toUri());
}

----------

mockMvc.perform(delete("/api/notices/{noticeId}", 1L)
                .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isOk());
MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = "http://localhost/api/notices"
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

 

참고할 링크입니다.

 

www.baeldung.com/etags-for-rest-with-spring

 

ETags for REST with Spring | Baeldung

ETags with the Spring - ShallowEtagHeaderFilter, integration testing of the REST API, and consumption scenarios with curl.

www.baeldung.com

docs.spring.io/spring-framework/docs/3.0.0.RC1/reference/html/ch15s11.html

 

15.11 ETag support

An ETag (entity tag) is an HTTP response header returned by an HTTP/1.1 compliant web server used to determine change in content at a given URL. It can be considered to be the more sophisticated successor to the Last-Modified header. When a server returns

docs.spring.io

 

 

해당 글은 MapStruct Library를 실무에서 사용하기 이전에 학습했던 예제와 장, 단점을 옮겨온 글입니다.

 

(2022-10-26 수정 Benchmark 게시물 링크 추가)

현재 저는 약간의 수고로움을 감수하며 Java Code 기반의 Mapping을 사용하고 있으며, 최대한 Model의 단위를 작게 유지하고 있습니다.

 

최근 어떤 분께서 ModelMapper가 MapStruct에 비해 그렇게 많이 느리냐는 질문을 보내주셔서, BenchMark 자료 링크를 추가하였는데요. 적은 데이터를 처리하는 경우에는 별 차이가 없지만 처리량이 많아질수록 유의미한 차이가 발생함을 확인할 수 있습니다.

 

 

자바 코드로 매핑하기

어떠한 라이브러리를 사용하지 않고 직접 객체 상태 간의 매핑 로직을 구현하는 방식은 약간의 수고스러움은 있으나 ModelMapper와 같이 Reflection 기반의 라이브러리보다 안전하다. 

 


entity, dto

@ToString
@Getter
@NoArgsConstructor
public class SampleEntity {

    private Long id;
    private String name;
    private String email;
    private Long age;
    private List<String> sampleInfo;
    private String value;

    @Builder
    public SampleEntity (Long id, String name, String email, Long age, List<String> samples, String value) {
    	this.id = id;
        this.name = name;
        this.email = email;
        this.age = age;
        this.sampleInfo = samples;
        this.value = value;
    }
}

@ToString
@Getter
@NoArgsConstructor
public class SampleDto {

    private final String name;
    private final String email;
    private final List<String> infos;
    private final Long age;

    @Builder
    public SampleDto(String name, String email, List<String> sampleinfo, Long age) {
    	this.name = name;
        this.email = email;
        this.infos = sampleinfo;
        this.age = age;
    }

    public SampleEntity toEntity() {
        return SampleEntity.builder()
                .name("lob")
                .email("...@test")
                .age(20L)
                .value("value")
                .sampleInfo(new ArrayList<>(Collections.singleton("aaa")))
                .build();
    }

    public static SampleDto toDto(SampleEntity entity) {
        return SampleDto.builder()
                .name(entity.getName())
                .email(entity.getEmail())
                .infos(entity.getSampleInfo())
                .age(entity.getAge())
                .build();
    }
}

기존 코드 방식은 모든 필드에 대해서 일일이 매핑을 진행하여야 한다.

public void dtoToEntity() {
       SampleDto dto = SampleDto.builder()
                .name("dto")
                .email("...@hello")
                .age(20L)
                .infos(new ArrayList<>(Collections.singleton("aaa")))
                .build();

        SampleEntity entity = dto.toEntity();
        System.out.println(entity);
}

public void EntityToDto() {
        SampleEntity entity = SampleEntity.builder()
                .id(1L)
                .name("lob")
                .email("...@test")
                .age(20L)
                .value("value")
                .sampleInfo(new ArrayList<>(Collections.singleton("aaa")))
                .build();

        SampleDto dto = SampleDto.toDto(entity);
        System.out.println(dto);
}
SampleEntity(id=null, name=lob, email=...@test, age=20, sampleInfo=[aaa], value=value)
SampleDto(name=lob, email=...@test, infos=[aaa], age=20)


해당 방식의 장, 단점

장점

  • 객체 변환을 위한 별도의 과정을 거치지 않고 메서드 호출만 하기 때문에 성능에 대한 영향이 없다.
  • 이름이 다른 필드 간의 매핑도 그저 Getter 등을 작성하여 올바르게 조합하기만 하면 된다.
  • 매핑하는 필드 타입이 다른 경우에 컴파일 타임에 이를 식별할 수 있다. 

단점

  • 객체의 필드 명 변경이나 추가 시 매핑하는 코드 부분도 같이 수정하여야 한다. (변경 지점이 늘어날 수 있다.)
  • 필드가 너무 많거나 조합하는 형태의 데이터가 많다면 흔히 말하는 휴먼 에러가 발생할 수 있다. (다른 필드와의 매핑이나 데이터 누락 등)

 

 

 

MapStruct


간결한 객체 간의 변환을 위해 사용되는 라이브러리이다. 컴파일 시점에 매핑 정보를 생성하고 이를 사용하여 객체를 매핑하기에 보일러 플레이트 코드를 제거하고 깔끔한 코드를 유지하게 된다.

Annotation processor를 이용해 메서드 인자와 반환할 값이 될 객체에 필요한 메서드를 호출(builder, getter)하여 자동으로 객체 간의 매핑을 제공한다.  


기능 지원

  • 기본 값과 상수에 대한 매핑을 지원한다.
  • 다른 필드 타입을 변환하여 매핑하는 기능을 제공한다.
  • @Mapping 어노테이션의 expression에서 문자열로 표현식을 줄 수 있다. 여러 개의 속성을 하나의 필드에 매핑할 수도 있다.
  • @InheritConfiguration을 사용하여 매핑 구성 정보를 상속할 수 있다.
  • 역방향 매핑 상속의 경우 @InheritInverseConfiguration을 통해 쉽게 반전시킬 수 있다.

build.gradle

// JDK 11 기준
implementation "org.mapstruct:mapstruct:1.3.0.Final"
annotationProcessor "org.mapstruct:mapstruct-processor:1.3.0.Final"

entity와 dto 코드는 동일하다.

interface

@Mapper
public interface DataMapper {
        DataMapper INSTANCE = Mappers.getMapper(DataMapper.class);

        @Mapping(source = "entity.sampleInfo", target = "infos")
        SampleDto toDto(SampleEntity entity);

        @Mapping(source = "dto.infos", target = "sampleInfo")
        SampleEntity toEntity(SampleDto dto);
}


example

DataMapper dataMapper = DataMapper.INSTANCE;

public void dtoToEntity() {
        SampleDto dto = SampleDto.builder()
                .name("dto")
                .email("...@hello")
                .age(20L)
                .infos(new ArrayList<String>(Collections.singleton("aaa")))
                .build();

        SampleEntity entity = dataMapper.toEntity(dto);
        System.out.println(entity);
    }

public void EntityToDto() {
        SampleEntity entity = SampleEntity.builder()
            .id(1L)
            .name("lob")
            .email("...@test")
            .age(20L)
            .value("value")
            .sampleInfo(new ArrayList<String>(Collections.singleton("aaa")))
            .build();

        SampleDto dto = dataMapper.toDto(entity);
        System.out.println(dto);
}
SampleEntity(id=null, name=dto, email=...@hello, age=20, sampleInfo=[aaa], value=null)
SampleDto(name=lob, email=...@test, infos=[aaa], age=20)


생성된 interface impl

public class MapStructExample$DataMapperImpl implements DataMapper {
    public MapStructExample$DataMapperImpl() {
    }

    public SampleDto toDto(SampleEntity entity) {
        if (entity == null) {
            return null;
        } else {
            SampleDtoBuilder sampleDto = SampleDto.builder();
            List<String> list = entity.getSampleInfo();
            if (list != null) {
                sampleDto.infos(new ArrayList(list));
            }

            sampleDto.name(entity.getName());
            sampleDto.email(entity.getEmail());
            sampleDto.age(entity.getAge());
            return sampleDto.build();
        }
    }

    public SampleEntity toEntity(SampleDto dto) {
        if (dto == null) {
            return null;
        } else {
            SampleEntityBuilder sampleEntity = SampleEntity.builder();
            List<String> list = dto.getInfos();
            if (list != null) {
                sampleEntity.sampleInfo(new ArrayList(list));
            }

            sampleEntity.name(dto.getName());
            sampleEntity.email(dto.getEmail());
            sampleEntity.age(dto.getAge());
            return sampleEntity.build();
        }
    }
}


해당 방식의 장, 단점

장점

  • 간결한 코드 작성이 가능하다.
  • 객체 필드의 변경 사항이 다른 로직에 영향을 주지 않는다.
  • 컴파일 시점에 코드를 생성하면서 타입이나 매핑이 불가능한 상태 등의 문제가 발생한 경우 컴파일 에러를 발생시킨다. 이는 상대적으로 런타임에서 안전성을 보장한다.
  • 앞서 보았던 자바 코드 매핑 방식과 같은 수준의 성능을 가진다.

단점

  • 전혀 다른 형태의 필드 매핑을 시도하는 경우 제공되는 기능으로 해결 가능한 경우가 많으나, Mapping 로직이 매우 복잡해진다.
  • 변경 불가능한 필드에 대한 매핑을 제공하지 못한다. (final 필드 - Constructor 주입)
  • Lombok Library와 충돌이 발생할 수 있다. (실제로는 Lombok annotation processor가 getter나 builder 등을 만들기 전에 mapstruct annotation processor가 동작하여 매핑할 수 있는 방법을 찾지 못해 발생하는 문제이다. )

 

 

 

ModelMapper


MapStruct와 같이 객체 간의 변환을 위해 사용되는 라이브러리이며, MapStruct와 다른 점은 런타임 시점에 Reflection API를 사용하여 객체를 매핑한다는 것이다.

build.gradle

implementation group: 'org.modelmapper', name: 'modelmapper', version: '2.3.9'

entity와 dto 코드는 동일하다.

example

ModelMapper modelMapper = new ModelMapper();

public void dtoToEntity() {
        SampleDto dto = SampleDto.builder()
                .name("dto")
                .email("...@hello")
                .age(20L)
                .infos(new ArrayList<>(Collections.singleton("aaa")))
                .build();

        SampleEntity entity = modelMapper.map(dto, SampleEntity.class);
        System.out.println(entity);
}

public void EntityToDto() {
        SampleEntity entity = SampleEntity.builder()
                .id(1L)
                .name("lob")
                .email("...@test")
                .age(20L)
                .value("value")
                .sampleInfo(new ArrayList<>(Collections.singleton("aaa")))
                .build();

        // 이름이 다른 필드 매핑을 위해 PropertyMap 선언
        PropertyMap<SampleEntity, SampleDto> sampleMap = new PropertyMap<>() {
            @Override
            protected void configure() {
                map().setInfos(source.getSampleInfo());
            }
        };

        // ModelMapper 구성 정보에 PropertyMap 추가
        modelMapper.addMappings(sampleMap);

        // createTypeMap() 을 사용할 수도 있다.

        //modelMapper.createTypeMap(SampleEntity.class, SampleDto.class)
        //        .addMapping(SampleEntity::getSampleInfo, SampleDto::setInfos);

        SampleDto dto = modelMapper.map(entity, SampleDto.class);
        System.out.println(dto);
}

--------
SampleEntity(id=null, name=dto, email=...@hello, age=20, sampleInfo=null, value=null)
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access using Lookup on org.modelmapper.internal.ProxyFactory (file:/C:/Users/serrl/.gradle/caches/modules-2/files-2.1/org.modelmapper/modelmapper/2.3.9/8bb9110f8df3fbd6c1c2e4b69f7c6add737888e7/modelmapper-2.3.9.jar) to interface java.util.List
WARNING: Please consider reporting this to the maintainers of org.modelmapper.internal.ProxyFactory
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
SampleDto(name=lob, email=...@test, infos=[aaa], age=20)


해당 방식의 장, 단점

장점

  • 간결한 코드 작성이 가능하다.
  • 일반적으로 필드 변경 사항에 대해서 고려하지 않아도 된다.
  • Lombok 라이브러리와 충돌없이 같이 사용할 수 있다. (런타임에서 객체를 분석하고 매핑하기 때문에)

단점

  • (처음 학습하시는 분들의 오해를 일으킬 수 있어보여서 수정합니다. ㅎㅎ)
    • 컴파일 시점에 생성된 코드를 기반으로 최적화하여 동작하는 위의 방식들과 달리 해당 방식은컴파일러에 의해 최적화 되지 않으며, 최초 작업 시 캐싱되지 않고(이후 작업에는 만들어진 Map을 재사용합니다.), 매칭 및 매핑 로직에서 Refliection API를 사용해 객체 필드 정보를 추출하고 Map을 만든 다음 들어온 인자와 매칭시켜주고 Map의 정보를 기준으로 값을 매핑해주는 방식이기에 다른 방식보다 오버헤드가 많아 상대적으로 성능이 좋지 않은 것이다.
      • Reflection API를 사용한다는 이유만으로 느린 것이 아니다. 
  • 바이트코드 생성 방식을 이용하기에 문제 원인 발견과 디버깅이 어렵다. 특정 필드의 변경으로 인한 매핑 누락 문제가 발생하였을 때 이를 인지하기 힘들다.
  • 일반적으로 setter를 사용한다. (개인적으로 Setter를 통해 모든 필드를 열어놓는 행위를 싫어한다.)
    • 불변 객체나 Setter를 사용하지 않기 위해 설정하는 방법으로는 별도로 TypeMap과 provider를 정의하고 내부에서 생성자를 이용하는 방법, Converter를 구현하는 방법 (생성자를 로직에 활용하는 것은 Provider와 동일), ModelMapper의 필드 접근 수준을 private로 설정하는 방법이 있다.
      // 1
      modelMapper.createTypeMap(SampleEntity.class, SampleDto.class)
      		.setProvider( new Provider<SampleDto>() { 
              		public SampleDto get(ProvisionRequest<SampleDto> request) { 
                      		Source source = Source.class.cast(request.getSource()); 
                              return new SampleDto(source.name, source.address); 
                      } 
              });
           
           
      // 2
      Converter<SampleEntity, SampleDto> SampleConverter = () {
          @Override
          public SampleDto convert(MappingContext<SampleEntity, SampleDto> context) {
          	SampleEntity entity = context.getSource();
              return new SampleDto(entity.getName(), entity.getAddress());
          }
      };
              
              
      // 3
      ModelMapper modelMapper = new ModelMapper();
      modelMapper.getConfiguration()
              .setFieldMatchingEnabled(true)
              .setFieldAccessLevel(Configuration.AccessLevel.PRIVATE);

 

 

벤치 마크 자료

 

 

참고 자료



 

Spring DI?

Spring DI는 스프링이 내부에 있는 Bean 간의 관계를 관리, 주입할 때 사용하는 기법이다.

 

스프링 컨테이너를 통해 관리되고 있는 POJO들을 비즈니스 코드에서 필요한 시점에 선택적으로 주입받을 수 있으며 이후 관리는 모두 컨테이너가 담당한다. Singleton Scope Bean

 

DI를 통해 객체 외부에서 생성된 객체(Bean)를 Interface를 통하여 넘겨받게 되므로, 객체 간의 결합도를 낮출 수 있고, 런타임 시에 의존관계가 결정되기 때문에 유연한 구조를 가질 수 있다. 이를 통해 진정한 의미의 컴포넌트화를 가능케한다.

 

 

 

DI Annotations

 

 

 

@Autowired (AutowiredAnnotationBeanPostProcessor)

우선 빈의 타입을 기준으로 찾아서 주입하는 방식 (타입 → 이름 → @Qualifier)

 

 

사용할 수 있는 위치

  • 생성자
  • 세터
  • 필드

 

DI시 예외가 발생할 수 있는 경우

  • (컨테이너 안에) 해당 타입의 빈이 없는 경우 (NoSuchBean)
  • (컨테이너 안에) 해당 타입의 빈이 한 개인 경우 (정상 작동)
  • (컨테이너 안에) 해당 타입의 빈이 여러 개인 경우

 

해당 타입의 빈이 여러 개인 경우 (모든 경우)

 

1. @Primary의 유무를 확인한 후 주입한다. (우선순위 부여)

@Service 
@Primary 
public class RealService implements XxxService {

 

2. @Order 기준으로 확인한 후 주입한다. (빈들의 우선순위 설정)

@Service 
@Order(1) 
public class RealService implements XxxService {

 

3. Bean의 이름(빈의 ID = Class 명의 Small case)을 기준으로 주입한다.

 

4. @Qualifier 가 설정되어 있는지를 기준으로 확인한 후 주입한다.

@Qualifier("realService")


public RealController (@Qualifier("realService") RealService realService) { 
	this.realService = realService; 
}

 

 

5. 혹은 모든 빈을 주입받기. (Collection 사용)

@Autowired 
private List<RealService> realService;

 

6. 필드명을 작성해서 넣기

@Service
public class RealService implements XxxService{

-------------------------------------------

    @Autowired
    RealService realService;

 

 

 

 

@Resource (CommonAnnotationBeanPostProcessor)

우선 빈의 ID (이름) 값을 기준으로 주입하는 방식 @Resource(name="beanName")

 

 

사용할 수 있는 위치

  • 필드
  • 세터

 

해당 타입의 빈이 여러 개인 경우

  1. @Primary
  2. @Order
  3. Bean Type
  4. @Qualifier

 

 

 

@Inject (JSR-330's : AutowiredAnnotationBeanPostProcessor)

Autowired처럼 빈 타입을 기준으로 찾아서 주입하는 방식 (타입 → @Qualifier → 이름)

 

 

사용할 수 있는 위치

  • 필드
  • 세터
  • 생성자
  • 메서

 

해당 타입의 빈이 여러 개인 경우

  1. @Primary
  2. @Order
  3. @Qualifier
  4. Bean 이름

 

 

Spring DI 방식

 

Constructor Injection

Bean을 등록하기 위한 Component Scan 중에 해당 Class 정보를 확인하게 된다.

 

Spring 4.2까지는 Autowired를 붙여야 한다.

 

 

해당 방식의 특징

  • 객체 생성 시 호출되는 생성자를 통해 1번만 주입되는 것을 보장한다.
  • 불변적이며, 우선적으로 주입된다.
  • Spring에서는 필수적인 의존성 주입에 대하여서 생성자 주입을 권장한다.
@Service 
public class UserService { 

    private final UserRepository userRepository; 
    
    // 해당 클래스가 다른 생성자를 가지지 않는다면 생략할 수 있다. Spring 4.3+ 
    // 그리고 추가적으로 Configuration 클래스에도 생성자 주입을 할 수 있게 되었다. 
    @Autowired 
    public UserAccountService(UserRepository userRepository) { 
    	this.userRepository = userRepository; 
    } 
    
    // Logic... 
    
}

 

해당 방식의 장점

  • 단위 테스트가 용이하다.
  • Bean을 생성하는 동안 다른 Bean을 초기화할 수 있다. 별도의 값을 저장한다던지..
  • 클래스 레벨에서 불변한 객체 주입이 가능하다.만약에 주입 대상이 되는 Bean들이 없거나, 오류로 인해 주입되지 않을 경우 Checked Exception을 통하여 알아차릴 수 있다. = NPE를 방지한다.
  • final Keyword와 생성 시점의 주입을 통해 갑작스러운 변경에 의한 문제를 해결하게 된다.
  • 쉽게 불변 객체를 만들어낼 수 있다.
  • 안정적인 상태를 유지할 수 있다. → 모든 의존성이 주입되거나 인스턴스화를 실패한다.
  • Reflection API를 사용하지 않는다. 주입 시에 Spring 상에서 최소의 자원을 사용한다.
  • 생성자를 사용하여 인스턴스를 생성하는 것이 OOP 관점에서 자연스럽다.

 

 

해당 방식의 단점

  • 해당 Bean이 선택적인 종속성을 가져야 하는 경우 가변적인 상태
  • Setter Injection을 사용하자. 생성자 주입과 설정자 주입은 결합이 가능하다.

 

 

이것들은 단점일까?

  • 해당 Bean이 적은 가짓수의 의존성을 가질 경우
  • 해당 Bean의 필수 의존성이 적을 경우 생성자 주입 코드는 "장황해 보일 수도" 있다.
  • 해당 Bean이 아주 많은 가짓수의 의존성을 가질 경우 Ugly Constructor
  • 종속성이 추가될 때마다 생성자의 매개 변수에 추가해야 하므로 생성자를 통해 과도한 결합을 노출하게 된다.

 

추악한 생성자? ( Ugly Constructor? )

클래스가 수많은 필드(의존성)를 가지고 작성되어 있다면, 생성자도 복잡해질 수밖에 없다.

// 점층적 생성자 패턴
public class Employee {

        // 필수적인 속성, 의존성
        private final String firstname;
        private final String lastname;

        // 선택적인 속성, 의존성
        private String email;
        private String address;
        private String phone;

        // Default Constructor 는 필수적인 의존성(final) 을 포함하여야한다. 
        public Employee(String firstname, String lastname) {
            this.firstname = firstname;
            this.lastname = lastname;
        }

        public Employee(String email, String address, String phone) {
            this.email = email;
            this.address = address;
            this.phone = phone;
        }

        public Employee(String firstname, String lastname, String email, String address, String phone) {
            this.firstname = firstname;
            this.lastname = lastname;
            this.email = email;
            this.address = address;
            this.phone = phone;
        }
}

사실 가정하는 상황에서는 이것보다 더 많은 인자를 가지고 있을 것이다. 그럴 경우에는 생성자가 더욱 복잡해질 것이고, 객체의 코드를 작성하거나 파악하는 것에 어려움을 겪을 것이다.

 

Spring의 경우에는 수많은 Service 혹은 Repository의 생성자 주입을 말할 수 있다.

 

사실 이러한 생성자가 만들어진다는 것은 그 객체의 책임이 혼재되어 있거나 과중한 책임을 지니고 있음을 의미하는 것이다.

 

그렇기에 이것은 생성자 주입의 문제로만 치부하기보다는 설계의 문제임을 생각하는 것이 좋다고 생각한다.

 

 

Setter Injection

생성된 Bean 정보들 중에서 @Autowired가 붙은 Setter를 찾아 호출하며 Bean을 주입한다.

 

Method Injection도 비슷하게 동작한다.

 

 

해당 방식의 특징

  1. 자바 빈 규약(setter)을 이용한 의존성 주입 방식이다.
  2. Spring에서는 변경 가능성 있는, 선택적인 종속성에 대해서 설정자 주입을 권장한다.
@Service
public class UserService {

        private final UserRepository userRepository;

        // 특별한 요청에 경우 필요하다고 가정. 
        private GuestRepository guestRepository;

    public UserAccountService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

        @Autowired
        public void setGuestRepository(GuestRepository guestRepository) {
                this.guestRepository = guestRepository;
        }

        // Logic...

}

 

해당 방식의 장점

  • 선택적인 종속성에 대하여서 별도의 설정자를 만들고 유연한 제공이 가능하다.

 

해당 방식의 단점

  • 의존성이 주입되는 시점이 많아질 수 있기에 가독성이 떨어지게 되고, 상대적으로 유지보수 능력을 떨어트린다.
  • 설정자가 호출될 때마다 객체가 새로 주입됨으로 참조에 대한 불변을 유지할 수 없다.
  • Null이 주입될 수 있고, 새로운 객체의 반환으로 기존 작업 값을 잃을 수 있다.
  • 설정자를 통해 클래스 내부의 의존성 부분을 노출함으로써 캡슐화를 위반하게 된다.
  • 객체 생성 시점에는 순환 종속성이 발견되지 않으나, 비즈니스 로직이 진행되는 도중에 해당 문제가 발생할 수 있다.

 

 

Field Injection

필드 주입을 위해 Reflection API를 이용하여 정보를 찾고 접근 제어자 등을 조작한다.

@Service
public class UserService {

        // 간단한 주입방식!
        @Autowired private UserRepository userRepository;


        // Logic...

}

 

해당 방식의 장점

  • 모든 주입 방식 중 가독성 측면에서 가장 간결하게 사용할 수 있다.생성자와 수정자 코드 등
  • 필드 주입을 선택하는 이유 중 하나는 보일러 플레이트 코드를 작성하지 않기 위함이다.
  • 많은 필드를 갖는 것이 불가피하고 추악한 생성자가 문제가 되는 상황에서는 필드 주입을 사용하는 것이 "나을 수도" 있다. 별도의 Null 체크가 필요하고.. 이건 설계 문제다!
  • 그런 상황에서 필드 주입을 선택하는 것이 최선인지는 의문이 든다.

사용할 것이라면..

  • Application 코드와 관계없는 테스트 코드에 사용 (Mock 객체 주입 등)
  • 스프링 설정을 목적으로 하는 @Configuration 클래스에서만 사용

 

해당 방식의 단점

  • Private 한 필드에 의존성을 주입할 수 있는 유일한 방법은 스프링 컨테이너가 클래스를 인스턴스 화하고 Reflection API를 사용하여 주입하는 것이다.
  • 이러한 주입 방식은 생성자, 설정자 주입 방식보다 많은 자원을 소모한다.
  • Spring의 DI에 의존적인 코드를 작성하게 된다. 순수한 자바 코드가 아니다.
  • 의존성 주입을 실패하게 된 경우 Exception이 터지지 않고 참조가 연결되지 않기에 비즈니스 로직 시에 문제가 발견될 수 있다
  • Bean을 찾지 못하였거나, 여러 Bean이 존재하고, Primary, Qualifier, Order 등이 설정되지 않은 경우

 

 

 

생성자 주입은 필드, 메서드 방식보다 적게 Reflection을 사용한다.

bean을 생성할 때, 다른 bean 정보를 가져오는 Dependency Lookup까지만 사용한다.

 

생성자 주입이 Reflection을 사용하지 않는다는 것은 Bean 생성 시에 주입하기에 다른 방식과 달리 추가적인 비용이 없음을 의미한다.

 

생성자 주입의 실행 시점 Stack trace

AbstractApplicationContext.finishBeanFactoryInitialization() 
→ defaultListableBeanFactory.preInstantiateSingletons() →

AbstractBeanFactory.getBean() → AbstractBeanFactory.doGetBean() →

defaultSingletonBeanRegistry.getSingleton() →

AbstractBeanFactory.getObject() → this.createBean(postController, bean definition) →

AbstractAutowireCapableBeanFactory.createBean() → this.doCreateBean() → 
this.createBeanInstance() → this.autowireConstructor() →

ConstructorResolver.autowireConstructor() →
SimpleInstantiationStrategy.instantiate() →
BeanUtils.instantiateClass() →
Constructor.newInstance() →
DelegatingConstructorAccessorImpl.newInstance() →

NativeConstructorAccessorImpl.newInstance() → (native) this.newInstance0() → 
Bytecode Generation

Constructor와 관련된 Reflection을 제외하고는 다른 Reflection이 보이지 않는다.

 

Field, Method Injection은 Bean이 생성된 이후 AutowiredAnnotationBeanPostProcessor의 processInjection() 메서드를 통해서 주입하게 된다.

public void processInjection(Object bean) throws BeansException {
    Class<?> clazz = bean.getClass();
    InjectionMetadata metadata = findAutowiringMetadata(clazz);
    try {
        metadata.inject(bean, null, null);
    }
    catch (Throwable ex) {
        throw new BeanCreationException("Injection of autowired dependencies failed for class [" + clazz + "]", ex);
    }
}

protected void inject(Object target, String requestingBeanName, PropertyValues pvs) throws Throwable {
    if (this.isField) {
        Field field = (Field) this.member;
        ReflectionUtils.makeAccessible(field);
        field.set(target, getResourceToInject(target, requestingBeanName));
    }
    else {
        if (checkPropertySkipping(pvs)) {
            return;
        }
        try {
            Method method = (Method) this.member;
            ReflectionUtils.makeAccessible(method);
            method.invoke(target, getResourceToInject(target, requestingBeanName));
        }
        catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
    }
}

 

생성자 주입이 순환 참조를 찾을 수 있는 이유?

생성자 주입은 Field, Method 방식과 동작하는 시점이 다르다.

 

이는 Bean이 Container에 등록된 시점에서 다른 Bean을 생성하고 주입하기 때문이다.

 

생성자 주입은 자바 코드 그대로 해당 객체를 생성하는데 필수적인 요소를 지정하는 것이다. 그렇기에 A가 필수적인 인자로 B를 가지고 동일하게 B가 A를 가진다면 당연히 Bean 생성 그 자체가 되지 않는 것이다.

 

스프링은 이를 확인하고 순환 참조임을 알려준다.

 

생성자 이외의 유형의 주입을 사용하는 경우 종속성이 필요할 때 주입되기에. 생성자 주입이 진행되는 컨텍스트로드 시점이 아니므로 순환 참조가 발견되지 않는다. 결국 Autowired는 우선 Dependency LookUp을 application context에 존재하는 빈 정보를 가져오고 Injection 하는 것이기에 선택적인 인자 주입이 가능하다.

 

 

잘못된 내용은 댓글 부탁드립니다.

Oauth를 사용하는 이유?

사용자의 권한을 인증, 인가하기 위하여 ID와 비밀번호의 직접적인 전달을 하는 것은 탈취될 가능성이 존재한다. 이를 방지하고 안전하게 필요한 정보를 넘겨받기 위해 사용하게 된다.

 

Oauth는 권한 위임을 위해 사용되는 프로토콜이며, 직접적으로 인가를 수행하기보단 요청할 수 있는 흐름과 수단을 제공한다.

 

웹 또는 모바일에서 개인 정보에 대한 액세스 권한을 요청받은 경우 해당 프로토콜을 사용하였을 것이다.

 

 

 

Oauth의 장점

  • HTTP / HTTPS를 이용해 동작하는 프로토콜이기에 범용적으로 사용할 수 있다. 
  • 단순하게 구현할 수 있고, 많은 양의 레퍼런스를 가진다.
  • 스펙이 강제하는 부분이 적고, 선택적으로 제공하거나 자유함으로 필요에 맞게 변경할 수 있는 부분이 많다.

 

 

Oauth의 단점

  • Oauth 스펙은 대부분의 요소 포맷 등이 명확히 정의되어있지 않고 확장 가능하기 때문에, 설계의 유연함을 가지지만 반대로 여러 Oauth 서비스가 크게 다른 구현부, 확장 점을 가짐으로써 각 서비스가 호환이 되지 않을 수 있다.
  • 스펙상 토큰의 수명 주기와 범위가 지정되어 있지 않기에 토큰이 만료되지 않을 수도 있다. 구현을 통해 해결할 수 있다.
  • 서비스에 필요한 사용자 정보를 가져오기 위해 추가적인 요청이 있을 수 있다. JWT로 대체 가능하다. 
  • 스펙상 SSL과 TLS를 권장하나, 강제하지 않고 설정하지 않는한 일반적인 HTTP 프로토콜을 사용하기에 통신 도청과 토큰 탈취 등이 쉽게 일어날 수 있다.

 

 

Oauth의 취약점

CSRF

Access Token 생성시 state 매개변수를 이용함으로써 방지할 수 있다. 

 

해당 변수 검증 누락이나 미흡시 토큰을 탈취당할 수 있다. 

 

 

Convert Redirect 

redirect_uri 파라미터에 대한 검증 누락이나 미흡시 발생하는 취약점이다. 

 

변조된 URI를 통해 공격받을 수 있다.

 

Full Path 검증 등 강력한 방법을 사용하여야 한다.

 

 

 

Oauth의 구성 주체

Resource Owner (User, Device)

Resource Server에서 제공하는 기능을 이용하려고 하는 주체이며, 클라이언트에게 API 접근 권한을 위임할 수 있다. 이는 인증 정보 전달과 권한 위임 절차 수행을 통해 이루어진다.

 

별도의 소프트웨어 구성 요소가 아니라 서비스(리소스 서버의) 사용자를 의미한다.

 

ID와 password등을 이용해 Resource Client에 접근 권한을 인가한다. (액세스 토큰)

 

 

Resource Client (Somaeja)

리소스의 소유자를 대신하여 인가 서버를 통해 발급된 액세스 토큰을 가지고 Resource Server의 API와 상호작용을 하는 주체이다.

 

Resource Owner로부터 권한 인가를 받아 액세스 토큰을 획득하고 API 서버에게 요청한다.

 

 

Resource Server (Google, Facebook)

보호된 Resource를 관리하며 Client에게 Resource와 관련된 API를 제공한다.

 

전달받은 액세스 토큰이 유효한지 확인하기 위해 매번 Authorization Server와 통신한다.

 

이러한 방식은 비효율적인 구조를 가지기에 인가 내역을 캐싱하거나 JWT등을 이용한다.

 

 

Authorization Server (Facebook-auth Server, Google-auth Server)

인가 서버는 리소스 소유자와 클라이언트의 정보를 인증하고, 권한을 위임할 수 있는 메커니즘을 제공한다.

 

Resource Server가 올바른 액세스 토큰을 받았는지 검증 요청하는 것을 처리하고, 만료된 액세스 토큰을 폐기하기도 한다. 추가적으로 Resource Client의 요청을 제어하기 위해 해당 권한들을 제어할 수 있는 Client ID와 Secret 정보를 관리한다.

 

 

 

Oauth의 구성 요소

Access tokens

클라이언트에게 권한이 위임되었다는 것을 나타내기 위해 인가 서버가 클라이언트에게 발급해주는 요청 정보이다.

 

사용자가 제공한 인증 정보를 통해 만들어진 인가 코드와, 클라이언트의 자격 증명 정보, 검증 정보, 토큰의 접근 권한 등급 등의 정보들을 통해 만들어진다.

 

 

Oauth accessToken 발급을 위한 정보, 요소들의 역할 (제네럴하게..?)

https://authorization-server.com/auth
 ?response_type=code
 &client_id=1
 &redirect_uri=https://example.com/callback
 &scope=A+B+C
 &state=asdaea23ffas214loxzcfh

STATE : CSRF를 방어하기 위해 사용하는 매개변수이다.

 

CLIENT_ID : Oauth API 관련 자격 증명 페이지에서 사용 승인을 통해 설정되는 식별 값이다.

 

CLIENT_SECRET : 클라이언트를 인증하는 수단으로 사용되는 값으로 해당 값을 클라이언트가 전달하고 인증 서버는 이 값을 확인함으로써 요청할 권한이 있는 클라이언트인지 확인한다.

 

이를 통해 승인되지 않은 악성 앱이 유효한 액세스 토큰을 얻지 못하게 한다.

 

SCOPE : 응용 프로그램이 요청하는 권한을 나타내는 문자열이다. Oauth API가 지원하는 범위를 정의한다.

 

REDIRECT_URI : 사용자가 요청을 승인한 후 리디렉트 할 위치를 인증 서버에 제공한다.

 

 

Refresh tokens

액세스 토큰과 달리 보호된 리소스들에 접근할 때 사용되지 않으며, 기존에 발급된 액세스 토큰을 재발급할 때 사용하는 토큰이다.

 

액세스 토큰의 만료로 리소스 서버의 접근할 수 없을 때, 리프래시 토큰을 인가 서버에 전달함으로써 새로운 액세스 토큰과 리프레시 토큰을 제공받을 수 있다.

 

 

 

Oauth Flow ( Authorization code grant )

  1. 클라이언트(Somaeja)는 소유자를 인가 서버의 인가 포인트로 리다이렉트 시킨다.
  2. 소유자는 인가 포인트에서 사용자 정보를 이용해 소유자 인증을 수행한다. id, password
  3. 인가 서버는 client id와 redirect_url 정보를 등록된 것과 비교한다. 틀린 경우 요청 취소
  4. 인가 서버가 클라이언트에 대해 권한을 인가할지 질의하고 소유자는 권한을 위임한다.
  5. 인가 서버가 해당 클라이언트의 포인트로 요청을 리다이렉트 하고, 인가 코드를 전달한다.
  6. 클라이언트의 요청 포인트에 인가 코드 정보가 전달된다.
  7. 클라이언트는 받은 인가 코드와 기존에 설정된 자격 증명 정보를 담아 토큰 생성과 관련된 인가 서버의 포인트에 요청을 전송한다.
  8. 인가 서버는 해당 정보를 검증하고 클라이언트에게 액세스 토큰을 생성하여 전달한다.
  9. 클라이언트는 액세스 토큰을 이용하여 API 서버 혹은 포인트에 요청을 전송한다.
  10. 해당 API 서버, 포인트는 요청을 확인하고 리소스를 제공한다.

 

 

제네릭이 나오게 된 이유?

제네릭이 도입되기 이전에는 상황에 따라 각기 다른 데이터를 다루기 위해 최상위 클래스인 Object를 사용하여 코드를 작성하곤 하였다.

 

그를 통해서 어떠한 데이터라도 받아 저장할 수 있었으나 몇가지 문제가 있었는데.

  • 저장했던 값을 사용해야 할 때 명시적으로 캐스팅을 해서 사용해야 한다.
  • 잘못된 캐스팅을 통한 오류가 발생할 수 있다. (String → Integer 등..)
  • 들어온 타입에 대한 검증하는 로직이 추가로 들어가야한다. ( instanceof 결국 런타임에서 발견 )
  • 에러의 유무를 컴파일 단계에서 체크할 수 없다. (사전에 방지할 수 없다.)

즉 모호하고 찾기 어려운 잠재적인 오류를 가지게 된다. Reifiable type의 문제점!

 

이러한 문제를 방지하기 위해서는 컴파일 시에 타입 체크를 해서 사전에 실수를 방지하고, 명시적인 캐스팅에 대한 런타임 에러가 해결되어야 했다.

 

그를 위해 제네릭이 등장하게 되었다.

 

 

 

제네릭의 기능?

 

Generic은 해당 기능들을 제공한다.

  • 컴파일러를 통해 타입 체크가 가능하다. 즉 컴파일 과정에서 문제를 제거할 수 있다.
  • 컴파일 시점에서 사용되는 타입을 체크하여 해당 타입에 맞게 컴파일한다.
  • 일반적으로는 캐스팅 비용과 타입 체크의 비용이 들어가지 않는다.
  • 타입의 경계를 지정하여 제한할 수 있다.

 

Generic을 사용하게 발생하는 제약으론

  • Primitive Type을 사용할 수 없다.
  • 컴파일 이후에 타입이 소거되기 때문에 런타임에서 타입 체크를 할 수 없다. Non-Reifiable
    • 해당 문제는 Java 간의 하위 호환성을 위해 VM에서 지우는 방식이다.
    • 타입 소거로 인해 캐스팅 코드가 만들어질 수 있다.

등이 있다.

 

 

 

구체화 타입, 비 구체화 타입, Erasure?

 

Reifiable type?

객체의 Type 정보를 Runtime 시에 확정하여 Application이 끝날 때까지 유지되는 타입이다.

 

제네릭 도입 전, 유사한 사용법을 위해 사용했던 Object 배열 방식은 런타임 시에 타입을 검증한다.

// 컴파일
Object[] array = new Long[10];

// 런타임
Long[] var2 = new Long[10];
var2[0] = Long.valueOf(1L);

 

 

non-Reifiable type? - Erasure

객체의 Type 정보를 컴파일 시점 시에 확인하여, 동일한 동작을 보장한 뒤 Runtime 전에 소거된다.

 

자바의 제네릭은 비 구체적인 타입을 가지며, 컴파일 시에 타입을 검증한다.


// 컴파일
List<String> list = new ArrayList<>();

// 런타임
ArrayList list = new ArrayList();

 

 

Type(class) Erasure

클래스의 파라미터 타입을 제외하고 첫 번째로 바인딩된 타입을 사용하거나 없는 경우 Object 타입으로 설정한다.

 

미 바인딩 시

public class Value<T> {
    private T value;

        public T getValue(){
        return value;
    }

        public void setValue(T value){
        this.value = value;
    }
}

--------------------
// 컴파일 후

public class Value {
    private Object value;

        public Object getValue(){
        return value;
    }

        public void setValue(Object value){
        this.value = value;
    }
}

 

바인딩 시

public class Value<T extends A> {
    private T value;

        public T getValue(){
        return value;
    }

        public void setValue(T value){
        this.value = value;
    }
}

--------------------
// 컴파일 후

public class Value {
    private A value;

        public A getValue(){
        return value;
    }

        public void setValue(A value){
        this.value = value;
    }
}

 

 

Method Type Erasure

메서드의 파라미터 타입은 바인딩되지 않은 경우 Object, 바인딩되었을 때에는 해당 클래스로 변환된다.

 

미 바인딩 시

public static <E> void someMethod(List<E> list) {
        System.out.println(list.toString());
}

--------------------
// 컴파일 후

public static void someMethod(List<Object> list) {
        System.out.println(list.toString());
}

 

바인딩 시

public static <E extends String> void someMethod(List<E> list) {
        System.out.println(list.toString());
}

--------------------
// 컴파일 후

public static void someMethod(List<String> list) {
        System.out.println(list.toString());
}

 

 

 

제네릭 사용법

 

제네릭의 변수 네이밍 관례

  • E : Element = Collection
  • K : Key
  • N : Number
  • T : Type Parameter
  • V : Value
  • S, U, V : 두 번째, 세 번째, 네 번째에 선언된 타입

 

제네릭 선언 및 사용 방법

public class Value<T> {
    private T value;

        public T getValue(){
        return value;
    }

        public void setValue(T value){
        this.value = value;
    }
}

---------------------------------

public static void main() {

        Value<String> stringValue = new Value<>();
        stringValue.setValue("hello");

        Value<Long> longValue = new Value<>();
        longValue.setValue(10L);

}

 

 

 

제네릭 주요 개념 (바운디 드 타입, 와일드카드)

 

Type Unbound 란? - Unbounded WildCard

모든 Type을 허용하는 것을 나타낸다.

별도의 상태를 저장하지 않는, 파라미터에 의존하지 않는 메서드를 만들 때 사용한다.

// List<Object> list

List<?> list

 

 

Type bound 란?

특정 타입의 객체를 다른 타입의 객체로 변환할 수 있는 범위를 말한다. - 가변성

  • 공변성 : 받은 Type을 확장(extend)하는 하위 Type 들에 대해서도 허용한다. 서브타입 와일드카드

    여러 하위 클래스들이 하나의 상위 클래스 (분류)에 속한다.

    메서드의 반환 타입에 사용할 수 있으나, 파라미터로는 사용할 수 없다.

     

    리스코프 치환 원칙 ( is a kind of )을 생각하면 좋다.

      // <E extends String> 형식은 지원하는 데이터 유형의 범위를 선언하는 데 사용된다.
    
      List<E extends Object> 
    
      List<E extends Collection>
    
      // 파라미터로 주어진 데이터 유형을 특정 범위로 제한하는 데 사용된다.
      List<? extends Object> 

     

    Multiple type 제약 지정

    해당 경계 안에는 제약 조건을 모두 만족하는 값만을 넣을 수 있다.

      // 해당 인터페이스들을 구현한 객체만을 받을 수 있다.
      List<E extends Serializeble & AutoCloseable & Cloneable>  
  • 무공변성 : 받은 Type 만을 허용한다.

      List<String> list = new ArrayList<>();
    
      String str = "string";
    
      public int add(int a, int b)
  • 반공변성 : 받은 Type이 구현하는 상위 타입만을 허용한다. 슈퍼 타입 와일드카드

    메서드의 파라미터에 사용할 수 있으나, 반환 타입으로는 사용할 수 없다.

      List<? super SomeClass> // 반환하는 값에 대해서 사용이 가능하다.
                                                      // 값을 쓰는 것이 가능하다.

 

 

제네릭 메서드 만들기

메서드의 선언부에 제네릭 타입을 사용한 메서드를 의미한다.

 

제네릭 타입의 선언 위치는 반환 타입 앞에 <>를 감싼 상태로 선언한다.

 

와일드카드의 경우 선언 순서를 변경할 수 있다.

public static <E> T someMethod(List<E> list) {
        return list.get(0);
}

public static <T> void print(T val) {
        System.out.println(val.toString());
}

public static <T, S> void print(T strVal, S longVal) {
    System.out.println(strVal.toString() + longVal.toString());
}

// 참고용?
public static void someExample(List<? extends String> val) {
        System.out.println(val.get(INDEX).toString());
}

public static <E extends String> void someExample2(List<E> val) {
      System.out.println(val.get(INDEX).toString());
}

---------------------------

private static int INDEX = 1;

public static void main(String[] args) {

        List<String> list1 = new ArrayList<>();
        list1.add("string");
        print(someMethod(list1.get(INDEX)));

        List<Long> list2 = new ArrayList<>();
        list2.add(1L);
        print(someMethod(list2.get(INDEX)));

        print(list1.get(INDEX), list2.get(INDEX));

}

 

자바 ORM 표준 JPA 프로그래밍 서적을 학습하면서, 정리하고 있는 내용입니다.

 

 

EntityManagerFactory

사용자의 요청이 들어옴에 따라 EntityManager를 생성하는 책임을 가지고 있다.

 

JPA를 동작시키기 위한 내부 객체들이 이 Factory를 통해 생성되고, 구현체에 따라선 커낵션 풀도 별도로 구성하므로 생성 비용이 크다고 한다.

 

여러 EntityManagerFactory를 생성하여 공유할 수 있으나. 하나의 객체만 생성해서 전 영역에 공유하는 것이 권장된다. (configuration lite mode를 사용하면 여러 개를 생성하고 공유할 수 있을 것 같다.)

 

 

 

EntityManager (Persistence Context)

엔티티 정보를 저장하고 있는 환경. 요청 결과가 DB에 반영되기 이전에 위치하는 논리적인 영역을 의미한다. 가상의 데이터베이스 영역이라고 봐도 무방하다.

 

사용자의 요청을 받아 Connection을 통해 DB와 로컬 트랜잭션을 맺으며, 데이터를 관리하고 영속적으로 반영하는 혹은 삭제하는 책임을 가지는 객체이다.

 

JPA의 대부분의 기능은 해당 객체가 지원한다. 커넥션과 밀접한 관계를 가지기에 스레드 간의 공유, 재사용이 어렵다.

멀티스레드에 안전하지 않은 상태가 된다는 것 아닐까?

 

 

 

Entity의 Life Cycle

  • 비영속 (new, transient) : 영속성 컨텍스트와 관계가 없는 새로 생성된 상태

      // 비영속 상태의 Entity. 즉 entityManager가 모르고 있는 Entity이다.
      Entity entity = new Entity();
      entity.setXxxA("test");
      entity.setXxxB("value");
  • 영속 (managed) : 영속성 컨텍스트에게 관리되는 상태 (환경 안에 정보가 있는 상태)

    JPA는 기본적으로 트랜잭션 안에서 데이터를 변경하여야 한다.

      // 요청에 따른 entityManager(Persistence Context) 생성
      EntityManager entityManager = entityManagerFactory.createEntityManager();
    
      // 영속성 컨택스트에 대한 글로벌 트랜잭션 관리 시작
      EntityTransaction transaction = entityManager.getTransaction();
      transaction.begin();
    
      try {
    
              // 비영속 상태의 Entity.
              Entity entity = new Entity();
              entity.setXxxA("test");
              entity.setXxxB("value");
    
              // 영속성 컨택스트에 Entity 정보를 저장. 즉 entityManager가 알고있는 Entity이다.
              entityManager.persist(entity);
    
              // transcation 결과 반영 - (DB 저장 X, 영속성 컨텍스트에만 반영)
              transaction.commit();
      } catch (RuntimeException exception) {
              // 위 로직 실패 시 결과 반영 X -> 원복
              transaction.rollback(); 
      } finally {
              // 성공, 실패 상관없이 transaction 종료
              transaction.close();
      }
  • 준영속 (detached) : 영속성 컨텍스트에 관리되다가 이후 분리된 상태

    영속성 컨텍스트가 지원하는 기능을 사용할 수 없기에 비영속 상태와 유사하다. 다른 점은 식별자 값(id)이 존재한다는 점이다.

      // 특정 회원 엔티티를 영속성 컨텍스트에서 분리한다.
      // 쉽게 말하면 해당 엔티티를 영속성 컨텍스트에 존재하는 1차 캐시에서 삭제한다.
      entityManager.detach(entity);
    
      // 해당 영속성 컨텍스트에서 관리되는 모든 엔티티를 준영속화한다.
      // 쉽게 말하면 영속성 컨텍스트의 1차 캐시를 초기화한다.
      emtityManager.clear();
    
      // 영속성 컨택스트를 종료시킨다.
      entityManager.close();
  • 삭제 (removed) : 엔티티가 삭제된 상태

      // 해당 객체를 삭제. 즉 DB에 저장된 정보를 삭제하는 요청을 전송한다. 
      entityManager.remove(entity);

    준영속과 삭제의 다른 점은 Entity가 DB에 반영되기 전이냐 이후이냐의 차이이다.

 

 

 

JPA Internal Flow

 

 

 

Persistence Context의 특징

  • 영속성 컨텍스트와 식별자 값

    영속성 컨텍스트는 @ID로 매핑된 값을 통해 엔티티를 구분한다. 즉 해당 값이 무조건 존재해야 하며, 없는 경우 예외가 발생한다.

  • 데이터베이스 저장 시점

    영속성 컨텍스트에 저장, 관리되는 엔티티는 flush()가 호출된 시점에 실제 DB에 반영된다.

  • 엔티티 관리 방식의 장점 - 아래

 

 

 

JPA 엔티티 관리 방식의 장점

  • 1차 캐시

    영속성 컨텍스트에서 관리되는 데이터를 우선적으로 조회하는 것을 의미한다.

    사용자의 요청이 종료된다면 해당 영역도 제거되기에 하나의 비즈니스 로직에서만 이점을 제공한다.

     

    전역적으로 관리되는 캐시는 2차 캐시라고 부른다.

     

     

    해당 정보가 컨텍스트 안에 존재하지 않는다면,

    • DB에서 해당 정보를 조회한 후 (SELECT)

    • 쿼리 결과를 이용하여 엔티티를 생성한 다음

    • 컨텍스트 내부에 엔티티를 저장하고 반환한다.

    • 1차 캐시는 내부적으로 Map을 통해 관리되며, @Id로 매핑된 값을 Key로 가진다. 이 값은 DB의 PK 값인 경우도 존재한다.*

       

  • 동일성 보장

    1차 캐시를 이용하여 반복 가능한 읽기 등급의 격리 수준을 Application 수준에서 제공한다. 즉 같은 트랜잭션 내에 여러 번 조회 쿼리를 진행하더라도 동일한 결과를 반환받는 것을 이야기한다.

     

    이때에는 동일성 비교가 가능하다. → Map에서 동일한 엔티티를 제공한다.

     

  • 트랜잭션을 지원하는 쓰기 지연

    한 트랜잭션 내에서 여러 엔티티를 등록할 때 각각의 엔티티에 대한 Insert Query를 DB에 바로 요청하지 않고, 일종의 버퍼처럼 컨택스트 내부에 존재하는 SQL 저장소에 Query를 쌓아놓다가 1번에 반영할 수 있다.

     

    entityManager.commit();

     

    큰 오버헤드를 가지는 i/o 처리를 최소화할 수 있다.

     

  • 변경 감지

    영속성 컨텍스트에게 관리되고 있는 엔티티 정보에 대해서 Service logic에서 setter 등을 통해 수정을 가할 경우, 그 변경 결과가 트랜잭션을 커밋하는 시점에 영속성 컨텍스트에게 감지되고 최종적으로 Update Query가 DB로 전송되어 최신 정보로 변경하게 된다.

     

    엔티티 정보와 엔티티 스냅샷 정보를 비교하여 감지한다.

     

    스냅샷 정보는 DB에서 값을 읽어와 캐시에 저장한 시점의 엔티티 정보를 복사하여 저장해둔 것이다. 이것을 트랜잭션이 커밋된 시점에서 엔티티 정보와 비교하여 변경된 사항들에 대해서 Update Query를 생성하는 것이다.

     

    엔티티 삭제도 동일 메커니즘을 공유하고 트랜잭션 커밋 시 Delete Query를 전송한다.

     

     

     

Flush?

영속성 컨텍스트의 변경 내용들을 데이터베이스에 반영시키는 기능이다.

 

영속성 컨텍스트에 저장된 정보를 비우는 것은 아니다.

 

 

Flush 발생 시 Flow는

  • 엔티티에 대한 변경을 감지하고
  • 추가, 수정, 삭제된 엔티티 정보에 대한 Query를 내부 SQL 저장소에 등록한 뒤
  • 해당 정보를 DB에 전송한다.

이렇게 이루어진다.

 

 

Flush를 발생시키는 방법으로는

  • em.flush()를 통한 직접 호출

  • 트랜잭션 commit() 을 통한 자동 호출

  • JPQL으로 만들어진 Query 실행 시 자동 호출

    영속성 컨텍스트에만 관리되고 있는 영속 상태의 엔티티들은 사실상 DB에 존재하지 않으므로 JPQL을 통한 Query 요청 시 결과에서 이 것들을 확인할 수 없게 된다. 이를 방지하기 위해 미리 Flush()를 호출하여 DB에 반영한 뒤 정상적인 결과를 제공하게 된다.

가 있다.

 

 

Flush 모드 옵션

  • FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 Flush를 실행한다.
  • FlushModeType.COMMIT : 커밋할 때만 Flush를 실행한다.

'Programming' 카테고리의 다른 글

Open Authorization Framework 2.0?  (0) 2021.03.01
Live Study_Week 14. Generic  (0) 2021.02.28
객체 지향 디자인의 핵심 개념 : 책임 [ GRASP ]  (4) 2021.02.11
Java의 Reflection API와 성능 이슈?  (0) 2021.02.02
G1 GC  (0) 2021.01.26

GRASP : General Responsibility Assignment Software Patterns?

해당 내용은 상호작용하는 클래스 혹은 객체에 책임을 할당하는데 도움이 되는 개념과 방법 즉 패턴들로 이루어져 있습니다. 이러한 내용들을 준수함으로써 좋은 객체지향 디자인 패턴을 만들어낼 수 있습니다.

 

 

 

Responsibility?

책임은 SOLID나 지금 다룰 GRASP 등 Oriented-Object-Design 패턴과 원칙에서 핵심이 되는 개념입니다. 이는 클래스와 객체가 어떤 메시지(요청)에 대해 처리할 수 있거나, 적절한 행동을 해야 하는 의무가 있는 경우 해당 객체가 이러한 책임을 가진다라고 이야기할 수 있습니다.

 

책임은 메서드를 통해 구현되게 됩니다.

 

 

Example.

  • String은 문자열을 표현하고 처리하는 책임을 가집니다.
  • File은 파일에 대한 정보를 알려주고 처리하는 책임을 가집니다.
  • DTO는 데이터를 다른 Layer로 운반하고 특별한 경우 비즈니스 로직에 따라 데이터 값을 반영시키는 책임을 가질 수 있습니다.

이런 것이 모두 책임 입니다.

 

책임의 분류는 행위 관점과 상태 관점으로 나눌 수 있습니다.

 

행위 관점.

  • 객체를 생성하는 행위, 결과를 계산하는 행위 - 객체 스스로 하는 것.
  • 다른 객체의 행동을 지시하는 것 - Controller
  • 다른 객체의 활동을 제어하고 조절하는 것 - Chain of Responsibility, Proxy

상태 관점.

  • private로 접근 제어된 데이터에 관하여 아는 것
  • 관련된 객체에 대해 아는 것
  • 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것

 

 

 

Creator

새로운 객체를 생성하는 책임을 말합니다.

 

이러한 책임을 가지기 위해서는 하나 이상의 조건을 만족하여야 합니다.

  • 생성하는 객체를 포함합니다.
  • 생성하는 객체에 대한 초기화 정보를 가집니다.
  • 생성하는 객체를 사용합니다.
  • 생성하는 객체에 대한 정보를 기록합니다.

이와 관련된 것은 Creation Pattern 중 Abstract Factory, Factory Method가 있습니다.

 

 

 

Information Expert

새로운 기능이나 방법을 추가하는 것은 그에 필요한 정보를 가지는 클래스, 객체를 대상으로 하여야 합니다. 그러한 클래스, 객체는 최소한의 변경으로 기능을 구현할 수 있기 때문입니다.

 

결국 객체가 가지는 데이터에 대한 기능을 직접 처리하게끔 하라는 것입니다.

 

하지만 모든 필드에 대한 무분별한 Getter, Setter는 캡슐화를 지키지 못합니다.

해당 행위를 주관하는 객체(Client, Service)에서 데이터 흐름을 만들어내는 객체(DTO)의 데이터를 꺼내오는 것보다는 다음 행위를 하는 객체에게 해당 객체를 전달하는 것이 캡슐화를 지키는 방법입니다.

 

그리고 상태에 대한 질의 후 분기해야 하는 로직의 경우 Tell Don't Ask 원칙을 준수하는 것이 좋습니다.

Tell Don't Ask : https://martinfowler.com/bliki/TellDontAsk.html

 

 

 

Controller

요청에 대한 비즈니스 로직과 요청을 전달, 지시하는 로직은 분리되어야 합니다. 그리고 우리는 일반적으로 MVC Pattern을 통해 이러한 요구사항을 만족합니다.

 

Controller는 요청을 받고 적절한 행위를 하는 객체에게 지시하는 행위 관점의 패턴입니다.

 

이러한 책임을 가지는 객체는 내부적으로 별도의 비즈니스 로직을 가져서는 안 되며, 요청을 전달, 위임하는 것에 중점을 두어야 합니다.

 

 

 

Low Coupling

낮은 결합도를 준수하는 것은 시스템 설계의 전체적인 결합도를 낮게 유지되도록 구현하고 책임을 할당하라는 의미입니다. 객체 간의 결합이 없을 수는 없기에 우리는 최대한 영향을 주지 않도록 노력하여야 합니다.

 

이를 위해서는

  • 클래스 간의 종속성을 낮추어야 합니다.
  • 한 클래스의 변경이 다른 클래스에 주는 영향이 적어야 합니다.
  • 객체는 더 높은 재사용 가능성을 가져야 합니다.

이는 Interface 등을 이용한 상호 작용이나 Facade 패턴 등을 사용하여 이루어낼 수 있습니다.

 

반대로 높은 결합도는 각각의 객체가 서로의 구현 세부사항을 인식할 때(접근 가능할 때, 사용할 때) 발생되게 되는 것입니다. 이런 경우 한 객체의 변경이 다른 객체의 변경을 야기하게 됩니다. extend의 경우 이런 변경에 대한 파급효과가 강해지게 됩니다.

 

 

 

High Cohesion

높은 응집력을 준수하라는 것은 객체 책임에 의해 변경될 수 있는 요소들을 한 곳에 모아서 관리하라는 것입니다.

이것을 통해 요구사항으로 인해 책임의 변경이 이루어진다면, 그러한 영향이 하나의 객체에 대해서만 이루어질 수 있습니다.

 

이것과 관련된 원칙은 SOLID의 Single responsibility principle가 있습니다. 이 원칙은 객체를 변경할 수 있는 책임이 하나만 존재해야 한다는 원칙입니다.

 

 

 

Indirection

간접이란 두 객체 간의 직접적인 결합을 분리하고 간접적인 결합을 지원하게끔 설계하라는 것입니다. 이는 별도의 중개자 객체를 통해 Dependency Injection을 진행하거나, 요청에 대한 위임, 지시하는 컨트롤러와 같은 요소를 도입하는 것을 말합니다.

 

Observer, Facade, Bridge, Mediator, Adapter는 간접 접근 방식을 활용하는 패턴입니다.

 

 

 

Polymorphism

하나의 타입을 가지는 서로 다른 객체들이 동일한 책임, 메시지에 대해서 서로 다르게 반응하는 것을 말합니다. 즉 책임을 공유하는 상태입니다.

 

이러한 원칙은 객체들 사이의 대체 가능성을 의미하며 그를 통해 설계를 유연하고 재사용 가능하게 만들 수 있습니다.

 

각각의 요청에 대하여 Reflection API 등의 동적 로딩을 통하여 Runtime Dependency를 구현하거나 Chain of Responsibility와 같은 행위 패턴을 구현하고 분기를 위한 상태 변수를 전달함으로써 컴파일 시점에서 필요한 객체를 Dependency Injection 할 수 있습니다.

 

 

 

Protected Variations

요구사항에 대한 책임, 동작 변화는 객체의 내부에서만 이루어져야 하며, 공용 인터페이스를 동일하게 유지함으로써 Client 역할을 하는 객체에게는 변경 사실을 숨겨야 합니다.

 

변경될 여지가 있는 로직에 대해서는 객체 간의 캡슐화를 지켜야 한다는 것입니다.

 

이와 관련된 원칙으로는 SOLID의 Open/closed principle이 있습니다. 이 원칙은 객체의 책임의 확장과 변경이 다른 객체의 행위에 영향을 주지 않아야 된다는 것을 말합니다.

 

 

 

Pure Fabrication

객체가 광범위한 책임을 가지지 않도록 (God Class) 분리하라는 것을 말합니다. 이는 기능에 대한 공통적인, 비즈니스 로직을 하나의 객체를 만들어 담당하게끔 하는 것을 뜻합니다.

 

Layer Architecture에서 이러한 책임을 담당하는 객체를 Service라고 합니다.

 

 

 

참고 자료

'Programming' 카테고리의 다른 글

Live Study_Week 14. Generic  (0) 2021.02.28
JPA 학습 정리 Persistence Context, Entity, Flush?  (0) 2021.02.27
Java의 Reflection API와 성능 이슈?  (0) 2021.02.02
G1 GC  (0) 2021.01.26
Concurrency, Parallelism, Parallel and Concurrency  (0) 2021.01.23

+ Recent posts