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);

 

 

벤치 마크 자료

 

 

참고 자료



글의 시작

프로그래밍 학습을 시작한 지는 6개월 정도 블로그를 시작한 지는 4개월이 되었다.

 

일반적인 국비 교육 기간과 비슷한 시간을 공부해왔는데 독학과 멘토링을 통해 학원 수료생들보다 더 많은 것을 학습하였는지 성찰하는 시간을 가져보았다.

 

개발은 (다른 직업도 그렇겠지만) 평생 학습하여야 하는 것인데, 회사를 다니며, 공부를 지속할 수 있는 다른 방법도 계속해서 고민해보아야겠다.

1만 시간의 법칙은 학원을 다니는 시간, 업무를 진행하는 시간을 제외하고 "본인이 노력, 투자한 시간을 이야기하는 것"이라고 생각한다.

이력서는 지속적으로 개선 중이며, 현재는 따로 관리하고 있다 

 

 

2020년 회고록을 쓴 이후 있었던 일

2020년 12월 31일 이후에 생각보다 많은 일이 있었다.

 

 

 

Somaeja Project ( ~ 현재)

현재 개인적으로 개발하고 리뷰받고 있는 프로젝트의 기능을 80% 정도까지 개발하였기에, 특정 부분에 대하여서 고도화를 진행하고 있고, 다이렉트(유저) 채팅 기능을 추가하기 위해 WebSocket 구현체를 찾아보며, 학습하려고 한다.

 

 

기존에는 HttpSession을 이용하여 간단하게 세션 방식의 인증을 얹어 놓은 상태였는데, 우선 이부분을 Spring Security + JWT 방식으로 고도화하고 있다.

 

금주에 이 부분을 완료하고 이후 Oauth 2.0을 적용하려고 한다.

 

---

 

최종적으로 Session 방식을 Security + Jwt 로 변경하였지만, 몇가지 문제로 작업하던 프로젝트를 내려놓게 되었다. 지금은 회사에서 배우고 있는 것들을 해당 프로젝트에 적용해볼까 고민해보고 있다. 

 

 

 

NextStep Blog Study 3기 ( ~ 02.15 )

넥스트 스탭에서 진행하는 블로그 스터디 3기를 신청하였었고, 한 달이라는 시간 동안 글을 작성하고 회고하는 시간을 가졌었다.

프로그래밍과 관련이 있을 수도 있고, 없을 수도 있는 그런 과정이었지만, 다른 사람들의 글을 보는 재미와 회고 시간에 듣는 근황, 마음가짐들 그리고 나 자신을 꾸준히 나아가도록 하는 채찍질이 되는 그런 의미있는 과정이었다고 생각한다.

 

현재 Blog Study 4기도 지원하였고, 다음 회고 시간을 위한 한 주의 글 작성을 준비하고 있다.

 

 

---

 

Blog Study 4기는 제대로 참여할 수 없었다. 회사가 생각보다 많은 시간을 차지하고, 나의 체력이 부족하다는 것을 알 수 있었다.

 

지금은 별도의 스터디를 진행하지 않고 개인적인 학습을 진행하고 있다.

 

 

 

NextStep Blog Study 4기 ( 02.29 ~ )

  • 진행 중이다. -

 

 

선장님의 Live Study 1기 ( ~ 이번 주)

백기선 님의 라이브 스터디 시즌 1이 이번 주 (15주 차 진행)에 종료된다. 자바를 학습한 뒤에 잊어가던 여러 개념들을 복습하는데 도움이 되었고, 다른 사람들의 학습 포인트(요점?)를 보는 것과 매주 진행되는 라이브 리뷰 시간이 정말 의미있었던 것 같다.

 

10주 차쯤부터 번아웃과 다른 일들 때문에(변명) 과제를 제대로 제출하지 못했었지만, 15주 차는 일요일 바로 제출하였고, 하지 못했던 10주 차도 오늘 작성하여 제출하였다.

 

시즌 2도 정말 많이 기대가 되는데, 아무 문제없이 시작되었으면 좋겠다.

 

---

 

Live Study 티셔츠를 받았다! 입고 다니기는 아까워서 그냥 옷걸이에 걸어두고 쳐다만 보고 있다.

 

 

전문대 졸업, 취직 ( 02.19, 02.22 )

대학 졸업식이 2월 19일에 온라인으로 진행되었다. 참여해보니 대기하는 시간에 심심하지 말라고 보여주는 것 같은? 각종 영상들만 길었고, 실제 졸업식 시간은 10분이 채 되지 않았다.

헐..

 

코로나 때문에 군 복무를 마치고 나서 대학을 다니는 느낌이 전혀 들지 않았었는데 이 느낌이 졸업식까지 이어지니 대학을 졸업한 게 맞는지 체감이 되지 않는다.

 

그리고 약간? 뜬금 없게도 취직을 성공하였다. 아직 수습 기간이 남아있지만.....

 

사실 앞선 글에서 번아웃을 이야기했던 것은 이 취업의 여파(?)가 아닌가 싶다. 계속 달려오다 확 긴장이 풀리니 1주일 정도 동안 정말 무기력한 느낌을 많이 받았던 것 같다.

 

1년 차 스타트업이고 연봉이 높은 편은 아니지만 원하던 스킬 셋들을 사용하고, 고 경력의 개발자들이 많은 곳이기에 설계, 기술, 경험들을 기대하고 선택하게 되었다.

 

출근 전에 업무 장비를 구매해야 되어서 노트북들을 찾아보고 있었는데 그때는 정말 마음이 설레었다.

 

수습 기간에는 Spring MVC를 사용하는 과제와 업무 진행 Flow (1주일 단위의 스프린트를 진행하고, 지라, 컨피리언스, 깃 랩을 통해 협업을 하고 있었다.), 서비스 요청 Flow, JPA 등 여러 교육이 있어 매우 바쁜 시간을 보낼 것 같다.

 

그렇다고 회사에 배우러 간다고 생각하지는 않는다. 회사는 학원이 아니다. 도입 전, 사용하기 전까지 스스로 찾아서 빠르게 학습하는 것이 중요하다고 생각한다.

 

 

---

 

회사의 컨벤션과 지금도 개발을 해오고, 아키텍처를 설계하시는 대표님, 연차와 상관없이 계속 공부하는 동료 개발자들을 통해 많은 것을 배우고 있다. 

 

더욱더 열심히 공부해야할 것 같다.

 

 

기타.

  • 토비의 스프링 1권 완독 + 2권은 반절
  • Practical 모던 자바 1독 

 

---

 

DDD 철저 입문 1독

 

 

구매한 책

 

도메인 주도 설계란 무엇인가?

도메인 주도 설계로 시작하는 마이크로서비스 개발

아파치 카프카 애플리케이션 프로그래밍 with 자바

 

 

구매할까 고민하는 책

 

Akka 코딩 공작소

대용량 서버 구축을 위한 Memcached와 Redis

 

 

