객체의 책임을 다양하게 구현할 수 없을까?
개요
객체의 책임을 정의하고, 그 책임을 수행하는 방법을 다양화한다.
객체 지향의 세계는 객체들의 협력으로 이루어진다.
각 객체는 본인의 책임을 충실하게 수행하며 다른 객체들에게 요청에 대한 응답을 주는데
가끔, 이 객체의 책임이 다양하게 수행되어야 할 때가 있다. 그리고 그때 전략 패턴이 유용하게 쓰인다.
콘솔 어플리케이션을 구현하면서 하나의 역할에 대해 어플리케이션을 위한 구현과 테스트를 위한 구현이 모두 필요했고,
전략 패턴으로 쉽게 문제를 해결할 수 있었다.
이번 기록에서 그 예제를 간단하게 구현하여 복습하려고 한다.
숫자 리스트 출력 프로그램
숫자 리스트 출력 기능을 구현해야 한다고 해보자.
숫자 리스트를 받아 단순히 출력하는 객체가 있고, 숫자 리스트를 생성하는 객체가 있다.
Main.java에서 출력하는 객체를 생성하고, 출력을 요청할 것이다.
역할 생성
여기서 숫자 리스트 생성 에 대한 책임을 다양하게 구현하고 싶다고 해보자.
이 책임을 인터페이스로 생성해 보자.
import java.util.List;
public interface NumberGenerator {
List<Integer> generateNumbers();
}
NumberGenerator라는 역할은 외부 객체로부터 숫자 리스트를 생성해 달라는 요청을 받을 것이고,
이 요청은 generateNumbers라는 행동(메서드) 요청을 통해 NumberGenerator 역할의 객체에게 전달될 것이다.
여기서 요청을 하는 객체는 숫자 리스트를 반환하는 generateNumbers가 어떻게 숫자 리스트를 생성하는 것인지 전혀 모른다.
즉 generateNumbers라는 행동(메서드)만 할 수 있다면, 위 역할을 수행할 수 있는 것이다.
구현
아래 두 가지 방법으로 숫자를 생성하고 싶다고 해보자.
- 랜덤 숫자 리스트 반환
- 지정한 숫자 리스트 반환
랜덤 숫자 리스트 반환
RandomNumberGenerator.java
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class RandomNumberGenerator implements NumberGenerator {
@Override
public List<Integer> generateNumbers() {
Random random=new Random();
List<Integer> pickedNumbers = new ArrayList<>();
while (pickedNumbers.size() < 3) {
int randomNumber = random.nextInt(1, 9);
if (!pickedNumbers.contains(randomNumber)) {
pickedNumbers.add(randomNumber);
}
}
return pickedNumbers;
}
}
위 객체는 generateNumbers라는 메서드를 수행할 수 있기 때문에 숫자 생성이라는 역할을 수행할 수 있다.
첫 번째로 구현하고 싶었던 랜덤 숫자 리스트를 반환하는 구현체를 생성했다.
이제 지정한 숫자 리스트를 반환하는 객체를 생성해 보자.
지정한 숫자 리스트 반환
SelectedNumberGenerator
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class SelectedNumberGenerator implements NumberGenerator{
private final int selectedNumbers;
public SelectedNumberGenerator(final int selectedNumbers) {
this.selectedNumbers=selectedNumbers;
}
@Override
public List<Integer> generateNumbers() {
List<Integer> numberList = new ArrayList<>();
String numberToString = String.valueOf(selectedNumbers);
Arrays.stream(numberToString.split(""))
.map(Integer::parseInt)
.forEach(numberList::add);
return numberList;
}
}
위 객체 또한 generateNumbers 메서드를 수행할 수 있으므로 숫자 생성 역할을 수행할 수 있다.
원하는 것은 지정한 숫자들로 만들어진 리스트를 반환하는 것이다.
때문에 해당 객체를 생성할 때 generateNumbers로 반환받을 리스트 숫자들을 생성자에 전달해 줬다.
활용
이제 숫자 리스트를 출력해 보자.
숫자 출력에 대한 책임을 가지는 객체는 아래와 같다.
NumbersPrinter.java
import java.util.List;
public class NumbersPrinter{
private final NumberGenerator numberGenerator;
public NumbersPrinter(final NumberGenerator numberGenerator) {
this.numberGenerator=numberGenerator;
}
public void printNumbers() {
List<Integer> numbers=numberGenerator.generateNumbers();
numbers.forEach(number-> System.out.println(number));
}
}
이 객체는 위에서 생성한 인터페이스 즉, 숫자 생성에 대한 책임을 가지고 있는 객체로부터
리스트를 응답으로 받고 그 리스트의 숫자들을 출력한다.
NumbersPrinter 객체에 위에서 만든 두 생성기 역할 객체들을 주입해 보자.
Main.java
public class Main {
public static void main(final String[] args) {
System.out.println("=====Selected Numbers=====");
NumbersPrinter selectedNumbersPrinter = new NumbersPrinter(new SelectedNumberGenerator(123));
selectedNumbersPrinter.printNumbers();
System.out.println("=====Random Numbers=====");
NumbersPrinter randomNumbersPrinter = new NumbersPrinter(new RandomNumberGenerator());
randomNumbersPrinter.printNumbers();
}
}
Main 클래스는 Numbers 객체를 생성하고, 이때 숫자 생성 전략을 선택한다.
리스트 생성 전략을 선택하는 것만으로 다양하게 생성된 리스트를 출력할 수 있다.
NumbersPrinter 객체는 리스트를 출력하는 역할을 하는 객체이다.
따라서 리스트 내부 숫자들의 생성에 대해서는 관여하지 않는다.
그저 출력을 해달라는 요청을 받고, 생성해 달라는 요청을 보내 응답으로 받은 리스트를 출력했을 뿐이다.
NumbersPrinter와 두 NumberGenerator는 본인의 역할에만 충실하게 임하고 있다.
이처럼 객체들은 본인의 역할에만 충실하며, 본인의 역할밖의 동작에 대해서는 다른 객체에게 요청하고,
그에 대한 응답을 믿고 사용한다.
즉, 객체 간의 협력에서 중요한 것은 요청을 수신하는 객체가 "무엇을" 해주는가 이다. "어떻게" 해주는가는 해당 객체가 결정할 일이며, 이를 다양하게 구현하고 활용할 수 있게 도와주는 것이 전략 패턴이다.
마무리
제가 공부하면서 알게 되거나 느낀 것들에 대한 기록입니다.
틀린 부분에 대한 지적이나 질문은 환영입니다:)
참고
객체지향의 사실과 오해 - 예스24
『객체지향의 사실과 오해』는 객체지향이란 무엇인가라는 원론적면서도 다소 위험한 질문에 답하기 위해 쓰여진 책이다. 안타깝게도 많은 사람들이 객체지향의 본질을 오해하고 있다. 가장
m.yes24.com
'Design Pattern' 카테고리의 다른 글
[Design Pattern] MVC 패턴 (1) (0) | 2023.10.25 |
---|