Java의 MVC 구조

Jeongbeen Lee·2025년 1월 19일

TechTalk

목록 보기
3/4

MVC 구조?

MVC 구조란 소프트웨어 개발에 사용되는 디자인 패턴 중의 하나이다.
애플리케이션을 Model-View-Controller 로 나누어 유지보수성과 확장성을 높이는데 그 목적이 있다.


1. MVC의 구성 요소

1) Model

  • 역할: 애플리케이션의 데이터를 DB와 상호작용하여 관리하고 비즈니스 로직을 처리한다.
    최근의 MVC 구조에서는 Model은 도메인 객체를 정의하는 역할을 하고 Entity 클래스 또는 DTO로 구현이 된다.
    나머지 DB에 접근하는 역할은 Repository (또는 DTO)에서 담당하고, 비즈니스 로직 처리의 경우 Service 계층이 담당하게 된다.

2) View

  • 역할: 사용자에게 데이터를 표시하고, 사용자의 입력을 받는다.
    스프링을 사용한 웹 애플리케이션 같은 경우 JSP, Thymeleaf, HTML/CSS/JS 파일 등이 사용된다.

3) Controller

  • 역할: 사용자 요청을 처리, 비즈니스 로직을 호출하여 데이터를 생성한 후 뷰에 전달한다.

2. 간단한 CLI 기반 프로그램: PhoneBook

1) Model

Contact.java

package phonebook.model;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public class Contact {
    private String name;
    private String nickName;
    private int age;
    private String phone;
}

이름, 닉네임, 나이, 전화번호의 속성을 가진 Contact 클래스 정의.
어노테이션을 이용해서 간단하게 정의했지만, 유효성 검사를 추가하거나 Builder패턴을 도입하여 안전성을 강화시킬 수 있을 것 같다. 또한 출력을 위한 toString()메서드의 오버라이딩 역시 추가하면 좋을 것 같다.

PhoneBookService.java

package phonebook.service;

import phonebook.model.Contact;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class PhoneBookService {
    private final List<Contact> contactList = new ArrayList<>();

    public Contact getContactByIndex(int index) {
        if (checkIndex(index)) {
            return contactList.get(index);
        }
        return null;
    }

    public void addContact(Contact contact) {
        if (validateContact(contact)) {
            contactList.add(contact);
        }
    }

    public void removeContact(int index) {
        if (checkIndex(index)) {
            contactList.remove(index);
        }
    }

    public List<Contact> getAllContacts() {
        return new ArrayList<>(contactList);
    }

    public List<Contact> searchContactByKeyword(String keyword) {
        return contactList.stream().filter(contact -> {
            String name = contact.getName();
            String nickname = contact.getNickName();
            return name.toLowerCase().contains(keyword.toLowerCase()) || nickname.toLowerCase().contains(keyword.toLowerCase());
        }).collect(Collectors.toList());
    }

    private boolean validateContact(Contact contact) {
        if (contact.getName() == null || contact.getNickName() == null || contact.getName().isEmpty() || contact.getNickName().isEmpty()) {
            System.out.println("Contact name or nickname is empty");
            return false;
        }
        if (contact.getPhone() == null || contact.getPhone().isEmpty()) {
            System.out.println("Contact phone is empty");
            return false;
        }
        return true;
    }

    private boolean checkIndex(int index) {
        if (index >= 0 && index < contactList.size()) {
            return true;
        }
        System.out.println("Index out of bounds");
        return false;
    }
}

연락처 추가, 삭제, 특정 인덱스 연락처 조회, 키워드 검색 등 연락처 관리 프로그램에 들어갈 기능을 위한 비즈니스 로직들이 정의되어 있다. 연락처 추가와 인덱스 확인 같은 경우 유효성 체크를 위한 메서드를 private으로 정의해 놓았다. 또한 getAllContacts메서드 같은 경우 새로 리스트를 만들어 반환하여 외부에서 연락처 리스트를 변경하지 못하도록 하였다.

2) View

스프링 애플리케이션과 다르게, 이 CLI기반 프로그램에서는 Java의 입출력을 사용한다.

package phonebook.view;

import phonebook.model.Contact;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;

