왜 WebJars 인가?

 

   쉽기 때문에 !

   Browser(PC/Client)에서 사용하는 라이브러리의 경우 유지관리가 어렵다.

  예를 들면, Jquery 1.x 버전을 사용하다가 Jquery 3.x 의 버전으로 바꾸고 싶을 때 libary의 경로에 대해서 정적 파일도 

  다시 받아야 하고, 프로젝트에 추가하는 작업들이 필요하다. 심지어 해당 라이브러리의 버전이 무엇인지 알기도 쉽지 

  않다. 왜냐! Client Side에서 돌아가는 라이브러리의 버전을 별도 문서로 관리하기도 어렵고 파일명에 추가하기도 

  어렵기 때문에 버전 관리가 너무 어렵다. 

 

   하지만 !

 

spring boot로 구성한 resources/templates/index.html 파일에 대해서 head 부분을 아래와 같이 추가하고 

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
    <script src="/webjars/jquery/dist/jquery.min.js"></script>
    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
</head>
<body>

 

build.gradle 설정을 아래와 같이 한다면, 

dependencies {
    ...

    compile('org.webjars.bower:jquery:3.3.1')
    compile('org.webjars:sockjs-client:1.1.2')
    compile('org.webjars:stomp-websocket:2.3.3-1')
    compile('org.webjars:webjars-locator:0.30')

    ...
}

 

각 Client Side에서 제공되는 라이브러리 ( Dependancy )에 대해서 버전 및 파일 관리를 손쉽게 할 수 있게 제공한다.

 

실제 webJars 사이트에 접속해보면, 

 

 

   Maven, Gradle , Ivy, Buildr, Leiningen 등에서 라이브러리를 제공하고 쉽게 찾아서 적용할 수 있기 때문에 상당히 편리하다. 

 

webjars 사이트 메인화면

 

지원되는 범위 또한 상당히 넓다. 

 

  • Play Framework 2
  • Xltrum
  • Servlet 3
  • Servlet 2
  • JSF
  • Grails
  • Dropwizard
  • Spring Boot
  • Sprint MVC
  • Apache Tapesty
  • Apache Wicket
  • Pippo
  • Ring ( Clojure )
  • Dandelion
  • Vert.x Web

 

 

 

'알아보기' 카테고리의 다른 글

0002 Reactive Programming 4  (0) 2019.12.09
0002 Reactive Programming 3  (0) 2019.12.08
0004 Reactive Programming 2  (0) 2019.12.05
0004 Reactive Programming 1  (0) 2019.12.04
0002 Realm 활용하기 2  (0) 2019.11.22

React Native 는 프로젝트 구성 및 효과적인 폴더 구성은은 어떤 방법들이 있을 지 알아보려 한다. 

 


dream$ npx react-native init folderStructure

좌측의 이미지는 React Native를 이용해 처음으로 생성한 폴더&파일이다. 

 

효과적인 폴더 구성을 알아보기 위해서 기본적으로 생성되는 폴더와 파일에 대해서 알아보겠다. 

 

index.js : react native app이 실행될 때, 진입점이 된다.

App.js : App이 로딩 되었을 때 개발자에 의해 개발되어 표시되는 최초화면이다. index.js 파일에서 import하여 App의 진입화면으로 사용한다.

App.json : RunTime 시점에 자바스크립트 코드를 이용해서 접근할 수 있다. 

ios : IOS를 기동하기 위한 프로젝트 폴더이다.

android : Android를 기동하기 위한 프로젝트 폴더이다. 

babel.config.js - React Native Application 을 위한 Preset 설정

metro.config.js - Metro Bundler 설정 파일

node_modules - node module이 저장되는 폴더  

package.json - node modules 종속성 관리 설정 파일 

 

 

 

 

 

React Native 를 활용해서 모바일 App을 만들때 가장 고민되었던 부분이 폴더 구성이었다. 

관련 자료과 내가 구성한 방식들을 고민해봤을 때, 어떤 방식으로 구성하는 것이 가장 효율적이냐에 대한 부분은 정답이 없다는 생각이 들었다. 

 

다만, 다수의 React Native 폴더를 보았을 때는 아래와 같이 구성하는 것이 가장 적합한 것 같았고, 오히려 폴더의 구성 보다 React Native Component 상에서 각각의 폴더 내의 Component들을 가져오는 방식이 더 중요하다고 생각한다.

 

폴더의 구성 방식은 아래와 같이 구성해봤다.

 

src 폴더 구성

위의 폴더 구성을 바탕으로 필요한 Component 를 Import 할 때, 

 

 이전에는 아래와 같이 했다면,

 

import { DataBase, DataBaseWrite } from '../../../components/database';

 

아래와 같이 조정해봤다.

 

import { DataBase, DataBaseWrite } from '_components/database';

 

위와 같이 처리한 이유는 ../../../은 가독성이 유지보수 측면에서 최악이라고 생각했기 때문에 이를 _components 라는 alias를 대신해서 사용한 것이다.

 

 


1. Package.json에 아래 항목 추가 

 

"babel-plugin-module-resolver": "^4.0.0",

"eslint-import-resolver-babel-module": "^5.1.0",

"eslint-plugin-import": "^2.19.1",

 

 

2. 아래의 빨간색으로 표시한 항목에 대해서 파일 추가 또는 내용 추가 

 

react-native init {프로젝트 명} 로 생성한 프로젝트 폴더 경로 안에 아래의 빨간색 표시 박스의 파일을 수정 또는 추가한다. 

 

아래에 설정된 폴더의 경로는 자신이 구성한 프로젝트 폴더의 구성에 따라 변경하여 사용하면 된다. 

 

추가된 파일 

 

 

3. .babelrc 파일 추가

babel-plugin-module-resolver를 설치하고 난 뒤에 .babelrc 파일에 아래와 같이 설정하면 alias를 절대경로로 사용할 수 있다.

root를 사용하면, 내가 원하는 src 폴더에 대해서 프로젝트 root 를 지정할 수 있고, 위에서 봤던 이미지 처럼 

 

 _components/database 와 같이 ../../../ 을 사용하지 않고 직관적으로 이해할 수 있게 경로 설정이 가능하다. 이는 폴더의 depth가 깊어지기 시작하면 정말 필요해진다. 

