소프트웨어에서 이름은 어디에서든 사용합니다.

변수에도 이름을 붙이고, 함수, 인수, 클래스, 패키지 등에 이름을 붙입니다. 또한, 파일, 디렉터리, war, jar 등 여기저기 이름을 많이 붙이네요. 


이렇듯 이름을 짓는 일은 많으며, 좋은 이름을 지으려면 시간이 걸리지만 좋은 이름으로 절약하는 시간이 더 많습니다. 

좋은 이름을 지으면, 자신을 포함해 코드를 읽는 사람이 행복해 집니다.


이번 포스팅에서는 이름 잘 짓는 몇 개의 규칙에 대해 다뤄보려 합니다.


1. 의도를 분명히 밝혀라.


많은 개발자들이 "의도가 분명하게 이름을 지으라" 는 이야기를 많이 합니다.

의도가 분명한 이름은 정말로 중요합니다.


변수나 함수 클래스 이름은 다음과 같은 굵직한 질문에 모두 답해야 합니다.


- 왜 존재해야 하나?


- 수행 기능은?


- 사용 방법은?


따로 주석이 필요하다면, 의도를 제대로 나타내지 못함을 의미합니다.



1
int d;  // 경과 시간 (단위: 날짜)
cs


이름 d 는 아무 의미도 드러나지 않습니다. 해당 변수가 표현하는 논리적인 이름이 필요합니다.

아래처럼 말이죠.


1
int daysInAppraisalDeadline; 
cs



의도가 드러나는 이름을 사용하면, 코드 이해와 변경이 쉬워집니다.

한번, 다음 함수를 주목해주세요.


1
2
3
4
5
6
7
8
9
10
11
public List<int[]> getThem() {
    ArrayList<int[]> list = new ArrayList<>();
        
    for (int[] n : theList) {
        if (n[0== 4) {
            list.add(n);
        }
    }
 
    return list;
}
cs


함수 function 은 복잡한 문장은 없어 보입니다. 하지만, 무슨 일을 하는지 짐작하기 어렵습니다. 


문제는 코드의 단순함이 아닌 함축성에 있습니다. 코드 맥락이 명시적으로 드러나지 않습니다.

위에서 제공하는 코드는 아래와 같은 질문을 남깁니다.


1. theList 에 무엇이 들어있는가?


2. theList 에서 0번째 값이 어째서 중요한가?


3. 값 4는 무엇인가?


4. 함수가 반환하는 list 는 어떻게 사용되는가?


코드만 보고는 위 질문에 대한 답을 알 수 없습니다. 

하지만, 다음과 같이 코드를 변경하면 어떨까요? 아래 코드는 단순함에 변화를 주지 않고, 이름만 변경했습니다.


1
2
3
4
5
6
7
8
9
10
11
public List<int[]> getFlaggedCellS() {
    ArrayList<int[]> flaggedCellS = new ArrayList<>();
 
    for (int[] cell : gameBoard) {
        if (cell[STATUS_VALUE] == FLAGGED) {
            flaggedCellS.add(cell);
        }
    }
 
    return flaggedCellS;
}
cs


이 코드를 보고, 위의 질문에 답을 해 볼 수 있습니다.


1. theList 에 무엇이 들어있는가?

    -> gameBoard 로 변경되면서, 게임판임을 알 수 있습니다.


2. theList 에서 0번째 값이 어째서 중요한가?

    -> 0 은 STATUS_VALUE 라는 값을 통해, 셀의 상태를 의미함을 알 수 있습니다.


3. 값 4는 무엇인가?

-> 4라는 상수에 FLAGGED 라는 이름을 붙임으로, 플래그가 된 상태임을 알 수 있습니다.


4. 함수가 반환하는 list 는 어떻게 사용되는가?

-> 반환하는 list 에 flaggedCellS 라는 이름을 붙임으로, 플래그 된 셀 목록을 구한다는 것을 알 수 있습니다.


각 개념에 이와 같이 이름만 붙여도 충분히 나아진 것을 알 수 있습니다. 

단순함은 변하지 않았지만, 코드는 더욱 명확해졌습니다.


조금 더 리팩토링을 해볼 수 있을 것 같습니다. 

int 배열 사용 대신 Cell 이라는 클래스를 만들고, Cell::isFlagged 라는 메소드를 사용하여 FLAGGED 라는 상수를 감춰도 좋을 것 같습니다.


1
2
3
4
5
6
7
8
9
10
11
public List<Cell> getFlaggedCellS() {
    ArrayList<Cell> flaggedCellS = new ArrayList<>();
 
    for (Cell cell : gameBoard) {
        if (cell.isFlagged) {
            flaggedCellS.add(cell);
        }
    }
 
    return flaggedCellS;
}
cs



2. 그릇된 정보를 피하라.


프로그래머는 코드에 그릇된 정보를 남기면 안됩니다.

그릇된 단서는 코드의 의미를 흐리기 때문이죠.


예를들어, 우리는 resumeList, appraisalList 와 같이 목록을 나타내는 변수명을 짓습니다.

프로그래머에게 있어 List 는 자료구조를 나타내는 특수한 의미이며, 해당 변수들이 List가 아니라면, 그릇된 정보를 제공하는 셈입니다.

간단하게 resumeS(s가 눈에 안뛸 수도 있기 때문에 대문자로 강조..), appraisalGroup 등으로 명명하는 것을 책에서는 추천하고 있습니다.


또한, 서로 흡사한 이름을 쓰지 않도록 주의할 것을 말합니다.

한 모듈에서는 getStringForHandlingOfResume 라 하고, 다른 곳에서 getStringForStorageOfResume  라 하면 두 모듈의 차이를 알 수 없습니다.


조금은 유머같지만, 알파벳 l, o 들의 문자들은 지양하는 것이 좋습니다.


1
2
3
4
5
int i = l;
int x = 1;
        
int f = o;
int fx = 0;
cs


폰트나 IDE 에 따라, 위의 코드는 헷갈릴 수 있습니다.



3. 의미 있게 구분하라.


종종, 어떤 코드들은 이름에 연속된 숫자나 불용어(의미없는 글자)를 추가하는 경우가 있습니다.

이런 코드들은 모듈의 가독성을 꽤 나쁘게 만듭니다.


예를들어, 한번 아래 메소드 서명을 볼 수 있습니다. 

연속적인 숫자를 덧붙인 이름(a1,a2... aN) 은 그릇된 정보를 제공하지도, 어떤 의도도 주지 못하는 이름입니다.


1
public void copyPropertise(Object a1, Object a2)
cs


위의 서명을 source, destination 등 의미있는 이름을 사용하는 것으로도, 충분히 어떻게 사용하면 되는지 나타낼 수 있습니다. 


1
public void copyPropertise(Object source, Object destination);
cs


불용어를 사용하는 경우 역시, 어떤 정보를 주지 못하며 혼란을 줄 수 있습니다.

Product 라는 클래스가 있고 ProuductInfo, ProductData 라 부른다면, info 나 data 는 의미없는 정보입니다.

a, an, the 등의 접두어 역시 불용어입니다.

그렇다고, 접두어나 접미어의 사용이 나쁘다는 것이 아닙니다. product 라는 이름이 있다고, theProduct 라는 이름을 짓지 말자는 것입니다.


변수 이름에 variable 을 쓰거나, 테이블에 table 이라는 단어 역시 마찬가지로 불용어 입니다.

name 과 nameString 은 다를 것이 없습니다. name 이 숫자가 될 일은 없을테니까요...



4. 인터페이스와 구현 클래스


예를들어, 커피를 제작하는 Abstract Factory를 구현한다고 가정해보죠.


이 절과 관련은 없지만, Abstract Factory 가 무엇인지 궁금하다면, 이 곳을 참고! :-)



이 패턴에 따라 클래스를 그려보면, 인터페이스와 구현체로 나눠 제작하게 될 것입니다.

이 때, 인터페이스의 이름을 어떻게 할까요? ICoffeeFactory? CoffeeFactory? 


책의 저자는 옛날 코드에서 많이 보이는 인터페이스에 붙이는 접두어(I)에 대하여, (잘해봤자) 주의를 흐트리고, (나쁘게는) 과도한 정보를 제공한다고 주장합니다.

굳이 다루는 클래스가 인터페이스라는 사실을 알리고 싶지 않고, 사용자는 CoffeeFactory 라고만 생각하면 좋겠다고 합니다.

책의 저자는 차라리 CoffeeFactoryImp 가 ICoffeeFactory 보단 좋다고 말합니다.



5. 클래스 이름


클래스 이름과 객체 이름은 명사나 명사구가 적합합니다.

Resume, Account 등이 좋은 예입니다. Data, Info 등과 같은 단어는 피하고 동사는 사용하지 않습니다.



6. 메소드 이름


메소드 이름은 동사나 동사구가 적합합니다. 

postPayment, save, deletePage 등이 적합하며, javabean 규칙에 따라 접근자(getXXX), 변경자(setXXX), 조건자(isXXX) 를 사용하길 권장합니다.


생성자 같은 경우는 중복정의를 하기 보단 팩토리 메소드를 사용하길 권장합니다.

팩토리 메소드는 직접적으로 생성자를 이용하지 않고, 어떤 상태를 가진 객체를 생성하는 것을 의미합니다.


예를들어, 중복정의된 생성자 보단,


1
Complex fulcrumPoint = new Complex(23.0);
cs


아래와 같이, 의미를 가진 이름으로 표현된 팩토리 메소드를 이용하는 것이 좋습니다.


1
Complex fulcrumPoint = Complex.FromRealNumber(23.0);
cs



7. 한 개념에 한 단어를 사용하라.