구매할까 고민하는 강의

 

4천만 MAU를 지탱하는 서비스 설계와 데이터 처리 기술.

 

 

 

올해 더 배우고 싶은 것들 

 

Kotlin

개인적으로 자바 코드를 작성하다 보면 겉치레가 많아 보여서 보기가 좀 그렇다.

그렇다고 Lombok만을 믿고 가기에는 문제가 있어 보인다.

 

코틀린은 간결한 문법과 자바보다 강력한(사실 자바는 그냥 우겨넣은게 아닌가..?) 함수형 패러다임의 지원이 되고, 스프링의 모든 기능에 대한 호환성을 보장하는 등의 여러가지 요소 덕분에 선호되고, 사용하는 회사가 늘어가고 있는 것 같다.

 

 

JPA

김영한 님의 강의를 통해 공부하고 있는데, 정말 재미있게 보고 있다.

여러 채팅방에서 활동하다 보면 요즘 강의가 대부분 JPA 를 사용해서 그렇게 생각하는 건지 모르겠지만 단순한 측면에서만 보고, 왜 회사에선 JPA를 쓰지 않는거죠? 라고 하는 것이 많다. 하지만 러닝 커브는 절대적으로 JPA가 높을 수밖에 없다.

 

마이바티스랑 JPA를 비교하시는 분들도 있더라.. 그런 분들에게는 https://happy-coding-day.tistory.com/101 이 글을 추천을 드리고 싶다. 이 글에서 많은 것을 느낄 수 있었다.

 

CRUD가 기본적으로 제공되기에 Query 방식보다 쉽지 않느냐 라고 하는 사람들이 간혹 보이는데, 조금만 서비스, 데이터가 복잡해지면 MyBatis가 더 쉽게 사용 가능하고 유연해 보인다. 여기에선 트랜잭션 스크립트를 의미한 것이다.

 

개인적으로 JPA는 회사에 취업한 후에 공부하고 싶었던 요소 중 하나이다. 기본편과 코배웹의 부트 버전을 쭉 보고 현재는 Query DSL을 찾아보고 있다.

 

 

Database, HTTP - TCP - UDP 등의 통신, 좀 더 확실한 Infra 개념 등

데이터베이스 인터널스, 러닝 SQL, HTTP 완벽 가이드, 서버/인프라를 지탱하는 기술

해당 CS 내용들에 대해서는 매번 부족하다는 것을 느끼고 있고, 이러한 갈증을 해소하기 위해 공부할 책을 선정하였고, 학습하고 있다. ( 진도는 잘 못나가지만..)

 

 

알고리즘, 코딩 테스트

올해가 가기 전에 프로그래머스 기준으로 3~4래밸까지는 완전히 다 풀어볼 수 있으면 좋겠다.

큰일이다. 이젠 알던 것도 기억이 나지 않는다!

 

여러 기술, 도메인 지식 공부할 땐 그렇게 재미있는데 이건 왜...

난 진짜 알고리즘 문제에 대해서 잼병이다.

 

 

추가

요즘 대용량 데이터 처리 방법과 오픈소스들에 관심을 가지게 되고 있다.

 

특히 카프카와 스파크, 비동기(Akka Framework)에 관심이 가고 있어서 책을 구매할까 생각중이다.

근데 앞에서 언급한 책도 다 못봤다.

 

 

뭐.. 이 정도인 것 같고! 올해도 잘 완주할 수 있기를 소망하고 있다.

 

이 글을 보는 다른 분들도 힘내시길 바란다.

 

제출하지 못했던 10주차까지 완료!

 

프로세스?

스레드 단위 작업을 지원하기 위한 자원의 할당 단위이며, 커널 위에서 현재 실행 중인 프로그램을 의미한다.

 

프로세스 안에는 하나 이상의 스레드가 존재한다.

 

 

왜 멀티 프로세스 방식을 사용하지 않는가?

프로세스를 이용하여서도 멀티 프로세싱이라고 하는 방법을 이용하여 하나의 작업을 병렬적으로 처리할 수 있다. 하지만 이 방식은 각각의 프로세스가 쉽게 공유되지 않는 자신만의 데이터 영역들을 가지고 있기 때문에 데이터 처리, 공유 방식과 메모리 공간 점유에 대한 문제가 발생한다.

  • 작업을 진행할 때마다 프로세스의 작업 내용이 cpu로 로딩이 된다. 이후 다른 작업을 진행하여야 할 때 기존에 사용하는 것을 내리고 다른 프로세스를 로딩하게 되는데 스레드 방식보다 들어가는 비용이 크다.
  • 여러 프로세스가 하나의 작업을 같이 하는 상황에서 각각의 프로세스들은 다른 프로세스가 가지는 정보를 사용하기 위해 별도의 ICP(Inter Process Communication)가 필요하다.

 

하나의 작업을 병렬로 처리하고 진행하는 작업을 변경하는 과정에서 멀티 쓰레드를 사용한 작업 방식이 더 좋은 효율을 보인다.

 

 

스레드

프로세스 안에서 실제로 작업을 처리하는 단위를 의미하며, 가지고 있는 자원을 이용하여 연산을 처리하는 주체이다.

 

 

멀티 스레드

하나의 프로세스 내에서 둘 이상의 스레드가 여러 작업을 동시에 수행하는 방식을 말한다.

 

CPU 코어는 한 번에 하나의 작업만 수행하므로, 코어의 개수와 동시에 처리되는 최대 작업 개수는 동일하다.

 

 

멀티스레드의 장점

  • 오랜 시간이 걸리는 작업 때문에 Blocking 되지 않고 별도의 작업을 처리할 수 있다.
  • 작업 간의 대기 시간을 효율적으로 활용할 수 있다. 결과가 필요한 작업 외에는 처리한다.
  • 같은 시간 내에 더 많은 작업을 처리할 수 있다. 한도가 정해진 상황
    • 각 스레드의 처리가 동시에 끝나도록 잘 분할해야 하며,
    • 각 쓰레드의 처리가 최대한 다른 스레드를 의존하지 않게끔 해야 한다.

멀티스레드의 단점

  • 멀티 프로세스보단 덜하지만 싱글 스레드보다 사용하는 자원(메모리) 량이 크다.
  • 커널 스레드의 경우 전환할 때 오버헤드(Context Switching)가 발생하기에 총 처리시간이 단일 작업 처리보다 상대적으로 길다. 멀티 프로세스 방식보단 덜하다.
  • 동시성 특유의 문제가 발생한다.
    • 잘못된 데이터의 덮어쓰기
    • 교착 상태의 발생 (각 스레드가 상대방의 리소스가 해제되는 것을 기다리는 상황)
    • 순서가 정해지지 않은 스레드들의 처리 때문에 예외가 발생할 수 있다.
      • 어떠한 자원을 자료구조에 넣어놨을 경우 필요한 스레드가 접근하기 이전에 다른 쓰레드가 가져가게 되고, 그에 따른 예외가 발생한다.
    • 무한 루프가 발생할 수 있다. (Hashmap의 put 연산 등)