{
	"plugins": [
		[
			"module-resolver",
			{
				"cwd": "babelrc",
				"root": ["./src"],
				"extensions":[".js",".ios.js",".android.js"],
				"alias": {
					"_assets":"./src/assets",
					"_components":"./src/components",
					"_atoms":"./src/components/atoms",
					"_molecules":"./src/components/molecules",
					"_organisms":"./src/components/organisms",
					"_navigations":"./src/navigations",
					"_scenes":"./src/screens",
					"_services":"./src/services",
					"_styles":"./src/styles",
					"_utils":"./src/utils"
				}
			}
		]
	]
}

 

4. jsconfig.json

Visual Studio Code의 코드 인텔리센스를 위해서 프로젝트 디렉터리에 jsconfig.json 파일을 만든다. 

이는 npm plugin을 통해서 babel-plugin-module-resolver를 import 하여 사용하는 것이기 때문에, VS Code에서 import 시에 

지정한 alias에 맞게 작업시 파일을 불러오지 못하는 경우가 생기는데, 이 경우에 해당 파일슬 생성하여 alias에 맞게 자동완성 기능을

적용할 수 있다.

{
	"compilerOptions": {
		"baseUrl":".",
		"paths": {
			"_assets":["src/assets/*"],
			"_components":["src/components/*"],
			"_atoms":["src/components/atoms/*"],
			"_molecules":["src/components/molecules/*"],
			"_organisms":["src/components/organisms/*"],
			"_navigations":["src/navigations/*"],
			"_scenes":["src/screens/*"],
			"_services":["src/services/*"],
			"_styles":["src/styles/*"],
			"_utils":["src/utils/*"]
		}
	}
}

 

5. .eslintrc.js

eslint가 설정되어 있는 경우에는 eslint에서 사용하는 소스 코드의 경로 상의 문제를 해결하기 위해서 아래와 같이 alias를 지정한다. 

module.exports = {
  root: true,
  extends: '@react-native-community',
    plugins: ['import'],
    settings: {
      'import/resolver': {
        node: {
          paths: ['src'],
          alias: {
            _assets: './src/assets',
            _components: './src/components',
            _atoms: './src/components/atoms',
            _molecules: './src/components/molecules',
            _organisms: './src/components/organisms',
            _navigations: './src/navigations',
            _scenes: './src/screens',
            _services: './src/services',
            _styles: './src/styles',
            _utils: './src/utils',
          },
        },
      },
    },
};

 

'따라해보기' 카테고리의 다른 글

0001 React Native App 개발기 6-3  (0) 2019.12.13
0001 React Native App 개발기 6-2  (0) 2019.12.13
0001 React Native App 개발기 6-1  (0) 2019.12.12
0001 React Native App 개발기 5  (0) 2019.12.08
0001 React Native App 개발기 4  (0) 2019.11.22

Reactive Native App 개발기 6-2 에서 알아본 Component Lifecycle 상에 제공되는 함수들의 용도와 활용방법에 대해서 알아보겠다.

 


Mounting

 

  • constructor

       만약 state를 초기화 하거나 함수를 바인딩 하는 것이 아니라면, 해당 Component를 위한 constructor는 구현할 필요가 없다.   

       constructor는 마운트 되기 전에 호출된다. react component의 constructor를 구현할 때, 다른 문자을 작성하기 전에 super(props)

       를 반드시 호출해야한다. 그렇지 않으면 this.props 는 constructor에서 undefined으로 찾을수 없다. 

 

        constructor는 주로 아래의 2가지를 위해서 사용되는데, 

   

          1. this.state 를 초기화 할 때. 

          2. event handler 함수를 객체에 바인딩할 때. 

 

        constructor 내부에서 setState를 호출해서는 안되고 필요한 경우, this.state를 이용하여 선언시에 초기값으로 직접 값을 설정해야한다. side-effect(외부간섭/부가효과) 또는 구독을 constructor에서 처리하는 것을 피해야한다. 필요한 경우 componentDidMount를 대신 사용해야한다. 

 

  • static getDerivedStateFromProps(props, state)

        해당함수는 render 함수가 호출되고 난 이후에 바로 실행된다. 초기 마운트 시점과 구독 갱신 시점에 모두 영향을 받으며, 상태를 업데이트 하기 위해서 객체를 하거나 아무것도 업데이트 하지 않기 위해서 null을 반환해야 한다. 

 

        1. 만약 부가 효과를 실행해야 한다면, componentDidUpdate를 활용하라. 

        2. prop가 변경되었을 때 일부 데이터에 대해서만 재계산하고 싶다면, 메모제이션을 활용해야한다.

        3. 만약 일부 state를 초기화하고 싶다면, 해당 링크의 방식(fully controlled  , fully uncontrolled)을 활용할수 있다. 

 

  • render()

        render 함수는 class 컴포넌트에서 필요한 함수로써, function 컴포넌트에서는 필요하지 않다. 

 

        render 함수는 shouldComponentUpdate의 반환 값이 false 일 경우 실행되지 않는다.  

 

  • componentDidMount()

        Component가 마운트되고 난후 즉시 componentDidMount가 실행된다. Dom 노드에 대한 초기화가 필요한 경우 여기에서 작업해야한다. 원격의 Endpoint로 부터 데이터를 가져와야하는 경우에도 해당 함수의 위치에서 진행하면 된다. 만약 setState를 이 함수안에서 사용한다면 rendering을 발생시킬 것이고 이는 브라우저가 해당 화면을 갱신하기 전에 발생한다. 이것은 비록 render가 두번 호출되더라도 사용자가 중간과정을 보지 않을 수 있도록 보장한다. 하지만 이 패턴은 성능 상의 이슈를 야기할 수 있기 때문에 주의해야한다. 

 따라서 가능하다면 constructor()에서 초기값을 할당할 수 있도록 해야한다./

 


