
MVC 구조란 소프트웨어 개발에 사용되는 디자인 패턴 중의 하나이다.
애플리케이션을 Model-View-Controller 로 나누어 유지보수성과 확장성을 높이는데 그 목적이 있다.
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메서드 같은 경우 새로 리스트를 만들어 반환하여 외부에서 연락처 리스트를 변경하지 못하도록 하였다.
스프링 애플리케이션과 다르게, 이 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: ");
}
}
메인 화면, 연락처 리스트, 하나의 연락처 상세정보를 보여주는 메서드가 정의되어 있고, 새로 연락처를 등록할 때 입력받는 메서드, 키워드를 입력받는 메서드 등이 정의되어 있다.
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 만 놓고, 각 항목을 분리하면 더 깔끔할 것 같다.
main 메서드에는
PhoneBookService phoneBookService = new PhoneBookService();
PhoneBookView phoneBookView = new PhoneBookView();
PhoneBookController phoneBookController = new PhoneBookController(phoneBookService, phoneBookView);
phoneBookController.run();
해당 코드를 사용하면 실행될 것이다.
다음날 발표 시간을 가졌는데 주제는 자유였지만 프로그램 내용은 거기서 거기일거라 생각해 잘 듣지 않았다. (죄송합니다 ㅠㅠ..) 그것보다는 코드에 더 관심이 있어서 조마다 파일을 뜯어보고 코드를 슥 봤는데 다들 파일 하나에 몰아서 구현하거나 Model(도메인 객체) 정도 만든 사람들이 대부분이었다. 내가 괜히 오바했나 싶지만 이번 기회에 그래도 MVC 모델에 대해 알아보는 기회가 되었다.
스프링에서는 MVC 구조를 이용하여 계층을 분리함으로써 각 부분이 독립적으로 동작하게 하여 대규모 웹 애플리케이션의 복잡성을 관리한다.
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>
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 실행 진입점)
src/main/resources
├── static # 정적 파일 (CSS, JS, 이미지 등)
│ └── styles.css
├── templates # 동적 HTML 파일 (Thymeleaf, Freemarker 등)
│ └── user.html
└── application.properties # Spring 설정 파일
Spring에서 모든 것을 다 하지 않더라도, RESTful API를 설계할 때 역시 MVC 구조는 적합하다. View 대신 Controller를 통해 처리한 데이터를 JSON, XML등으로 가공하여 통신이 가능하다.
MVC 구조는 모든 곳에서 사용할 필요는 없다. 다만 Java와 Spring에서 사용하는 이유는 대규모 애플리케이션에서 구조를 명확하게 하고 유지보수와 확장성을 높이기 위함이다. 또한 Spring에서는 이를 지원하기 위한 다양한 기능과 도구를 제공한다.