그렇기에 구현 시 스레드의 안정성을 고려하여야 한다.

 

 

쓰레드의 안정성?

여러 스레드가 어떤 변수나 함수 또는 클래스 객체에 접근할 때 계속해서 개발자가 의도한 대로 정확하게 동작한다는 것으로 정의한다. 동기화 방식 이외에도 호출하는(사용하는) 쪽에서 동기화 코드 없이도 올바르게 동작할 수 있음을 의미한다.

 

멀티 스레드의 궁극적인 목표는 쓰레드 안정성을 지키면서 성능을 최대한 뽑아내는 것이다.

 

이를 지키기 위해 사용되는 것으로는 불변 객체 구현, Atomic API, 순수 함수 등이 있다.

 

 

스레드의 종류

커널 래벨 (네이티브) 스레드란?

커널을 통해 관리되는 커널 종속적인 스레드이다.

  • 애플리케이션 프로세스의 첫 번째 스레드는 커널 스레드이다.
  • 프로그래머의 요청에 따라 스레드를 생성하고 해당 스레드가 커널을 통해 스케줄링된다면, 커널 레벨 스레드라고 한다.

 

커널 래벨 (네이티브) 스레드의 장점

하나의 프로세서 안에 스레드들을 몇몇 프로세서에 한꺼번에 디스 패치하는 것이 가능함으로 멀티 프로세스 환경에서 매우 빠르게 동작한다.

  • 디스패치? : 준비 상태에 있던 스레드가 CPU에 할당받아 실행되는 것을 말한다.
  • 다른 스레드가 입출력 작업이 끝날 때까지 다른 쓰레드를 사용하여 작업을 진행할 수 있다.
  • 커널이 각 쓰레드를 개별적으로 관리할 수 있다.
  • 쓰레드 작업 시에 Context Switching 이 발생한다.

 

커널 래벨 (네이티브) 스레드의 단점

  • 쓰레드의 관리를 위하여 커널을 호출하는데 일정 시간이 소비된다.
  • 스레드 간의 동기화 문제가 발생할 수 있으며, 그에 따른 처리가 복잡하다.
  • 사용자 스레드에 비하여 자원을 더 많이 소비한다.

 

사용자 (그린) 스레드란?

사용자 영역에서 연산을 수행한다.

  • 프로세스 내부에 존재하는 Main 스레드를 통해 호출되며, Context Switching을 하지 않기에 커널 스레드보다 오버헤드가 적다.
    • Main Thread : Main Method를 실행하는 스레드, JVM이 Application 구동을 위해 사용.
  • 프로세스 내의 하나의 스레드가 커널로 진입하게 된다면, 모든 쓰레드가 Blocking 된다.
  • 사용자 스레드는 커널을 통해 스케줄링이 되지 않기에, 각 CPU에 효율적으로 스레드를 분배할 수 없다.

 

Thread behavior in the JVM

 

Thread behavior in the JVM

The JVM does what it wants to do, so how can you predict the order of thread execution?

www.infoworld.com

 

데몬 스레드란?

(프로세스의) 메인 스레드의 작업을 돕는 보조적인 역활을 하는 쓰레드를 말한다.

  • JVM의 Main 스레드의 작업이 종료된다면, 데몬 쓰레드는 동작을 중지한다.
  • 쓰레드 생성 시에 SetDaemon() 메서드를 이용하여 설정할 수 있다.

 

JVM에서의 데몬 스레드?

  • VM Background Thread : Compile, Optimization, GC를 수행하는 Background 데몬 스레드.
  • 기타 User Thread : thread.SetDaemon()

 

 

Concurrency, Parallelism, Parallel and Concurrency

 

Concurrency?

https://blog.kakaocdn.net/dn/XHuvv/btqUo8YDtkZ/EDBIYFN00kik3nGk1khCAk/img.png

하나의 코어가 여러 프로세스를 번갈아가며 실행하는 것을 의미한다. 이는 사용자에게 동시에 실행되는 것처럼 보이게 만드는 효과를 가지며, 단위 시간 내에 더 많은 일을 처리한다.

 

프로세스 간의 콘텍스트 스위칭이 발생한다.

 

Concurrency의 장단점

장점

  • CPU의 처리량이 증가한다.
  • 자원의 활용도가 증가한다.
  • 프로세스 간의 대기시간이 감소된다.

단점

  • Context Switching에 대한 Overhead가 발생한다.

 

Parallelism?

하나의 프로세스를 분할하여 처리

https://blog.kakaocdn.net/dn/lsJcS/btqUtXB0wCK/whsWh7APILE2QEkbHJpnP0/img.png

여러 개의 코어가 하나의 프로세스의 작업을 분할하여 처리하는 것을 의미할 수 있다. 이는 내부적으로 동작하는 스레드의 개수만큼 CPU에 할당할 수 있음을 의미한다.

 

화면을 랜더링 하는 스레드, 계산을 진행하는 스레드, 서버와 통신하는 스레드 등...

 

한 번에 여러 프로세스를 실행

https://blog.kakaocdn.net/dn/cYwXYo/btqUseRMDBc/AX17UVqB6Xrb59UMMCp1Rk/img.png

이와 같이 각 코어가 별개의 프로세스를 동작시킴으로써 단위 시간 내에 여러 프로세서를 동작시키는 것을 의미할 수도 있다.

 

Parallelism의 장단점

장점

  • 하나의 프로세스를 분할하여 여러 작업으로 처리할 수 있다.
  • 하나의 작업에 대해 가용 자원을 더 많이 할당할 수 있다.
  • 여러 프로세스에 대해서도 동시에 수행할 수 있다.

단점

  • 단일 코어 방식보다 어려운 작성 방식을 가진다.
  • 프로세스 분할 처리 시 발생하는 추가 비용이 더 크다. 데이터 전송, 동기화, 통신, 전환 등
  • 각각의 시스템 아키텍처에 맞게 알고리즘 로직의 조정이 필요하다.

 

Parallel and Concurrency

https://blog.kakaocdn.net/dn/bzCy2i/btqUvCxoJng/dkGcWQfs1JGR9bkIY6CbU1/img.png

여러 개의 코어에서 여러 프로세스들을 번갈아 실행하는 상황을 의미한다.

 

물리적인 개념(Parallel)과 논리적인 개념(Context Switching)이 연계된 것이다.

 

 

Thread 클래스와 Runnable 인터페이스

자바에서 멀티 스레드 방식을 지원하기 위해 존재하는 API이다.

 

 

Java에서 Thread를 생성하는 방법

  1. Runnable 인터페이스를 구현하는 것 필요한 기능만 포함하는 구현체가 필요한 경우
  2. Thread Class를 사용하는 것 일반적인 로직 처리 상에서 Async 한 메서드, Thread Pool 사용 시
  3. Thread Class를 상속받는 것 기존 스레드 기능을 확장해야 하는 경우

 

