3편에서 커맨드 패턴을 도입해 명령어마다 독립적인 클래스를 만들었다. 그런데 CommandManagerV3의 execute()를 보면 아직 한 가지 거슬리는 부분이 남아 있다
Command command = commands.get(key);
if (command == null) {
session.send("처리할 수 없는 명령어입니다: " + totalMessage);
return;
}
command.execute(args, session);
명령어를 찾고, null이면 처리하고, 아니면 실행한다. 로직 자체는 맞지만 null 체크가 흐름을 끊는다. 이상적인 코드는 이렇다
Command command = commands.get(key); command.execute(args, session);
명령어를 찾고, 바로 실행한다. 이 두 줄을 위한 Null Object 패턴을 적용한다
DefaultCommand — null을 객체로
public class DefaultCommand implements Command {
@Override
public void execute(String[] args, Session session) throws IOException {
session.send("처리할 수 없는 명령어입니다: " + Arrays.toString(args));
}
}
null인 상황, 즉 “등록되지 않은 명령어가 들어왔을 때 어떻게 할 것인가”를 if문으로 처리하지 않고 객체의 책임으로 만든 것이다. 이것이 Null Object 패턴의 핵심이다
CommandManagerV4
public class CommandManagerV4 implements CommandManager {
private static final String DELIMITER = "\\|";
private final Map<String, Command> commands = new HashMap<>();
private final Command defaultCommand = new DefaultCommand();
public CommandManagerV4(SessionManager sessionManager) {
commands.put("/join", new JoinCommand(sessionManager));
commands.put("/message", new MessageCommand(sessionManager));
commands.put("/change", new ChangeCommand(sessionManager));
commands.put("/users", new UsersCommand(sessionManager));
commands.put("/exit", new ExitCommand());
}
@Override
public void execute(String totalMessage, Session session) throws IOException {
String[] args = totalMessage.split(DELIMITER);
String key = args[0];
Command command = commands.getOrDefault(key, defaultCommand);
command.execute(args, session);
}
}
V3와 비교해 execute() 내부에서 바뀐 것은 딱 한 줄이다
// V3
Command command = commands.get(key);
if (command == null) {
session.send("처리할 수 없는 명령어입니다: " + totalMessage);
return;
}
command.execute(args, session);
// V4
Command command = commands.getOrDefault(key, defaultCommand);
command.execute(args, session);
Map.getOrDefault(key, defaultValue)는 키에 해당하는 값이 있으면 그것을, 없으면 defaultValue를 반환한다. 이 메서드 덕분에 호출하는 쪽은 항상 유효한 Command 객체를 받게 되고, null을 신경 쓸 필요가 없어진다
동작 흐름
클라이언트: "/hello" (등록되지 않은 명령어)
→ split("\\|") → ["/hello"]
→ key = "/hello"
→ commands.getOrDefault("/hello", defaultCommand)
→ Map에 없음 → defaultCommand 반환
→ DefaultCommand.execute(["/hello"], session)
→ "처리할 수 없는 명령어입니다: [/hello]" 전송
Null Object 패턴 정리
- Null Object 패턴은
null대신 사용할 수 있는 특별한 객체를 만들어null로 인해 발생할 수 있는 문제를 방지하는 패턴이다 - 이 패턴이 없다면
null을 반환받은 쪽, 즉 호출하는 코드(클라이언트 코드)가 null 체크를 직접 해야 한다. 이 체크가 코드 곳곳에 퍼지면 유지보수가 어려워진다. Null Object 패턴은 이 책일음 객체 자체에 넘긴다 - 정리하면 이 패턴은 불필요한 조건문을 줄이고, 예외 케이스를 기본 동작으로 정의해 코드를 더 선언적으로 만든다