[Java] To do List 구현 (Swing, MVC패턴)

HodooHa·2024년 4월 28일
0
post-thumbnail

MVC (모델-뷰-컨트롤러) 는 사용자 인터페이스, 데이터 및 논리 제어를 구현하는데 널리 사용되는 소프트웨어 디자인 패턴입니다.

  • 모델: 데이터와 비즈니스 로직을 관리합니다.
  • 뷰: 레이아웃과 화면을 처리합니다.
  • 컨트롤러: 모델과 뷰로 명령을 전달합니다.


[출처] MDN Web Docs, https://developer.mozilla.org/ko/docs/Glossary/MVC

수업에서 맛보기로 MVC패턴을 살짝 배웠다. MVC패턴의 예시로 간단한 제품 등록/조회/변경 프로그램을 만들어봤는데 이를 활용한 프로그램 하나를 만들어보았다.

바로 TodoList! 예전 코알누 쌤의 NodeJS 과정을 배우면서 웹으로 TodoList를 만들어본 경험이 있는데 이번에는 java의 swing으로 구현했다.
JList를 사용하면 쉽게 구현할 수 있을 것 같지만 아쉽게도 난 수업에서 배우지 않았다... 또 내가 생각하는 UI를 구현할 수 없어서 JLabel을 사용했다.

[모델] Task 클래스

public class Task {
    private String content; // task 내용
    private boolean isComplete; // task 완료 여부

    public Task(String content) {
        this.content = content;
        isComplete = false;
    }

    public String getContent() {
        return content;
    }

    public boolean getIsComplete() {
        return isComplete;
    }

    public void setIsComplete(boolean isComplete) {
        this.isComplete = isComplete;
    }
}

[뷰] View 클래스

import model.Task;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import static java.awt.BorderLayout.*;

public class View {
    private JFrame f;
    private JPanel contentP;
    private JPanel taskP = new JPanel();
    private JButton addBtn;
    private JButton completeBtn;
    private JButton deleteBtn;
    private JTextField addField;
    private JLabel taskField;
    private Controller con = new Controller();
    private Font font = new Font("굴림", Font.BOLD, 30);

    public View() {
        f = new JFrame();
        f.setSize(700, 800);
        f.getContentPane().setBackground(Color.gray);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setTitle("TodoList");
        initHeadPanel();
        initContentPanel();
        f.setVisible(true);
        addField.setFocusable(true);
    }

    private void initHeadPanel() { // 상단 task 추가 영역
        JPanel headP = new JPanel();
        headP.setLayout(new FlowLayout());
        headP.setBackground(Color.yellow);

        JLabel addLabel = new JLabel("할일: ");
        addLabel.setFont(font);

        addField = new JTextField(15);
        addField.setFont(font);

        addBtn = new JButton("추가");
        addBtn.setBackground(Color.green);
        addBtn.setFont(font);
        addBtn.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {

                String content = addField.getText();
                if (content.length() != 0) { // 내용이 없으면 추가하지 않음
                    if (con.findTask(content) == null) { // 기존 taskList에 중복이 있는지 확인
                        Task task = con.addTask(content); // 중복 없으면 DB에 task 추가 후 Task 타입으로 리턴받음.
                        initTaskPanel(task); // 추가한 Task 타입의 task 전달
                    } else {
                        JOptionPane.showMessageDialog(f, "이미 존재합니다.");
                    }
                    addField.setText(""); // 추가 후 input창 초기화
                }
            }
        });
        headP.add(addLabel);
        headP.add(addField);
        headP.add(addBtn);
        f.add(headP, PAGE_START);
    }

    private void initContentPanel() {
        contentP = new JPanel();
        contentP.setBackground(Color.pink);
        f.add(contentP);
    }

    private void initTaskPanel(Task task) {
        JPanel flow = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 5)); // 개별 task 영역
        flow.setBackground(Color.white);
        taskP.setLayout(new GridLayout(0, 1)); // 복수의 tasks 영역
        taskP.setBackground(Color.white);
        taskField = new JLabel();
        taskField.setText(task.getContent()); // 매개변수 task 내용 조회하여 보여주기
        taskField.setFont(font);
        completeBtn = new JButton("완료");
        completeBtn.setBackground(Color.CYAN);
        completeBtn.setFont(font);
        completeBtn.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                con.taskCheck(task); // DB에서 task의 완료여부 변경
                if (task.getIsComplete()) {
                    flow.setBackground(Color.gray); // task 완료되었으면 회색                
                } else {
                    flow.setBackground(Color.white); // task 완료 안되었으면 기본색(흰색)
                }
            }
        });
        deleteBtn = new JButton("삭제");
        deleteBtn.setBackground(Color.red);
        deleteBtn.setFont(font);
        deleteBtn.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                con.deleteTask(task); // DB에서 task 삭제
                taskP.remove(flow);
                f.repaint();
                f.revalidate();
            }
        });
        flow.add(taskField);
        flow.add(completeBtn);
        flow.add(deleteBtn);
        taskP.add(flow);
        contentP.add(taskP);
        f.repaint();
        f.revalidate();
    }
}