스레드의 상태

효율적인 스레드 관리를 위해서 알아야할 쓰레드 상태 값

 

NEW : 스레드가 생성되었지만, 아직 시작되지 않은 상태

 

RUNNABLE : 실행 중 또는 실행 가능한 상태

 

BLOCKED : 동기화 블록에 의해서 실행 중지된 상태 lock이 풀릴 때까지 대기

 

WAITING : 스레드의 작업이 대기하고 있는 상태

 

TIMED_WAITING : 특정 시간만큼 대기하도록 지정된 상태

 

TERMINATED : 스레드가 종료된 상태

 

 

스레드의 우선순위

자바 스레드에는 우선순위라는 상태 변수가 존재한다. 이 값에 따라 스레드가 접근 권한을 얻을 때까지의 대기시간이 달라질 수 있는데, 우선순위가 높은 쓰레드가 자주 동작하는 로직을 처리하는 경우 낮은 우선순위를 가지는 스레드는 처리되지 못하고 기아 상태에 빠질 수 있다.

private int priority;

// 우선 순위 상수 값

public static final int MAX_PRIORITY = 10 // 최대 우선 순위

public static final int MIN_PRIORITY = 1 // 최소 우선 순위

public static final int NORM_PRIORITY = 5 //보통 우선 순위

// 우선 순위 관련 메서드

public final int getPriority() {
    return priority;
}

public final void setPriority(int newPriority) {
    ThreadGroup g;
    checkAccess();
    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
        throw new IllegalArgumentException();
    }
    if((g = getThreadGroup()) != null) {
        if (newPriority > g.getMaxPriority()) {
            newPriority = g.getMaxPriority();
        }
        setPriority0(priority = newPriority);
    }
}

 

 

동기화?

여러 개의 스레드가 하나의 변수, 객체를 사용하고자 할 때, 선점한 스레드를 제외하고 나머지 스레드들은 접근할 수 없도록 대기하게 하는 것이다.

 

자바에서는 synchronized를 사용하여 해당 기능을 지원한다.

  • Java의 synchronized 범위는 Object 단위의 단일 개체 잠금이다.
    • 모든 Object는 하나의 모니터를 지니고 있다.
  • 하나의 스레드가 Monitor의 Lock을 획득하면 내부의 다른 임계 영역에도 접근할 수 있다.
  • 스레드가 객체 내부의 임계영역에 접근할 경우, Lock 을 통해서 접근하게 되며, 다른 쓰레드는 해당 쓰레드가 임계영역에서 나올 때까지 Wait Queue 에서 대기하여야 한다.

 

synchronized Method

  • 해당 Method를 사용하기 위해서는 우선적으로 Lock을 획득하여야 한다.

  • 해당 Method가 Instance Method인 경우 Method를 호출하는 객체에 대하여서 Lock을 획득한다.

    어떠한 스레드가 해당 객체의 method 단위의 임계 영역을 접근한다면, 이 객체의 다른 메서드를 사용하는 스레드들도 모두 Block 상태가 된다.

     

      class b {
    
      A a = new A();
          a.someMethod(); // instance a 에 대한 Lock을 획득하고 호출한다.
    
      }
    
      class A {
          public synchronized void someMethod() {
              // Do SomeThing
        }
    
      }
  • Class (static) Method인 경우 해당 method가 속해진 Class Instance에 대한 Lock을 획득한다.

    즉 해당 class insatance가 여러 개가 존재하더라도, 해당 메서드에 대해 동시 접근이 불가능하다.

    왜냐하면 Class (Static) Object는 Heap 영역에 하나만 존재하기 때문이다.

      class B {
          A.someMethodIsStatic(); // class A 에 대한 Lock을 획득하고 호출한다.
      }
    
      class A {
          public synchronized static void someMethodIsStatic() {
              // Do SomeThing
        }
    
          public synchronized void someMethod() {
              // Do SomeThing
        }
      }

 

 

synchronized Statement

해당 방식은 런타임 내의 분기를 이용하여 동기화가 필요한 작은 공간에서만 Lock을 획득하게끔 합니다.

if (조건식) {
        synchronized (object) {
                // do something
        }
} else {
        // do something
}

 

해당 조건이 맞는 경우에만 동기화를 진행하고 로직 실행

 

Object 내부의 동기화 관련 메서드

 

wait

해당 Object를 선점하고 있다가 접근 권한을 다른 스레드에게 넘기고 notify, notifyAll 이 호출될 때까지 해당 스레드를 대기하게끔 설정하는 메서드이다.

  • Wait Set에 들어가게 된다.
  • 추가 : Block 은 아직 접근조차 하지 못한 상태를 말하는 것이다.

 

notify

해당 Object의 선점을 위해 Wait Set에 대기 중인 하나의 스레드를 실행 대기 상태로 만든다.

  • Entry Set의 스레드들과 같이 경합에 참여한다.

 

notifyAll

해당 Obejct의 선점을 위해 Wait Set에 대기 중인 모든 스레드를 실행 대기 상태로 만든다.

  • notify와 동일한 경합 상태가 발생한다.

 

Object 선점 흐름

 

Object에 대한 Thread 초기 접근.

  • Thread → Entry set → Monitor Lock 획득 시도
  • 선점 Thread Wait() → notify, notifyAll → 대기 중인 Thread 중 Monitor Lock 획득

 

Object에 대한 Thread 접근 (Wait 이후)

  • Wait set → notify, notifyAll → 대기중인 Thread 중 Monitor Lock 획득

Wait Set의 Thread가 임계 영역을 벗어나기 위해서는 접근 권한을 선점하고 Lock을 놓는 방법뿐이다.

 

 

 

synchronized vs java lock?

1.5에 추가된 Concurrent 패키지의 Lock 은 기존의 synchronized 블록을 확장한 좀 더 유연하고 정교한 동기화 방식이다.

 

독점적인 락을 사용해야 할 때, 여러 범위에서 락을 확보하고 사용되는 경우, 세심한 락 조절을 통해 독점적인 락을 사용하는 부분과 시간을 최소화하고, 여러 유틸 성 기능을 제공할 수 있게끔 지원한다.

 

 

synchronized 와의 차이점

  • 별도의 synchronized Statement 등을 만들어 사용하던 방식을 사용하지 않고도 메서드를 통하여서 임계 영역을 지정, 제공할 수 있다. (lock(), unlock())

  • synchronized를 사용할 때 발생했던 문제인 기아상태에 대한 해결책을 지원한다. 공정성 제공 fairness Property

    • synchronized는 스레드 들의 진입 순서를 보장하지 않는다.

      Thread의 기아상태가 발생한다.

  • tryLock()을 통해 해당 임계 영역 접근 불가능할 때 스레드를 차단하지 않고 가능한 경우에만 잠금을 획득하게끔 지원하고, 스레드가 차단된 시간을 감소시킵니다.

  • 임계 영역에 대한 접근 권한을 얻기 위해 대기 중인 스레드는 인터럽트 할 수 없다.