Updating 

 

  • static getDerivedStateFromProps(props, state) 
  • shouldComponentUpdate(nextProps, nextState)

        해당 함수는 React가 component의 결과아 state나 props의 변화에 의해 영향 받지 않았을 경우 shouldComponetUpdate에 의해 알릴 수 있다. 해당 함수는 신규 props나 state의 변화에 의해 rendering되기 전에 실행된다. 기본 값은 true이며 forceUpdate가 실행된 경우에는 해당 함수를 호출되지 않는다. 

 

        이 함수는 성능 최적화로써 오로지 존재하는데, shouldComponentUpdate를 사용하는 것대신에 PureComponent를 사용하는 것을 고려하라. 

 

       결과값을 false로 반환할 경우, UNSAFE_componentWillUpdate, render, componentDidUpdate를 실행하지 않을 것이다. 

 

  • render()
  • getSnapshotBeforeUpdate(prevProps. prevState)

      가장 최근의 렌더링된 결과가 commit 되었을 때 실행된다. 이 함수는 정보가 잠재적으로 변경될 가능성이 있을때, 컴포넌트가 Dom으로 부터 일부 정보를 보관하는 것을 허용한다. 생명주기 족에서 반환된 모든 값은 파라미터로 componentDidUpdate()로 전달될것이다. 

 

  • componentDidUpdate(prevProps, prevState, snashot)

       해당 함수는 갱신이 일어난 후에 즉시 실행된다. 해당 함수는 초기 render를 위해서 호출되지 않는다. 

 

       해당 함수는 컴포넌트가 갱신되었을 때, DOM에 대해서 처리할 수 있는 기회로 사용하면 된다. 이는 네트워크 요청등의 처리를 하기에 적합하다. 

 


UnMounting

  • componentWillUnMount

        해당함수는 컴포넌트가 언마운트 되기 전이나 제거될 때 호추된다. 해당 함수안에서 초기화 및 정리를 실행할 수 있다. 

 


ErrorHandling 

  • static getDerivedStateFromError()

       해당 생명 주기는 상위 Component(해당 함수를 선언한 컴포넌트)를 감싸고 있는 자식(하위)컴포넌트에서 에러가 발생하였을 경우 실행된다. 파라미터로 전달된 에러를 받을 수 있고 state를 갱신하기 위한 값을 반환해야 한다. 

 

  • componentDidCatch(error, info)

      해당 생명 주기는 상위 Component(해당 함수를 선언한 컴포넌트)를 감싸고 있는 자식(하위)컴포넌트에서 에러가 발생하였을 경우 실행된다. 파라미터로 전달된 에러를 받을 수 있고 state를 갱신하기 위한 값을 반환해야 한다. 

 

     2개의 파라미터를 전달받는데, 

      

       1. error - 전달된 Error

       2. info - 컴포넌트(에러를 던진 컴포넌트에 대한 정보를 포함하는 키) 스택가지고 있는 객체 

 

 


기존의 생명주기 함수들

 

  아래의 함수들은 version 17 까지는 동작할 예정이다. 하지만 16.3 이후로 정립된 생명주기 함수들을 활용하여 개발하는 것을 추천한다. 

이전부터 컴포넌트의 생명주기와 관련된 함수들에 대한 많은 이슈가 있어왔고 이를 해소하고자 16.3 이후 부터 위의 함수들을 제공하고 있다. 

 

 UNSAFE_componentWillMount()

    마운팅이 일어났을 때, 실행된다. 이 것은 render 함수 전에 실행된다. 

 

 UNSAFE_componentReceiveProps()

    해당 함수의 경우, 버그와 불일치성을 초래하고 있다. 따라서 사용하지 않는 것을 권고한다. 

    마운트된 컴포넌트가 새로운 props를 받았을 때 실행된다. 

 

 UNSAFE_componentWillUpdate()

     새로운 props나 state를 받았을 때, 실행된다. 

 

    해당함수는 만약 shouldComponentUpdate가 false를 반환할 경우 실행되지 않을 것이다. 

 

 

3편에 걸쳐서 컴포넌트의 생명주기에 대해서 알아보았다. 

'따라해보기' 카테고리의 다른 글

0001 React Native App 개발기 7  (0) 2019.12.14
0001 React Native App 개발기 6-2  (0) 2019.12.13
0001 React Native App 개발기 6-1  (0) 2019.12.12
0001 React Native App 개발기 5  (0) 2019.12.08
0001 React Native App 개발기 4  (0) 2019.11.22

지난 편(React Native App 개발기 6-1)에서 알아본 컴포넌트 생명주기에 대해서 코드로 알아보려 한다. 

 

 아래의 설명한 사항은 React Version 16.4를 기준으로 동작하는 함수 및 절차이다. 


React Native 메인  

메인 화면 ( 클릭버튼 ) 

 - Component의 생명 주기를 알아보기 위해서 Main Component를 Import하여 아래와 같이 사용하였다. 

 - React Hook의 useState를 이용해서 함수 내에서 state를 사용하여 상태를 Main Component로 전달하였다. 

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 * @flow
 */

import React, { useState } from 'react';
import { View,  Text, Button } from 'react-native';
import Main from './src/pages/Main';
import ErrorBoundary from './src/pages/Error'

const App = () => {

  const [ value, setValue ] = useState({ data : "" });

  const onPressEvent = () => {
    setValue({ data : "11" });
  }


  return (
    <>
      <ErrorBoundary>
        <View style={{ flex : 1, justifyContent : 'center', alignItems:'center' }} >
          <Text>Component Life Cycle 입니다.</Text>
          <Button title="클릭" onPress={onPressEvent} ></Button>
        </View>
        <Main stateProps={value} propsData={11} />
      </ErrorBoundary>
    </>
  );
};

export default App;

 

React Native ErrorBoundary

import React, { Component } from 'react';


class ErrorBoundary extends Component {
    constructor(props) {
      super(props);
    }

    static getDerivedStateFromError(){
        console.log("getDerivedStateFromError");
    }
  
    render() {
      return this.props.children;
    }
}

export default ErrorBoundary;

 

React Native Main

 

 - Component Lifecycle에 활용되는 모든 함수를 아래와 같이 정의하여 Component 호출 시점, Props 전달 시점, State 전달 시점을 알아보려한다.  

 - 각각의 함수에 console.log를 추가하여 호출 시점을 알아보겠다.

import React, { Component } from 'react';
import {
  View,
  Text,
} from 'react-native';

class Main extends Component{
    constructor(props){
        super(props);

        console.log("constructor");
    }

    static getDerivedStateFromProps(props, state){
        console.log("getDerivedStateFromProps") ;  
    }

    shouldComponentUpdate(nextProps, nextState){
        console.log("shouldComponentUpdate");
    }

    getSnapshotBeforeUpdate(prevProps, prevState){
        console.log("getSnapshotBeforUpdate");
    }

    componentDidMount(){
        console.log("componentDidMount");
    }
    