추상적인 개념 하나에 단어 하나를 선택하고 이를 고수하기를 권장합니다.


예를들어, 똑같은 일을 수행하는 접근자 메소드를 클래스마다 get, fetch 등 제작기 부르면 혼란스러워 집니다.

메소드 이름은 독자적이고 일관적이어야 합니다. 그래야 주석을 뒤져보지 않고 프로그래머가 올바른 메소드를 선택할 수 있습니다.


마찬가지로 동일 코드 기반에서 controller, manager, driver 를 섞어쓰는 것을 지양합니다.

DeviceManager 와 DeviceController 는 근본적으로 다를까요? 둘 다 Controller 가 아닐지? 혹은 둘 다 Manager 가 아닐까요? 

이름이 다르면 독자는 당연히 클래스도 다르고 타입도 다르다고 생각할 것입니다.



8. 해법 영역에서 가져온 이름을 사용하라.


코드를 읽는 사람 역시도 프로그래머입니다. 그렇기 때문에 전산용어, 패턴명, 알고리즘 명 등은 써도 무방합니다.

Factory 패턴에 익숙한 개발자는 CoffeeFactory 를 금방 이해하며, JobQueue 역시 특정 일을 하는 큐임을 인지할 수 있습니다.



9. 문제 영역에서 가져온 이름을 사용하라.


문제 영역에서 가져온 이름을 사용하면, 해당 분야의 전문가에게 의미를 물어 문제를 이해할 수 있습니다.

문제 영역의 개념과 깊은 문제를 해결하는 코드라면, 문제 영역에서 이름을 가져와야 합니다.



10. 의미있는 맥락을 추가하라.


스스로 의미가 분명한 이름이 없진 않습니다. 

하지만 대다수 이름은 분명하지 못하며, 클래스, 함수 등에 공간을 넣어 맥락을 부여합니다.

모든 방법이 실패한다면, 마지막 수단으로 접두어를 사용합니다.


예를들어, 아래 코드를 확인해봅시다.


1
2
3
4
5
6
7
8
public class TestClass {
 
    String firstName, lastName, streetName, city, state, zipCode;
 
    public static void main(String[] args) {
 
    }
}
cs


변수 명을 본다면, 해당 변수들이 주소를 나타낸다는 것을 알 수 있습니다.

하지만 어떤 메소드가 오직 state 하나만 사용한다면, 그 메소드만 보고 주소라는 사실을 바로 알긴 쉽지 않을 것 같아 보입니다.


그래서 다음과 같이 addr 이라는 접두어를 사용하여, addrState 라 하면 조금 더 의미가 명백해집니다. 


1
2
3
4
5
6
7
8
9
10
11
12
public class TestClass {
 
    String addrFirstName, addrLastName, addrStreetName, addrCity, addrState, addrZipCode;
 
    public static void main(String[] args) {
 
    }
 
    public String getAddrState() {
        return addrState;
    }
}
cs


하지만, Address 라는 클래스를 제작한다면 변수가 조금 더 큰 개념에 속한다는 것이 컴파일러에게도 명백해집니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * 주소 클래스
 *
 * Created by Doohyun on 2018. 1. 7..
 */
public class Address {
    String firstName;
    String lastName;
    String streetName;
    String city;
    String state;
    String zipCode;
}
 
cs



11. 불필요한 맥락을 없애라.


사내에서 개발한 InJob 이라는 어플리케이션이 있었습니다.

저는 이 프로젝트에서 모든 유틸 클래스 및 기본이 되는 클래스들에 INJOB 이라는 접두어를 붙인 적이 있었습니다.


아래와 같이 말이죠...


1
2
3
4
5
6
7
8
/**
 * inJob 에서 구현된 리소스 유틸
 *
 * Created by Doohyun on 2018. 1. 7..
 */
public class InJobResourceUtil {
    private InJobResourceUtil() {}
}
cs


어차피, InJob 이라는 안드로이드 프로젝트에서만 독릭접으로 사용될 코드이며, 이 클래스의 명은 ResourceUtil 로 변경하는 것이 옳다고 생각합니다.


비슷하지만, 조금은 다른 케이스로는 다음과 같은 사례를 볼 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * inJob 에서 사용하는 코드 정의
 *
 * Created by Doohyun on 2018. 1. 7..
 */
public class InJobCodeDefinition {
 
    public static class APPLICATION_TYPE {
        public static final String APP = "INJOB_APPLICATION_TYPE_APP";
        public static final String WEB = "INJOB_APPLICATION_TYPE_WEB";
    }
}
 
cs


위와 같은 경우 코드 클래스 자체가 다른 응용에서 사용할 수 있기 때문에, InJobCodeDefinition 이라는 이름을 사용하고 있습니다.

또한 코드의 value 역시 다른 솔루션과 구분을 짓기 위해 "INJOB_APPLICATION_TYPE_APP" 을 사용하지만, 클래스와 상수명에는 접두어가 없고 계층적인 구조를 사용하고 있습니다.

개념적으로 불필요한 맥락과 필요한 맥락을 나누고, 개념에 속하도록 제작이 된 좋은 사례인 듯 합니다. :-)



책의 저자는 위와 같은 규칙을 소개하며, 마치면서 다음과 같은 말을 했습니다.


사람들은 이름을 바꾸지 않으려는 이유는 다른 개발자가 반대할까 두려워서다. 

여느 코드 개선 노력과 마찬가지로 이름 역시 나름대로 바꿨다가는 누군가 지책할지도 모른다. 

그렇다고 코드를 개선하려는 노력을 중단해서는 안 된다.


이 문구를 읽었을 때, 3년차가 되면서 협업이라는 그늘 안에서 보수적이라는 두려움을 외면 시 했던 된 저를 보게 되었습니다.

남들이 안좋게 작성했기 때문에, 어쩔 수 없다는 핑계로 안좋은 코드를 이어갔었던 듯 합니다.


더불어 제가 코드를 작성하는 안좋은 버릇이 충분히 이 절에서 꽤 많이 보였습니다. 하하...

충분히 반성을 하게 된 포스팅인 듯 합니다.


이 글이 저처럼 조금 더 여러분들이 나아지는 것에 도움이 되었으면 좋겠습니다.


Clean Code 클린 코드
국내도서
저자 : 로버트 C. 마틴(Robert C. Martin) / 박재호,이해영역
출판 : 인사이트 2013.12.24
상세보기



반응형

'개발이야기 > Clean Code' 카테고리의 다른 글

깨끗한 코드  (1) 2018.01.07
[프롤로그] Clean Code  (0) 2018.01.05
Posted by N'

Clean Code 의 첫 도입부는 책 이름답게 "깨끗한 코드" 라는 주제를 다루고 있습니다.

첫번째 장에서는 깨끗한 코드의 중요성과 유명하고 노련한 프로그래머들이 생각하는 깨끗한 코드에 대한 생각들이 담겨 있었습니다.

이번 포스팅에선, 위 내용에 대한 주관적인 요약을 다뤄보려 합니다.



1. 코드가 존재하리라.


최근 프로그램을 제작하기 위한 많은 기술들이 등장하고 있으며, 아마 프로그램을 제작하는 방법은 점점 편리해졌습니다.

제작하는 방법이 쉬워지는 만큼, 개발자들은 코드보단 요구사항에 조금 더 집중해야한다고 생각하는 의견들도 많이 있습니다.

생각해보면, 새로 등장하는 많은 언어들은 어렵거나 귀찮은 내용을 점점 추상화하고 있습니다.

(어느덧 C++ 이후 부터의 언어는 포인터를 찾아볼 수 없고, 병렬 처리같은 복잡한 수준의 구현은 함수형 등으로 쉽게 나타낼 수도 있습니다.)


이러한 구현과정의 추상화는 계속될 것이며, 우리는 요구사항에 조금 더 집중할 수 있을 것입니다. 

그렇지만 요구사항을 나타내는 구체적인 방법은 결국 코드이며, 코드는 계속 우리 곁에 남아있을 것입니다.



2. 나쁜 코드


협업을 하게 되면, 동료들의 많은 코드를 볼 수 있습니다. 

또한, 이 코드를 사용하거나 고쳐야할 상황도 많으며 종종 누군가가 작성했던 나쁜 코드들에게 시달렸을 것입니다.

(아마 종종 누구가는 자신일 확률이 높을 수도 있습니다. ㅡㅡ^)


나쁜 코드는 당연한 이야기이지만 개발 속도를 크게 떨어트립니다. 

간단한 변경이란 없고, 코드를 변경할 시 여기저기서 문제가 발생할 것입니다. 

이러한 일들은 팀 생산력을 크게 저하시킬 것입니다.


생산력을 올리고자, 새로 인원을 충원해도 그들은 설계의도를 모르기 때문에 더 나쁜 코드가 생산될 수 있죠.

덕분에 생산력은 0를 향해 내려갈 것입니다.



 

이 상황이 계속되면, 개발자들은 재설계를 요구할 수 밖에 없습니다. 

모듈 하나 추가할 때마다 너무 많은 시간이 들며, 이곳 저곳 버그가 많다면 계속 헝겊을 짜집는 것보다는 새로 만들기를 희망합니다.

관리자들 역시 재설계에 자원을 쏟기 싫지만, 생산성이 0이기에 어쩔 수 없이 허락을 할 것입니다.


이제, 개발자들은 두 팀으로 나눠집니다. 

기존 시스템을 유지할 인력과 새로 재개발을 할 타이거 팀으로 말이죠. 

타이거 팀은 기존 시스템을 대체할 새 시스템을 내놓아야 합니다. 또한 기존 시스템에 추가되는 변경점도 따라잡아야 하죠.

이 상태는 오랫동안 이어질 수 있습니다. 

