모던 자바 인 액션 9장

작년 이맘때 모던 자바 인 액션 9장을 리뷰했었는데 다시 보니 새롭네요.

9장?

이 장에서 다루고자 하는 내용은 다음과 같다.

  • 람다와 스트림을 배우긴 배웠다
  • 람다와 스트림을 사용하면 가독성이 개선 된다는 것도 알고 있다
  • 그럼, HOW? 너댓줄짜리 코드야 예제에서 수없이 보았지만….실무 코드는?
  • 따라서, 9장은 그 ‘HOW’ 에 대한 내용을 대표적인 디자인 패턴에 입각하여 설명하겠다
    • 전략패턴
    • 템플릿 메소드
    • 옵저버 패턴
    • 의무체인 패턴
    • 팩토리 패턴

뭐 이런 서두를 통해 글을 작성하였다.

이제 9장을 보면서 진짜로 그렇게 리팩토링을 할 수 있는지 없는지 한번 알아보도록 하자.


가독성 개선은 생략

1~8장까지 리뷰를 하면서 너무 많이 한 작업들…

  • 명령형 데이터 처리(반복문, 필터링 등등등등)를 스트림처리
  • 익명 클래스를 람다 표현식으로
    • 간단한 람다 표현식은 메소드 참조로도 변경

9장에 새로운 내용에 집중하고자 생략한 것이지, 절대로 양이 많다거나 귀찮아서가 아님………..


코드의 유연성 개선

조건부 연기 실행 (feat.번역 극혐)

한 30분동안 이 문단을 읽고 또 읽어봤다… 일단 한글로 검색해 보면 잘 안나오고, 나오더라도 그냥 책에 있는 구문을 그대로 복붙한 느낌(이해불가) 그래서 걍 영어로 deferred execution에 대해 검색을 해 봤다…

일단 결론 : 뭔 소린가 싶은데 책 번역이 잘못된 것 같은 느낌이 강력하게 든다.ㅋㅋ argument passing 을 할 때, ‘코드 그 자체(?)’를 넘기지 말고 ‘함수’ 를 넘겨서, 실행 주체를 caller가 아닌 callee에게 위임한다는 단순한 개념인듯.

예제를 보자

if (logger.isLoggable(Log.FINER)) {
  logger.finer("Problem: " + generateDiag());
}

일단 이 코드가 좀 문제가 많다는 건 확실하다

  • 매 실행시마다 if로 인한 분기를 통해 로깅이 가능한지,그렇지 않은지 체크를 한다
  • logger 객체 자체가, isLoggable 메소드를 통해 호출당함(?)으로써 logger의 상태가 노출된다

그렇게 해서 요렇게 수정을 한다.

logger.log(Level.FINER, "Problem:" + generateDiag());

if문도 제거하고, isLoggable을 호출당하지 않고 로그를 호출할지 말지 결정짓는 코드가 log() 메소드 안에 캡슐화를 됨으로써 logger의 상태가 노출되지 않는다.

이 이후에 문제의 번역이 시작되는데

네? 인수로 전달된 메시지 수준에서….? 평가….? 뭐라구요?

혼란하다….느낌적인 느낌으로 저 ‘수준’이란 단어는 ‘로그 레벨’ 을 뜻하는 level을 잘못 번역한 것일거고……’평가’ 라는 단어는 ‘evaluate’을 모호하게 번역한 것인게 분명하다 ㅋㅋㅋㅋㅋㅋ (‘연산,계산’ 정도로 번역되었다면 이렇게 헤메지 않았을듯-_-)

번역가가 마감시간에 쫓겨서 번역기를 돌렸나보다.

‘log() 메소드의 두번째 argument가, 로그레벨이 FINER가 아닌 상황에서도 씨잘데기없이 실행된다 이말이야’

그러니까, 이 말이 무슨 뜻이냐 하면, 로그 레벨이 OFF상태인데, 다음과 같이 코드를 수행하면

for (int i = 0 ; i < 99999 ; i++) {
  logger.log(Level.FINER, "Problem:" + generateDiag());
}