synchronized는 스레드의 진입 순서를 보장하지 않는 반면에 Lock은 진입 순서를 보장한다.

 

 

 

Thread의 기아상태?

다수의 스레드가 동일한 임계영역에 계속해서 접근을 한다면, 하나 이상의 쓰레드가 접근 권한을 받지 못하는 경우가 발생할 수 있다.

 

기아 상태의 원인

  • 높은 우선순위에 따른 스레드들의 CPU 독점

    자바에선 쓰레드들 각각에 대한 우선순위를 설정할 수 있다. (1~10) 일반적으로는 우선순위를 지정하지 않고 사용하는 것이 좋다.

  • 임계 영역 동기화로 인하여 진입 대기 중 Block 상태 (기아 상태)

    스레드가 임계영역으로 진입하기 위해 계속 접근 시도를 하지만, 다른 쓰레드 들의 진입권 획득으로 인하여 영원히 Block된 상태로 남아있을 수 있다는 것이다.

  • wait 호출로 인한 객체에 대한 무한한 대기 상태

    둘 이상의 쓰레드가 동일한 객체의 wait 호출로 인하여 대기 상태에 놓였을 때. notify 메서드는 어떤 스레드를 깨울 것인지 알 수 없다. 깨어나지 못하고 계속 대기할 수 있다.

이 문제를 해결하기 위해선 공정성의 개념을 지원하여야 한다.

 

 

 

Volatile long vs Atomic long

Volatile

해당 변수를 메인 메모리에서만 접근 가능하게끔 하는 것을 말한다.

  • 읽기 연산, 쓰기 연산 등의 결과를 Cache 메모리가 아닌 메인 메모리에서 반영한다.
  • volatile 변수와 같이 사용되는 모든 변수들도 메인 메모리에서 함께 조작된다.
  • Cache 메모리에 변수의 값을 복사해가지 않는다.

스레드 간의 가시성을 해결하기 위한 것으로, 각 스레드는 다른 스레드에서 기록하지 않은 값을 확인할 수 없기에 발생하는 것이 가시성이며, 잘못된 결과로 나타날 수 있다.

 

volatile 만을 사용해볼 만한 상황.

하나의 스레드만이 해당 변수에 대한 연산을 진행하고, 다른 모든 스레드가 조회만 할 경우

  • 읽는 스레드에서 가장 최근에 쓰인 값을 보는 것을 보장한다.

 

volatile의 성능 이슈.

JVM은 프로그램 성능의 향상을 위해 내부 코드에 대하여서 Code reordering를 진행하게 되는데, 동기화되지 않는 코드나 Volatile에 대하여선 진행을 하지 않기에, 상대적으로 성능상의 문제점이 있을 수 있다.

 

사용하지 못하는 상황

여러 스레드가 읽기, 쓰기 연산을 모두 조작하는 경우에는 변경점이 많으므로 문제가 발생할 수 있다.

 

여러 쓰레드 캐싱 값을 쓰기에 메인 메모리에 존재하는 변수 값을 이상하게 덮어쓸 수 있다.

 

이 상황을 해결하기 위해 사용하는 것이 Atomic 패키지이며, Atomic 변수의 개념은 volatile 변수에 대하여서 원자성을 만족하는 연산을 제공하는 것이다. CAS 알고리즘

 

 

 

간단하게 CAS 짚고 넘어가기

CAS에 대한 세 가지 매개변수

  • 현재 값을 교체해야 하는 메모리 위치 V
  • 제일 최근에 스레드를 통해 읽은 값 A
  • V 위치에 덮어써야 되는 새로운 값 B

 

V는 A라는 값을 가지고 있어야 되며, 해당 경우 B를 덮어쓰면 된다. (V == A , V = B)

  • 같은 위치, 연산이라고 판단하게 된다.

그렇지 않은 경우에는 값을 반영하지 않고 재시도한다.

 

 

 

데드락

프로세스, 스레드가 작업에 필요한 모든 자원을 얻지 못하여 선점한 자원을 가지고 대기 중인 상태로 교착 상태라고도 한다.

 

멀티프로그래밍 환경에서 한정된 자원을 여러 곳에서 사용하려 할 때 발생할 수 있다.

 

데드락의 발생 조건

  • 상호 배제

    자원은 한 번에 한 프로세스만이 사용할 수 있어야 한다.

  • 점유 대기

    최소환 하나의 자원을 점유하고 있으면서, 다른 프로세스에 할당되어 사용되고 있는 자원을 사용하기 위해 대기하고 있어야 한다.

  • 비선점

    다른 프로세스, 스레드에 할당된 자원의 접근 권한은 그 작업이 끝나기 전까지 뺏어올 수 없다.

  • 순환 대기

    각각의 프로세스, 스레드가 상대의 자원을 점유하는 상태여야 한다.

'Live Study' 카테고리의 다른 글

Live Study_Week 15. Lambda Expression  (0) 2021.03.01
Live Study_Week 13. I/O  (0) 2021.02.20
Live Study_Week 12. Annotation  (0) 2021.02.02
Live Study_Week 11. Enum  (0) 2021.01.28
Live Study_Week 09. 예외 처리  (0) 2021.01.11

 

 

함수형 프로그래밍이란?

순수 함수들을 조합하여 사이드 이펙트(부작용)를 제거하고, 모듈화를 높여 유지보수와 생산성을 올리는데 초점을 둔 패러다임이다. Non Blocking과 Asynchronous , Parallel Programming을 구현, 지원하는데 적합하다고 한다.

 

함수형 프로그래밍의 사고방식은 문제 해결에 대해 선언적인 행위(함수)들을 조합(구성)하여 해결하는 것이다.

자바도 스칼라, 자바스크립트와 같은 함수형 패러다임 언어 혹은 지원하는 언어, 기술들의 대두로 인하여 JDK 8부터 해당 기능을 도입하게 되었다.

 

함수형 인터페이스, 람다, 메서드 레퍼런스, 디폴트 메서드, Future, Fork-Join, 리액티브 등 추가

 

 

1급 객체

함수형 프로그래밍의 중요한 조건 중 하나를 의미한다. 이는 변수나 데이터 구조 안에 넣을 수 있고, 인자로 전달 가능하고, 동적으로 속성 값을 할당 가능하며, 리턴 값으로도 사용될 수 있는 메서드를 말한다.

 

 

순수 함수

같은 입력에 대해서 항상 같은 출력을 반환하는 형태의 메서드를 의미한다. 이는 인자로 주어지는 것들만 사용하고, 상태를 유지하지 않으며, 함수 외부 변수를 사용하지 않는 형태이다. 멀티스레드 환경에서 안전한 메서드.

public int minus(int a, int b) {
    return a - b;
}

 

 

고차 함수

1급 객체의 서브셋으로 메서드의 인자로 전달할 수 있고, 리턴 값으로 사용할 수 있는 메서드를 의미한다.

 