오랫동안 이어진다면 기존 타이거팀은 모두 떠났고, 또 다른 인력이 새 시스템을 설계 하겠다고 할 것입니다. 

(현재 시스템 역시 엉망이기 때문이겠죠..)


위의 이야기는 책에 등장하는 경험담이며, 현재 제가 재직하고 있는 회사에서도 겪고 있습니다...  ㅜㅡㅜ

깨끗한 코드를 만드는 노력은 비용 절감뿐 아니라, 전문가로써 살아남는 방법이라 주장하고 있습니다.



3. 태도


나쁜 코드가 심각한 장애물이라는 것은 개발자라면 많이 공감을 할 것입니다.

왜 우리들의 코드는 이렇게 되었을까요?


요구사항이 원래 설계를 뒤집는 방향으로 변해서? 일정이 촉박해서? 멍청한 관리자와 조급한 기획자 때문에?

이 책에서는 이러한 잘못은 모두 프로그래머에게 있다고 합니다.


관리자와 기획자는 우리에게 현실성을 자문하고, 도움을 요청합니다. 

우리에게 정보를 구하지 않더라도, 우리는 적극적으로 그들에게 정보를 제공해야 합니다.

우리는 프로젝트에 가장 깊게 관여하고 있으며, 프로젝트 실패는 우리에게 커다란 책임이 있습니다. 나쁜코드가 초래하는 실패라면 더더욱 책임이 큽니다.


"아니, 잠깐만요.. 상사가 시키는 대로 하지 않으면 짤린다구요!" 우리의 의견은 보통 이럴 텐데 말이죠..

하지만, 대다수의 관리자들은 진실을 원하며, 일정에 쫓기더라도 좋은 코드를 원합니다. 

관리자들이 일정을 밀어붙이는 것은 그들의 책임이며, 좋은 코드를 사수해야하는 것은 우리들의 일입니다.


이 책에서는 한가지 비유를 했습니다.


자신이 의사라 가정하자. 

어느 환자가 수술전에 손을 씻지 말라고 요구한다. 시간이 너무 오래걸리니깐?

하지만, 의사는 단호하게 거부한다. 왜? 질병과 감염의 위험은 환자보다는 의사가 잘 아니까. 

환자 말을 그대로 따르는 행동(범죄일 뿐만 아니라)은 전문가 답지 못하다.


개발자 역시, 나쁜코드의 위험을 이해하지 못하는 관리자 말을 그대로 따르는 것은 전문가 답지 못할 것입니다.



4. 깨끗한 코드란?


나쁜 코드는 이와 같이 나쁘며, 빨리 가려면 깨끗한 코드를 유지해야 한다고 인정해야 한다고 가정합시다.

그렇다면, 깨끗한 코드는 어떻게 작성할까요?


이 책에서는 깨끗한 코드를 구현하는 행위는 그림을 그리는 것과 같다고 비유합니다.

대부분의 사람들은 그림이 잘그렸는지 엉망인지 알고 있습니다. 

하지만, 그림을 구분하는 능력이 그림을 잘 그리는 능력은 아닙니다


깨끗한 코드를 작성하려면 '청결' 이라는 어렵게 습득한 코드감각을 활용해 자잘한 기법을 이용하는 절제와 규율이 필요합니다. 나쁜 모듈을 보면 좋은 모듈로 개선할 방안을 떠올리며, 최고 방안을 선택한 후 여기서 거기까지 이동하는 경로를 세웁니다.


즉, 깨끗한 코드를 작성하려면 코드 감각을 익혀야 하며 그 전에 깨끗함이 무엇인지 알 필요가 있습니다.


깨끗한 코드의 정의는 매우 다양합니다. 프로그래머 수만큼 말이죠..

그래서, 이 책에서는 유명하고 노련한 프로그래머들이 말하는 깨끗한 코드에 대한 의견을 소개했습니다.


이번 절에서는 이 의견들 중 인상깊은 내용에 대한 소개를 하고자 합니다.


- 깨끗한 코드는 잘 읽혀야 한다.


많은 의견 중 공통적인 가장 첫번째 의견은 잘 읽히는 코드여야 한다고 합니다.

잘 읽히는 코드는 우아하며, 보기에 즐거워야 합니다. 

잘 쓴 문장처럼 잘 읽히며, 설계자의 의도를 숨기지 않습니다. 추측이 아닌 사실에 근거해야 하며, 필요한 내용만 담아야 합니다.

또한, 표현력 역시 명확해야 할 것입니다. (이름짓기 등..)


이와 같이 깨끗한 문장은 작성자와 구독자 모두 읽기 쉬워야 하며, 고치기도 쉬워야 합니다.



- 깨끗한 코드는 한가지 일을 제대로 한다.


수많은 소프트웨어 원칙은 이 간단한 교훈 하나로 귀결됩니다.

나쁜 코드는 너무 많은 일을 하려 애쓰다가 의도가 뒤섞이고 목적이 흐려집니다. 


깨끗한 코드는 한가지에 '집중'하며, 각 함수, 클래스, 모듈은 주변 상황에 현혹되거나 오염되지 않은 채 한 길만 걷는다고 합니다.


'메소드 추출' 과 같은 리팩토링은 이 교훈을 따르기 위한 방법입니다.




- 깨끗한 코드는 짐작했던 기능을 그대로 수행한다.


이 내용은 매우 당연하지만, 심오한 내용입니다. 

하지만, 짐작했던 그대로 수행하는 모듈은 생각보다 많지 않을 것입니다. 

(헷갈리고, 모듈끼리 복잡하게 엉켜있고, 또는 엉뚱한 기능도 수행하고.. ㅜㅡㅜ)


깨끗한 코드는 읽으면서 놀랄 일이 없어야 합니다. 

각 모듈은 다음 무대를 준비하며, 다음에 벌어질 상황이 보여야 합니다. 

즉, 코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 된다고 합니다.



실제 책의 내용에는 조금 더 공감되고 좋은 내용이 더 있었습니다.

하지만, 모든 내용은 결국 모듈을 한 가지 일만 하도록 최대한 작게 구현하며, 의도를 잘 표현할 수 있도록 세심한 주의 할 것을 말합니다.


지막으로 한가지 생각해 볼 것은 코드를 잘짜는 것이 전부가 아니라는 것입니다. 

시간이 지나도 언제나 깨끗함을 유지하는 것이 더 중요한 듯 합니다.


'소프트웨어 장인정신' 에서도 언급하는 보이스카우트 규칙은 이 내용을 말하고 있습니다.


"캠프장은 처음 왔을 때보다 더 깨끗하게 해놓고 떠나라!"


시간이 지날수록 코드가 좋아지는 프로젝트에서 작업한다면 얼마나 좋을까요? 

사실 전문가라면, 너무도 당연한 이야기입니다.


'지속적인 개선이야말로 장인 정신의 본질'이라는 저자의 말을 남기며, 이번 포스팅을 마칩니다.


Clean Code 클린 코드
국내도서
저자 : 로버트 C. 마틴(Robert C. Martin) / 박재호,이해영역
출판 : 인사이트 2013.12.24
상세보기














반응형

'개발이야기 > Clean Code' 카테고리의 다른 글

의미 있는 이름  (6) 2018.01.07
[프롤로그] Clean Code  (0) 2018.01.05
Posted by N'

안녕하세요. 블로그 주인장 N` 입니다.


개발자에게 있어서 코드를 작성한다는 것은 어떤 의미라 생각하시나요?

제가 회사에서 존경하는 선배님 중 한 분은 이런 말씀을 하셨습니다. 


"코드를 작성하는 것은 저자가 글을 작성하는 것과 같다고 볼 수 있어.."


2년전에 들었던 저 이야기는 어느덧 3년차 개발자가 된 제게 많은 의미를 되돌아 보게 하였습니다.


그동안 제가 회사 사람들과 공부했던 OOP(객체지향)의 개념이나, 무엇을 구현할지에 조금 더 집중할 수 있는 테크닉을 볼 수 있던 함수형 프로그래밍 등은 모두 코드의 품질을 올리기 위한 "기본에 조금 더 충실하자."는 철학이 있었습니다.

코드 품질을 올리겠다는 것은 유지보수에 좋은 코드를 작성 하자는 의미로도 볼 수 있을 것 같은데요.

같이 일하는 모두가 행복(?)하려면, 조금 더 잘 읽히는 코드를 작성하는 것은 정말 중요해 보입니다. 


다시 한번, 위의 이야기를 빌리면 우리는 누군가에게 코드를 제공하는 저자이며 누군가의 코드를 읽는 독자가 될 수 있습니다. 이 점은 아마 코드를 펼쳐보았을 때, 그 이야기에 잘 빠져들 수 있도록 글쓰기 연습을 할 필요성을 느껴볼 수 있습니다.


이번에는 이 연습을 위한 카테고리로 "Clean Code" 를 주제로 합니다.

누구나 처음부터 좋은 글을 쓸 수 없고, 저 역시도 좋은 글을 쓰기는 쉽지 않은 듯 합니다.

냄새나는 나쁜 버릇을 고칠 수 있도록, 다시 한번 기본으로 돌아가며 포스팅을 해보겠습니다. ^^;


많이 봐주세요 :-)


Clean Code 클린 코드
국내도서
저자 : 로버트 C. 마틴(Robert C. Martin) / 박재호,이해영역
출판 : 인사이트 2013.12.24
상세보기




반응형

'개발이야기 > Clean Code' 카테고리의 다른 글

의미 있는 이름  (6) 2018.01.07
깨끗한 코드  (1) 2018.01.07
Posted by N'

해당 포스팅에서 언급된 내용은 Ndroid 에서 제공합니다.

https://github.com/skaengus2012/Ndroid


최근, 사내에서 개발하는 프로젝트에서 주로 다루게 되었던 요구사항 중 하나는 특정 범위에 대하여 등급을 부여하는 것이었습니다.


예를들어, [80.01~100 이면 "S", 70.01~80.00 이면 "A"] 이런 식으로 특정 구간에 대한 점수를 구합니다. 


이를 구현하는 문제는 생각보다 어려운 문제는 아니었지만, SWIFT 와 같은 최신언어에서는 범위에 관한 TYPE 을 제공하고 있는 것을 확인했던 차, JAVA 에도 이런 클래스가 있으면 좋겠다는 생각을 하게 되었습니다.


JAVA8 에서는 범위와 비슷한 문제에 대하여, [날짜 클래스 관련 Duration, Between 과 같은 클래스들이 배포]되었지만 아쉽게도 여러 형식을 호환하기 위한 구간 클래스는 아니었습니다.


그래서, N`s 오픈소스 프로젝트에서는 구간 관련 요구사항을 담을 수 있는 모듈을 제공하고자 하였습니다. 