    componentWillUnmount(){
    	console.log("componentWillUnmount");
    }

    render(){
        return (
            <View style={{flex : 1, justifyContent : 'center', alignItems:'center' }} >
                <Text>Component 입니다.</Text>
            </View>
        )
    }
}

export default Main;

 - 메인화면이 호출될 때, ( command + R 을 눌러 메인화면을 새로고침 했다. )

메인화면 로딩시

 

호출 순서 : constructor -> getDerivedStateFromProps -> componentDidMount 

 

 - 메인화면에서 버튼을 클릭했을 때, ( React Hook을 이용해 state를 component로 전달했다. )

버튼 클릭 시

 

 호출 순서 : getDerivedStateFromProps -> shouldComponentUpdate 

 

 - 메인화면에서 다른 화면으로 전환될 때

 

 호출 순서 : componentWillUnmount 

 

 - ErrorBoundary를 감싸고 있는 하위 컴포넌트에서 에러가 발생했을 때, 

 

 호출 순서 : getDerivedStateFromError

 

 

Mounting에서 부터 Unmounting 까지의 절차는 아래의 Link에서 가져온 이미지에서 잘 정리되어 있었다. 

http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

 

 

다음편에서 16.3 기점으로 나뉜 컴포넌트 생명주기 함수들에 대해서 자세히 설명할 예정이다. 

'따라해보기' 카테고리의 다른 글

0001 React Native App 개발기 7  (0) 2019.12.14
0001 React Native App 개발기 6-3  (0) 2019.12.13
0001 React Native App 개발기 6-1  (0) 2019.12.12
0001 React Native App 개발기 5  (0) 2019.12.08
0001 React Native App 개발기 4  (0) 2019.11.22

( 해당들은 2019년 12월 12일 기준으로 작성되었음 )

 

React Native App을 개발하기 위해서 Component Cycle에 대해서 잘 알아두어야 하는데, 이번에는 Component Cycle 및 Component를 구성하는 방식에 대해서 알아보려 한다. 

 


Component's lifecycle

 

Component Lifecycle

Component의 생명주기는 4가지로 분류할 수 있다. 

 

1. Mounting

 

  컴포넌트 객체가 생성된 후, DOM으로 주입(삽입)될 때

 

  React Native의 실행되는 함수의 순서는 

 

    - constructor

    - static getDerivedStateFromProps ( React v16.3 부터 제공 )  - componentWillReceiveProps ( React v16.3 이전 ) 

    - render

    - componentDidMount

 

2. Updating

 

  브라우저 상에 React 컴포넌트가 표시되거나, 새로운 갱신(변경)이 발생했을 때

 

  React Native의 실행되는 함수 순서는 

 

    - static getDerivedStateFromProps ( React v16.3 부터 제공 )   - componentWillReceiveProps ( React v16.3 이전 ) 

    - shouldComponentUpdate

    - render 

    - getSnapshotBeforeUpdate ( React v16.3 부터 제공 ) - componentWillUpdate ( React v16.3 이전 )

    - componentDidUpdate

 

3. UnMounting

 

  해당 컴포넌트가 더이상 필요하지 않고, 컴포넌트가 해제(분리)될 때

 

  React Native 의 실행되는 함수 순서는 

   

    - componentWillUnmount

 

4. Error Handling

 

  생명주기 함수 내, 생성자 내, 모든 하위 컴포넌트의 생성자 내의 렌더링 과정 상에서 에러가 발생할 때

 

   React Native 의 실행되는 함수 순서는 

 

     - getDerivedStateFromError

     - componentDidCatch ( React v16.3 부터 제공 ) 

 

     > getDerivedStateFromError, componentDidCatch는 동일한 에러를 잡는데, 실제 전달되는 인자가 다르다. 

        ~ getDerivedStateFromError 는 "render" 단계에 호출되며, 외부 함수에 의한 영향 ( SideEffect ) 까지 포함되지 않는다. 

        ~ sideEffect가 포함되어야하는 경우에는 componentDidCatch를 사용해야 한다. 

 

위의 4가지 상태에 따라 React Native(React)에서 제공해주는 함수들이 존재하며, 화면 초기화 시점이나 state가 SetState에 의해서 변경될 때, state에 의해서 갱신이 일어날 경우, 아래에 제공하는 함수를 이용해서 우리가 원하는 기능 또는 초기화 작업 등을 처리할 수 있다. 

 

Component Lifecycle은 React v16.3 이전과 이후로 나뉘고, React v17 부터는 이전에 Deprecated된 함수들은 사용할수 없게 되었다. 

 

다음 편에서 실질적인 코드와 상세한 생명주기에 대해서 알아보겠다. 

 

'따라해보기' 카테고리의 다른 글

0001 React Native App 개발기 6-3  (0) 2019.12.13
0001 React Native App 개발기 6-2  (0) 2019.12.13
0001 React Native App 개발기 5  (0) 2019.12.08
0001 React Native App 개발기 4  (0) 2019.11.22
0001 React Native App 개발기 3  (1) 2019.11.17

이번에는 Reactive Stream을 이용해서 실질적으로 현업에서 사용할 만한 코드를 작성해보겠다. 

 

 요구사항 

 

   - Oracle DB , OJdbc, DBCP2

   - org.reactivesreams

   - ExecutorService

 

  A, B라는 Oracle DB User에 대해서 TB_USER로 부터 USER_ID를 가져와 출력하라는 요구 사항을 접수 받았다. 하지만 이 DB User의 수는 상황에 따라서 증가하거나 줄어들 수 있으며 그에 따라 고객이 빠른 응답을 받을 수 있도록 구성이 필요하다고 하자. 이와 같은 요구사항을 ReactiveStream(Java8)을 이용해서 구현해본다. 


1번 : Main 진입점 

 

 DataSource를 생성한뒤 각각의 Command 객체 생성시 전달하여 DB Select시에 사용하려고 한다. 

 해당 부분은 Controller 를 통해서 전달받은 파라미터 및 구분자에 의해서 동적으로 변경될 수 있는 부분이라고 가정한다. 

즉 commandHadlerList의 배열이 요청 갯수에 따라서 증가하거나 감소할 수 있다. 

