본문으로 바로가기
반응형


lambda를 접하고 나면 이를 활용하는 방법으로 execute around pattern이란 용어가 나옵니다.


이번에는 가장 기본이 되는 Execute around pattern에 대해서 얘기합니다.


Execute Around Pattern 이란??

어떤한 작업을 할 때 준비 -> 실행 -> 정리의 역할이 구분되어 있는 코드들이 있습니다. 준비와 정리는 틀처럼 정해져 있고 실행부분만 바뀌는 코드의 형태들을 실행 어라운드 패턴이라고 합니다 

어떤 동작을 할지 코드내에서 구현하는것이 아니라, 공통적으로 사용되는 코드를 template처럼 만들어 두고, 변경되는 부분은 호출(caller)하는 곳에서 구현해서 넘겨줍니다. 아래 예제에서는 파일 IO로 설명합니다. 파일IO를 쓰려면 스트림이나 파일을 열고 원하는 내용을 넣고 close()불러 명시적으로 자원을 반환합니다. 쓰는 내용만 다를뿐 파일을 열고 닫는 형태는 같은 코드들입니다. 

 유사하게 동작 파라미터화를 한다는 말도 많이 언급됩니다. 예를 들어 Strategy pattern을 사용할 때 각각의 algorithm을 구현한 객체를 생성하지 않고 lambda를 이용해서 넘긴다" 라고 표현하면 더 어려운 가요? ^^a


Strategy pattern 얘기가 나왔으니, Strategy pattern으로 잠깐 예를 들어 보겠습니다.


삼각형, 사각형, 원, 타원, 마름모 등등의 넓이를 구하고 싶습니다. 

그래서 Util 클래스를 만들고 area()란 함수를 만들었습니다.

근데, 어떤 도형이 들어오든 면적을 return해주는 만능 함수로 만들고 싶습니다.

일단 아래와 같이 계획을 세웁니다.


1. public static final int TRIANGLE = 0, public ... RECTANGLE = 1 이란 상수를 타입별로 만듭니다.

2. area(int type) 함수에 parameter로 넘겨줍니다.

3. area(int type) 함수 안에서는 switch나 if ~ else로 어떤 type인지 분기합니다.

4. 분기문 안에는 해당 도형에 맞는 넓이를 구하는 공식이 들어갑니다.


"이건 Strategy pattern볼때 맨날 보는 예제아냐??" 란 생각이 떠오르면 아래와 같이 코드를 수정합니다.


1. 공통이 되는 Shape이란 interface를 만들고, area()를 abstract method로 정의합니다.

2. TRIANGLE, RECTANGLE, CIRCLE등등의 객체를 만들고 Shape을 implement합니다.

3. 각각의 도형들은 area()란 함수에 면적 구하는 공식을 각각 구현하여 넣습니다.

4. util 클래스에 calculate(Shape shape)함수를 하나 만듭니다.

5. calculate 내부에서는 shape.area()를 호출하여 값을 반환 합니다.(면적을 계산합니다.)

6. calculate를 호출하는 부분에서는 parameter로  TRIANGLE, RECTANGLE,등등의 객체를 만들어서 넣어줍니다.


여기까지 했다면 객체간 dependency가 줄었기 때문에 수정과 추가에 유연한 코드(loose coupling)가 됨은 물론이고, 분기문도 없는 코드가 됩니다. 

게다가 OOP의 장점인 다형성도 이용했습니다.


여러책에서는 design pattern을 소개하면서 대부분 위와 같은 장점을 얘기하고, 단점으로 class 수가 증가한다 라고 언급합니다. 

말 그래도 그냥 언급만 하지요.


다행이도 java 8부터는 lambda가 이 단점을 극복할 수 있도록 도와줍니다.

Strategy pattern에 lambda를 어떻게 써야할지는 아래 다른 예제를 보고 개인적으로 고민해 보시기 바랍니다.


Resource를 명시적으로 close해야하는 경우

코드를 사용하고 명시적으로 close로 닫는 코드들은 어떤것들이 있는지요?

File IO, Network IO, Memory IO 등등 IO 작업을 하는 코드들은 대부분 사용후 close()를 명시적으로 불러서 resource를 해체 시킵니다. 그렇지 않으면 leak이 발생합니다.

close()사용하는 경우 Lambda를 어떻게 이용하는 한번 보겠습니다.


예제로는 File을 write하는 간단한 코드를 사용하겠습니다.

흥미유발??을 위해서  java 버전별로 sample code를 만들어 보겠습니다.

Java6 까지는 아래와 같이 할 수 있습니다.

public static void main(String[] args) throws IOException {
	FileWriter java6Writer = new FileWriter("sample.txt");
	try {
	    java6Writer.write("java 6 version");
	} catch (IOException ie) {
	    //
	} finally {
	    java6Writer.close();
	}
}