JFrame과 JPanel의 Layout에 대해 자세히 배우지 않아서 내가 머릿속으로 그리는 UI를 구성하는데 정말 힘들었다. Layout에 대해서 잘 몰랐기 때문이다. GridLayout, FlowLayout, BorderLayout의 차이를 찾아보고 공부해서 생각하는 것을 (나름) 비슷하게 구현할 수 있었다.
'아니 이걸 이렇게 어렵게 만들었어?', '아니 이걸 왜 이렇게 사용했어?' 라고 생각할 수 있다... 하지만 현재 나의 수준으로는 이게 최선이었다.
사실 찾아보니 Swing이 현업에서나 일상에서나 잘 사용하지 않는다고 한다. 이번 기회로 Swing에 대해 공부하였음에 만족하며 이정도 수준으로 만족한다... :)

[컨트롤러] Controller 클래스

import model.Task;

import java.util.ArrayList;
import java.util.List;

public class Controller {
    private Task task = null;
    private List<Task> taskList = new ArrayList<>();

    public Task addTask(String str) { // task 추가
        task = new Task(str);
        taskList.add(task);
        return task;
    }

    public Task findTask(String content) { // 내용으로 task 조회
        Task task = null;
        for (int i = 0; i < taskList.size(); i++) {
            Task temp = taskList.get(i);
            if (temp.getContent().equals(content)) {
                task = temp;
            }
        }
        return task;
    }

    public void taskCheck(Task task) { // task 완료 여부 변경 (true면 false로, false면 true로)
        task.setIsComplete(!task.getIsComplete());
    }

    public void deleteTask(Task task) { // task 삭제
        taskList.remove(task);
    }
}

[런] Main 클래스

public class Main {
    public static void main(String[] args) {
        View v = new View();
    }
}

이번 과정에서 가장 중요하게 생각했던 점이 바로 "무조건 DB에 먼저!" 이다.
입력받은 내용을 바로 UI로 보여주는 것이 아니라 MVC 패턴을 따라 뷰에서 입력받은 내용을 컨트롤러에서 모델 데이터로 저장한 다음, 뷰에서는 단지 저장된 데이터를 보여주고자 했다.
따라서 많은 시행착오와 오류를 경험하고 해결하여 완성하였다.

처음에는 initTaskPanel 메소드를 매개변수 없이 아래와 같이 구상했다.

  1. Controller에서 taskList를 가져온다.
  2. taskList.size()만큼 반복하며 내용, 완료버튼, 삭제버튼을 만든다.

이러면 추가 버튼과 삭제 버튼을 누를 때(taskList가 바뀌게 될 때) 다시 contenPanel이 새로고침되어 render될 것이라고 생각했다..

추가 버튼을 누를 땐 잘 추가되었다. 문제는 완료나 삭제 버튼을 누를 때였다. 어떤 삭제 버튼을 누르더라도 가장 마지막 데이터가 삭제되었고 실제 UI는 변화없다가, 다시 추가 버튼을 누를 때서야 삭제된 taskPanel에 새로운 내용이 대체되었다. 사실 초반에는 panel도 없이 그냥 frame에 textfield와 버튼들을 바로 넣어 버튼이 어떤 task의 버튼인지 모르게끔 만들었다. 엉망이었다.

그래서 아래와 같이 바꿨다.

  1. taskPanel 추가 (내용+완료버튼+삭제버튼) 하나의 덩어리로!
  2. initTaskPanel에서 바로 taskList를 불러오는 대신 addBtn 이벤트 발생 시 매개변수 task(추가된 데이터)를 전달!

따라서 panel을 추가하여 (내용+완료버튼+삭제버튼)을 하나의 덩어리로 만들었고 initTaskPanel에서 바로 taskList를 불러오는 대신 매개변수 task(추가된 데이터)를 전달하여 어떤 task의 완료, 삭제 버튼인지 알게끔 하였다. 한 번에 렌더링하는 것이 아닌 task 한개씩 그때그때 렌더링하는 것으로 바꾼 것이다.

※※ repaint() & revalidate()

아니, addTask를 했는데! 데이터는 잘 반영이 되었는데! 왜!! UI는 그대로인것이더냐ㅠㅠ 사실, 이 문제로 꽤 오랜시간동안 고생했다. 위에도 언급했듯이 구글링을 했을 때 swing에 대한 글들이 별로 없어 원인과 해결책을 찾는데에도 좀 걸렸다.
해답은 repaint() & revalidate()!!!
컴포넌트의 모양이나 위치가 바뀌었을 때에는 repaint() & revalidate()를 호출하여 재배치와 다시 그리기를 해줘야 한다.

f.revalidate(); // 컴포넌트 재배치를 지시
f.repaint(); // frame 다시 그리기

[참고] https://velog.io/@mdev97/%EC%9E%90%EB%B0%94%EC%9D%98-Graphics-4

촌스러운 UI는 이해해주길 바란다... ㅠ

부족한 실력이지만 나름 만족스러운 결과물이다. 뿌듯뿌듯
추후 DB와 연동하는 것 배우면 해봐야겠다.

[결과물]

본 포스팅은 멀티캠퍼스의 멀티잇 백엔드 개발(Java)의 교육을 수강하고 작성되었습니다.

profile
성장하는 개발자, 하지은입니다.

0개의 댓글