자바에서의 고차 함수는 하나 이상의 인자로 Lambda 식을 가지고 있거나, Lambda 식을 반환하는 메서드를 말한다.

default Comparator<T> reversed() {
    return Collections.reverseOrder(this);
}

 

 

익명 함수

이름이 없는 함수를 의미하며, 이는 Lambda expression으로 구현되는 메서드를 의미한다.

 

 

합성 함수

원하는 값을 도출하기 위해, 둘 이상의 메서드를 조합하는 것을 말한다. Stream API 처럼 데이터가 흐르는 파이프라인을 구성하고, 필요한 메서드를 연속적으로 호출하여 구현한다.

 

 

 

람다식 사용법

Example

람다는 익명 클래스를 단순화하고 표현식을 메서드의 인수로 넘기거나, 객체를 생성하는 데 사용한다.

 

익명 객체 생성하기

Thread thread = new Thread(new Runnable() {

        @Override
        public void run() {
                System.out.println("Hello Lambda!");
        }

});

thread.start();

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

// 익명 객체 내부에 한줄 코드만 존재하는 경우, { }와 return을 생략하고 작성할 수 있다.

Thread thread = new Thread( () -> System.out.println("Hello Lambda!") ); 

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

// 2줄인 경우

Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello Lambda!");
            System.out.println("Line");
        }
});

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

Thread thread = new Thread(() -> {
        System.out.println("Hello Lambda!");
        System.out.println("Line");
});

 

 

메서드의 인수로 넘기기

// JDK Dynamic Proxy code
TargetObject getRealObject = (TargetObject) Proxy.newProxyInstance(TargetObject.class.getClassLoader(), new Class[]{TargetObject.class}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            TargetObject targetObject = new TargetObjectImpl();

            System.out.println("Before");
            Object invoke = method.invoke(targetObject, args);
            System.out.println("After");

            return invoke;
        }});

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

// InvocationHandler 인터페이스의 익명 객체를 Lambda Expression으로 대체하였다.

TargetObject realObject = (TargetObject) Proxy.newProxyInstance(TargetObject.class.getClassLoader(), new Class[]{TargetObject.class},
            (proxy, method, args) -> {
                TargetObject targetObject = new TargetObjectImpl();

                System.out.println("Before");
                Object invoke = method.invoke(targetObject, args);
                System.out.println("After");

                return invoke;
            });

 

 

인자 값 사용(소비) 하기

인자로 전달된 값을 사용하여 데이터를 처리하고 로직을 완료한다. 리턴 타입은 void이다.

String value = "val";
someMethod(value, (value) -> System.out.println(value));

 

 

불 값 리턴하기

인자로 전달된 값을 기반으로 불 값을 리턴한다. 주로 값의 유효성 검증 및 비교 작업을 담당한다.

String value = "val";
someMethod(value, (value) -> "val".equals(value));

 

 

두 객체 비교하기

각각의 타입으로 전달된 두 객체를 비교하여 결과 값을 리턴한다.

String value1 = "val";
String value2 = "val";

Boolean result = someMethod(value1, value2, (value1, value2) -> value1.equals(value2));

 

 

객체 생성하기

인자로 전달되는 것 없이 객체를 생성한다. 리턴 타입은 void이다.

Lob lob = someMethod(() -> new Lob());

 

 

객체를 변경하기

인자로 전달된 값을 변경해서 다른 객체로 리턴한다.

String value = "hello";
String subString = someMethod(value, (value) -> value.substring(0, 3))

 

 

값을 조합, 병합하기

인자로 전달된 값을 조합해서 새로운 값을 리턴한다.

String prefix = "hello ";
String suffix = "world";
String fullText = someMethod(prefix, suffix, (prefix, suffix) -> prefix + suffix);

 

 

 

함수형 인터페이스 ( java.util.function )

하나의 추상 메서드를 가지고 있는 인터페이스나 @FunctionaInterface Annotation이 작성된 인터페이스를 말한다.

 

@FunctionaInterface는 함수형 인터페이스 형식을 제약 사항으로 지정한다.

 

Function <T, R>

T라는 타입의 한 인자를 받아서 R 타입으로 반환하는 함수형 인터페이스이다.

 

받은 인자를 다른 값으로 변환해서 리턴할 때, 값을 변경하거나 매핑할 때 사용한다.

 

R apply (T value)

compute(), merge(), replaceAll() 등의 메서드를 구현하는 데 사용된다.

public static Long parseLong(String value, Function<String, Long> function) {
    return function.apply(value);
}

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

public static void main(String[] args) {

        System.out.println(parseLong("100", Long::valueOf));
}

 

유사한 함수형 인터페이스

  • BiFunction <T, U, R>

    각각의 타입을 가지는 두 인자를 받아서 다른 타입으로 반환하는 함수형 인터페이스이다.

     

    R apply(T t, U u);

    compute(), merge(), replaceAll() 등의 메서드를 구현하는 데 사용된다.

     

    String, Integer  -> Long

      public static Long parseLong(String value1, Integer value2 , BiFunction<String, Integer, Long> function) {
          return function.apply(value1, value2);
      }
    
      ----------------------
    
      public static void main(String[] args) {
    
              System.out.println(parseLong("100", 100, (value1, value2) -> Long.parseLong(value1)+value2));
      }
  • DoubleFunction

    입력되는 인자가 double인 함수형 인터페이스이다.

    R apply(double value);

     

  • DoubleToIntFunction

    입력되는 인자가 double, 반환 타입은 int인 함수형 인터페이스이다.

    int applyAsInt(double value);

     

  • DoubleToLongFunction

    입력되는 인자가 double, 반환 타입은 long인 함수형 인터페이스이다.

    long applyAsLong(double value);

     

  • IntFunction

    입력되는 인자가 int인 함수형 인터페이스이다.

    R apply(int value);

     

  • IntToDoubleFunction

    입력되는 인자가 int, 반환 타입은 int인 함수형 인터페이스이다.

    double applyAsDouble(int value);

     

  • IntToLongFunction

    입력되는 인자가 int, 반환 타입은 long인 함수형 인터페이스이다.

    long applyAsLong(int value);

     

  • LongFunction

    입력되는 인자가 long인 함수형 인터페이스이다.

    R apply(long value);

     

  • LongToDoubleFunction

    입력되는 인자가 long, 반환 타입은 double인 함수형 인터페이스이다.

    double applyAsDouble(long value);

     

  • LongToIntFunction

    입력되는 인자가 long, 반환 타입은 int인 함수형 인터페이스이다.

    int applyAsInt(long value);

     

  • ToDoubleBiFunction <T, U>

    각각의 타입을 가지는 두 인자를 받아서 double를 반환하는 함수형 인터페이스이다.

    double applyAsDouble(T t, U u);

     

  • ToIntBiFunction <T, U>

    각각의 타입을 가지는 두 인자를 받아서 int를 반환하는 함수형 인터페이스이다.

    int applyAsInt(T t, U u);

     

  • ToIntFunction

    T라는 타입의 한 인자를 받아서 int를 반환하는 함수형 인터페이스이다.

    int applyAsInt(T value);

     

  • ToLongBiFunction <T, U>

    각각의 타입을 가지는 두 인자를 받아서 long을 반환하는 함수형 인터페이스이다.

    int applyAsInt(T t, U u);

     

  • ToLongFunction

    T라는 타입의 한 인자를 받아서 long를 반환하는 함수형 인터페이스이다.

    long applyAsLong(T value);

     