해당 모듈의 주목적은 특정 Generic Type 에 대한 구간을 객체로써 가지는 것을 목적으로 하며, 이를 이용할 수 있는 추가 모듈을 제공하는 것입니다.


사용법은 아래와 같습니다.



1. 구간 객체 생성 및 활용.


구간을 생성하고, 특정 값이 구간에 포함되는가에 대한 표현입니다.


1
2
3
4
5
6
7
// 3 에서 7 사이의 구간 객체 생성.
Between<Integer> between3to7 = Between.Just(37);
 
System.out.println("3에서 7 사이, 4 는 구간에 포함되는가? -> " + between3to7.contains(4+ "\n");
 
// CONSOLE LOG
// 3에서 7 사이, 4 는 구간에 포함되는가? -> true
cs


객체 생성 방식은 RxJava 의 Maybe 와 비슷한 경험을 제공하고자 하였으며, 

기존 집합개념의 Set 의 Set::contains 와 개념적으로 비슷하기 때문에 같은 서명을 사용하고자 했습니다. 



2. 구간 객체 관리 Map


Between 객체를 관리할 수 있는 자료구조가 있다면, 꽤 멋진 활용을 할 수 있을 것이라 생각했었습니다. 

가장 먼저 생각한 것은 구간을 key 로 가지며, 구간에 포함되는 값을 매칭하면 해당 구간에 대한 value 를 얻을 수 있는 Map 을 구현하는 것이었습니다.


자료구조적으로 HashMap 은 아니지만, Map 과 비슷한 메소드 서명을 취할 수 있도록 Wrapping 하여 위의 요구사항을 충족하고자 했습니다. :-)


사용방법은 다음과 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BetweenMap<Between<BigDecimal>String> betweenStringBetweenMap = new BetweenMap<>();
 
// 
betweenStringBetweenMap.put(Between.Just(BigDecimal.valueOf(0.0), BigDecimal.valueOf(5.0)), "A");
betweenStringBetweenMap.put(Between.Just(BigDecimal.valueOf(5.1), BigDecimal.valueOf(10.0)), "S");
betweenStringBetweenMap.put(Between.Just(BigDecimal.valueOf(6.1), BigDecimal.valueOf(13.0)), "S++");
 
// BigDecimal map getter.
// Return type -> Rx.Maybe
betweenStringBetweenMap.getFirst(BigDecimal.valueOf(3.87)).subscribe(grade -> System.out.println("Maybe Type 결과 출력 ([3.87] 에 대한 등급) : " + grade));
 
// List Type Return.
System.out.println("List Type 결과 출력 ([8.7] 에 대한 등급) : " + betweenStringBetweenMap.getToList(BigDecimal.valueOf(8.7)));
 
// CONSOLE LOG
// Maybe Type 결과 출력 ([3.87에 대한 등급) : A
// List Type 결과 출력 ([8.7] 에 대한 등급) : [S, S++]
cs



해당 모듈과 관련된 원본 주소는 아래에서 확인하실 수 있습니다.



이 포스팅이 보다 간결한 코딩을 하는 것에 도움이 되길 바래요 ~ @.@ ~


반응형
Posted by N'

해당 포스팅에서 언급된 내용은 Ndroid 에서 제공합니다.

https://github.com/skaengus2012/Ndroid


매일 코딩을 하던 중 같은 것을 반복한다고 느끼는 경우가 있습니다.


그 중 하나가 아마 예외처리를 하는 것이라 생각합니다.


예외처리는 개발자가 의도적으로 어떤상황이 일어났을 때의 상황을 처리 못하니, 위의 stack 에서 알아서 하라고 던지는(throw) 행위입니다. (사실 이 행위는 프로그램코드가 아닌 실세계에서도 많이 일어납니다. ㅡㅡ^)


일단 예외처리에 대한 제 입장은 이러한 예외처리를 꼼꼼하게 계속해야 한다고 생각합니다. 


조금 회사에서 일을 해보니, 프로그램 작성 시에는 일부로 어떠한 경우들을 일부로 체크안해서 UnCheckException (꼭 처리를 안해도 되는 예외) 을 내보내도 되지 않을까(쉽게는 NullPointerException)생각하여 작성해보았는데 디버깅 시, 유지보수가 더 쉽지가 않음을 깨달았습니다. 


로직의 스탭(모듈 등)간의 관계에 대하여 강결함(?) 보장이 의도적으로 되야 하는데, 의도적으로 예외를 내지 않아버리면 갑자기 어디선가 죽어버리는 녀석을 계속 찾아야하는 것 같습니다. (같은 NullPointer 에 대한 예외를 처리한다 하더라도, 아무것도 안하고 Runtime 중 일으키는 것보다는 의도적으로 RuntimeException 이라도 넘기는 것이 좋다고 생각합니다. 메시지도 같이 말이죠....)


어쨌든 이러한 처리를 아마 아래와 같이 코드를 작성하곤 합니다.


1
2
3
if (a == null) {
    throw new MessageException("잘못된 접근입니다.");
}
cs


비지니스로직에 흔히 작성될 수 있는 코드입니다. 

간단해보이지만 아래와 같이 체크해야할 경우가 많다면 어떨까요?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (a == null) {
    throw new MessageException("잘못된 접근입니다.");
}
 
if (list == null || list.isEmpty()) {
    throw new MessageException("잘못된 접근입니다.");
}
 
if (string == null || string.toString() == null || string.toString().isEmpty()) {
    throw new RuntimeException("잘못된 접근입니다.");
}
 
if (map == null || map.isEmpty()) {
    throw new MessageException("잘못된 접근입니다.");
}
 
if (!map.containsKey("a")){
   throw new RuntimeException("잘못된 접근입니다.");
}
cs


뭔가 꼼꼼하긴 하지만, if 블럭 안에 예외 한개씩 입력하고 있는 공통점이 있어보입니다. 이러한 단순 작업을 요즘은 툴(인텔리J)이나 어노테이션(NonNull)등으로 어느정도 커버할 수 있지만, 안드로이드나 웹프로젝트에서 같이 작업하는 입장에서 생각할 때는 호스트코드에 작성하는 방식을 아에 무시할 수는 없어보입니다. 

(잘 모르는 것일 수도 있습니다. ~ @.@ ~)


일단은 같이 일하는 선배님께서 이러한 부분을 간결하게 해보자는 것을 제안하셔서, 아래와 같은 코드를 한번 배포해 보았습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * 단순한 NullCheck 매크로
 *
 * @param object
 * @param message
 */
public void NullCheck(Object object, String message) throws MessageException {
   if (object == null) {
      throw new MessageException(message);
   }
}
 
 
NullCheck(vo, "잘못된 접근입니다.");
cs


MessageException은 CheckedException 종류로 컨트롤러로 이 예외가 던져진다면, 프론트에서 사용자가 메시지를 볼 수 있도록 합니다.


이 코드로 인하여, 예외를 처리하는 것은 매우 단순하게 간결 해졌습니다. 


복잡한 방식을 간결하게 바꾸는 시도는 여러가지로 시도 되고 있고(인터페이스를 람다로 치환한다는 등..), 이러한 방식을 제안한 선배님께는 감사하고 있습니다.


단순하게 Null 뿐 아니라, string 빈 값, Container 의 빈 상태 등 자주 사용하는 여러 상황을 체크하는 메소드를 만들었고, 잘 사용하고 있었지만 이 코드에는 문제가 있음을 깨닫게 되었습니다.


간결 예외처리 코드는 아쉽게도 MessageException 밖에 출력을 못합니다. 

경우에 따라서는 RuntimeException 을 내보내야할 때도 있으며, 안드로이드와 라이브러리를 공유한다고 했을 때 웹에서만 사용하는 MessageException 을 안고 갈 수는 없었습니다. 


이를 해결하기 위해 예외의 형태를 제네릭으로 받아 동적으로 예외를 만들자는 생각이 들었습니다. 아래와 같은 초안 메소드를 작성하게 되었고, 오버로딩으로 간략하게 사용할 수 있는 메소드도 같이 제공하였습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
 * Check with CustomException.
 *
 * @param check
 * @param message
 * @param exceptionClass
 * @param <T>
 * @throws T
 */
public final static <extends Exception> void Check(
            @NonNull Boolean check
            , @NonNull String message
            , @NonNull Class<T> exceptionClass) throws T{
    if (!check) {
        final T exception;
 
        try {
            exception = exceptionClass.getConstructor(String.class).newInstance(message);
        } catch (Exception e) {
            throw new RuntimeException(e);
         }
 
        throw exception;
    }
}
 
