티스토리 뷰
* 이펙티브 자바 2/E를 읽고 공부하기 위해 기록한 게시글입니다.
21. 전략을 표현하고 싶을때는 함수 객체를 사용하라
프로그래밍 언어 중에는 함수 포인터, 람다식, 대리자와 같이 특정 함수를 호출할 수 있는 능력을 저장하고 전달할 수 있도록 하는 것들이 있다. 이런 기능은 보통 함수의 인자로 함수를 전달하기 위해 널리 사용되는데, 이때 인자로 전달되는 함수는 호출된 함수의 기능을 변경하는 구실을 한다.
-> 어떤 함수를 인자로 전달하느냐에 따라 여러가지 기능을 만들어낼 수 있다. 이는 대표적인 "전략 패턴"
[전략 패턴, Strategy Pattern]
자신의 기능 컨텍스트에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔 사용할 수 있도록 하는 디자인 패턴.
- 오브젝트를 아예 둘로 분리하고, 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만든다
자바는 함수 포인터를 지원하지 않는다. 하지만 객체 참조를 통해 비슷한 효과를 얻을 수 있다.
객체의 메서드는 보통 호출 대상 객체에 뭔가를 한다. 하지만 다른 객체에 작용하는 메서드, 즉 인자로 전달된 객체에 뭔가를 하는 메서드를 정의하는 것 역시 가능하다. 가지고 있는 메서드가 그런 메서드 단 하나 뿐인 객체는 해당 메서드의 포인터 구실을 할 수 있다. 그러한 객체를 "함수 객체(Function object)"라고 부른다.
public class StringLengthComparator {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
- compare 메서드는 String 인자를 두개 받아 서로의 길이를 비교하는 비교자 메서드.
- StringLengthComparator 객체에 대한 참조는 해당 비교자에 대한 함수 포인터 구실을 한다. 이 참조를 통해 임의의 문자열 두 개를 비교할 수 있다. = StringLengthComparator는 문자열을 비교하는데 사용될 수 있는 "실행가능 전략"이다.
public class EffectiveJava {
public static void main(String[] args) throws IOException {
method(new StringLengthComparator());
}
public static int method(StringLengthComparator s) {
String s1 = "java";
String s2 = "python";
return s.compare(s1, s2);
}
}
(아마 이런 느낌? 일 것이다)
근데 이걸 보면 알 수 있다시피 저 코드에서는 StringLengthComparator 말고는 다른 전략을 전달할 수가 없는 상황이다.
그럼 어떻게 해야할까?
바로 "전략 인터페이스"를 만드는 것!!
비교자 메서드가 모두 동일한 인터페이스를 구현하도록 하면 메서드의 인자로 특정 인터페이스를 구현한 구현 메서드를 넘겨줄 수 있게 된다.
// 실행 가능 전략을 여러개 만들어낼 수 있음 1
public class StringLengthComparator implements Comparator<String> {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
// 실행 가능 전략을 여러개 만들어낼 수 있음 2
public class NoStringComparator implements Comparator<String> {
public int compare(String o1, String o2) {
return 0;
}
}
public class EffectiveJava {
public static void main(String[] args) throws IOException {
method(new NoStringComparator());
}
// Comparator 인터페이스를 구현하고 있는 전략이면 뭐든 넘겨줄 수 있다
public static int method(Comparator<String> s) {
String s1 = "java";
String s2 = "python";
return s.compare(s1, s2);
}
}
이제야 좀 사람다운 코드가 만들어졌다!
전략 인터페이스로 Comparator를 두고, 실행가능 전략들은 각자의 방법으로 전략 인터페이스를 구현해내도록 한다.
그런데 위와 같이 각 전략마다 따로따로 클래스를 만들라고 하니 클래스가 너무 많아지고 부담이 되는 것 같다.
그래서 전략을 "익명 클래스"로 만들어서 사용하기도 한다!
public static void main(String[] args) throws IOException {
method(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
});
}
클래스를 따로 만들 필요가 없어서 편리하긴 한데 문제점이 있다면 익명 클래스를 사용하면 메서드를 호출할 때마다 새로운 객체가 만들어지게 된다. 재사용이 안된다는 것이 뭔가 좀 아쉽다.
그래서 코드가 여러번 반복해서 수행되는 클래스라면 함수 객체를 private static final 필드에 저장해두고 재사용 하는 것을 고려할 수도 있다. 이 방법은 함수 객체에 의도가 뚜렷한 이름을 부여할 수 있다는 장점이 존재한다.
public class EffectiveJava {
private static final Comparator<String> STRING_LENGTH_COMPARATOR = new Comparator<String>() {
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
};
public static void main(String[] args) throws IOException {
method(STRING_LENGTH_COMPARATOR);
}
public static int method(Comparator<String> s) {
String s1 = "java";
String s2 = "python";
return s.compare(s1, s2);
}
}
이렇게 하면 비교자 메서드가 아무리 여러번 호출되도 똑같은 객체로 재사용이 가능하다.
여기에 더해 전략 메서드를 또 별도의 host class를 만들어서 한데 모아놓고 여러 곳에서 돌려쓰도록 만들기도 가능하다. 귀찮아서 생략..
아무튼
(결론)
* 함수 객체를 사용해 전략 패턴을 구현할 수 있다.
* 자바를 이용해 전략 패턴을 구현하기 위해서는 전략을 표현하는 인터페이스를 선언하고, 실행 가능 전략 클래스가 전부 해당 인터페이스를 구현하도록 하면 된다.
* 실행 가능 전략이 한번만 사용되는 경우에는 익명 클래스로 구현하고, 반복적으로 사용된다면 public static 멤버 클래스로 전략을 표현한 다음 전략 인터페이스가 자료형인 public static final 필드를 통해 외부에 공개하는 것이 좋다.
책을 보면 볼수록 자바는 인터페이스가 참 중요하다는 것을 느낀다..
'JAVA' 카테고리의 다른 글
raw type은 사용하지 마라 (0) | 2022.07.25 |
---|---|
자바의 내부 클래스와 중첩 클래스에 대해.. (0) | 2022.07.25 |
추상 클래스 보다는 인터페이스 (0) | 2022.07.24 |
기타 (0) | 2022.07.24 |
상속 전에 데코레이터 패턴을 고려해보자 (0) | 2022.07.23 |