Consumer

T라는 타입의 한 인자를 받아서 소모하고 아무것도 반환하지 않는 함수형 인터페이스이다.

 

인자를 전달하여 처리한 뒤 결과를 리턴 받을 필요가 없을 때 사용한다.

 

void Aceept(T t)

forEach() 등의 메서드를 구현하는 데 사용된다.

public static void printList(List<String> list, Consumer<String> consumer) {
    for (String item : list) {
        consumer.accept(item);
        }
}

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

public static void main(String[] args) {

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");

        printList(list, System.out::println);
}

 

유사한 함수형 인터페이스

  • BiConsumer <T, U>

    입력되는 인자가 2개인 함수형 인터페이스이다.

    void accept(T t, U u);

     

  • DoubleConsumer

    기본형 타입인 double의 인자를 사용하는 함수형 인터페이스이다.

    void accept(double value);

     

  • IntConsumer

    기본형 타입인 int 타입의 인자를 사용하는 함수형 인터페이스이다.

    void accept(int value);

     

  • LongConsumer

    기본형 타입인 long 인자를 사용하는 함수형 인터페이스이다.

    void accept(long value);

     

  • ObjDoubleConsumer

    입력되는 인자가 2개이며 1번째 인자로는 Object, 2번째로는 double 타입의 인자를 사용하는 함수형 인터페이스이다.

    void accept(T t, double value);

     

  • ObjIntConsumer

    입력되는 인자가 2개이며 1번째 인자로는 Object, 2번째로는 int 타입의 인자를 사용하는 함수형 인터페이스이다.

    void accept(T t, int value);

     

  • ObjLongConsumer

    입력되는 인자가 2개이며 1번째 인자로는 Object, 2번째로는 long 타입의 인자를 사용하는 함수형 인터페이스이다.

    void accept(T t, long value);

     

Supplier

T라는 타입의 한 인자를 반환하는 함수형 인터페이스이다.

 

여기서 T는 받는 인자가 아닌 반환 타입을 지정한다.

 

T get();

orElseThrow(), orElseGet(), requireNonNull() 등의 메서드를 구현하는 데 사용된다.

public static String executeValue(Supplier<String> supplier) {
    return supplier.get();
}

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

public static void main(String[] args) {

        String val = "value";
    System.out.println(executeValue(() -> val) );
}

 

유사한 함수형 인터페이스

  • BooleanSupplier

    boolean 값을 반환하는 함수형 인터페이스이다.

    boolean getAsBoolean();

     

  • DoubleSupplier

    double 값을 반환하는 함수형 인터페이스이다.

    double getAsDouble();

     

  • IntSupplier

    int 값을 반환하는 함수형 인터페이스이다.

    int getAsInt();

     

  • LongSupplier

    long 값을 반환하는 함수형 인터페이스이다.

    long getAsLong();

 

Predicate

T라는 타입의 한 인자를 받아서 그에 대한 Boolean 값을 제공하는 함수형 인터페이스이다.

 

주로 데이터를 필터링하거나, 조건에 맞는지 여부를 확인하는 데 사용한다.

 

boolean test(T t);

removeIf(), filter() 등의 메서드를 구현하는 데 사용된다.

public static boolean check(String value, Predicate<String> predicate) {
    return predicate.test(value);
}

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

public static void main(String[] args) {

        System.out.println(check("hello", (value) -> value.length() > 4) );
}

 

유사한 함수형 인터페이스

  • BiPredicate <T, U>

    각각의 타입을 가지는 두 인자를 받고 boolean을 반환하는 함수형 인터페이스이다.

    boolean test(T t, U u);

     

  • DoublePredicate

    double 타입인 인자를 받아 boolean을 반환하는 함수형 인터페이스이다.

    boolean test(double value);

     

  • IntPredicate

    int 타입인 인자를 받아 boolean을 반환하는 함수형 인터페이스이다.

    boolean test(int value);

     

  • LongPredicate

    long 타입인 인자를 받아 boolean을 반환하는 함수형 인터페이스이다.

    boolean test(long value);

     

Operator

특정한 정수, 실수형 데이터를 처리하는 데 사용되는 함수형 인터페이스이다.

 

Operator 인터페이스

  • UnaryOperator extends Function <T, T>

    T라는 타입의 한 인자를 받아서 해당 타입으로 반환하는 함수형 인터페이스이다.

     

    내부적으로 Function을 상속받았다.

      static <T> UnaryOperator<T> identity() {
          return t -> t;
      }

 

  • BinaryOperator extends BiFunction <T, T, T>

      public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
          Objects.requireNonNull(comparator);
          return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
      }
    
      public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
          Objects.requireNonNull(comparator);
          return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
      }

 

  • DoubleBinaryOperator

    double 타입의 두 인자를 받고 해당 타입으로 값을 반환하는 함수형 인터페이스이다.

    double applyAsDouble(double left, double right);

     

  • DoubleUnaryOperator

    인자와 반환 타입이 double인 메서드를 제공하는 함수형 인터페이스이다.

    double applyAsDouble(double operand);

     

  • IntBinaryOperator

    int 타입의 두 인자를 받고 해당 타입으로 값을 반환하는 함수형 인터페이스이다.

    int applyAsInt(int left, int right);

     

  • IntUnaryOperator

    인자와 반환 타입이 int인 메서드를 제공하는 함수형 인터페이스이다.

    int applyAsInt(int operand);

     

  • LongBinaryOperator

    long 타입의 두 인자를 받고 해당 타입으로 값을 반환하는 함수형 인터페이스이다.

    long applyAsLong(long left, long right);

     

  • LongUnaryOperator

    인자와 반환 타입이 long인 메서드를 제공하는 함수형 인터페이스이다.

    long applyAsLong(long operand);

     

 

번외 : Runnable

해당 인터페이스는 JDK 1.0 부터 존재해왔던 오래된 인터페이스이지만, 자바의 함수형 인터페이스 제약을 지키고 있기에 추가하였다.

 

인자 타입과 반환 타입이 존재하지 않는 메서드를 제공하는 (논리적인) 함수형 인터페이스이다.

 

public abstract void run();

Runnable runnable = () -> System.out.println("class.run");
runnable.run();

 

 

번외 : Comparator<T>

해당 인터페이스는 JDK 1.2 부터 존재해왔던 인터페이스로 역시 자바의 함수형 인터페이스 제약을 지키고 있다. 