/**
 * Check.
 *
 * @param check
 */
public final static void Check(@NonNull Boolean check) {
    Check(check, ERROR_BAD_ASSESS, RuntimeException.class);
}
 
 
Check(object != null"[에러] object 는 Null 일 수 없음", RuntimeException.class);
Check(object != null);
cs


이러한 형태로 자주 사용하는 예외체크 타입을 간략화한 유틸클래스를 릴리즈하였으며, 사용은 아래와 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Check
CheckUtil.Check(object != null"Occurred Error! Please ask administrator!", RuntimeException.class);
 
// Null check
CheckUtil.NullCheck(object, "Occurred Error! Please ask administrator!", MessageException.class);
 
// String empty check.
CheckUtil.EmptyToStringCheck(object);
 
// Maybe Empty check.
CheckUtil.EmptyMaybeCheck(MaybeUtil.JustNullable(object));
 
// Container Check.
CheckUtil.EmptyContainerCheck(Collections.emptyList());   // Collection
CheckUtil.EmptyContainerCheck("Test""Test2");           // Array
CheckUtil.EmptyContainerCheck(Collections.emptyMap());    // map
cs


RxJava2 에서 제공하는 Maybe 개념까지 CheckUtil 에 넣게 되었습니다.


보다 자세한 내용은 아래 url 에서 확인하실 수 있습니다.



이 포스팅이 보다 간결한 코딩을 하는 것에 도움이 되길 바래요 ~ @.@ ~

반응형
Posted by N'

JAVA8 IN ACTION 의 포스팅은 끝났습니다.


이번 포스팅은 지식공유 보다는, 추 후 제가 함수형을 공부하기 위한 용어 및 키워드를 정리합니다.


- 일급함수 


인수로 전달하거나, 결과로 반환하거나, 자료구조에 저장할 수 있는 함수를 말함. 

즉 람다나 메소드 레퍼런스 등 값으로 가질 수 있는 함수를 일급함수라고 정의


- 고차원 함수


여러 함수들을 묶어, 새 기능을 만드는 것을 말함 (comparing, andThen, compose).

즉 람다 조합을 말함



- 커링


함수를 모듈화하는 기법, 함수를 완성시키지 않고 인수를 받아 계속 다른 함수를 만드는 기법이라고 할 수 있다. 


예제


f(x, y, z) = x + y - z 를 다음과 같이 여러 형태로 변형 가능


g(x, y) = f(x, y, 2);

z(x) = f(x, 1, 3);


JAVA8 IN ACTION 에서는 변환요소, 기준치 조정 등을 위해 유용하다고 정리하고 있음


EX. swift 책에서는 커리함수를 파라미터를 받는 함수를 반환하는 함수라 기술.

아래는 커리함수에 대한 예제 (swift).


