Java Swing 이미지 필터 프로젝트 | 취소 기능(다중 레이어 Undo) 완벽 구현

서론

이미지 처리 기능은 대부분 구현 완료했지만, 사용자들의 주요 불편 사항이 있었습니다. "실수로 잘못된 필터를 적용했으면 다시 되돌릴 수 없을까?"라는 질문이 많았습니다.

초기 구현에서는 BufferedImage에 직접 그렸기 때문에 취소 기능 자체가 불가능했습니다. 하지만 레이어 배열 개념을 활용하면 50줄 이하의 코드만 추가하면 진정한 의미의 다단계 취소 기능을 구현할 수 있습니다. 이론적으로 100단계까지 지원하며, 기존 리페인팅과 스크롤 줌 기능에도 전혀 영향을 주지 않습니다.

이번에는 기존 Swing 프로젝트에 빠르게 "취소" 버튼을 추가하는 방법을 단계별로 설명하겠습니다.

문제 분석

다음과 같은 시나리오를 생각해 보세요:

  • 이미지를 열고 "원본" → "모자이크" → "회색조" → "유화" 순서로 필터 적용
  • 突orum에서 "회색조" 상태로 돌아가고 싶지만 방법이 없음
  • 창을 닫고 다시 시작해야 하는 상황

이러한 제한이 사용자 경험을 크게 저해합니다.

핵심 구현 방식

BufferedImage[] historyStack 배열을 사용하여 각 처리 단계 후 이미지를 저장합니다(전형적인 레이어 개념):

  • 필터를 적용할 때마다 현재 이미지를 배열에 저장
  • "취소" 버튼 클릭 시 배열의 마지막 요소를 null로 설정하고 이전 이미지 표시
  • ImagePanel의 paint 메서드에서 레이어 배열优先적으로 이미지 가져오기

장점

  • 새로운 코드만 추가하고 기존 필터 로직은一根도 변경하지 않음
  • 연속 취소 지원
  • 창 드래그, 최소화, 스크롤 줌 모두 정상 작동

구현 코드

1. ImagePanel.java (레이어 지원 추가)

// 클래스 내 새 필드 추가 (기존 코드 불변)
private BufferedImage[] historyStack;

// 새 setter 메서드
public void setHistoryStack(BufferedImage[] historyStack) {
    this.historyStack = historyStack;
}

// paint(Graphics g) 메서드에서 추가 (super.paint(g); 이후)
if (historyStack != null) {
    BufferedImage latestImage = null;
    for (int i = 0; i < historyStack.length; i++) {
        if (historyStack[i] != null) {
            latestImage = historyStack[i];
        } else {
            break;
        }
    }
    if (latestImage != null) {
        g.drawImage(latestImage, 0, 0, null);
        return;
    }
}

2. FilterListener.java (핵심 로직)

// 클래스 상단에 추가
private BufferedImage[] historyStack = new BufferedImage[100];
private int stackPointer = 0;

// getter 추가 (MainFrame에서 호출용)
public BufferedImage[] getHistoryStack() {
    return historyStack;
}

// actionPerformed의 switch结束后、新保存逻辑追加
if (currentImage != null && !"열기".equals(buttonName)) {
    historyStack[stackPointer++] = currentImage;
}

// "취소" case 추가 (switch 내부)
case "취소":
    if (stackPointer > 0) {
        historyStack[--stackPointer] = null;
        if (stackPointer > 0 && historyStack[stackPointer - 1] != null) {
            imagePanel.setBufferedImage(historyStack[stackPointer - 1]);
        }
    }
    // 연속 취소 문제 해결
    stackPointer = 0;
    for (int k = 0; k < historyStack.length; k++) {
        if (historyStack[k] != null) {
            stackPointer = k + 1;
        } else {
            break;
        }
    }
    return; // 중요! 흰색 currentImageによる덮어쓰기防止

3. MainFrame.java (취소 버튼 추가 + 배열 전달)

String[] buttons = {"열기", "원본", "모자이크", "회색조", "유화", "이진화", "양각", "필름", "레트로", "반전", "회전", "저장", "취소"};

// 마지막에 추가
centerPanel.setHistoryStack(listener.getHistoryStack());

실행 결과

구현 후 테스트 결과:

  • 10개의 서로 다른 필터를 연속으로 적용
  • "취소"를 10번 연속 클릭 → 완벽하게 초기 상태로 복귀
  • 창 드래그, 가려짐, 최소화, 마우스 스크롤 줌 모두 정상 동작
  • 콘솔에서 각 리페인팅 로그 확인 가능

마무리 및 발전 방향

이 취소 기능의 핵심은 레이어 배열 +每次 취소 후 인덱스 재보정입니다. 코드 양은极少하지만 전문적인 Undo 환경을 구현할 수 있습니다.

발전 가능한 기능:

  • 배열을 ArrayList로 변경 (동적 용량 확장)
  • "다시 실행"(Redo) 기능 추가
  • 각 단계의 작업 이름도 저장하여 "취소됨: 모자이크" 형태로 표시

태그: java swing undo image-processing bufferedimage

7월 14일 19:52에 게시됨