좋던 싫던 어쨌든 logger의 log()는 호출이 되어야 하기에, 로그 레벨이 OFF라도, 99999번의 log() 호출 전 필수 과정인 “Problem:” + generateDiag() 구문이 실행된다는 뜻이라는거다.

혹시 내가 잘못 이해했나 싶어서, 영어로 ‘deffered execution’ 을 검색해 봤다. https://www.informit.com/articles/article.aspx?p=2171751 의 3.1. 항목을 참고해 보자.ㅋㅋ

당신이 영알못이면 첫 문장만 읽어도 눈치챌 수 있을 것이다.(근데 나도 영알못임)

그러니까, log() 의 두번째 argument를 코드가 아닌 함수 그 자체로 passing시켜서, caller가 아닌, callee가 실행할지 말지 여지를 남겨주게 하는 기법이 ‘조건부 연기 실행’ 이라는 뜻이다.

암튼 그래서 자바8부터는 두번째 argument를 String뿐만이 아닌 Supplier도 제공해서 다음과 같이 편-안 하게 람다를 사용할 수 있게 해 주었다고 한다. 갓자바.

logger.log(Level.FINER, () -> "Problem:" + generateDiag());

p.s : C에서 함수포인터나, js에서 이벤트 핸들링같은거 써본 경험 있으면 쉽고 익숙한 개념..

실행 어라운드

매번 반복되는 준비/종료 과정을 람다로 변환하는 기법. 3장에서 이미 한번 다룬 패턴이고 추가적인 설명이 없다. 생략..


디자인 패턴!!!

디자인 패턴을 통해 해결하던 문제들을, 모던 자바가 제공해 주는 람다로 간단하게 구현할 수 있다!!!(고요?)

  • 전략패턴
  • 템플릿 메소드 패턴
  • 옵저버 패턴
  • 의무 체인 패턴
  • 팩토리 패턴

하나하나씩 훑어보도록 하자.

전략패턴

우선 전략 패턴이라 함은..

말 그대로 ‘전략(strategy)’에 해당하는 동작 그 자체를 인스턴스화 시켜서, 필요시에 적절한 동작을 끌어다 쓰는 기법이다.

책 예제랑은 좀 다르게, 작년에 대충 직접 만든 예제를 통해 알아보자…ㅋ

이건 전략패턴이 아닌 일반적인 코드다.

public interface Earphone {
    public void listen();
}
 
 
public class WireEarphone implements Earphone {
    public void listen() {
        plugin(); // 단자를 디바이스에 꽂음
        play(); // 재생
    }
}
public class CodelessEarphone implements Earphone {
    public void listen() {
        bluetooth2(); //블루투스 2 연결
        play(); //재생
    }
}
public class AirpodEarphone {
    public void listen() {
        bluetooth4(); //블루투스 4 연결
        play(); //재생
    }
}

‘이어폰’이라는 물건을 객체화 한 것 까지는 좋은데, ‘듣는 동작(전략!!!)’ 을 객체화 한것까지는 생각을 못했다.

따라서, ‘듣는 동작(전략)’ 그 자체를 객체화 해 버리면 전략 패턴대로 구현되었다고 볼 수 있다.

public interface ListenStrategy {
    public void listen();
}
public class PluginStrategy implements ListenStrategy { // 단자를 디바이스에 꽂는 행위를 객체화
    public void listen() {
        plugin();
    }
}
public class Bluetooth2Strategy implements ListenStrategy { // 블루투스2를 통해 연결하는 행위를 객체화
    public void listen() {
        bluetooth2();
    }
}
public class Bluetooth4Strategy implements ListenStrategy { // 블루투스4를 통해 연결하는 행위를 객체화
    public void listen() {
        bluetooth4();
    }
}
 
 
public interface PlayStrategy {
// 이하 생략.....
}
 
 
public class Earphone {
    private final ListenStrategy strategy;
    public Earphone(ListenStrategy strategy) {
        this.strategy = strategy;
    }
    public void listen() {
        strategy.listen();
    }
}
public class WireEarphone extends Earphone {}
public class CodelessEarphone extends Earphone {}
public class AirpodEarphone extends Earphone {}
 
 
//User code
// 블루투스 2만 지원하는 이어폰
Earphone oldCodelessEarphone = new CodelessEarphone(new Bluetooth2Strategy());
 