1
2
3
4
5
6
7
8
9
10
11
func Test () -> (Int-> String {
    return {
        (intValue : Int-> String in
            return String(intValue) + " 함수 커링 샘플 제작"
    }
}
 
print(Test()(5))
 
// PRINT RESULT
// 5 함수 커링 샘플 제작
cs



- 패턴매칭


if-then-else 등을 피하기 위해, 각 분기의 블록을 일급함수화 하여 입력받고, 분기 패턴에 따라 일치하는 일급함수를 실행(언랩)하는 것으로 보입니다.


조건부 연기를 생각하면 쉬울 듯합니다. (JAVA 에서는 아직 지원 안합니다.)



- 영속자료구조


자료구조의 형태는 함수형을 이용해 제작하고, 자료구조 내부를 바꾸지 말라는 원칙. 

의도치 않은 사이드 이펙트를 발생시키고 싶지 않음 -> 참조투명성 원칙.


- 동시성 문제 해결


함수형을 사용하면, 동시성 문제를 해결할 수 있음 (참조투명성 원칙에 의해..)


아래 포스팅 참고.



작년 이 맘 때부터 시작했던 포스팅 했던 JAVA8 의 포스팅이 끝나서 보람이 있네요. :-)

물론 JAVA8 in Action 의 모든 내용을 전문적으로 알고 기억하는 것도 아니지만 나름 뿌듯합니다.


좋은 소식은 현재 직장에서 JDK 버전을 8 로 올려준다는 것입니다. (미래를 내다본 것은 아닌지..)


하지만 안드로이드 개발을 위해 RxJava 및 오픈소스를 계속해야할 것으로 보이고, 곧 새 포스팅할 주제인 IOS-swift 도 보면서 다양한 함수형을 지원하는 언어 활용에 익숙해져 봐야할 것 같습니다.


이상 올 해 목표 중 하나를 끝냅니다.


자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기



반응형
Posted by N'

자바에서 시간을 다루기 위한 클래스가 존재합니다.


JAVA8 IN ACTION 에 따르면,


자바 1.0 에서부터 Date 클래스를 지원했지만 0부터 시작한 달, 애매한 offset, 자체적인 시간대의 부재 등 부실한 설계가 있었고, 자바 1.1 에서는 호환성을 깨트리지 않으면서 조금 더 유용하게 사용할 수 있게 설계한 Calendar 가 등장했습니다.


Calendar 에서 Date 의 개선사항이 존재했었지만 0부터 시작하는 달 등 애매한 내용은 아직 남아있었고, 제일 큰 문제는 Date 와 Calendar 가 동시에 존재함으로써 자바개발자들에게 혼란을 야기시켰다고 합니다.


아래는 기존 시간 관련 클래스를 사용하기 위한 방법입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Date 클래스 사용
Date date = new Date();
{
    System.out.println(date.getTime());
            
    // 다른 getXXX 는 Deprecated 되었습니다.
}
 
// Calendar 사용.
Calendar calendar = Calendar.getInstance();
{
    calendar.get(Calendar.YEAR);    // 년도.
    calendar.get(Calendar.MONTH);   // 0 부터 시작하는 애매한 달
}
cs


JAVA8 부터는 오라클에서 새로운 시간 관리 API 를 내놓았으며, 이는 JAVA8 의 다른 API (Stream, Optional 등)와 같이 Builder 패턴을 사용하여 사용성을 높였다는 것에 초점을 맞춰볼 수 있습니다.


이번 포스팅에서는 핵심이 되는 부분을 중점으로 다뤄볼 생각이며, 나머지는 메소드명이나 API 문서를 보면 쉽게 사용할 수 있을 것이라 생각합니다.


1. LocalDate, LocalTime, LocalDateTime


종종 우리는 기존 Calendar 나 Date 에서 날짜로부터 시간(시,분,초)을 제거하여 데이터처리를 하는 경우가 빈번하였었습니다. 그러나 JAVA API 에서는 날짜와 시간을 분리하여 관리할 수 있으며, 혹은 원할 경우 합쳐서 사용할 수도 있습니다.


아래는 새 자바 시간 관리 클래스들의 사용법입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 2017년 1월 31일 표현
LocalDate localDate = LocalDate.of(2017131);
 
// 1~12 로 관리됨에 주목. 객체인 Month 로 관리되는 것에 한번 더 주목
{
    // 달 출력
    Month month = localDate.getMonth();
    System.out.println(month.getValue());
}
 
// 10시 5분 40초 표현
LocalTime time = LocalTime.of(10540);
 
// 날짜와 시간의 조합
{
    LocalDateTime dateTime = LocalDateTime.of(localDate, time);
    System.out.println(dateTime.format(DateTimeFormatter.ISO_DATE_TIME));
}
 
// Print Result
// 1
// 2017-01-31T10:05:40
cs


개념적인 날짜와 시간을 객체로 분리하였으며, 이를 조립할 수 있다는 것이 매우 인상적입니다. 또한 기존 시간 클래스들과 달리 달 출력이 1부터 시작된다는 것이 마음에 드는군요. ㅡㅡ^



2. 날짜 조정


날짜에 대해서 특정정보를 출력하는 것과 더불어 날짜 데이터를 조정할 수 있습니다. 

예를들어 "3일 전 혹은 후의 날짜는?" 과 같이 특정 날짜개념을 연산할 수 있고, 입력 또한 할 수 있습니다.


날짜를 입력을 하기 위해서는 withXXX 메소드를 연산을 하기 위해서는 plusXXX, minusXXX 등의 메소드를 사용할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 2017년 1월 31일 표현
LocalDate localDate = LocalDate.of(2017131);
 
// 2016 년 1월 5일로 세팅.
LocalDate withDate =  localDate.withYear(2016).withDayOfMonth(5);
System.out.println(withDate.format(DateTimeFormatter.ISO_DATE));
 
// 2017 년 1월 31일의 2일 뒤는?
LocalDate plusDays =  localDate.plusDays(2);
System.out.println(plusDays.format(DateTimeFormatter.ISO_DATE));
 
// 2017 년 1월 31일의 3일 전은?
LocalDate minusDays =  localDate.minusDays(2);
System.out.println(minusDays.format(DateTimeFormatter.ISO_DATE));
 
// PRINT RESULT
// 2016-01-05
// 2017-02-02
// 2017-01-29
cs


앞서 수행했던 간단한 연산 이외에도 특정 전략에 맞춘 날짜 조정도 가능합니다. 


예를들어 "현재 달의 첫 번째 날짜는?", "현재 달의 마지막 날짜는?", "다음주 수요일은" 과 같은 질의처럼 말이죠.


with 메소드를 통해 전략적인 날짜조정이 가능합니다. 

전략 인터페이스인 TempralAdjusters 를 작성해주면 되죠. ㅡㅡ^


1
2
3
4
@FunctionalInterface
public interface TemporalAdjuster {
    Temporal adjustInfo(Temporal temporal);
}
cs


예제를 한번 만들어 보죠. 


보통 사람들은 평일에 일을 하지만, 제 사수 형오리의 경우 목요일에 사정이 있어서 토요일에 대체 근무를 하고 있습니다. 즉 형오리의 쉬는 날은 목요일과 일요일입니다. 형오리의 다음 근무일을 구하는 전략을 구해보죠.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 TemporalAdjuster 형오리의_근무일_전략 = temporal -> {
 
     // week 객체 생성.
    DayOfWeek week = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
 
    // 형오리의 쉬는 날의 전날
    Collection<DayOfWeek> hollyDays = Arrays.asList(DayOfWeek.WEDNESDAY, DayOfWeek.SATURDAY);
 
    return temporal.plus(hollyDays.contains(week) ? 2 : 1, ChronoUnit.DAYS);
};
 
IntStream.range(010).boxed().reduce(LocalDate.of(201736), (date, num) -> {
        LocalDate nextDay = date.with(형오리의_근무일_전략);
 
        Optional.of(nextDay).
                        map(localDate -> localDate.format(DateTimeFormatter.ofPattern("yyyy. MM. dd (E)").withLocale(Locale.KOREA))).
                        ifPresent(System.out::println);
 
        return nextDay;
 
    }, (d1, d2) -> d1);
 
// PRINT RESULT
// 2017. 03. 07 (화)
// 2017. 03. 08 (수)
// 2017. 03. 10 (금)
// 2017. 03. 11 (토)
// 2017. 03. 13 (월)
// 2017. 03. 14 (화)
// 2017. 03. 15 (수)
// 2017. 03. 17 (금)
// 2017. 03. 18 (토)
// 2017. 03. 20 (월)
cs


하지만 모든 전략을 구현해야하는 것은 아닙니다. 이미 JAVA8 에서는 많이 사용할 법한 전략을 팩토리 메소드로 제공하며, TemporalAdjusters 클래스에서 확인해 볼 수 있습니다. (예를들면, 현재 달의 마지막 날짜라던지.. 등 ㅡㅡ^)



3. Duration, Period


각 객체의 시간 및 날짜의 간격을 알고 싶은 경우가 존재하며, 이를 위해 JAVA8 에서는 Duration, Period 클래스를 제공합니다.


Duration 은 시간단위로 간격을 표현하며, 앞서 언급한 LocalTime, LocalDateTime, Instant(기계를 위한 시간객체) 를 이용할 수 있습니다. (LocalDate 는 시간단위로 표현할 수 없기 때문에 Duration 을 사용할 수 없습니다.)


Period 는 날짜단위의 간격을 표현하며, LocalDate 를 사용할 수 있습니다. 뒤의 나올 내용인 시간대 적용 시, 섬머타임을 바탕으로 간격을 표현하려면 Period 를 사용해야합니다.


또한 of 메소드를 사용하여, 기타의 임의 간격을 표현할 수 있습니다.


1
2
3
4
5
6
7
8
// 10:00 ~ 23:20 간격
Duration duration = Duration.between(LocalTime.of(1000), LocalTime.of(2320));
 
// 2016.1.5 ~ 2018.1.20 간격
Period period = Period.between(LocalDate.of(201615), LocalDate.of(2018120));
 
// 5일 간격
Period fiveDays = Period.ofDays(5);
cs



4. 시간의 파싱과 포맷팅


기존 SimpleDateFormat 과 같이 문자열에서 날짜를 parse 나 날짜 객체를 format 하여 출력하는 기능 역시 수행할 수 있습니다. 


새 API 의 등장과 함께 DateFormatter 라는 팩토리 클래스에서 포맷팅 객체를 생산할 수 있으며, 많이 사용할 법한 ISO_LOCAL_DATE (yyyy-MM-dd) 같은 패턴은 바로 사용할 수 있습니다.


1
2
3
4
5
6
LocalDate date = LocalDate.parse("2017-04-07", DateTimeFormatter.ISO_LOCAL_DATE);
 
Optional.of(date).map(d -> d.format(DateTimeFormatter.ofPattern("yyyy. MM. dd"))).ifPresent(System.out::println);
 
// PRINT RESULT
// 2017. 04. 07
cs



5. 특정 시간대 적용


만드는 제품이 세계적이라면, 특정 지역의 시간대를 사용하고 싶을 경우가 있습니다. 


기존에는 TimeZone 클래스가 그 역할을 했으며, 새 API 로는 ZoneId 클래스가 등장했습니다.

ZoneId 를 사용하면, 시간대 변경 시 썸머타임과 같은 복잡한 문제에 대해 고려하지 않아도 됩니다.


사용법은 지역ID 를 통해 객체를 생성할 수 있으며, LocalDate 군 클래스들을 ZonedDate 군 클래스로 랩핑할 수 있습니다. (즉 로컬 시간을 특정 시간대로 변환한다는 이야기이며, 아래 사진은 Local-Zoned 간의 관계를 나타낸 사진입니다. )


[출처 : JAVA8 IN ACTION]



예제는 아래와 같이 사용할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
ZoneId zoneId = ZoneId.of("Europe/Jersey");
 
 
LocalDateTime date = LocalDateTime.now();
ZonedDateTime zonedDateTime = date.atZone(zoneId);
 
System.out.println(date.format(DateTimeFormatter.ISO_DATE_TIME));
System.out.println(zonedDateTime.format(DateTimeFormatter.ISO_DATE_TIME));
System.out.println(zonedDateTime.format(DateTimeFormatter.ISO_INSTANT));
 
// Print Result
// 2017-04-05T00:47:21.346
// 2017-04-05T00:47:21.346+01:00[Europe/Jersey]
// 2017-04-04T23:47:21.346Z
cs

 

새로운 JAVA 날짜&시간 API 는 하위 호환성을 위해 Calendar, Date 를 fromXX 등으로 지원합니다. 즉 이전 작성된 코드들을 크게 변경하지 않아도 됩니다.


이번 포스팅을 하기 전에 Android 에서 사용할 Date, Calendar 를 이용한 빌더를 만든적이 있었는데요. (JAVA8 지원이 안되기 때문에....) 



어떤면에서는 쓰기 익숙한면도 있지만, JAVA8 의 API 의 설계에 비해 많이 약소함을 느끼고 있습니다. (그들은 많은 시간 고민해서 배포한 API 겠죠. TimeBuilder 역시 조금 더 손을 봐야겠다는 생각이 듭니다.  ㅡㅡ^)


이번 포스팅을 하며 각 API 의 내부를 들여다 보게되니, 더 노력해야겠다는 생각이 드는 초보 프로그래머였습니다.


자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기







반응형
Posted by N'

해당 포스팅에서 언급된 내용은 Ndroid 에서 제공합니다.

https://github.com/skaengus2012/Ndroid


Java 에서 시간 관련 클래스 (Calendar, Date) 들은 꽤 많이 사용됩니다. 


하지만 언제나 사용할 때마다 헷갈리는 부분이 존재하며, 간단한 기능도 직관적이지 않은 패턴을 사용해야합니다. (예를들면, Calendar 에서 달을 출력하려면? 현재 년도에 특정 값을 더하고 싶다면?)


1
2
3
4
5
Calendar calendar = Calendar.getInstance();
        
calendar.get(Calendar.MONTH); // 출력되는 달은 0-11 입니다. (1-12 가 안나온다는 것을 기억해야하죠.
        
calendar.add(Calendar.YEAR, 2); // 년도를 더하고 싶습니다. 덧셈을 하고 싶다면, Calendar 의 상수를 이용해야하죠.
cs


제 생각에 직관적이지 못하다고 생각하는 이유는 메소드명을 보고 행위를 하는 것이 아닌, 상수를 넣고 처리를 해야하기 때문입니다. 물론 Calendar 클래스에 익숙하다면 능숙하게 사용하겠지만, 날짜 연산에 대해 조금만 복잡하게 계산을 한다고 하면 저 상수를 이용한 연산을 줄줄이 입력해야할 수도 있습니다. 


Ndroid 프로젝트에서는 조금 더 이를 편하게 사용하고 싶었습니다. 


Calendar 의 사용목적이 결국 날짜 데이터를 핸들링하는 것이고, 일련의 연산과정 중 최종으로 핸들링된 날짜만 알고 싶습니다. 이를 마치 질의를 하는 것과 같은 선언형식으로 할 수 있다면 매우 코드가 아름다워지지 않을까라는 기대가 있었습니다.


Ndroid 의 TimeBuilder 모듈은 위와 같은 요구사항을 담도록 하였습니다. 


예를 들어 이러한 요구사항이 있다고 합시다.


- String 으로 된 시간을 파싱할 것.

- 파싱한 날짜에 년도를 1 덧셈

- 파싱한 날짜에 달을 5 덧셈.

- 파싱한 날짜에 일을 1 뺄셈

- 시,분,초는 생략할 것.

- 날짜는 영어권으로 보여줄 것.


기존 자바 코드로 작성을 한다면, 이 정도 되겠네요.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// simple java code.
 String dateString = "2017-3-26 16:40";
 
try {
    // parse.
    Date formatData = new SimpleDateFormat("yyyy-MM-dd hh:mm").parse(dateString);
 
    // calculating
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(formatData);
    calendar.add(Calendar.YEAR, 1);
    calendar.add(Calendar.MONTH, 5);
    calendar.add(Calendar.DAY_OF_MONTH, -1);
 
    // to yyMMdd
    calendar.set(Calendar.HOUR_OF_DAY, 0);
    calendar.set(Calendar.MINUTE, 0);
    calendar.set(Calendar.SECOND, 0);
    calendar.set(Calendar.MILLISECOND, 0);
 
    System.out.println(new SimpleDateFormat("yyyy.MM.dd (hh,mm,ss a)", Locale.ENGLISH).format(calendar.getTime()));
 
catch (ParseException e) {
    e.printStackTrace();
}
cs


간단하지는 않네요. 사용하려면 상수의 기능부터 알아야하고 (DAY_OF_MONTH, HOUR_OF_DAY 등은 한 번 찾아봐야겠죠 .ㅡㅡ^), SimpleDateFormat 클래스의 기능도 알아야합니다.


위의 기능을 TimeBuilder 를 통해 작업해보도록 하겠습니다.


1
2
3
4
5
6
7
8
TimeBuilder.Create(dateString, "yyyy-MM-dd hh:mm").
                addYear(1).
                addMonth(5).
                addDay(-1).
                setLocale(Locale.ENGLISH).
                to_yyMMdd().
                getStringFormat("yyyy.MM.dd (hh,mm,ss a)").
                subscribe(System.out::println);
cs


Builder 클래스에서 제공하는 메소드를 사용함으로써, 상수들을 알 필요가 없어졌으며 파싱 및 포맷을 위해서 사용할 SimpleDateFormat 역시 몰라도 됩니다.


파이프라인 메소드 중 getStringFormat 의 출력 타입은 RxJava 의 Maybe 입니다. 

포맷의 형식이 잘못되어 포맷에 실패할 수도 있으며, 이런 결과를 이전 MaybeUtil 에서 제공하던 기능들과 같이 값이 있거나 없거나의 문제로 보았습니다.


MaybeUtil 의 기능은 아래에서 참고 



TimeBuilder 의 인스턴스를 만드는 방법은 아래와 같이 다양합니다.


1
2
3
4
5
6
7
8
9
10
11
 // non-param : current time.
TimeBuilder currentTimeBuilder = TimeBuilder.Create();
 
// param : Calendar.
TimeBuilder calendarBuilder = TimeBuilder.Create(TimeUtil.GetCalendar());
 
// param : date
TimeBuilder dateBuilder = TimeBuilder.Create(new Date());
 
// param : string, format
TimeBuilder stringBuilder = TimeBuilder.Create("2017-3-26""yyyy-MM-dd");
cs


보다 자세한 내용은 아래 url 에서 확인하실 수 있습니다.



이 기능을 제작하게 된 첫 번째 배경은 JAVA8 에서 Date 관련 API 의 개편이 있던 것으로 알았고 (아직 공부해보지는 않았습니다. 아마 다음 올릴 포스팅이 해당 부분일 것입니다.), 안드로이드 진영에서는 아직 JAVA8 을 온전히 지원하지 않기 때문에 추가하고자 하는 배경이 되었습니다.


JAVA8 의 기능을 보고 TimeBuilder 의 기능이 더 추가될 여지가 있길 바랍니다. :-)


반응형
Posted by N'

지난 포스팅에서는 현재 만들어지는 어플리케이션이 대부분 네트워크 작업을 수행하며, 그에 따라 비동기 처리가 중요함을 생각할 수 있었습니다. 


JAVA 에서는 이를 위해 Future 인터페이스를 제공했으며, JAVA8 에서는 조금 더 쓰기 쉬운 CompleteableFuture 의 사용법을 알아보았습니다.



CompleteableFuture 에 대해서 조금 더 봐야할 부분은 Future 에 비해 쓰기 쉬워진 점과 더불어 비동기 처리를 조합할 수 있다는 것입니다.


예를들어, 비동기 처리를 하는 중 동기 처리를 수행하고 비동기 처리를 계속해서 진행해야할 수도 있고 각자 시작한 비동기 처리의 결과의 싱크를 맞춰 처리해야할 수도 있습니다. CompleteabeFuture 에는 이를 위한 파이프라인 메소드를 지원합니다.


오늘 포스팅은 시나리오에 따른 구현을 살펴보려 합니다.


1. 두 비동기작업(A, B) 간의 순서가 존재하며, A의 결과로 B 를 수행.


비동기 처리를 해야하는 두 가지 작업 A, B 가 있다고 가정합시다. 


그런데 상황이 조금 복잡합니다.


비동기 처리 A 를 수행하는 것은 문제가 아니지만, B 의 결과를 도출하기 위해서는 A 의 결과가 필요합니다. 즉 A 가 끝난 다음, B 가 실행되어야함을 의미합니다.


CompleteabeFuture 에서는 아래와 같은 파이프라인 작업으로 이를 쉽게 해결할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 Supplier<String> A = () -> {
    try {
        System.out.println("A 스레드 작업 시작");
        Thread.sleep(2000);
        System.out.println("A 스레드 작업 완료");
        return "A 실행";
    } catch (InterruptedException e){
        e.printStackTrace();
        return "실패";
    }
};
 
Function<StringString> B = (aResult) -> {
    try {
        System.out.println("B 스레드 작업 시작");
        Thread.sleep(1000);
        System.out.println("B 스레드 작업 완료");
        return aResult + " B 실행";
    } catch (InterruptedException e){
        e.printStackTrace();
        return "실패";
    }
};
 
 Future<String> result = CompletableFuture.
                supplyAsync(A).
                thenApply(aResult -> aResult + " A 성공 -> ").
                thenCompose(aSucceedResult -> CompletableFuture.supplyAsync(() -> B.apply(aSucceedResult)));
 
System.out.println(result.get());
 
// 결과
// A 스레드 작업 시작
// A 스레드 작업 완료
// B 스레드 작업 시작
// B 스레드 작업 완료
// A 실행 A 성공 -> C 실행
cs


새로운 문법이 등장했습니다.


thenApply 


A의 결과를 받아, 다른 결과를 내보는 Function 과 같은 역할을 수행합니다. 

기존 파이프라인 메소드인 map 과 동일합니다.


생각해봐야할건 앞서, A 의 결과가 끝날 때까지 thenApply 에서 블록이 걸리지 않는다는 것입니다. 즉 A 의 비동기 결과가 모두 끝나야 실행이 됩니다.


thenCompose


A로부터의 최종 결과가 끝나는 즉시, B를 실행합니다. 


함수 디스크립터를 보면, 

A 의 결과인 aSucceedResult 를 받음을 알 수 있으며 B 실행 시 이를 사용합니다.


2. 두 비동기작업(A, B) 간 순서는 없지만, A, B 결과를 합쳐야 하는 경우


이번엔 다른 케이스를 생각해보겠습니다. 


시간이 오래 걸리는 A, C 의 순서 관계는 없으며, 이 둘의 결과가 합쳐지기만 기다리면 됩니다.

비동기 처리는 동시에 보내지만, 최종 결과는 A,C 가 모두 끝날 때까지 기다려야합니다.


이 기능을 위해 우리는 CompleteabeFuture 의 파이프라인 메소드인 thenCombine 을 사용할 수 있습니다. 아래 예제에서 사용법을 확인할 수 있습니다.


가정 : C 의 작업을 A 보다 빨리도록 조정하였습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 Supplier<String> A = () -> {
    try {
        System.out.println("A 스레드 작업 시작");
        Thread.sleep(2000);
        System.out.println("A 스레드 작업 완료");
        return "A 실행";
    } catch (InterruptedException e){
        e.printStackTrace();
        return "실패";
    }
};
 
Supplier<String> C = () -> {
    try {
        System.out.println("C 스레드 작업 시작");
        Thread.sleep(500);
        System.out.println("C 스레드 작업 완료");
        return "C 실행";
    } catch (InterruptedException e){
        e.printStackTrace();
        return "실패";
    }
};
 
Future<String> result2 = CompletableFuture.
                supplyAsync(A).
                thenApply(aResult -> aResult + " A 성공 -> ").
                thenCombine(CompletableFuture.supplyAsync(C), (a, c) -> a + c);
 
System.out.println(result.get());
 
// 결과
// A 스레드 작업 시작
// C 스레드 작업 시작
// C 스레드 작업 완료
// A 스레드 작업 완료
// A 실행 A 성공 -> C 실행
cs


역시 새로운 문법인 thenCombine 입니다.


thenCombine


파라미터로 또 다른 CompleteableFuture 를 받으며, 동시에 각 비동기 작업의 결과로 다른 결과를 도출하는 BiFunction 을 받음을 알 수 있습니다.


A,C 의 실행은 동시에 되지만, 결과는 A,C 가 모두 끝난다음에 도출되는 것을 볼 수 있습니다.

즉 비동기 프로그래밍에서 싱크 맞추기 문제가 이렇게 쉽게 처리가 됨을 알 수 있습니다. 


3. 동작을 미리 등록하고, 실행계획 세우기 


비동기로 실행하는 여러 작업이 있다는 가정하에 우리는 join 메소드를 통해 모든 작업이 완료가 되고 결과를 받아볼 수 있었습니다.


하지만 작업이 너무 많아, 실행이 너무 오래 걸리고 또한 어떤 작업은 타임아웃이 되버릴 수 있습니다. 결국 작업이 많은 게 문제네요. ㅡㅡ^


하지만 이런 것을 생각해볼 수 있습니다. 


future 에 결과로 미리 할 작업을 등록하고, 


비동기 작업이 모두 완료 해야하는지


어느 한개만 완료 해도 되는지,


등을 생각해 볼 수 있습니다. 


물론 타임아웃 시간도 등록하여, 해당 시간내에 작업이 끝났는지 혹은 타임아웃이 되었는지 알려 줄 수도 있겠죠?


일단 미리 작업을 등록하는 것 부터 살펴보죠.


future 에 대한 결과를 받아, 소비하는 Consumer 를 등록하는 CompleteableFuture 의 thenAccept 메소드를 주목합시다.


thenAccept


파라미터로 future 의 결과를 받아, 할 일을 지정하는 Consumer 를 받습니다.

즉 Future 의 결과를 받을 수 있을 때 일을 정의하는 것이 아닌, 미리 일을 정의하고 실행 계획에 따라 Consumer 를 실행합니다.


thenAccept 를 사용하여 일을 미리 저장하고, 실행 계획을 지정하는 방법에 대한 예제는 아래와 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 해야할 일에 대한 Supplier 목록 정의
List<Supplier<Integer>> supplierList = IntStream.range(050).
        mapToObj(n -> {
            // 각 Supplier 는 랜덤한 delay 작업 후, 해당 delay 시간을 출력하는 역할.
            Supplier<Integer> supplier = () -> {
                int time = new Random().nextInt(2000+ 1000;
 
                try {
                    Thread.sleep(time);
                    return time;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return -1;
                }
            };
 
            return supplier;
        }).
        collect(Collectors.toList());
 
CompletableFuture[] completableFutures = supplierList.stream().
                map(CompletableFuture::supplyAsync).
                map(f -> f.thenAccept(System.out::println)).        // thenAccept 를 이용하여, 할 일 정의.
                toArray(size -> new CompletableFuture[size]);       // 이 후, Future 의 제네릭은 Void,
                                                                    // 배열로 출력하자.
 
// allOf 사용. 모든 Supplier 는 전부 실행. join 메소드를 통해 모든 실행이 끝나길 기다립니다.
CompletableFuture.allOf(completableFutures).join();
 
// anyOf 사용. 한 Supplier 만 실행되도 작업 마무리, get 메소드를 사용하여 timeout 지정.
CompletableFuture.anyOf(completableFutures).get(5000, TimeUnit.MILLISECONDS);
cs


Stream API 처럼 any와 all 과 같은 형식으로, anyOf, allOf 메소드를 지원합니다. 각 실행 전략에 따라 각 Future 의 결과를 적어도 한 가지만 실행할 지, 모두 실행해야하는 지를 지정합니다.


오늘 포스팅에서는 여러 비동기 처리에 따른 싱크를 맞추는 방법, 실행전략 등을 파이프라인식으로 간단하게 처리할 수 있음을 알 수 있었습니다. 


비동기처리를 하는 방식 역시 선언형으로 간단하게 제어할 수 있다는 것은 매우 흥미로운 점이며, 어플리케이션 만드는 방법은 더욱 간편해지고 우리는 비지니스 로직에 집중하기가 매우 좋아질 것이라 생각합니다.


이처럼 새로운 기술을 익힌다는 것은 매우 즐거운 일이며, 우리의 어플리케이션의 질은 더 좋아질 것입니다. :-)


자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기


반응형
Posted by N'

요즘 제작되는 어플리케이션은 대부분 네트워크 작업을 필요로 하는 경우가 늘어나고 있습니다.


단순 정보만 요청하고 받던 클라이언트 기반 프로그램부터 시작하여, 인터넷이 필요가 없을 것 같은 메모나 사진 촬영 앱도 공유 혹은 클라우드 처리를 지원하기 때문에 네트워크 작업이 없는 경우는 거의 없다고 생각할 수 있을 것 같습니다.


이러한 네트워크와 연결을 시도하는 프로그램을 제작해본 사람들이라면 알고 있겠지만, 네트워크 처리나 File IO 등 오래걸리는 일은 비동기 처리를 해야 합니다. 


비동기 처리는 하는 이유는 IO 작업이 일어나는 동안 메인스레드(아마도 UI 스레드)가 그동안 놀고 있는 상태(블럭 상태 - CPU 사이클이 낭비됨.)를 피하기 위해서입니다. 사용자 측면에서 어떤 정보를 읽어오는 동안 UI 가 멈춘다면, 불편하다고 충분히 느낄 수 있을 것입니다. ㅡㅡ^


아래는 비동기 처리 작업에 대한 그림입니다. 


비동기 프로세스(Async) 의 경우 Process B 가 실행되는 동안 Process A 는 계속해서 작업을 할 수 있습니다.



Java7 부터는 Future 인터페이스를 통해서, 이러한 비동기 프로세스를 수행할 수 있었습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ExecutorService executorService = Executors.newCachedThreadPool();
 
Future<Double> future = executorService.submit(()-> {
    // 편의 상 람다 사용. 자바7 에서는 사용 불가.
    Thread.sleep(2000);
 
    return 1000.0;
});
 
System.out.println("비동기 처리를 하는 동안 다른 일처리.");
 
try {
    // 타임아웃 3초로 지정.
    System.out.println("결과 : " + future.get(3000, TimeUnit.MILLISECONDS));
catch (Exception e) {
    e.printStackTrace();
}
 
// 출력 결과
// 비동기 처리를 하는 동안 다른 일처리.
// 결과 : 1000.0
cs


Future 클래스는 비동기 계산이 끝났는지 확인할 수 있는 isDone, 타임아웃 기간을 결정하고 결과를 출력하는 get 메소드 등이 있습니다.


간단히 비동기 처리는 되지만 조금 아쉽습니다. 


실무에서는 비동기 처리가 꼭 하나씩 생긴다고 볼 수 없으며, 각 비동기 처리에 대한 결과를 동기를 맞춰 또 다른 결과를 내야할 수 도 있습니다. 즉 각 Future 클래스 간 여러 의존성에 대한 관리가 힘들 수 있습니다.


JAVA8 에서는 복잡한 비동기처리를 선언형으로 이용할 수 있는 CompleteableFuture 를 제공하며, Stream API 나 Optional 같이 람다표현식과 파이프라인을 사용하여 비동기 작업을 조합할 수 있습니다.


일단 CompleteableFuture 의 간단한 예제는 아래와 같습니다. CompleteableFuture 는 기본적으로 supplyAsync, runAsync 등 팩토리 메소드를 제공하며, 쉽게 비동기 작업을 수행할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Future<Double> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(2000);
            } catch (Exception e){
                e.printStackTrace();
            }
 
            return 1000.0;
        });
 
