일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- BOJ
- Dxerr.h
- 11286번
- 2206번
- 코딩테스트
- 11758번
- 2166번
- Adapater Pattern
- 2156번
- 프로그래머스
- SerialDate 리펙터링
- 9장
- Spring
- 코딩 테스트
- 클린코드
- Design Pattern
- java
- 백준
- 1300번
- 1043번
- programmers
- 가장 긴 증가하는 부분 수열2
- java의 정석
- 17장
- springboot
- DxTrace
- 냄새와 휴리스틱
- 10830번
- Design Patterns
- 자바의 정석
- Today
- Total
Don't give up!
[Design Pattern] 커맨드 패턴(Command Pattern) 본문
[Design Pattern] 커맨드 패턴(Command Pattern)
Heang Lee 2021. 11. 30. 18:42이 글은 에릭 프리먼의 'Head First Design Patterns'를 읽고 TIL(Today I Learned)로써 정리한 내용을 기록하는 글입니다.
자세한 내용은 책을 통해 확인하실 수 있습니다.
알라딘: Head First Design Patterns (aladin.co.kr)
커맨드 패턴(Command Pattern)
커맨드 패턴은 요구사항을 객체로 캡슐화하여 클라이언트를 서로 다른 요청 내역에 따라 매개변수화하는 디자인 패턴입니다.
커맨드 패턴에서 정의하는 클래스/인터페이스는 다음과 같습니다.
- Receiver : 요구 사항을 수행하기 위해 어떤 일을 처리해야 하는지 알고 있는 객체입니다. 리시버는 커맨드 클래스의 호출을 통해 행동을 수행하기 때문에 요청하는 객체에 대해 신경쓰지 않고, 실제 행동에 대해서만 관심을 가지면 됩니다.
- Invoker : Client의 요청을 받아 Receiver에게 전달하는 컨트롤러 객체입니다. 커맨드의 execute() 메서드를 호출함으로써 커맨드 객체에게 특정 작업을 수행해 달라는 요구를 하게 됩니다.
- Command : 모든 커맨드 객체에서 구현해야 하는 인터페이스입니다. 모든 명령은 execute() 메서드를 호출하여 수행되며 Receiver에 특정 작업을 처리하라는 지시를 전달합니다.
- ConcreteCommand : 실제 구현되는 커맨드 클래스입니다. Invoker에서 execute() 호출을 통해 요청을 하면 커맨드 객체에서 리시버에 있는 메서드를 호출함으로써 그 작업을 처리합니다.
- Client : 명령을 내리는 객체입니다. ConcreteCommand를 생성하고 Receiver를 설정합니다.
커맨드 패턴은 기존 코드를 수정하지 않고 요구사항을 추가할 수 있으며, 명령 호출자(Invoker)와 수신자(Receiver) 사이의 의존성을 없앨 수 있다는 장점이 있습니다.
반면, 명령이 추가될 때마다 커맨드 클래스를 정의해야 하므로 클래스의 수가 많아진다는 점은 커맨드 패턴의 단점입니다.
※ Client와 Invoker의 관계
Head First Design의 예제들은 Client (책의 RemoteControlTest, RemoteLoader)에서 Invoker 객체 (책의 RemoteControl 객체)를 사용함으로써 커맨드 패턴을 설명합니다.
그러나, 커맨드 패턴의 클래스 다이어그램에는 Client와 Invoker 사이의 관계가 표현되어 있지 않습니다.
이것은 Client와 Invoker의 관계가 꼭 있어야 하는 것은 아니라는 것을 의미합니다.
다음은 Client와 Invoker 사이의 관계가 형성되어 있지 않는 예제입니다.
//Command 인터페이스.
public interface Command {
public void execute();
}
//ConcreteCommand 클래스들.
public class CopyFilesCommand implements FilesCommand {
private CopyFilesReceiver receiver;
public CopyFilesCommand(CopyFileReceiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
receiver.copyFiles();
}
}
public class ZipFilesCommand implements FilesCommand {
private ZipFilesReceiver receiver;
public ZipFilesCommand(ZipFilesReceiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
receiver.zipFiles();
}
}
public class MailZipFilesCommand implements FilesCommand {
private MailZipFilesReceiver receiver;
public MailZipFilesCommand(MailZipFilesReceiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
receiver.zipFiles();
}
}
//Command 객체들을 추가하고, 반환하는 Config 클래스.
public class Config {
private List<Command> commands = new ArrayList<>();
private Config() { }
private static class ConfigHelper { //Bill Pugh Singleton
private static final Config INSTANCE = new Config();
}
public static List<Command> getCommands() {
return Collections.unmodifiableList(INSTANCE.commands);
}
public static void addCommand(Command command) {
INSTANCE.commands.add(command);
}
}
//클라이언트 클래스. Config, Receiver, ConcreteCommand 객체들만을 알고 있다. Invoker와 관계를 형성하지 않음.
public class Client {
public void setUpConfig() {
CopyFilesReceiver receiver_copy = new CopyFilesReceiver();
Config.addCommand(new CopyFilesCommand(receiver_copy));
ZipFilesReceiver receiver_zip = new ZipFilesReceiver();
Config.addCommand(new ZipFilesCommand(receiver_zip));
MailZipFilesReceiver receiver_mail = new MailZipFilesReceiver();
Config.addCommand(new MailZipFileCommand(receiver_mail));
}
}
//Invoker 클래스. Config의 커맨드 객체를 받고, execute 메서드를 호출하도록 구현되어 있음.
public class Invoker implements Runnable {
@Override
public void run() {
for (Command command : Config.getCommands()) {
command.execute();
}
}
}
위의 예제에서 Client는 Invoker 클래스와 어떠한 관계를 맺고 있지 않음에도 실제 요구사항들은 Command 객체가 되어 있으며, Invoker는 Client에서 Command 객체를 추가함과는 관계없이 Config에 등록한 Command 객체의 execute 메서드를 호출합니다.
위와 같은 반례가 존재하기 때문에 커맨드 패턴의 클래스 다이어그램에서는 Client와 Invoker 사이의 관계를 정의하고 있지 않습니다.
람다식을 사용한 커맨드 패턴
JAVA 8부터는 함수형 프로그래밍을 지원하며 람다식과 메서드 참조를 사용하여 커맨드 패턴을 개선할 수 있습니다.
//Command 인터페이스. 함수형 인터페이스임을 명시.
@FunctionalInterface
public interface Command {
public void execute();
}
//Command 객체들을 추가하고, 반환하는 Config 클래스.
public class Config {
private List<Command> commands = new ArrayList<>();
private Config() { }
private static class ConfigHelper { //Bill Pugh Singleton
private static final Config INSTANCE = new Config();
}
public static List<Command> getCommands() {
return Collections.unmodifiableList(INSTANCE.commands);
}
public static void addCommand(Command command) {
INSTANCE.commands.add(command);
}
}
//클라이언트 클래스.
//람다식or메소드 참조를 사용하여 ConcreteCommand 객체를 생성하지 않아도 패턴을 완성할 수 있다.
public class Client {
//람다식을 사용한 setUpConfig함수.
public void setUpConfig_Lambda() {
Config.addCommand(() -> CopyFilesReceiver.copyFiles());
Config.addCommand(() -> ZipFilesReceiver.zipFiles());
Config.addCommand(() -> MailZipFileReceiver.zipFiles());
}
//메소드 참조를 사용한 setUpConfig함수.
public void setUpConfig_MethodReference() {
Config.addCommand(CopyFilesReceiver::copyFiles);
Config.addCommand(ZipFilesReceiver::zipFiles);
Config.addCommand(MailZipFileReceiver::zipFiles);
}
}
//Invoker 클래스. Config의 커맨드 객체를 받고, execute 메서드를 호출하도록 구현되어 있음.
public class Invoker implements Runnable {
@Override
public void run() {
for (Command command : Config.getCommands()) {
command.execute();
}
}
}
이전 예제로부터 메소드 참조와 람다식을 적용하여 수정한 예제입니다.
Client 클래스에서 람다식 또는 메소드 참조를 사용한 것 만으로도 ConcreteCommand 클래스의 정의를 생략할 수 있으며, Client 클래스를 들여다보는것 만으로도 각 Command의 의도를 파악할 수 있다는 장점이 있습니다.
SpringBoot의 ApplicationRunner, CommandLineRunner
SpringBoot에서 Command 패턴이 적용된 예시로는 ApplicationRunner, CommandLineRunner가 있습니다.
//CommandLineRunner.class
@FunctionalInterface
public interface CommandLineRunner {
void run(String... args) throws Exception;
}
//ApplicationRunner.class
@FunctionalInterface
public interface ApplicationRunner {
void run(ApplicationArguments args) throws Exception;
}
CommandLineRunner와 ApplicationRunner는 모두 함수형 인터페이스로 선언된 Command 인터페이스이므로 두 인터페이스를 구현한 클래스들은 run에 행동을 선언하는데 집중합니다.
Bean으로 등록된 ConcreteCommand 객체들은 SpringApplication 클래스의 callRunners 메소드에서 실행됩니다.
ApplicationContext에서 ApplicationRunner, CommandRunner 타입의 빈 객체들을 받아 List에 저장하고, 각 타입에 맞는 callRunner 메소드를 호출합니다.
callRunner 메소드는 두 인터페이스의 run 메소드를 호출하고, 에러 발생을 catch하는 것만으로 완료됩니다.
이렇게 구성된 SpringBoot 환경에서, 개발자는 다음과 같이 ConcreteCommand를 구현할 수 있습니다.
다음은 구현되는 ConcreteCommand의 예시입니다.
@Bean
@Transactional
public CommandLineRunner runner(UserRepository userRepository, BoardRepository boardRepository, CommentRepository commentRepository) throws Exception{
return (args -> {
User user = userRepository.save(User.builder()
.name("havi")
.password("test")
.email("havi@gmail.com")
.userType(UserType.admin)
.principal("textPrincipal")
.socialType(SocialType.GOOGLE)
.build());
User user2 = userRepository.save(User.builder()
.name("Haeng LEE")
.password("test2")
.email("test@gmail.com")
.userType(UserType.commonuser)
.principal("textPrincipal")
.socialType(SocialType.GOOGLE)
.build());
IntStream.rangeClosed(1,3).forEach(index -> boardRepository.save(Board.builder()
.title("게시글"+index)
.content("콘텐츠")
.boardType(BoardType.notice)
.user(user)
.build()));
IntStream.rangeClosed(1,200).forEach(index -> boardRepository.save(Board.builder()
.title("게시글"+index)
.content("콘텐츠")
.boardType(BoardType.free)
.user(user2)
.build()));
Board board = boardRepository.findById(1L).get();
IntStream.rangeClosed(1,8).forEach(index -> commentRepository.save(Comment.builder()
.board(board)
.user(user2)
.content("댓글"+index)
.build()));
IntStream.rangeClosed(9,15).forEach(index -> commentRepository.save(Comment.builder()
.board(board)
.user(user)
.content("댓글"+index)
.build()));
});
}
해당 예시는 Bean 등록되는 CommandLineRunner 객체이며, 각 Repository가 Receiver에 해당합니다.
CommandLineRunner의 구현에만 관심을 가지는 것만으로도 실행이 이루어지므로 개발자는 요구사항을 쉽게 적용할 수 있습니다.
참고
uml - Command Pattern: relationship between Client and Invoker - Stack Overflow
'개발서적 > Head First Design Patterns' 카테고리의 다른 글
[Design Pattern] 어댑터 패턴(Adapter Pattern)과 퍼사드 패턴(Facade Pattern) (0) | 2021.12.16 |
---|---|
[Design Pattern] 싱글톤 패턴(Singleton Pattern) (0) | 2021.11.02 |
[Design Pattern] 팩토리 패턴 (Factory Pattern) (0) | 2021.09.20 |