// 블루투스 4를 지원하는 이어폰
Earphone newCodelessEarphone = new CodelessEarphone(new Bluetooth4Strategy());
 
//블루투스 2와 4를 다 지원하는 최신 이어폰을 제작하고 싶으면? --> 새로운 형태의 Earphone을 만드는게 아닌, ListenStrategy라는 전략만 추가하여 새로운 형태의 Earphone에 전략을 추가하면 된다.

엄…. 근데 전략 패턴을 사용하니까 너무너무너무너무 코드가 길어졌다….. 자바의 종특이 그대로 묻어나는 순간(손가락 아파…)

그래서 람다를 사용해서!!!!!! 간단간단하게 전략 패턴과 비슷한 개념의 코드로 리팩토링 할 수 있다!

//User code
Earphone oldCodelessEarphone =  CodelessEarphone(() -> bluetooth2());
oldCodelessEarphone.listen();
Earphone newCodelessEarphone =  CodelessEarphone(() -> bluetooth4());
newCodelessEarphone.listen();

템플릿 메소드 패턴

템플릿 메소드 패턴은 뭐… 별거 없다.

알고리즘을 ‘사용하는’ 쪽에게, 변경의 여지가 있는 부분을 직접 구현하라고 역할을 위임해 주는 고런 패턴이다.

알고리즘을 ‘만드는’ 쪽은, 가변적일 수 있는 로직을 추상화를 하여 인터페이스를 제공해 주면 되는거고.

간단하고 직관적인 패턴이니 예제를 봐 보도록 하즈아

public abstract class OnlineBanking {
    public void depositToCustomer(int id) {    //전체 플랫폼 흐름(플랫폼 제공자의 코드)
        Customer c = Dataabase.getCustomerWithId(id);
        sysout("환영합니다 고객님!!!!");
        depositCash(c);
        sysout("입금 완료했어요~!");
    }
 
    abstract void depositCash(Customer c);  // 클라이언트가 상속받아서 구현해야 함
}
 
 
//User code
public class SeoulSquareOnlineBanking extends OnlineBanking {  //서울스퀘어 지점의 클라이언트 코드
    void  depositCash(Customer c) {   //override....
        sysout("입금해드릴게요. 저희 지점은 보너스 포인트를 지급해 드려요");
    }
}

여기서 depositCash() 가 일종의 템플릿 메소드이다.

이 패턴을 사용하다보면, 상당히 귀찮은게, 알고리즘을 사용하는 쪽에서 강제로 알고리즘을 만드는 쪽의 추상화된 클래스를 강제적으로 extends 하는 하위 클래스가 매번 만들어져야 한다는 것이다.

요런 경우 손가락이 아프지 않게 간단히 람다식으로 바꿔줄 수가 있다(이럼 ‘템플릿 메소드 패턴’에 대한 디자인패턴을 몰라도, 자연스레 그 기법을 사용할 수가 있을 것이다.)

public abstract class OnlineBanking {
    public void depositToCustomer(int id, Consumer<Cutsomer> depositCash) {
        Customer c = Database.getCustomerWithId(id);
        depositCash.accept(c);
    }
}
 
 
//User code
new OnlineBanking().depositToCustomer(1234321,
(Customer c) ->  sysout("입금해드릴게요. 저희 지점은 보너스 포인트를 지급해 드려요.")); //depositCash() 를 직접 lambda를 통해 구현
 
new OnlineBanking().depositToCustomer(1234321,
(Customer c) -> sysout("입금해드릴게요. 저희지점은 로또 한장씩 보너스로 드려요"));

옵저버 패턴

약간 메시지 브로드캐스트같은 느낌이라 생각하면 될 것이다.