public static void main(String[] args) {

        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("oracle.jdbc.driver.OracleDriver");
        dataSource.setUrl("jdbc:oracle:thin:@***********:****/****");
        dataSource.setUsername("*****");
        dataSource.setPassword("*****");

        BasicDataSource dataSource2 = new BasicDataSource();
        dataSource2.setDriverClassName("oracle.jdbc.driver.OracleDriver");
        dataSource2.setUrl("jdbc:oracle:thin:@***********:****/****");
        dataSource2.setUsername("*****");
        dataSource2.setPassword("*****");

        CommandHandler commandHandler1 = new CommandHandler(1);

        commandHandler1.add(new SelectDBCommand(dataSource));
        commandHandler1.add(new SelectDBCommand(dataSource2));

        List<CommandHandler> commandHandlerList = new ArrayList<>();

        commandHandlerList.add(commandHandler1);

        Publisher<CommandHandler> publisher = new SamplePublisher(commandHandlerList);

        Subscriber<CommandHandler> subscriber = new SampleSubscriber(commandHandlerList.size());

        publisher.subscribe(subscriber);
        
}

2번 : Publisher 

 

 Observer 패턴의  Observable과 같은 역할을 하며, Subscriber를 구독할 수 있게 설정하여 실행시 Publihser에서 Subscriber로 onNext, onComplete, onError를 처리할 수 있다. 

public class SamplePublisher implements Publisher<CommandHandler> {

    private final List<CommandHandler> commandHandlerList;

    public SamplePublisher(List<CommandHandler> commandHandlerList){
        this.commandHandlerList = commandHandlerList;
    }

    @Override
    public void subscribe(Subscriber<? super CommandHandler> subscriber) {
        subscriber.onSubscribe(new Subscription() {
            @Override
            public void request(long n) {
                commandHandlerList.forEach(subscriber::onNext);

                // 데이터 전송 처리 완료
                subscriber.onComplete();
            }

            @Override
            public void cancel() {

            }
        });
    }
}

3번 : Subcriber - Publisher 내부에서 호출한 각기 함수(onNext, onComplete, onError)에 맞게 진행된다. 

 

 SampleSubscriber가 생성되는 시점에 만들어진 ExectorService에 대해서  Java의 Future를 활용하여 onComplete 시점에 ExecutorService를 Shutdown 시킬 수 있게 처리하였다. 

public class SampleSubscriber implements Subscriber<CommandHandler> {

    private final ExecutorService executorService;

    List<Future<?>> futures = new ArrayList<>();

    public SampleSubscriber(int executeCount){
        this.executorService = Executors.newFixedThreadPool(executeCount);
    }

    @Override
    public void onSubscribe(Subscription subscription) {
        subscription.request(1);
    }

    @Override
    public void onNext(CommandHandler commandHandler) {
        futures.add(executorService.submit(commandHandler::StartAPICall));
    }

    @Override
    public void onError(Throwable t) {

    }

