

구조
Request 클래스:
body)을 저장하고 제공.public class Request {
private String body;
public Request(String body) {
this.body = body;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
}
RequestHandler 클래스:
handler() 메서드 제공.public class RequestHandler {
public void handler(Request request) {
System.out.println(request.getBody());
}
}
LoggingRequestHandler 클래스:
public class LoggingRequestHandler extends RequestHandler {
@Override
public void handler(Request request) {
System.out.println("로깅");
super.handler(request);
}
}
AuthRequestHandler 클래스:
public class AuthRequestHandler extends RequestHandler {
@Override
public void handler(Request request) {
System.out.println("인증이 되었나?");
System.out.println("이 핸들러를 사용할 수 있는 유저인가?");
super.handler(request);
}
}
Client 클래스:
LoggingRequestHandler)로 요청을 처리.public class Client {
public static void main(String[] args) {
Request request = new Request("무궁화 꽃이 피었습니다.");
RequestHandler requestHandler = new LoggingRequestHandler();
requestHandler.handler(request);
}
}
문제점
핸들러 체인 부재:
확장성 부족:
핵심 변경점
핸들러 체인 도입:
nextHandler)를 참조하며, 요청을 처리한 뒤 다음 핸들러로 전달.클라이언트와 핸들러의 분리:
구조
RequestHandler (추상 클래스):
모든 핸들러가 상속받는 추상 클래스.
다음 핸들러(nextHandler)를 참조하며, 체인을 구성.
handle() 메서드에서 다음 핸들러로 요청을 전달.
public abstract class RequestHandler {
private RequestHandler nextHandler;
public RequestHandler(RequestHandler nextHandler) {
this.nextHandler = nextHandler;
}
public void handle(Request request) {
if (nextHandler != null) {
nextHandler.handle(request);
}
}
}
인터페이스만 사용할 경우, 모든 구현 클래스에서 setNext나 handler과 같은 메서드를 직접 구현해야 한다! 공통 로직이 있다면 이를 각 클래스에 반복적으로 생성하거나 별도의 유틸리티 클래스에서 관리해야 하므로, 코드가 장황해질 수 있다.
그렇다면 추상 클래스를 사용하면 장점이 무엇이 있을까?
1) 책임 기본 흐름을 보장한다.
다음 핸들러로 요청을 전달하는 공통 로직이 강제되므로, 실수로 생략하는 일을 방지한다.
2) 확장성을 제공한다.
하위 클래스에서 공통 로직을 오버라이드하거나 기본 동작 위에 추가 작업을 구현할 수 있다!
3) 간결한 설계가 가능하다.
인터페이스만 사용할 경우, 모든 구현 클래스가 동일한 코드를 반복 작성해야 하거나 별도의 기본 구현 클래스가 필요하다. 추상 클래스는 이런 추가적인 구조 없이 간결하게 설계할 수 있다!
AuthRequestHandler:
public class AuthRequestHandler extends RequestHandler {
public AuthRequestHandler(RequestHandler nextHandler) {
super(nextHandler);
}
@Override
public void handle(Request request) {
System.out.println("인증이 되었는가?");
super.handle(request);
}
}
LoggingRequestHandler:
public class LoggingRequestHandler extends RequestHandler {
public LoggingRequestHandler(RequestHandler nextHandler) {
super(nextHandler);
}
@Override
public void handle(Request request) {
System.out.println("로깅");
super.handle(request);
}
}
PrintRequestHandler:
public class PrintRequestHandler extends RequestHandler {
public PrintRequestHandler(RequestHandler nextHandler) {
super(nextHandler);
}
@Override
public void handle(Request request) {
System.out.println(request.getBody());
super.handle(request);
}
}
Client 클래스:
public class Client {
private RequestHandler requestHandler;
public Client(RequestHandler requestHandler) {
this.requestHandler = requestHandler;
}
public void doWork() {
Request request = new Request("이번 놀이는 뽑기입니다.");
requestHandler.handle(request);
}
public static void main(String[] args) {
// 핸들러 체인 구성
RequestHandler chain = new AuthRequestHandler(
new LoggingRequestHandler(
new PrintRequestHandler(null)
)
);
Client client = new Client(chain);
client.doWork();
}
}
핸들러 체인 구성:
AuthRequestHandler → LoggingRequestHandler → PrintRequestHandler.클라이언트 실행:
AuthRequestHandler)를 호출.처리 순서:
AuthRequestHandler → LoggingRequestHandler → PrintRequestHandler.출력 결과:
인증이 되었는가?
로깅
이번 놀이는 뽑기입니다.