1개의 subject – N개의 observer 로 묶여서, subject가 특정 이벤트를 발동하면, 모든 observer에 그 이벤트가 브로드캐스트 되는 그런 구조….(물론 모든 이벤트를 다 처리할 필요는 없으니, 받은 이벤트를 처리할지 말지는 observer가 책임지도록 한다.)

예제를 보자. 뉴스피드 예제이다.

public interface Subject {
    void register(Observer o);
    void notifyAll(String feedMsg);
}
 
 
public interface Observer { // 클라이언트에게 '넌 Observer만 상속받아 notify 수행을 하는 코드만 작성하면 돼.'
    void notify(String feedMsg);
}
 
 
 
 
public NewsFeed implements Subject {
    private final List<Observer> newsObserverList = new ArrayList<Observer>();
     
    @Override
    public void register(Observer o) {
        this.newsObserverList.add(o);
    }
    @Override
    public void notifyAll(String feedMsg) {
        newsObserverList.forEach(elem -> elem.notify(feedMsg));
    }
}

이건 subject에서 구현되는 코드이고,

public class MBCNews implements Observer {
    public void notify(String feedMsg) {
        if (feedMsg.contains("MBC")) {
            sysout("MBC뉴스 속보입니다! 속보내용은 다음과 같습니다! : " + feedMsg);
        }
    }
}
public class JtbcNews implements Observer {
    //이하 생략...
}

이건 observer가 구현한 코드.

뉴스피더(subject)가 뉴스피드를 날리면, 각 방송사들(observer)는 그 피드를 받고 본인이 돌려야 할 로직들을 수행하는 방식이다.

// 뉴스피드 발동!!
NewsFeed nf = new NewsFeed();
nf.register(new MBCNews());
nf.register(new JtbcNews());
nf.notifyAll("MBC에게 속보를 줄까 JTBC 에게 줄까 아님 다줄꺼야");

자…..이게 observer 패턴이다.

그럼 이걸 lambda로 바꿀 수 있을까?

//nf.register(implements Observer 그 무언가...);
 
 
nf.register((String feedMsg) -> {
    if(feedMsg.contains("MBC")) {
        sysout("MBC뉴스 속보입니다! 속보내용은 다음과 같습니다! : " + feedMsg);
    }
});
 
 
nf.register((String feedMsg) -> {
    if(feedMsg.contains("JTBC")) {
        sysout("JTBC 속보내용은 공중파랑 달라요~ 두번 알려드려요. 내용은 다음과 같습니다! : " + feedMsg + feedMsg);
    }
});

코드가 훨씬 깔끔해졌다. 구질구질하게 Observer를 상속받은 클래스를 만들 필요가 없다.

개인적인 생각

이런 간단한 경우야…best case를 뽑았으니 훨씬 깔끔해 보이지, 이 옵저버 패턴같은 경우는 아마 바꾸지 못할 확률이 높은 느낌이다.
각 observer가 자기가 쓸 상태값 하나만(뭐 ‘피드 수신시간’ 이라던가 그런거… 혹은 모든 observer가 아니고, 특정 하나의 observer만이라도) 갖게 되는 순간, lambda로 전환하는것보다 그냥 plain old java 형식으로 구현해 놓는게 차라리 더 깔끔할 거라는 생각.
그리고, 이 코드를 작성하는 쪽에서야 손가락이 덜 아프겠지만 읽는 사람 입장에서는, 람다로 바꾸면서 클래스 이름이 없어졌으니… 이 Observer의 정체가 뭔지 클래스명이 아니라 람다 로직을 봐야 이해를 할 수가 있다. 가독성을 높이고자 한 시도일텐데 오히려 람다의 바디를 봐야 로직을 이해할 수 있는, 더 많이 읽어야 하는 상황이 발생하는 듯 한 느낌임.

암튼 요 패턴을 람다로 바꿀떈 좀 생각을 하고 바꾸는 시도를 하는게 좋지 않을까 싶다.

의무 체인 패턴