    @Override
    public void onComplete() {
        for(Future<?> future : futures) {
            try {
                future.get();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        executorService.shutdown();
    }
}

4번 : Command Handler 

 

3번 항목과 처리되는 방식은 거의 유사하며, Command Handler에서 각각의 Command를 ExecutorService를 이용하여  실행시키는 구조이다. 

public class CommandHandler {

    private final List<Command> commandList = new ArrayList<>();
    List<Future<?>> futures = new ArrayList<>();

    private final ExecutorService executorService;

    public CommandHandler(int threadCount){
        executorService = Executors.newFixedThreadPool(threadCount);
    }

    public void StartAPICall() {

        commandList.forEach(command -> {
            futures.add(executorService.submit(command::execute, "Success"));
        });

        for(Future<?> future : futures) {
            try {
                future.get();
            }catch(Exception e){
                e.printStackTrace();
            }
        }

        executorService.shutdown();
    }

    public synchronized void add(Command command) {
        commandList.add(command);
    }
}

5번 : Command 

public interface Command {
    public void execute();
}

6번 : Command 구현 - SelectDBCommand 

 

 실질적으로 데이터를 조회하고, 이를 용도에 맞게 처리하는 Command의 구현체이다. SelectDBCommand 객체가 

생성되는 시점에 필요한 정보를 모두 합성 방식을 이용하여 미리 준비하고, execute가 호출 되는 시점에 활용한다. 

아래와 같은 방식으로 구성했을 때 ThreadSafe하게 구현되었다고 할 수 있다. 

public class SelectDBCommand implements Command {

    private final DataSource dataSource;

    public SelectDBCommand(DataSource dataSource){
        this.dataSource =  dataSource;
    }

    @Override
    public void execute() {

        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

        List<User> list =  jdbcTemplate.query("SELECT USER_ID FROM *********", new CustomMapper());

        System.out.println("START" + Thread.currentThread().getName());
        list.stream().forEach(item -> {
            System.out.print(item.getUSER_ID() + ",");
        });
        System.out.println("END" + Thread.currentThread().getName());
    }
}

7번 : CustomMapper - JDBC Rowmapper 

 

JdbcTemplate의 RowMapper를 구현하여 필요에 맞게 값을 바인딩 처리한다. 

public class CustomMapper implements RowMapper<User> {
    @Override
    public User mapRow(ResultSet resultSet, int i) throws SQLException {
        User user = new User();
        user.setUSER_ID(resultSet.getString("USER_ID"));
        return user;
    }
}

'알아보기' 카테고리의 다른 글

0006 WebJars  (0) 2019.12.18
0002 Reactive Programming 3  (0) 2019.12.08
0004 Reactive Programming 2  (0) 2019.12.05
0004 Reactive Programming 1  (0) 2019.12.04
0002 Realm 활용하기 2  (0) 2019.11.22

Reactive Programming 2 편에서 다루었던 개발건에 추가적인 요구사항에 생겼다고 해보자, 

 

 

 요구사항

 

 ( 2편의 요구사항 )

   일정한 시간을 주기로 2000건의 정수 배열을 받은 후 해당 각각의 결과 값을 합하는 요구사항이 발생하였다.

   생성해야할 정수 배열은 난수로 이루어진 값으로 1000개의 아이템을 가지고 있다. 

 

 추가 요구사항 

    난수로 생성되는 2000건의 정수 배열에 대해서 각 배열의 총합이  음수인 값은 제외하고, 양수인 값을 추출하여하 출력한다.  

 

 ( 사실, 자바8의 Stream API를 사용한다면 이런 힘들고 복잡한 작업을 전혀할 필요가 없다. 우리는 Reactive Stream의 기본이 되는 개념을 익히고 이를 실질적으로 활용하기 위한 Akka, RXJava, Java9 Flow API 등등을 쉽게 이해하기 위해 진행 중인 것이다. )

 


2편에서 다룬 코드 샘플을 아래와 같이 변경하였다. 

아래의 코드에서 중요한 부분은 Publisher 1, Publisher 2를 연결하면서 처리되는 Data 흐름이다.  

데이터 전달 순서 : CustomPublisher -> FilterPublisher -> CustomSubscriber  

 

1번 : Main 진입점

public static void main(String[] args) {
	// 1000개의 난수 배열을 가진 아이템 생성 - 각각을 합쳐

	BlockingQueue<Command> blockingQueue = new ArrayBlockingQueue<Command>(500);

	for (int i = 0; i < 500; i++) {

	IntStream intStream = new Random().ints();

	List<Integer> integerList = intStream.limit(1000).boxed().collect(Collectors.toList());

	Command command = new IntegerSumCommand(integerList);

	blockingQueue.add(command);
}

// Publisher 1
Publisher<Command> publisher = new CustomPublisher(blockingQueue);

// Publisher 2
Publisher<Command> publisher1 = new FilterPublisher(publisher);

// Subscriber
Subscriber subscriber = new CustomSubscriber();

// Start !
publisher1.subscribe(subscriber);
}


2번 : Publisher 1 : Publisher를 구현한 CustomPublisher 

public class CustomPublisher implements Publisher<Command> {

    private final BlockingQueue<Command> blockingQueue;

    private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();

    public CustomPublisher(BlockingQueue<Command> blockingQueue){
        this.blockingQueue = blockingQueue;
    }

    @Override
    public void subscribe(Subscriber subscriber) {

        subscriber.onSubscribe(new Subscription() {
            @Override
            public void request(long n) {
                while (!blockingQueue.isEmpty()){
                    try {
                        Command command = blockingQueue.take();

                        subscriber.onNext(command);

                    } catch (InterruptedException e) {

                        e.printStackTrace();
                    }
                }

                scheduledExecutorService.schedule(() -> {
                    if(Counter.get() == 500){
                        subscriber.onComplete();

                        if(!scheduledExecutorService.isShutdown()){
                            scheduledExecutorService.shutdown();
                        }
                    }
                }, 1, TimeUnit.SECONDS);

            }

            @Override
            public void cancel() {

            }
        });
    }
}

 

3번 : Publisher 2 : Publisher 1을 전달 받아 값을 필터링하는 FilterPublisher

public class FilterPublisher implements Publisher<Command> {

    private Publisher<Command> publisher;

    public FilterPublisher(Publisher<Command> publisher){
        this.publisher = publisher;
    }

    @Override
    public void subscribe(Subscriber<? super Command> subscriber) {
        this.publisher.subscribe(new Subscriber<Command>() {
            @Override
            public void onSubscribe(Subscription subscription) {
                subscriber.onSubscribe(subscription);
            }

            @Override
            public void onNext(Command command) {
                if(!command.isMinus()){
                    subscriber.onNext(command);
                }
            }

            @Override
            public void onError(Throwable t) {
                subscriber.onError(t);
            }

            @Override
            public void onComplete() {
                subscriber.onComplete();
            }
        });
    }
}

4번 : Subscriber : 최종적으로 Publisher 2의 데이터 처리를 전달받아 최종값을 출력한다.

public class CustomSubscriber implements Subscriber<Command> {

    private ExecutorService executorService = Executors
            .newFixedThreadPool(1000);

    private Subscription subscription;

    @Override
    public void onSubscribe(Subscription subscription) {
        this.subscription = subscription;
        subscription.request(1);
    }

    @Override
    public void onNext(Command command) {
        executorService.execute(() -> {
            int total = command.getTotal();

            System.out.println(String.format("총 값은 %s", total));
        });
    }

    @Override
    public void onError(Throwable throwable) {
        subscription.cancel();
    }

    @Override
    public void onComplete() {
        if(!executorService.isShutdown()){
            executorService.shutdown();
        }
    }
}

 

5번 : Command Interface 

public interface Command {
    public int getTotal();
    public boolean isMinus();
}

 

6번 : Command Implementation 

public class IntegerHandlerCommand implements Command {

    private Optional<Integer> optionalInteger;

    public IntegerHandlerCommand(List<Integer> integerList){

        Counter.addAndGet();

        this.optionalInteger = integerList.stream().reduce(Integer::sum);
    }

    @Override
    public int getTotal() {
        return this.optionalInteger.orElse(0);
    }

    @Override
    public boolean isMinus() {
        return this.optionalInteger.orElse(0) < 0;
    }
}

 

7번 : Counter Class - Executor Service 종료를 위한 Counter 서비스 

public class Counter {
    private static int count = 0;

    public synchronized static int addAndGet(){
        return count ++;
    }

    public synchronized static int get(){
        return count;
    }
}

'알아보기' 카테고리의 다른 글

0006 WebJars  (0) 2019.12.18
0002 Reactive Programming 4  (0) 2019.12.09
0004 Reactive Programming 2  (0) 2019.12.05
0004 Reactive Programming 1  (0) 2019.12.04
0002 Realm 활용하기 2  (0) 2019.11.22

Flex를 활용한 화면 구성에 어느정도 익숙해졌다면 데이터를 이용해서 목록을 조회하여 이를 화면에 뿌려보자.

 

 


React Native의 List 컨트롤에 대해서 알아보겠다. 

 

  • FlatList

  React Native 에서 제공하는 Flat List를 이용에 데이터를 바인딩 했을 때 아래와 같이 표현된다.

Flat List

<FlatList
          data={[
            {key: 'Devin'},
            {key: 'Dan'},
            {key: 'Dominic'},
            {key: 'Jackson'},
            {key: 'James'},
            {key: 'Joel'},
            {key: 'John'},
            {key: 'Jillian'},
            {key: 'Jimmy'},
            {key: 'Julie'},
          ]}
          renderItem={({item}) => <Text style={styles.tab_1_item}>{item.key}</Text>}
          />

 

  • SectionList

React Native에서 제공하는 Section List를 이용해 데이터를 바이딩 했을 때 아래와 같이 표현할 수 있다. 

Section List

 

<SectionList
   sections={[
   {title: 'D', data: ['Devin', 'Dan', 'Dominic']},
   {title: 'J', data: ['Jackson', 'James', 'Jillian', 'Jimmy', 'Joel', 'John', 'Julie']},
   ]}
   renderItem={({item}) => <Text style={styles.tab_2_item}>{item}</Text>}
   renderSectionHeader={({section}) => <Text style={styles.tab_2_sectionHeader}>{section.title}</Text>}
   keyExtractor={(item, index) => index}
 />

 

  • List 의 아이템을 사용자의 요구사항에 따라 변경하고 싶을 때, 

FlatList나 SectionList 속성의 renderItem을 이용해서 리스트의 아이템에 아이콘을 추가하거나 버튼 외에 화면 이동 등 우리가 원하는 다양한 작업을 진행할 수 있다. 

 <View style={[styles.tab_1_container]} >
        <FlatList
          data={[
            {key: 'Devin'},
            {key: 'Dan'},
            {key: 'Dominic'},
            {key: 'Jackson'},
            {key: 'James'},
            {key: 'Joel'},
            {key: 'John'},
            {key: 'Jillian'},
            {key: 'Jimmy'},
            {key: 'Julie'},
          ]}
          renderItem={({item}) => renderItem(item)}
          />
  </View>
    
 const renderItem = (item) => {
    return (
        <TouchableOpacity style={{flex:1, flexDirection : "row"}} activeOpacity={0.8}  >  
            <View style={{flex:8, padding : 10}} >
                <Text>{item.key} </Text>
            </View>
            <TouchableOpacity style={{flex :2}} >
                 <View style={{flex:2, padding : 10}}>
                    <Image
                        source={require('./icon.png')}
                        style={styles.tab_3_icon} 
                    />  
                </View>
            </TouchableOpacity>
            
        </TouchableOpacity> 
    )
  }

 

 


그렇다면 Flat List를 Realm의 데이터를 이용해서 조회하고자 할 경우,  아래와 같이 사용할 수 있다. 

Realm의 데이터로 조회했을 때,

 

<View>
   <TouchableOpacity style={{flex:1, flexDirection : "row"}} activeOpacity={0.8}  >  
      <View style={{flex:8, padding : 10}} >
          <Text>{item.userId} </Text>
      </View>
      <TouchableOpacity style={{flex :2}} >
          <View style={{flex:2, padding : 10}}>
            <Image
            source={require('./icon.png')}
            style={styles.tab_3_icon} 
            />  
          </View>
      </TouchableOpacity>
  </TouchableOpacity> 
</View>


const LineSchema = {
  name : 'LineUser',
    properties : {
      userId : 'string',
      userPwd : 'string',
      position : 'string?'
  }
}

const realm = new Realm({schema : [ LineSchema ]});

let data = realm.objects("LineUser").map((data, index) => (
  data
));

return (
  <View style={[styles.tab_1_container]} >
    <FlatList
      data={data}
      renderItem={({item}) => renderItem(item)}
    />
  </View>
)

 

기본적인 List를 설정하고, Realm을 이용해서 바인딩하는 방식에 대해서 알아보았다. 

'따라해보기' 카테고리의 다른 글

0001 React Native App 개발기 6-2  (0) 2019.12.13
0001 React Native App 개발기 6-1  (0) 2019.12.12
0001 React Native App 개발기 4  (0) 2019.11.22
0001 React Native App 개발기 3  (1) 2019.11.17
0001 React Native App 개발기 2  (0) 2019.11.15

Reactive Stream을 다루기 위해서 아래의 예제를 바탕으로 요구조건을 변경해가면서 코드 위주로 알아보겠다. 

 

 요구사항 

 

   일정한 시간을 주기로 2000건의 정수 배열을 받은 후 해당 각각의 결과 값을 합하는 요구사항이 발생하였다.

   생성해야할 정수 배열은 난수로 이루어진 값으로 1000개의 아이템을 가지고 있다. 

 

 

Reactive Stream Java 8 버전 기준 


1번, Main :  진입점

public class Sample05_reactiveStream04 {

    public static void main(String[] args) {

        BlockingQueue<Command> blockingQueue = new ArrayBlockingQueue<Command>(2000);

        for (int i = 0; i < 2000 ; i++) {
            IntStream randomValueList = new Random().ints();

            List<Integer> integerList = randomValueList.limit(1000).boxed().collect(Collectors.toList());

            Command command = new InsertToDataBaseCommand(integerList);

            blockingQueue.add(command);
        }

        Publisher publisher = new CustomPublisher(blockingQueue);

        Subscriber subscriber = new CustomSubscriber();

        publisher.subscribe(subscriber);
    }
}

 

2번, Publisher : Publisher를 구현한 CustomPublisher 

public class CustomPublisher<Command> implements Publisher<Command> {

    private final BlockingQueue<Command> blockingQueue;

    private ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

    public CustomPublisher(BlockingQueue<Command> blockingQueue){
        this.blockingQueue = blockingQueue;
    }

    @Override
    public void subscribe(Subscriber<? super Command> subscriber) {
        subscriber.onSubscribe(new Subscription() {
            @Override
            public void request(long n) {
                while (!blockingQueue.isEmpty()) {
                    try {
                        Command command = blockingQueue.take();

                        subscriber.onNext(command);

                    } catch (InterruptedException e) {
                        // Exception handling.
                    }
                }

                executorService.schedule(() -> {
                    if(Counter.get() == 2000){
                        System.out.println("onComplete");
                        subscriber.onComplete();

                        executorService.shutdown();
                    }
                }, 1, TimeUnit.SECONDS);
            }

            @Override
            public void cancel() {

            }
        });
    }
}

 

3번, Subscriber : Subscriber를 구현한 CustomSubscriber 

public class CustomSubscriber implements Subscriber<Command> {

    private Subscription subscription;

    private final ExecutorService executorService = Executors.newFixedThreadPool(1000);

    @Override
    public void onSubscribe(Subscription subscription) {
        this.subscription = subscription;

        this.subscription.request(1);
    }

    @Override
    public void onNext(Command command) {
        executorService.execute(command::work);
    }

    @Override
    public void onError(Throwable t) {
        System.out.println("We Error");
    }

    @Override
    public void onComplete() {
        System.out.println("We Finished");

        executorService.shutdown();
    }
}

 

4번, Command Interface

public interface Command {
    public void work();
}

 

5번, Command Implementation : 실제 구현 객체 

public class InsertToDataBaseCommand implements Command {

    private final List<Integer> integerList;

    public InsertToDataBaseCommand(List<Integer> listInteger){
        integerList = listInteger;
    }

    @Override
    public void work() {
        Optional<Integer> sum = integerList.stream().reduce(Integer::sum);

        System.out.println(Thread.currentThread().getName() +  " Final Result : " + sum);
        System.out.println(Thread.currentThread().getName()  + " " + Counter.incrementAndGet());

    }
}

 

6번, Counter Class :  수행된 횟수를 저장하고, 가져온다. 

public class Counter {

    private static final AtomicInteger atomicInteger = new AtomicInteger();

    public static int incrementAndGet(){
        return atomicInteger.incrementAndGet();
    }

    public static int get(){
        return atomicInteger.get();
    }
}

 


 

'알아보기' 카테고리의 다른 글

0002 Reactive Programming 4  (0) 2019.12.09
0002 Reactive Programming 3  (0) 2019.12.08
0004 Reactive Programming 1  (0) 2019.12.04
0002 Realm 활용하기 2  (0) 2019.11.22
0002 Realm 활용하기 3  (0) 2019.11.22

 

 Reactive Programming은 선언적인 프로그래밍 패러다임으로써 데이터 스트림(Data Stream)과 변화의 전파에 초점을 맞추고 있다. 또한 이것의 핵심은  비동기 (Async) 이벤트와 Observer 디자인 패턴이다. 외부의 이벤트나 데이터가 발생하였을 때, 사용자에게 자연스러운 응답을 주고, 규모 탄력적으로 리소스를 사용하며 실패에 있어서 유연하게 대처할 수 있다. 

 

 

 Reactive Stream은 Reactive Programming을 근간으로 한 개발을 하기 위한 명세이며, Netflix, Pivotal, TypeSafe의 발의로 시작되었다. 

 

Reactive Streams : https://www.reactive-streams.org/

 

 Reactive Stream은 비동기적인 스트림 프로세싱을 막힘없이 ( Non Blocking) 처리하기 위한 하나의 표준이다. 

 

Reactive Stream의 상세 스펙 보기

 

 

기본적인 API 컴포넌트 구성 


  1. Publisher
  2. Subscriber
  3. Subscription
  4. Processor

코드로 작성된 추상화 수준으로 확인을 해보면, 

// Publisher
public interface Publisher<T> {
    public void subscribe(Subscriber<? super T> s);
}

// Subscriber
public interface Subscriber<T> {
    public void onSubscribe(Subscription s);
    public void onNext(T t);
    public void onError(Throwable t);
    public void onComplete();
}

// Subscription
public interface Subscription {
    public void request(long n);
    public void cancel();
}

// Processor
public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {
}

 

 

 

위의 코드에서 Subscriber의 역할에 대해서 아래의 그림으로 확인해보면, 

 

Subscriber 의 동작흐름

 

 

Publisher,  Subscriber, Subscription 사이의 연관관계를 아래와 같이 구성해봤다.

 

Reactive Stream 구성

 

실제로 위의 구성 관계를 이용해서 코드를 작성해보면, 아래와 같이 작성해볼 수 있다. 

 

package com.reactive.reactive.basic11;

import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class PublishOnWithSubscribeOnBoth {

    private static Logger logger = LoggerFactory.getLogger(PublishOnWithSubscribeOnBoth.class);

    public static void main(String[] args) {
        Publisher<Integer> publisher = subscriber -> {
            subscriber.onSubscribe(new Subscription() { 
                @Override
                public void request(long n) {
                    logger.debug("request()");
                    subscriber.onNext(1);
                    subscriber.onNext(2);
                    subscriber.onNext(3);
                    subscriber.onNext(4);
                    subscriber.onNext(5);
                    subscriber.onComplete();
                }

                @Override
                public void cancel() {

                }
            });
        };

        Publisher<Integer> subOnPub = subscriber -> {
            ExecutorService executorService = Executors.newSingleThreadExecutor(new CustomizableThreadFactory(){
                @Override
                public String getThreadNamePrefix() {
                    return "subOn-";
                }
            });

            executorService.execute(() -> publisher.subscribe(subscriber));

            executorService.shutdown();
        };

        Publisher<Integer> pubOnPub = subscriber -> {
            subOnPub.subscribe(new Subscriber<Integer>() {

                ExecutorService executorService = Executors.newSingleThreadExecutor(new CustomizableThreadFactory(){
                    @Override
                    public String getThreadNamePrefix() {
                        return "pubOn-";
                    }
                });

                @Override
                public void onSubscribe(Subscription s) {
                    subscriber.onSubscribe(s);
                }

                @Override
                public void onNext(Integer integer) {
                    executorService.execute(() -> {
                        subscriber.onNext(integer);
                    });
                }

                @Override
                public void onError(Throwable t) {
                    executorService.execute(() -> {
                        subscriber.onError(t);
                    });

                    executorService.shutdown();
                }

                @Override
                public void onComplete() {
                    executorService.execute(() -> {
                        subscriber.onComplete();
                    });

                    executorService.shutdown();
                }
            });
        };

        pubOnPub.subscribe(new Subscriber<Integer>() {
            @Override
            public void onSubscribe(Subscription subscription) {
                logger.debug("OnSubscribe");
                subscription.request(Long.MAX_VALUE);
            }

            @Override
            public void onNext(Integer integer) {
                logger.debug("onNext :{}", integer);
            }

            @Override
            public void onError(Throwable t) {
                logger.debug("onError:{}" , t);
            }

            @Override
            public void onComplete() {
                logger.debug("onComplete");
            }
        });
    }
}

 

이번편은 개요로써 간단히 기본 항목들에 대해서 알아보았다.

 

 Java 8 부터 시작해서 차근차근 알아보도록 하겠다. 

 

다음편 보기

'알아보기' 카테고리의 다른 글

0002 Reactive Programming 3  (0) 2019.12.08
0004 Reactive Programming 2  (0) 2019.12.05
0002 Realm 활용하기 2  (0) 2019.11.22
0002 Realm 활용하기 3  (0) 2019.11.22
0003 자바를 이용한 병렬 프로그래밍 1  (0) 2019.11.20

+ Recent posts