리팩토링
코드 가독성을 높이려면 코드의 문서화를 잘하고 표준 코딩 규칙을 준수하는 등의 노력이 필요하다.
자바 8의 람다, 메서드 참조, 스트림을 활용하여 기존 코드의 가독성을 개선할 수 있다.
익명 클래스를 람다 표현식으로 리팩토링
익명 클래스는 코드를 장황하게 만들고 쉽게 에러를 일으키기 때문에 람다를 사용도록 변경해 간결하고 가독성 좋은 코드를 구현할 수 있다.
Copy // 익명 클래스
Runnable r1 = new Runnable() {
@ Override
public void run () {
System . out . println ( "Hello" );
}
};
// 람다
Runnable r2 = () -> System . out . println ( "Hello" );
익명 클래스와 람다에서의 this, super 지시자는 다른 의미를 갖는다.
익명 클래스의 this는 자기 자신의 클래스를 의미하지만, 람다의 this는 람다를 감싸는 클래스를 의미한다.
익명 클래스는 감싸고 있는 클래스의 변수를 가릴수 있지만, 람다 표현식으로는 가릴수 없다.
Copy int a = 10 ;
Runnable r1 = new Runnable() {
int a = 20 ;
@ Override
public void run () {
System . out . println ( "Hello" );
}
};
Runnable r2 = () -> {
int a = 20 ; // compile error
System . out . println ( "Hello" );
};
익명 클래스는 인스턴스화 할때 명시적으로 형식이 정해지는 반면 람다의 형식은 콘텍스트에 따라 달라지기 때문에 익명 클래스를 람다 표현식으로 변경하면 콘텍스트 오버로딩에 따른 모호함이 발생한다.
아래와 같이 람다식이 Task 구체 클래스와 Runnable 구체 클래스 중 어떤 것인지 알 수 없게된다. 따라서 명시적 형변환을 해주어야 해결할 수 있다.
Copy interface Task {
public void execute ();
}
public static void doSomeThing( Runnable r) {
r . run ();
}
public static void doSomeThing( Task t) {
t . execute ();
}
doSomeThing(() -> System . out . println( "do" )) ; // 어떤 것이 실행되어야 하는지 알 수 없다.
doSomeThing((Task)() -> System . out . println( "do" )) ;
인텔리제이에서도 이 부분에 대해 오류가 있다고 감지해주며, 명시적 형변환을 하도록 제안해주고 있다.
람다 표현식을 메서드 참조로 리팩토링
comparing
이나 maxBy
, groupingBy
같은 정적 헬퍼 메서드를 메서드 참조와 조합해 사용할 수 있다.
Copy // 람다
Map < CaloricLevel , List < Dish >> dishByCaloricLevel = menu . stream ()
. collect ( groupingBy(dish -> {
if ( dish . getCalories() <= 400 ) return CaloricLevel . DIET ;
else if ( dish . getCalories() <= 700 ) return CaloricLevel . NORMAL ;
else return CaloricLevel . FAT ;
}) );
// 메서드 참조
Map < CaloricLevel , List < Dish >> dishByCaloricLevel = menu . stream ()
. collect ( groupingBy(Dish :: getCaloricLevel) );
public class Dish {
// ...
public CaloricLevel getCaloricLevel () {
if ( this . calories <= 400 ) return CaloricLevel . DIET ;
else if ( this . calories <= 700 ) return CaloricLevel . NORMAL ;
else return CaloricLevel . FAT ;
}
}
Copy // 람다
inventory . sort (( Apple a1 , Apple a2) -> a1 . getWeight () . compareTo ( a2 . getWeight ()));
// 메서드 참조
inventory . sort ( comparing(Apple :: getWeight) )
명령형 데이터 처리를 스트림으로 리팩토링
스트림 API는 데이터 처리 파이프라인의 의도를 명확하게 보여주고 최적화할 수 있으며 멀티코어 아키텍처를 쉽게 활용할 수 있도록 한다.
Copy List < String > dishNames = new ArrayList <>();
for ( Dish dish : menu) {
if ( dish . getCalories () > 300 ) {
dishNames . add ( dish . getName ());
}
}
menu . parallelStream ()
. filter (d -> d . getCalories () > 300 )
. map (Dish :: getName)
. collect ( toList() );
코드 유연성 개선
conditional deferred execution 패턴
클라이언트 코드에서 객체 상태를 자주 확인하거나 객체의 일부 메서드를 호출하는 상황이라면 내부적으로 객체의 상태를 확인한 후 메서드를 호출하도록 하는 새로운 메서드를 구현하는 것이 좋다.
아래는 3단계를 거쳐 리팩토링한 것으로, 조건에 맞는 상황이 되었을 때에만 메시지를 생성하도록 지연하는 conditional defferred execution 패턴을 사용했다.
Copy // 1단계: 직접 로그 객체의 상태를 확인한 후 메시지를 생성해 로깅
if ( logger . isLoggable ( Log . FINER )) {
logger . finer ( "Problem: " + generateDiagnostic() );
}
// 2단계: log 메서드 내부에서 로그 객체의 상태를 확인하고 메세지를 로깅
logger . log ( Level . FINER , "Problem: " + generateDiagnostic() );
// 3단계: log 메서드 내부에서 로그 객체의 상태를 확인하고 적합한 상황에만 람다를 통해 메세지를 생성해 로깅
logger . log ( Level . FINER , () -> "Problem: " + generateDiagnostic() );
execute around 패턴
매번 같은 준비, 종료 과정을 반복하는 코드를 람다로 변환하여 재사용할 수 있다.
아래는 file.txt
파일을 try-with-resource 문으로 접근하는 과정을 템플릿화 하고, 내부적으로 파일의 데이터를 어떻게 처리할지는 함수형 인터페이스인 BufferedReaderProcessor
를 구현한 람다로 작성하도록 해 execute around 패턴을 구현한 예제이다.
Copy String oneLine = processFile(( BufferedReader b) -> b . readLine()) ; // 람다 전달
String twoLine = processFile(( BufferedReader b) -> b . readLine() + b . readLine()) ; // 다른 람다 전달
public static String processLine( BufferedReaderProcessor p) throws IOException {
try ( BufferdReader br = new BufferedReader( new FileReader( "file.txt" )) ) {
return p . process (br); // 인수로 전달된 BufferedReaderProcessor 실행
}
}
public interface BufferedReaderProcessor {
String process ( BufferedReader b) throws IOException ;
}
람다와 디자인 패턴
전략 패턴
전략 클래스
에서는 다양하게 갈아끼울 수 있도록 인터페이스
필드를 가지고 있으며, 이 인터페이스 필드에 구체적인 구현 클래스가 주입되는 패턴이다.
전략 패턴에서 구현 클래스를 주입할 때 직접 구체 클래스를 정의하고 인스턴스를 생성하는 대신 람다를 사용해 간단히 주입할 수 있다.
아래 예제에서는 String을 입력받아 유효성을 검사할 수 있는 ValidationStrategy 인터페이스를 Validator 전략 클래스가 필드로 가지고 있으며, 이 전략 클래스 내부에서 어떻게 유효성 검사를 할 지는 람다로 주입받도록 한다.
Copy @ FunctionalInterface
public interface ValidationStrategy {
boolean execute ( String s);
}
public class Validator {
private final ValidationStrategy validationStrategy;
public Validator ( ValidationStrategy validationStrategy) {
this . validationStrategy = validationStrategy;
}
public boolean validate ( String s) {
return validationStrategy . execute (s);
}
}
// 람다로 원하는 검증 방식 주입
Validator lowerCaseValidator = new Validator(( String s) -> s . matches( "[a-z]+" )) ;
lowerCaseValidator . validate ( "bbbb" );
Validator numericValidator = new Validator(( String s) -> s . matches( "\d+" )) ;
numericValidator . validate ( "1234" );
템플릿 메서드 패턴
템플릿 메서드 패턴이란 변하는 부분을 추상 클래스의 추상 메서드를 구현하도록 하고 변하지 않는 부분은 추상 클래스의 메서드에 구현해두는 방식이다.
Copy abstract class OnlineBanking {
public void processCustomer ( int id) {
Customer c = Database . getCustomerWithId (id);
makeCustomerHappy . accept (c);
}
abstract void makeCustomerHappy ( Customer c);
}
람다를 사용할 경우 아래와 같이 추상 클래스의 추상 메서드를 두는 대신 람다식을 입력받아 수행하도록 할 수 있다.
Copy abstract class OnlineBanking {
public void processCustomer ( int id , Consumer < Customer > makeCustomerHappy) {
Customer c = Database . getCustomerWithId (id);
makeCustomerHappy . accept (c);
}
}
new OnlineBanking() . processCustomer ( 1337 , ( Customer c) -> print( "hello" + c . getName()) );
옵저버 패턴
어떤 이벤트가 발생했을 때 한 객체(Subject)가 다른 객체 리스트(Observer)에 자동으로 알림을 보내야 하는 패턴에서 옵저버 디자인 패턴을 사용한다.
NotiSubject 인터페이스에서 새로운 메시지를 notifyObservers 메서드를 통해 발행하면 등록된 모든 Observer로 해당 메시지가 전달되어 Observer의 notify 메서드가 호출된다.
Copy interface Observer {
void notify ( String tweet);
}
public class NyTimes implements NotiObserver {
@ Override
public void notify ( String tweet) {
if (tweet != null && tweet . contains ( "monety" )) {
System . out . println ( "Breaking news in NY ! " + tweet);
}
}
}
public class Guardian implements NotiObserver {
@ Override
public void notify ( String tweet) {
if (tweet != null && tweet . contains ( "queen" )) {
System . out . println ( "Yet more new from London .. " + tweet);
}
}
}
public interface NotiSubject {
void registerObserver ( NotiObserver o);
void notifyObservers ( String tweet);
}
public class Feed implements NotiSubject {
private final List < NotiObserver > observers = new ArrayList <>();
@ Override
public void registerObserver ( NotiObserver o) {
observers . add (o);
}
@ Override
public void notifyObservers ( String tweet) {
observers . forEach (o -> o . notify (tweet));
}
}
Feed f = new Feed() ;
f . registerObserver ( new NyTimes() );
f . registerObserver ( new Guardian() );
f . notifyObservers ( "The Queen ..." )
이 옵저버 패턴에서 각 Observer 구현체를 만들지 않고 람다식을 넘겨 옵저버를 등록하여 코드를 간결하게 만들 수 있다.
Copy feed . registerObserver (( String tweet) -> {
if (tweet != null && tweet . contains ( "money" )) {
System . out . println ( "Breaking news in NY ! " + tweet);
}
});
람다 표현식으로 불필요한 코드가 제거 되는것이 바람직하다 할때만 람다식으로 사용하는 것을 권장한다.
복잡한 로직이나 여러 메서드가 정의된다면 람다 표현식 보다 기존 방식을 고수하는 것이 오히려 가독성이 좋다.
의무 체인 패턴
작업 처리 객체의 체인을 만들 때 사용하는 패턴으로 한 객체가 작업을 처리한 다음 다른 객체로 결과를 전달하고 다음 객체가 작업을 처리후 그 다음 객체로 전달하는 형식이다.
일반적으로 아래와 같이 추상 클래스에 다음 객체를 필드로 갖고 있다가 본 클래스의 작업이 끝나면 다음 클래스의 작업을 수행해 결과를 반환한다.
Copy public abstract class ProcessingObject < T > {
protected ProcessingObject < T > successor;
public void setSuccessor ( ProcessingObject < T > successor) {
this . successor = successor;
}
public T handle ( T input) {
T r = hadleWork(input) ;
if (successor != null ) {
return successor . handle (r);
}
return r;
}
abstract protected T hadleWork ( T input);
}
public class HandleTextProcessing extends ProcessingObject < String > {
@ Override
protected String hadleWork ( String input) {
return "From Raoul, Mario and Alan: " + input;
}
}
public class SpellCheckProcessing extends ProcessingObject < String > {
@ Override
protected String hadleWork ( String input) {
return input . replaceAll ( "labda" , "lambda" );
}
}
ProcessingObject < String > p1 = new HandleTextProcessing() ;
ProcessingObject < String > p2 = new SpellCheckProcessing() ;
p1 . setSuccessor (p2);
String result = p1 . handle ( "Aren't ladbas really sexy?" );
// 결과: From Raoul, Mario and Alan: Aren't lambdas really sexy?
람다를 사용하여 UnaryOperator<String>
타입을 만들면, 구체 클래스를 생성하지 않고도 간단하게 부가 작업을 담당하는 구성 요소들을 만들어 체이닝할 수 있다.
Copy UnaryOperator < String > headerProcessing = ( String text) -> "From Raoul, Mario and Alan : " + text;
UnaryOperator < String > spellCheckProcessing = ( String text) -> text . replaceAll ( "labda" , "lambda" );
Function < String , String > pipeline = headerProcessing . andThen (spellCheckProcessing);
pipeline . apply ( "Aren't ladbas really sexy?" );
정적 팩토리 메서드 보완
인스턴스화 로직을 클라이언트에 노출하지 않고 객체를 만들 때 정적 팩토리 메서드를 사용한다.
아래는 상품명을 입력받아 상품 객체를 생성해 반환하는 메서드이다.
Copy public class ProductFactory {
public static Product createProduct ( String name) {
switch (name) {
case "loan" : return new Loan() ;
case "stock" : return new Stock() ;
case "bond" : return new Bond() ;
default: throw new RuntimeException( "" ) ;
}
}
}
Product p = ProductFactory . createProduct ( "loan" );
위와 같은 정적 팩토리 메서드를 람다를 사용해 아래와 같이 구현할 수 있다. (개인적으로는 이 부분은 람다 사용하지 않는 것이 더 깔끔해보인다.)
Copy final static Map < String , Supplier < Product >> map = new HashMap <>();
static {
map . put ( "loan" , Loan ::new );
map . put ( "stock" , Stock ::new );
map . put ( "bond" , Bond ::new );
}
public static Product createProduct( String name) {
Supplier < Product > p = map . get (name);
if (p != null ) {
return p . get ();
}
throw new IllegalArgumentException( "No such product " + name) ;
}
테스팅
보이는 람다 표현식의 동작 테스트
람다식은 함수형 인터페이스의 인스턴스를 생성하기 때문에 테스트할 때 인스턴스의 동작을 통해 테스트할 수 있다.
Copy public class Point {
public static final Comparator < Point > compareByXAndThenY = comparing(Point :: getX) . thenComparing (Point :: getY);
// ...
}
@ Test
void test() {
// ...
assertTrue( Point . compareByXAndThenY . compare(p1 , p2) < 0 ) ;
}
람다를 사용하는 메서드의 동작에 집중
람다의 목표는 정해진 동작을 다른 메서드에서 사용할 수 있도록 하나의 조각으로 캡슐화 하는 것이다.
람다 표현식을 사용하는 메서드의 동작을 테스트 함으로써 람다 내부 구현을 공개하지 않으면서도 람다 표현식을 검증 할 수 있다.
아래와 같이 람다를 적용한 결과가 예상대로인지 테스트하면 된다.
Copy assertEquals( new Point( 15 , 10 ) , Point . moveAllPointsRightBy( new Point( 5 , 10 ) , 10 )) ;
복잡한 람다 개별 메서드로 분할
복잡한 람다 표현식은 테스트코드에서 참조할 수 없으므로, 람다 표현식을 새로운 일반 메서드로 선언하고 메서드 참조할 수 있도록 바꾸어 테스트해야 한다.
고차원 함수 테스트
함수를 입력받는 메서드의 경우 테스트 코드에서 직접 함수를 입력하여 동작을 검증할 수 있다.
Copy List < Integer > nums = Arrays . asList ( 1 , 2 , 3 , 4 );
List < Integer > filtered = filter(nums , i -> i % 2 == 0 ) ;
assertEquals( Arrays . asList( 2 , 4 ) , filtered) ;
함수를 반환하는 메서드의 경우 반환된 함수의 동작을 테스트하여 검증해야 한다.
디버깅
문제가 발생한 코드를 디버깅할 때에는 스택 트레이스와 로깅을 확인해야 한다.
람다는 이름이 없기 때문에 스택 트레이스의 정보를 이해하기 어려울 수 있다.
스트림 파이프라인에서 요소를 처리할 때 peek 메서드로 스트림 파이프라인의 중간 데이터를 로깅할 수 있다.
Copy numbers . stream ()
. peek (x -> System . out . println ( "from stream: " + x)
. map (x -> x + 17 )
. peek (x -> System . out . println ( "after map: " + x)
. limit ( 3 )
// ...
Last updated 9 months ago