간단하게 그냥 메소드 체인을 객체 단위로 하는 패턴… 뭐 얘는 사실 ‘디자인패턴’ 이라는 이름을 붙이기도 뭐한 느낌 아닐까…ㅋㅋ

곧바로 예제를 통해 봐보자.

public abstract class ProcessingObject<T> {
    protected ProcessingObject<T> successor;
    public void setSuccessor(ProcessingObject<T> successor) {
        this.successor = successor;
    }
    public T haandle (T input) {
        T r = handleWOrk(input);
        if (successor != null) {
            return successor.handle(r);
        }
        return r;
    }
 
 
    abstract protected T handlwWork(T input);
}



public class HeaderTextProcessing extends ProcessingObject<String> {
    public String handleWork(String text) {
        return "From Raaoul, Mario and Alan : " + text;
    }
}
 
 
public class SpellCheckerProcessing extends ProcessingObject<String> {
    public String handleWork(String text) {
        return text.replaceAll("labda", "lambda");
    }
}
 
 
//구현부 코드
ProcessingObject<String> p1 = new HeaderTextProcessing();
ProcessingObject<String> p2 = new SpellCheckerProcessing();
p1.setSucessor(p2);  // 체이닝 부분. 이해가 어렵다면 상기 그림을 참조하여 보자.
 
 
sysout(p1.handle("Aren't labdas really sexy?!!"));
// From Raoul, Mario and Alan: Aren't lambdas really sexy?!!

p1의 handleWork가 성공하면, p1의 handleWork 결과물을… p2가 받아서 최종 로직을 수행하는 구조이다.

뭐 요런 느낌

이것도 람다로 변경할 수 있다.

UnaryOperator<String> headerProcessing = (String text) -> "From Raaoul, Mario and Alan : " + text;
UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda");
 
 
Function<String,String> pipeline = headerProcessing.andThen(spellCheckerProcessing); // 체이닝 부분.
pipeline.apply("Aren't labdas really sexy?!!");

하, 얘는 편-안 하다…..ㅋㅋㅋㅋㅋㅋ 이전 예제인 옵저버 패턴과는 다르게 의무 체인 패턴은 람다로 변경하는게 3만배는 더 좋은 것 같다.


팩토리 패턴

뭐…. 말 그대로 붕어빵 공장에서 붕어빵을 찍어내는 그런 패턴이라고 생각하면 된다. 개인적으로 OOP의 ‘다형성’이란 개념이 가장 직관적으로 잘 드러나는 패턴이라고 생각함.

빠르게 예제를 통해 얼마나 간단하게 만들 수 있는지 봐보자

//Pdp, Phone , Travel : extends Product
public class ProductFactory {
    public static Product createProduct(String productName) {
        switch(productName) {
            case "pdp" : return new Pdp();
            case "phone" : return new Phone();
            case "travel" : return new Travel();
        }
    }
}
 
 
 
 
//User Code
Product prd = ProductFactory.createProduct("pdp");
prd.addToCart(); prd.buy(); //etc
 
 
Product prd2 = ProductFactory.createProduct("phone");
prd2.addToCart(); prd2.buy(); //etc

영락없는 팩토리 패턴이다. 팩토리 패턴은 new를 통해 객체를 생성하는게 일반적이므로, 람다보다는 메소드 참조를 이용하여 모던 자바에 맞게 리팩토링을 할 수가 있을 것이다.

public class ProductFactory {
 
    final static Map<String, Supplier<Product>> map = new HashMap<>();
    static {
        map.put("pdp", Pdp::new);
        map.put("phone", Phone::new);
        map.put("travel", Travel::new);
    }
 
    public static Product createProduct(String productName) {
        Supplier<Product> p = map.get(productName);
        if (p != null) {
            return p.get();
        }
        throw new IllegalArgumentException("No Such Product" + productName);
 
    }
}

요렇게…….

근데 차이가 느껴지나?;;;;;;;

난 모르겠다~~~~~ ㅋㅋ

테스팅