public class PhoneBookView {
    public void displayPhoneBook() {
        System.out.println();
        System.out.println("Jeongbeen's Phone Book");
        System.out.println("1. 연락처 추가");
        System.out.println("2. 연락처 검색");
        System.out.println("3. 연락처 삭제");
        System.out.println("4. 연락처 목록");
        System.out.println("5. 종료");
    }

    public void displayContacts(List<Contact> contactList) {
        System.out.println();
        if (contactList.isEmpty()) {
            System.out.println("No contact found");
            return;
        }
        System.out.printf("%-5s %-20s %-15s%n", "Index", "Name", "Phone Number");
        int index = 0;
        for (Contact contact : contactList) {
            System.out.printf("%-5d %-20s %-15s%n", index, contact.getName(), contact.getPhone());
            index++;
        }
        System.out.println();
    }

    public void displayContact(Contact contact) {
        System.out.println();
        System.out.println("Name: " + contact.getName());
        System.out.println("NickName: " + contact.getNickName());
        System.out.println("Age: " + contact.getAge());
        System.out.println("Phone Number: " + contact.getPhone());
    }

    public Contact getContact() throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        String name;
        String nickName;
        int age;
        String phone;

        System.out.println("Enter name: ");
        name = reader.readLine();
        System.out.println("Enter nickname: ");
        nickName = reader.readLine();
        System.out.println("Enter age: ");
        age = Integer.parseInt(reader.readLine());
        System.out.println("Enter phone number: ");
        phone = reader.readLine();

        return new Contact(name, nickName, age, phone);
    }

    public String getKeyword() throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

        System.out.println("Enter name or nickname: ");
        return reader.readLine();
    }

    public void displayDetailContactNotice() {
        System.out.println("Enter index: ");
    }
}

메인 화면, 연락처 리스트, 하나의 연락처 상세정보를 보여주는 메서드가 정의되어 있고, 새로 연락처를 등록할 때 입력받는 메서드, 키워드를 입력받는 메서드 등이 정의되어 있다.

3) Controller

package phonebook.controller;

import phonebook.service.PhoneBookService;
import phonebook.view.PhoneBookView;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class PhoneBookController {
    private final PhoneBookService phoneBookService;
    private final PhoneBookView phoneBookView;

    public PhoneBookController(PhoneBookService phoneBookService, PhoneBookView phoneBookView) {
        this.phoneBookService = phoneBookService;
        this.phoneBookView = phoneBookView;
    }

    public void run() throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        while (true) {
            phoneBookView.displayPhoneBook();
            int choice = 0;
            try {
                choice = Integer.parseInt(br.readLine());
            } catch (NumberFormatException ignored) {
            }
            int index;
            switch (choice) {
            case 1:
                phoneBookService.addContact(phoneBookView.getContact());
                break;
            case 2:
                phoneBookView.displayContacts(phoneBookService.searchContactByKeyword(phoneBookView.getKeyword()));
                break;
            case 3:
                phoneBookView.displayContacts(phoneBookService.getAllContacts());
                phoneBookView.displayDetailContactNotice();
                try {
                    index = Integer.parseInt(br.readLine());
                } catch (NumberFormatException e) {
                    break;
                }
                phoneBookService.removeContact(index);
                break;
            case 4:
                phoneBookView.displayContacts(phoneBookService.getAllContacts());
                if (!phoneBookService.getAllContacts().isEmpty()) {
                    phoneBookView.displayDetailContactNotice();
                    try {
                        index = Integer.parseInt(br.readLine());
                    } catch (NumberFormatException e) {
                        break;
                    }
                    phoneBookView.displayContact(phoneBookService.getContactByIndex(index));
                }
                break;
            case 5:
                System.out.println("Thank you for using Phonebook");
                return;
            default:
                System.out.println("Invalid choice");
                break;
            }
        }
    }
}

위에서 정의한 PhoneBookView, PhoneBookService만으로 프로그램을 동작할 수 있게 하였다.
수업 시간 안에 프로그램을 짜다 보니 마지막 부분인 controller 부분에 아쉬운 점이 몇 가지 있는데, 먼저 인덱스를 받는 로직은 view에 있어야 할 것 같다고 느꼈다. 물론 단순히 숫자를 입력받고 바로 반환하는 메서드가 되겠지만, 현재 다른 모든 입력과 출력은 view에 존재하고 view에서 나이를 입력받는 로직 역시 중복되기에 한번에 정의하고 예외처리를 구현했다면 어땠을까 싶다.
두 번째는 프로그램을 완성하고 보니 run 메서드가 길어졌는데, 이것 역시 run 메서드 내부에는 case 만 놓고, 각 항목을 분리하면 더 깔끔할 것 같다.