finally에서 close()를 불러야만 exception이 발생해도 안전하게 닫을 수 있습니다.


Java7 에서는 close()의 호출이 누락되거나, 잘못되어 불리지 않는경우를 방지하기 위해 try-with-resources를 지원합니다.

public static void main(String[] args) throws IOException {
	
	try(FileWriter java7Writer  = new FileWriter("sample.txt")) {
	    java7Writer.write("java 7 version");
	} catch (IOException ie) {
	    //
	}
}

FileWriter는 AutoClosable interface를 상속받고 있습니다. AutoClosable interface는 close() 함수 하나만 가지고 있으며 자식 클래스에서 close()를 override해 놓으면 try 블럭이 끝나는 순간 자동으로 close()가 진행됩니다.


만약 직접 만든 클래스가 AutoClosable를 implements하도록 설계해 놓는다면, 직접 만든 클래스도 try-with-resources를 사용할 수있습니다.

단! 다른 사용자나 개발자들이 내가만든 class가 AutoClosable을 구현했다는 사실을 알아야 한다는 문제가 있습니다.


여러분은 AutoClosable 함수를 구현한 java class를 몇개나 알고 계신지요?

(전 확실하지 않은건 쓸때마다 framework까지 따라가서 AutoClosable 구현 했는지 확인해보고 씁니다.)


이런 단점을 극복한 Execute around pattern을 이용한 Java8 code를 만들어 보겠습니다.

public static void main(String[] args) {
    try {
        ResourceCleanUp.makeFile2("sample.txt",
    	    writer -> writer.write("Wow JAVA8~"));
    } catch (IOException ie) {
      // Do nothing
    }
}

public static void makeFile(final String fileName, Consumer<FileWriter> body)
                                                          throws IOException {    
    final FileWriter fw = new FileWriter(fileName);
    try {
	     body.accept(fw);
    } finally {
	     fw.close();
    }
}

makeFile()을 static으로 만들어 호출합니다.

makeFile은 파일 이름과, Consumer를 받기 때문에 호출부분에서 Consumer의 method signature인 (T) -> void 를 만족하도록 lambda식을 작성해서 전달하면 됩니다.

public static void main(String[] args) {
    try {
        ResourceCleanUp.makeFile2("sample.txt",
    	    writer -> {
                      writer.write("Wow JAVA8~");
                      writer.write("Good!!");
            });
    } catch (IOException ie) {
      // Do nothing
    }
}


실제 객체의 생성 및 resource 반환은 makeFile() 내부에서 일어나므로 호출부분인 main() 함수에서는 신경 쓸 필요가 없습니다.

여러개의 line을 넣기 위해서는 아래와 같이 호출 부분을 수정하면 됩니다.

혹시 눈치 채셨는지요?

위 코드는 아쉽게도 compile이 안됩니다.

wirter.write 밑에 빨간줄이 갑니다..Hint로는 IOException 처리가 필요하다는 문구가 표시 됩니다.

Lambda expression 내부에서 발생하는 Exception이기 때문에 아쉽게되 외부에서 try - catch로 묶어봤자 여전히 compile이 되지 않습니다.


compile이 되게 하려면 아래와 같이 바꿔야 합니다.

public static void main(String[] args) {
    try {
        ResourceCleanUp.makeFile2("sample.txt",
    	    writer -> {
                      try {
                          writer.write("Wow JAVA8~");
                          writer.write("Good!!");
                      } catch (IOException ie) {
                        // 람다 오류시 여기로 진입함.
                      }
            });
    } catch (IOException ie) {
      // Do nothing
    }
}

위 코드도 lambda expression을 사용할때 exception 처리의 한가지 방법입니다.

하지만  exception 발생시 외부 코드를 수행하기 위해 lambda expression 외부까지 exception을 전달 시키려면 Exception 전달이 가능한 Functional Interface를 만들어 써야 합니다.


아래 코드는 Exception을 외부로 보낼수 있도록 Consumer를 대체하는 functional interface를 만들어 사용한 코드 입니다.

public class ResourceCleanUp {

    @FunctionalInterface
    interface CustomInterface<T, V extends Throwable> {
	    void accept(T t) throws V;
    }

    public static void main(String[] args) {
	    try {
	        ResourceCleanUp.makeFile2("sample.txt",
	    	    writer -> writer.write("Wow JJANG"));
	    } catch (IOException ie) {
	        // 람식 내부에서 excpetion 발생시 여기로 들어옴
	    }
    }

    public static void makeFile2(final String fileName,
	    CustomInterface<FileWriter, IOException> body) throws IOException {
	    final FileWriter fw = new FileWriter(fileName);
    	try {
	        body.accept(fw);
    	} finally {
	        fw.close();
    	}
    }
}


반응형