이 부분에서 나는 여러 핸들러가 등장하고, 각 핸들러가 handle() 메서드를 오버라이드하길래 다중 상속인줄 알았지만, 단일 상속이라고 한다.
단일 상속이란 한 클래스가 하나의 부모 클래스만 상속받는 것을 의미하는데, 여기서 보면 AuthRequestHandler, LoggingRequestHandler, PrintRequestHandler는 모두 단일한 부모 클래스인 RequestHandler를 상속받고, 각 클래스는 부모 클래스의 handle() 메서드를 오버라이드할 뿐이기 때문이다.
다중 상속 예시를 보면
public class MultiHandler extends AuthRequestHandler, LoggingRequestHandler {
// 다중 상속은 자바에서 불가능
}
이런식이고, 물론 자바에서는 다중 상속을 지원하지 않는다!
확장성 증가:
핸들러와 클라이언트의 분리:
핸들러 간 결합도 감소:
유연성:
체인의 길이 증가:
디버깅 어려움:
chain.doFilter())할 수 있다.서블릿 필터 설정
@WebFilter 어노테이션으로 특정 URL 패턴(/hello)에 대해 필터를 적용.필터 동작
doFilter() 메서드는 요청/응답의 전처리 및 후처리를 정의.chain.doFilter(request, response)를 호출하여 다음 필터(또는 컨트롤러)로 요청을 전달.@WebFilter(urlPatterns = "/hello")
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("게임에 참가하신 여러분 모두 진심으로 환영합니다."); // 전처리
chain.doFilter(request, response); // 다음 필터 또는 컨트롤러 호출
System.out.println("꽝!"); // 후처리
}
}
/hello 엔드포인트에 요청이 들어오면:"게임에 참가하신 여러분 모두 진심으로 환영합니다." 출력.HelloController)."꽝!" 출력.필터 체인 구성
설정 예제
SecurityConfig 클래스에서 필터 체인을 구성.@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 요청 권한 검사 시작
.anyRequest().permitAll() // 모든 요청을 허용
.and(); // 체인 끝
}
}

구조
Button 클래스:
Light)에 직접적으로 의존하며, 요청을 수행.public class Button {
private Light light;
public Button(Light light) {
this.light = light;
}
public void press() {
light.off();
}
}
Light 클래스:
on()) 및 끄기(off()) 로직을 구현.public class Light {
private boolean isOn;
public void on() {
System.out.println("불을 켭니다.");
this.isOn = true;
}
public void off() {
System.out.println("불을 끕니다.");
this.isOn = false;
}
}
문제점:
Button은 요청(Light)의 구체적인 실행 방법에 강하게 결합.Button 클래스의 코드를 수정해야 함.핵심 변경점
커맨드 객체 도입:
Command 인터페이스를 정의.Light, Game)은 커맨드 객체로 감싸져 호출자(Button)와 분리.Button 클래스 수정:
장점:
Button)와 수신자(Light, Game) 간의 의존성 제거.구조
Command 인터페이스:
execute()와 undo() 메서드를 정의.public interface Command {
void execute();
void undo();
}
요청별 커맨드 객체:
각 요청(Game, Light)은 커맨드 객체로 캡슐화.
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
@Override
public void undo() {
light.off();
}
}
public class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.off();
}
@Override
public void undo() {
new LightOnCommand(this.light).execute();
}
}
public class GameStartCommand implements Command {
private Game game;
public GameStartCommand(Game game) {
this.game = game;
}
@Override
public void execute() {
game.start();
}
@Override
public void undo() {
game.end();
}
}
public class GameEndCommand implements Command {
private Game game;
public GameEndCommand(Game game) {
this.game = game;
}
@Override
public void execute() {
game.end();
}
@Override
public void undo() {
new GameStartCommand(this.game).execute();
}
}
Button 클래스:
public class Button {
private Stack<Command> commands = new Stack<>();
public void press(Command command) {
command.execute();
commands.push(command);
}
public void undo() {
if (!commands.isEmpty()) {
Command command = commands.pop();
command.undo();
}
}
}
클라이언트 코드:
Button)는 요청의 세부 사항을 알 필요 없이 커맨드 객체를 통해 요청을 실행.public static void main(String[] args) {
Button button = new Button();
button.press(new GameStartCommand(new Game()));
button.press(new LightOnCommand(new Light()));
button.undo();
button.undo();
}
요청 실행:
Button.press() 메서드가 호출되면, 전달된 커맨드 객체의 execute() 메서드 실행.Light, Game)을 처리.실행 취소:
Button.undo() 메서드가 호출되면, 가장 최근에 실행된 커맨드의 undo() 메서드 실행.출력 결과:
게임을 시작합니다.
불을 켭니다.
불을 끕니다.
게임을 종료합니다.

장점
요청과 호출자의 분리:
Button)와 요청(Light, Game) 간의 강한 결합 제거.유연성 증가:
Undo/Redo 기능 지원:
요청의 저장 및 관리:
단점
ExecutorService와 람다:
ExecutorService는 커맨드 패턴을 활용하여 작업을 캡슐화.public class CommandInJava {
public static void main(String[] args) {
Light light = new Light();
Game game = new Game();
ExecutorService executorService = Executors.newFixedThreadPool(4);
executorService.submit(light::on);
executorService.submit(game::start);
executorService.submit(game::end);
executorService.submit(light::off);
executorService.shutdown();
}
}
데이터 저장 커맨드:
public class CommandInSpring {
private DataSource dataSource;
public CommandInSpring(DataSource dataSource) {
this.dataSource = dataSource;
}
public void add(Command command) {
SimpleJdbcInsert insert = new SimpleJdbcInsert(dataSource)
.withTableName("command")
.usingGeneratedKeyColumns("id");
Map<String, Object> data = new HashMap<>();
data.put("name", command.getClass().getSimpleName());
data.put("when", LocalDateTime.now());
insert.execute(data);
}
}