4) 실행

main 메서드에는

PhoneBookService phoneBookService = new PhoneBookService();
PhoneBookView phoneBookView = new PhoneBookView();
PhoneBookController phoneBookController = new PhoneBookController(phoneBookService, phoneBookView);
phoneBookController.run();

해당 코드를 사용하면 실행될 것이다.
다음날 발표 시간을 가졌는데 주제는 자유였지만 프로그램 내용은 거기서 거기일거라 생각해 잘 듣지 않았다. (죄송합니다 ㅠㅠ..) 그것보다는 코드에 더 관심이 있어서 조마다 파일을 뜯어보고 코드를 슥 봤는데 다들 파일 하나에 몰아서 구현하거나 Model(도메인 객체) 정도 만든 사람들이 대부분이었다. 내가 괜히 오바했나 싶지만 이번 기회에 그래도 MVC 모델에 대해 알아보는 기회가 되었다.

3. Spring에서의 MVC

스프링에서는 MVC 구조를 이용하여 계층을 분리함으로써 각 부분이 독립적으로 동작하게 하여 대규모 웹 애플리케이션의 복잡성을 관리한다.

1) 동작 방식

1. 사용자가 요청을 보냄

  • 사용자가 HTTP 요청을 보내면 DispatcherServlet으로 전달.

2. DispatcherServlet

  • HandlerMapping을 통해 해당 요청을 처리할 controller를 찾아서 매핑.

3. Controller

  • 비즈니스 로직을 수행하여 요청을 처리.
  • ModelAndView 또는 @ResponseBody를통해 결과를 반환.

4. ViewResolver

  • Controller로부터 받은 결과를 통해 적절한 JSP 파일을 찾아서 렌더링.

5. JSP

  • JSP는 HTML 내부에 Java 코드를 사용하여 동적으로 내용을 생성하여 브라우저에 전달한다.

2) Thymeleaf

Thymeleaf는 Spring에서 JSP를 대체할 수 있는 템플릿 엔진이다. JSP는 HTML 태그 내에 Java 코드를 삽입하여 디버깅이 복잡하지만 (<%= ... %> 형식) Thymeleaf 는 tl 태그를 사용하여 자연스러운 HTML을 생성한다.

<!-- Thymeleaf -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Thymeleaf Example</title>
</head>
<body>
    <h1 th:text="'Hello, ' + ${user.name}">Hello, Guest</h1>
</body>
</html>

3) 기본 파일 구조

src/main/java/com/example/demo
├── controller      # Controller 클래스 (사용자 요청 처리)
│   └── UserController.java
├── model           # Model 클래스 (비즈니스 로직 및 데이터)
│   └── User.java
├── service         # 비즈니스 로직 처리 계층
│   └── UserService.java
├── repository      # 데이터베이스 액세스 계층 (DAO)
│   └── UserRepository.java
└── Application.java # 메인 애플리케이션 클래스 (Spring Boot 실행 진입점)

View

src/main/resources
├── static          # 정적 파일 (CSS, JS, 이미지 등)
│   └── styles.css
├── templates       # 동적 HTML 파일 (Thymeleaf, Freemarker 등)
│   └── user.html
└── application.properties # Spring 설정 파일

4) RESTful API

Spring에서 모든 것을 다 하지 않더라도, RESTful API를 설계할 때 역시 MVC 구조는 적합하다. View 대신 Controller를 통해 처리한 데이터를 JSON, XML등으로 가공하여 통신이 가능하다.


MVC 구조는 모든 곳에서 사용할 필요는 없다. 다만 Java와 Spring에서 사용하는 이유는 대규모 애플리케이션에서 구조를 명확하게 하고 유지보수와 확장성을 높이기 위함이다. 또한 Spring에서는 이를 지원하기 위한 다양한 기능과 도구를 제공한다.

0개의 댓글