@FunctionalInterface도 타입 래벨에 정의되어 있음을 알 수 있다.

 

 

T라는 타입을 가지는 두 인자를 받아 int 값을 반환하는 메서드를 제공하는 함수형 인터페이스이다. 

객체, 값 객체 간의 우선 순위 즉 정렬을 위한 값을 얻어내는데 주로 사용된다.

 

int compare(T o1, T o2);

 Arrays.sort(split, (o1, o2) -> o2.compareTo(o1));

 

 

 

Variable Capture (Lambda Capturing)

Lambda의 body에서 인자로 넘어온 것 이외의 변수를 접근하는 것을 Variable Capture라고 한다.

 

 

Lambda는 인스턴스, 정적 변수final로 선언된 혹은 final처럼 사용되고 있는 지역 변수를 참조할 수 있다.

 

지역변수를 사용할 때에는 해당 변수에게 값의 재할당이 일어나서는 안된다.

// final int value = 100;
// 값을 통한 초기화 이후에 value의 값은 변경되어서는 안된다.
int value = 100;

// 순수 함수 형태가 아닌 사용 방식.
Lob lob = () -> new Lob(value);

--------------------
예제

@FunctionalInterface
interface Lob {
    public abstract void print();
}

//private final int a = 10; 도 동일하게 재정의가 가능하다.
private int a = 10;

public void hello() {

    final int b = 20;
    int c = 30;
    int d = 40;

    final Lob lobA = () -> System.out.println(a);
    lobA.print();

    // a 재정의
    a = 20;
    final Lob lobB = () -> System.out.println(a);
    lobB.print();

    final Lob lobC = () -> System.out.println(b);
    lobC.print();

    final Lob lobD = () -> System.out.println(c);
    lobD.print();

    final Lob lobE = () -> System.out.println(d);
    lobE.print();
}

public static void main(String[] args) {
        Lambda lambda = new Lambda();
        lambda.hello();
}

 

왜 지역 변수를 재정의해서 사용할 수 없는가?

이는 지역 변수가 스택 영역에 존재하기에 발생하는 문제점인데, 해당 변수를 초기화하는 스레드가 사라져 변수 할당이 해제된 경우에, Lambda를 실행하고 있는 별도의 스레드가 해당 변수 정보에 접근하려는 경우가 발생할 수 있다.

 

지역변수는 스레드 간에 공유가 되지 않는다.

 

자바에서는 람다를 실행하는 스레드의 스택에 지역 변수 할당 시 생성한 변수의 복사본을 저장하여 동작시키게 되는데, 이 값이 변경되었을 경우를 예측할 수 없기에 final 혹은 재할당 방지 제약조건을 걸어둔 것이다.

 

 

인스턴스 변수와 정적 변수는 왜 이런 제약조건을 걸지 않았는가?

이는 앞서 말했던 스레드 간의 가시성 문제의 연장선인데, 인스턴스 변수와 정적 변수는 모든 스레드에서 접근 가능한 값이기 때문에, 값의 변경이 이루어져도 직접적으로 접근할 수 있다.

 

 

 

메서드, 생성자 레퍼런스

메소드 참조?

JDK 8에 추가된 기능으로 함수를 메서드의 인자로 전달하는 것을 메서드 참조라고 한다.

 

해당 방식을 사용함으로써 해당 메서드 시그니처를 여러 곳에서 재사용할 수 있고, 기본적인 제공 메서드와 커스텀한 메서드 모두를 사용할 수 있다는 장점이 있다.

 

추가적으로 메서드 참조는 람다 표현식을 한번 더 축약적으로 표현할 수 있으며, 그를 통해 가독성을 향상할 수 있다.

 

람다 표현식을 대체하기보다는 상호 보완적인 관계를 형성한다.

// example

public static void printList(List<String> list, Consumer<String> consumer) {
        for (String item : list) {
            consumer.accept(item);
        }
}

public static void main(String[] args) {

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");

        // 람다 표현식
        printList(list, (item) -> System.out.println(item));

        // 메서드 참조
        // 한 단계 더 축약된 것을 알 수 있다.
        printList(list, System.out::println);
}

 

메서드 참조를 사용하는 방법?

  • 정적 메서드의 참조 : ( Class::staticMethod )

    static으로 정의한 메서드를 참조할 때 사용하는 방식이다.

     

    Long.parseLong(value); → Long::parseLong

      // 람다 표현식
      someMethod(value, (value) -> Long.parseLong(value));
    
      // 메서드 참조
      someMethod(value, Long::parseLong);

 

  • 비한정적 메서드의 참조 (인스턴스) : ( Class::instanceMethod )

    public 혹은 protected로 정의된 메서드를 참조할 때 사용되는 방식이다.

     

    비한정적이라는 표현은 구문 자체가 특정한 객체를 참조하기 위한 변수를 지정하지 않는다는 것을 의미한다.

     

    string.length() → String::length

      // 람다 표현식
      someMethod(value, (value) -> value.length());
    
      // 메서드 참조
      someMethod(value, String::length);

 

  • 한정적 메서드 참조 (외부 인스턴스 변수) : ( Instance::instanceMethod )

    Lambda body 외부에서 선언된 객체의 메서드를 호출하거나, 객체를 생성해서 메서드 참조할 때 사용되는 방식이다.

     

    한정적이라는 표현은 참조하는 메서드가 특정 객체의 변수로 제한되는 것을 의미한다.

     

    lob.isLob() → lob::isLob

      Lob lob = new Lob(true);
    
      // 람다 표현식
      someMethod(() -> lob.isLob());
    
      // 메서드 참조
      someMethod(lob::isLob);

 

 

생성자 참조?

자바 언어에서는 메서드와 생성자를 구분하고 있다.

 

문법적인 구조상의 차이로는 리턴 타입이 없다는 것이 있지만, 메서드는 접근 권한만 있다면 호출 가능하나, 생성자는 객체가 생성될 때에만 호출할 수 있다.

 

생성자 참조는 ClassName::new 형식으로 작성된다.

 

생성자 참조는 새로운 객체를 생성하고 리턴하는 경우 등에서 사용된다.

// 람다 표현식
someMethod(String name -> new Lob(name)).forEach((Lob lob) -> System.out.println(lob.name))

// 생성자 참조
someMethod(Lob::new).forEach((Lob lob) -> System.out.println(lob.name))

// 생성자, 메서드 참조
someMethod(Lob::new).forEach(System.out::println);

 

 

 

참고 자료

  • Practical 모던 자바

 

 

 

'Live Study' 카테고리의 다른 글

Live Study_Week 10. Multithreading programming  (3) 2021.03.02
Live Study_Week 13. I/O  (0) 2021.02.20
Live Study_Week 12. Annotation  (0) 2021.02.02
Live Study_Week 11. Enum  (0) 2021.01.28
Live Study_Week 09. 예외 처리  (0) 2021.01.11

 

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));

}

+ Recent posts