System.out.println("비동기 처리를 하는 동안 다른 일처리.");
 
try {
    // 타임아웃 3초로 지정.
    System.out.println("결과 : " + future.get(3000, TimeUnit.MILLISECONDS));
catch (Exception e) {
    e.printStackTrace();
}
cs


비동기로 처리되어야 할 일이 많아지면 어떨까요? 동시에 앞의 예제처럼 Sleep 을 해야하는 task 가 다수일 때는 간단하게 두가지의 선택 경로를 생각해볼 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Supplier<Double> supplier = () -> {
    try {
        Thread.sleep(2000);
    } catch (Exception e){
        e.printStackTrace();
    }
 
    return 1000.0;
};
 
List<Supplier<Double>> supplierList = Arrays.asList(supplier, supplier, supplier, supplier);
 
// 병렬 스트림을 이용. 각 태스크를 병렬로 하여 성능을 높이자.
supplierList.parallelStream().
    map(Supplier::get).
    reduce(Double::sum).
    ifPresent(System.out::println);
 
// CompletableFuture 를 이용한 비동기적으로 처리
{
    List<CompletableFuture<Double>> completableFutures = supplierList.stream().
            map(CompletableFuture::supplyAsync).
            collect(Collectors.toList());
 
    // join 메소드는 모든 비동기 동작이 끝나길 기다립니다.
    completableFutures.stream().
            map(CompletableFuture::join).
            reduce(Double::sum).
            ifPresent(System.out::println);
}
cs


두 구현 방식에 따라 결과는 큰 차이가 나지 않을 수 있습니다. 


그러나 일반 순차 Stream 을 병렬 Stream 으로 변경한 첫 번째 방법이 간단해 보입니다.


굳이 CompletableFuture 를 쓸 필요가 없어보이지만, 병렬스트림과 달리 이를 이용한 방법은 executor 를 커스터마이징 할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
List<Supplier<Double>> supplierList = IntStream.range(0100).mapToObj(n -> supplier).collect(Collectors.toList());
 
supplierList.parallelStream().
        map(Supplier::get).
        reduce(Double::sum).
        ifPresent(System.out::println);
 
// CompletableFuture 를 이용한 비동기적으로 처리
{
 
    final Executor executor = Executors.newFixedThreadPool(Math.min(supplierList.size(), 100), r -> {
                Thread t = new Thread(r);
                // 데몬 스레드 정의
                // 일반 스레드가 실행 중일 때 자바 프로그램은 종료되지 않음 -> 어떤 이벤트를 한없이 기다리면서 종료되지 않은 일반 자바 스레드가 있으면 문제
                // 데몬 스레드는 자바 프로그램이 종료될 때 종료
                t.setDaemon(true);
                return t;
            });
 
    List<CompletableFuture<Double>> completableFutures = supplierList.stream().
            map(CompletableFuture::supplyAsync).
            collect(Collectors.toList());
 
    // join 메소드는 모든 비동기 동작이 끝나길 기다립니다.
    completableFutures.stream().
            map(CompletableFuture::join).
            reduce(Double::sum).
            ifPresent(System.out::println);
}
 
// 병렬스트림 걸린 시간 : 26066초
// CompletableFuture 사용 걸린 시간 : 2015초
cs


놀라운 결과입니다. 걸린 시간 자체가 무려 10배가 넘게 차이가 남을 알 수 있습니다.

즉 로직에 따라 Executor 를 다르게 하여, 최적화 시키는 것이 효과적일 수 있음을 알 수 있습니다.


비동기 처리의 최적화와 더불어, CompleteableFuture 는 람다표현식이나 파이프라인 메소드를 이용하여, 비동기 연산을 조합할 수 있습니다. 다음 포스팅에서는 이에 대해 다루어 보도록 하겠습니다. 



자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기


반응형
Posted by N'