암만 이쁘게 짜도 결국 잘 돌아가야 하는게 코드….

일반적인 테스트 코드야 많이많이 봤을테니 과감히 생략하고 곧바로 람다식의 동작 테스트 코드를 봐보자

테스트 하려는 메소드가 public인 경우는 테스트케이스 내부에서 그 메소드를 호출하는 방법으로 테스트를 할 수 있다.

하지만 람다식은 그러지 못한다. 태생 자체가 익명함수이기 떄문이다.

이를 우회하여 테스트 할 수 있는 방법은 있다. 람다식(내지는 메소드 참조) 을 클래스의 프로퍼티에 저장해서 테스트 하면 된다

예제를 보자.

public class Point {
  public final static Comparator<Point> compareByXAndThenY = 
    comparing(Point::getX).thenComparing(Point::getY);
}


@Test
public void testComparing() {
  Point p1 = new Point(10,15);
  Point p2 = new Point(10,20);

 int result = Point.compareByXAndY.compare(p1, p2);
 assertTrue (result < 0);
}

뭐 그렇다고 한다.

근데 뭐… 웹서비스 하는 지극히 정석적인 서비스 코드에서는, 대부분 그냥 서비스 메소드를 단위테스트로 돌려보니 이걸 쓸 일이 없을 것 같긴 하다 (뭔가 테스트 한다고 프로퍼티를 따로 만드는 것도 꺼림칙하고….ㅋㅋ)


디버깅

스택트레이스는 생략한다. 책 내용에서 크게 다루지 않고, 그냥 ‘람다식 스택트레이스는 이해하기 어렵게 출력된다’ 라고….

하지만 로깅은 조금 볼 가치가 있는 구간이다.

표준출력으로 특정 로직이 동작하는 결과값을 눈으로 직접 보고 싶은 경우가 있을 것이다.

List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7);

numbers.stream()
       .map(x -> x+17)
       .filter(x -> x%2 == 0)
       .limit(3)
       .forEach(sysout);

결과값이 잘 출력될 것이다.

다만…. 최종 결과값만이 호출되는 것이고, forEach 구문에 돌입하면서 스트림을 먹어버린다.(number를 다시 쓰려면 forEach 이후에 스트림을 다시 생성해야 한다는 말) 중간중간에 연산 중간값도 보고 싶은 경우도 있을거고…..

그럴때 사용하는게 peek() 이란 메소드이다. 스트림을 꾸역꾸역 먹지 않고, peek의 입력으로 들어온 값을 그대로 read만 하고 그대로 다음 스트림에 출력까지 해 준다.

List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7);

numbers.stream()
       .peek(sysout(x))
       .map(x -> x+17)
       .peek(sysout(x))
       .filter(x -> x%2 == 0)
       .peek(sysout(x))
       .limit(3)
       .peek(sysout(x))
       .collect(toList());

마치며

  • 람다 표현식을 잘~ 사용하면 가독성 있고 유연한. 좋은 코드를 만들 수 있다
  • 익명 클래스는 람다로 변경하는게 좋다. 단, 람다를 사용하는 순간 scope가 변경되면서 this접근자나, 변수의 scope또한 변경되니 주의해서 써야 한다.
  • 메소드 참조도 더더욱 많이 사용하다. 람다보다 더 간단히 쓸 수 있다.
  • 람다 표현식으로 여러가지 디자인 패턴에서 발생하는 불필요한 코드들을 꽤 많이 정리할 수 있다.
  • 람다 표현식도 테스트코드를 수행할 수 있다. 다만 지양하자(서비스 메소드를 호출하는 방식으로 테스트코드를 짜자….)
  • 람다 표현식을 쓰면 스택트레이스를 이해하기 어려운 경우가 있다.
    • 하지만 ‘스택’ 트레이스이기에, 프로젝트 레벨의 코드라면 어느 지점에서 무언가 잘못 되었는지 발견할 수는 있을 것이다..
  • 스트림 pipeline에서 peek이란 메소드를 사용해서, 중간값을 볼 수가 있다.

Leave a Comment