이 글은 단순한 예제 프로그램 완성을 넘어, 그 구현 과정을 상세히 기록하며 학습하고자 작성한 글입니다.
자바를 활용하여 콘솔 기반의 간단한 연락처 관리 시스템을 만들어보겠습니다.
📚 과제 내용 및 목표
만들어야 할 프로그램은 다음과 같은 기능을 수행합니다.
- 연락처 추가 (add): 이름과 전화번호를 입력받아 새로운 연락처를 저장합니다.
- 연락처 삭제 (remove): 이름을 기준으로 특정 연락처를 목록에서 제거합니다.
- 연락처 검색 (search): 이름을 기준으로 연락처를 찾아 그 정보를 출력합니다. (정렬, 이름 오름차순)
- 연락처 목록 출력 (list): 현재 저장된 모든 연락처를 이름 오름차순으로 정렬하여 보여줍니다.
- 프로그램 종료 (exit)
핵심 조건: 데이터 저장을 위해 ArrayList를 사용합니다.
출력 예시:

💡 Step 1: 프로그램의 뼈대 잡기
객체 지향 프로그래밍의 원칙을 적용하여, 프로그램의 역할을 명확히 분리하는 것으로 시작합니다.
Contact 클래스 : 연락처 정보 정의
가장 먼저 할 일은 하나의 연락처가 어떤 정보를 가질 것인지 정의하는 것입니다.
각 연락처는 이름과 전화번호를 가지므로, 이 두 가지를 필드로 포함하는 Contact 클래스를 만듭니다.
// Contact.java
class Contact {
private String name; // 연락처 이름. 캡슐화를 위해 private로 선언
private String phoneNumber; // 연락처 전화번호. 캡슐화를 위해 private로 선언
// 생성자: 이름과 전화번호를 초기화합니다
public Contact(String name, String phoneNumber) {
this.name = name;
this.phoneNumber = phoneNumber;
}
}
ContactApp : 프로그램의 시작점 (main 메소드)
ContactApp 클래스는 프로그램의 main 메소드를 포함하며, 사용자로부터 명령을 입력받고 적절한 기능을 호출하는 프로그램의 진입점(Entry Point) 역할을 합니다.
// ContactApp.java
import java.util.Scanner; // 사용자 입력을 위한 Scanner 클래스 임포트
public class ContactApp {
public static void main(String[] args) {
// 1. 연락처 관리를 담당할 ContactManager 객체를 생성합니다.
ContactManager manager = new ContactManager();
// 2. 사용자 입력을 받기 위한 Scanner 객체를 생성합니다.
Scanner scanner = new Scanner(System.in);
// 3. 무한 루프를 통해 사용자의 명령을 계속해서 받습니다.
while (true) {
System.out.println("\n\uD83D\uDCDE 연락처 관리 시스템 (add, remove, list, search, exit)");
System.out.print("명령 입력: ");
String command = scanner.nextLine().trim().toLowerCase();
// 4. 입력된 명령에 따라 적절한 기능을 수행합니다.
switch (command) {
case "add":
System.out.print("이름: ");
String name = scanner.nextLine();
System.out.print("전화번호: ");
String phone = scanner.nextLine();
manager.addContact(name, phone); // ContactManager의 addContact 호출
break;
case "remove":
System.out.print("삭제할 연락처 이름: ");
String removeName = scanner.nextLine();
manager.removeContact(removeName); // ContactManager의 removeContact 호출
break;
case "search":
System.out.print("검색할 이름: ");
String searchName = scanner.nextLine();
manager.searchContact(searchName); // ContactManager의 searchContact 호출
break;
case "list":
manager.listContacts(); // ContactManager의 listContacts 호출
break;
case "exit":
System.out.println("\uD83D\uDEAA 종료합니다.");
scanner.close(); // Scanner 자원 해제
return; // main 메소드를 종료하여 프로그램 종료
default:
System.out.println("⚠️ 올바른 명령을 입력하세요. (add, remove, list, search, exit)");
break;
}
}
}
}
- Scanner와 ContactManager 객체 생성 위치: while 루프 바깥에서 한 번만 생성하여 프로그램이 실행되는 동안 상태(연락처 목록)를 유지하도록 했습니다. 만약 루프 안에서 생성하면 매번 초기화되어 연락처가 저장되지 않을 것입니다.
- trim().toLowerCase(): 사용자 입력의 앞뒤 공백을 제거하고(trim), 모든 문자를 소문자로 변환하여(toLowerCase) 대소문자나 불필요한 공백에 관계없이 명령어를 정확히 인식하도록 했습니다.
- switch 문과 break: 각 case 끝에 break를 사용하여 해당 명령 처리 후 switch 문을 빠져나오도록 합니다. break가 없으면 다음 case로 실행이 계속 이어지는 'fall-through' 현상이 발생합니다.
- exit와 return: exit 명령 시 Scanner 자원을 닫고 return을 사용하여 main 메소드를 종료, 결과적으로 프로그램이 완전히 종료됩니다.
ContactManager 클래스 : 핵심 기능 관리
ContactManager 클래스는 실제 연락처 목록(ArrayList)을 관리하고, 연락처 추가/삭제/검색/출력 등 모든 핵심 비즈니스 로직을 담당합니다. 처음에는 각 메소드의 뼈대만 만들고, 다음 단계에서 기능을 구현해 나갑니다.
// ContactManager.java
import java.util.ArrayList; // ArrayList 사용을 위한 임포트
class ContactManager {
// 연락처들을 저장할 ArrayList 필드를 선언합니다.
// private로 선언하여 외부에서 직접 접근을 막고 캡슐화를 유지합니다.
private ArrayList<Contact> contacts;
// 생성자: ContactManager 객체 생성 시 contacts ArrayList를 초기화합니다.
public ContactManager() {
this.contacts = new ArrayList<>();
}
// 아래 메소드들은 각 기능이 구현될 자리입니다.
public void addContact(String name, String phoneNumber) {
System.out.println("🚧 add 기능 구현 중...");
}
public void listContacts() {
System.out.println("🚧 list 기능 구현 중...");
}
public void searchContact(String name) {
System.out.println("🚧 search 기능 구현 중...");
}
public void removeContact(String name) {
System.out.println("🚧 remove 기능 구현 중...");
}
}
- ArrayList 필드 선언 및 초기화: contacts는 ContactManager의 핵심 데이터이므로 private으로 선언하고, 생성자에서 new ArrayList<>()를 통해 반드시 초기화해줍니다. 이렇게 해야 NullPointerException 없이 add 등의 메소드를 사용할 수 있습니다.
- 역할 분리: ContactManager는 ContactApp으로부터 명령을 받아 실제 데이터(contacts)를 조작하는 역할을 수행합니다.
💡 Step 2: 핵심 기능 구현
이제 ContactManager 클래스에 정의된 메소드들을 하나씩 구현하여 프로그램에 실제 기능을 추가해봅니다.
2.1) 연락처 추가 (addContact)
새로운 연락처 정보를 받아 Contact 객체를 생성하고, 이를 ArrayList에 저장하는 기능입니다.
- contacts.add(new Contact(name, phoneNumber));: addContact 메소드가 호출될 때마다, 전달받은 이름과 전화번호로 새로운 Contact 객체를 만들고, 이를 ContactManager의 contacts ArrayList에 추가합니다.
// ContactManager.java (addContact 메소드 수정)
// ...
public void addContact(String name, String phoneNumber) {
// 새로운 Contact 객체를 생성하고, contacts ArrayList에 추가합니다.
contacts.add(new Contact(name, phoneNumber));
System.out.println("✅ 연락처가 추가되었습니다.");
}
// ...
}
2.2) 연락처 목록 출력 (listContacts)
현재 ArrayList에 저장된 모든 연락처 정보를 사용자에게 보여주는 기능입니다. 연락처가 없을 경우와 있을 경우를 구분하여 처리합니다.
이 기능을 구현하기에 앞서, Contact 객체를 System.out.println()으로 출력할 때 이름과 전화번호가 원하는 형식으로 예쁘게 나오도록 Contact 클래스에 toString() 메소드를 오버라이드하고, 이름 접근을 위한 getName() 메소드를 추가해 줍니다.
// Contact.java (수정 및 추가)
class Contact {
//... 필드, 생성자
// 연락처 이름을 반환하는 getName 메소드 추가 (캡슐화)
public String getName() {
return name;
}
// 객체를 문자열로 표현할 때 사용되는 toString() 메소드 오버라이드
// System.out.println(contact) 호출 시 자동으로 이 메소드가 실행됩니다.
@Override
public String toString() {
return name + " - " + phoneNumber;
} // 출력 예시 : 홍길동 - 010-1234-5678
}
이제 ContactManager의 listContacts 메소드를 구현합니다.
// ContactManager.java (listContacts 메소드 수정)
class ContactManager {
// ...
public void listContacts() {
if (contacts.isEmpty()) { // contacts 리스트가 비어있는지 확인
System.out.println("등록된 연락처가 없습니다.");
} else {
// 향상된 for 루프 (for-each)를 사용하여 contacts 리스트의 각 Contact 객체를 순회합니다.
for (Contact contact : contacts) {
System.out.println(contact); // Contact 클래스의 toString() 메소드가 자동 호출됩니다.
}
}
}
// ...
}
- contacts.isEmpty(): ArrayList가 비어있는지 확인하는 메소드입니다.
- 향상된 for 루프: for (Contact contact : contacts)는 contacts 리스트의 각 Contact 객체를 contact 변수에 할당하며 반복합니다. 코드가 간결하고 읽기 쉽습니다.
- System.out.println(contact): Contact 클래스에서 오버라이드한 toString() 메소드 덕분에, 객체 자체를 출력하면 우리가 원하는 "이름 - 전화번호" 형식으로 나타납니다.
2.3) 연락처 검색 (searchContact)
이름을 기준으로 연락처를 찾아 그 정보를 출력하는 기능입니다. 동일한 이름의 연락처가 여러 개 있을 경우 모두 출력하고, 찾지 못했을 때는 사용자에게 알려줍니다.
// ContactManager.java (searchContact 메소드 수정)
class ContactManager {
// ...
public void searchContact(String name) {
boolean found = false; // 연락처를 찾았는지 여부를 나타내는 플래그
for (Contact contact : contacts) {
if (contact.getName().equalsIgnoreCase(name)) {
System.out.println(contact); // 찾은 연락처 출력
found = true; // 찾으면 플래그를 true로 설정
}
}
if (!found) { // 연락처를 못 찾았으면 (found 플래그가 false이면)
System.out.println("❌ 연락처를 찾을 수 없습니다: " + name);
}
}
// ...
}
- boolean found = false;: 메소드 시작 시 found라는 플래그 변수를 false로 초기화합니다. 이 변수는 "검색된 연락처가 하나라도 있었는가?"를 추적하는 역할을 합니다.
- for (Contact contact : contacts): 모든 연락처를 순회하며 하나씩 확인합니다.
- if (contact.getName().equalsIgnoreCase(name)): 각 Contact 객체의 getName() 메소드를 통해 이름을 가져와, 검색하려는 name과 대소문자 구분 없이(equalsIgnoreCase) 비교합니다.
- found = true;: 만약 일치하는 연락처를 찾으면, found 값을 true로 변경합니다.
- if (!found): for 루프가 완전히 끝난 후, found가 여전히 false라면 (즉, 한 번도 일치하는 연락처를 찾지 못했다면) "연락처를 찾을 수 없습니다" 메시지를 출력합니다. 이렇게 하면 모든 연락처를 확인한 후에 최종적으로 결과를 알릴 수 있습니다.
2.4) 연락처 삭제 (removeContact)
이름을 기준으로 연락처 목록에서 특정 연락처를 삭제하는 기능입니다. 동일한 이름의 연락처가 여러 개 있다면, 해당 이름을 가진 모든 연락처를 삭제합니다.
// ContactManager.java (removeContact 메소드 수정)
class ContactManager {
// ...
public void removeContact(String name) {
System.out.println("--- 연락처 삭제 ---");
// 1. `removeIf()` 메소드를 사용하여 조건을 만족하는 모든 요소를 안전하게 제거합니다.
boolean removed = contacts.removeIf(contact -> contact.getName().equalsIgnoreCase(name));
// 2. `removeIf()`의 반환값을 통해 삭제 성공 여부를 판단하고 메시지를 출력합니다.
if (removed) {
System.out.println("✅ 연락처가 삭제되었습니다.");
} else {
System.out.println("❌ 삭제할 연락처를 찾을 수 없습니다: " + name);
}
}
}
- removeIf()는 Collection 인터페이스에 정의된 메소드로, ArrayList와 같은 컬렉션에서 특정 조건을 만족하는 모든 요소를 효율적이고 안전하게 제거합니다.
- contact -> contact.getName().equalsIgnoreCase(name): 이 부분은 람다 표현식(Lambda Expression)으로 작성된 Predicate입니다. removeIf()는 이 조건을 contacts 리스트의 각 Contact 객체에 대해 검사합니다. 만약 현재 contact의 이름이 삭제하려는 name과 대소문자 구분 없이 일치하면(equalsIgnoreCase), 해당 Contact 객체는 리스트에서 제거됩니다.
- 반환값: removeIf()는 하나 이상의 요소가 제거되었다면 true를, 아무것도 제거되지 않았다면 false를 반환합니다. 이를 boolean removed 변수에 저장하여 삭제 성공 여부를 판별합니다.
- 이전에는 Iterator 를 사용하여 컬렉션을 순회하며 요소를 제거했지만, Java 8 이후로는 removeIf() 로 그 과정을 더욱 간결하게 처리할 수 있습니다.
최종 코드
// Contact.java
class Contact {
private String name;
private String phoneNumber;
Contact(String name, String phoneNumber) {
this.name = name;
this.phoneNumber = phoneNumber;
}
@Override
public String toString() {
return name + " - " + phoneNumber;
}
public String getName() {
return name;
}
}
// ContactManager.java
import java.util.ArrayList;
class ContactManager {
private ArrayList<Contact> contacts;
public ContactManager() {
this.contacts = new ArrayList<>(); // contacts ArrayList 초기화
}
public void addContact(String name, String phoneNumber) {
contacts.add(new Contact(name, phoneNumber));
System.out.println("✅ 연락처가 추가되었습니다.");
}
public void listContacts() {
if (contacts.isEmpty()) {
System.out.println("❌ 등록된 연락처가 없습니다.");
} else {
for (Contact contact : contacts) {
System.out.println(contact);
}
}
}
public void searchContact(String name) {
boolean found = false;
for (Contact contact : contacts) {
if (contact.getName().equalsIgnoreCase(name)) {
System.out.println(contact);
found = true;
}
}
if (!found) {
System.out.println("❌ 연락처를 찾을 수 없습니다: " + name");
}
}
public void removeContact(String name) {
boolean removed = contacts.removeIf(contact -> contact.getName().equalsIgnoreCase(name));
if (removed) {
System.out.println("✅ 삭제 완료!");
} else {
System.out.println("❌ 연락처를 찾을 수 없습니다: " + name");
}
}
}
// ContactApp.java
import java.util.Scanner;
public class ContactApp {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
ContactManager manager = new ContactManager();
while (true) {
System.out.println("\uD83D\uDCDE 연락처 관리 시스템 (add, remove, list, search, exit)");
System.out.print("명령 입력: ");
String command = scanner.nextLine().trim().toLowerCase();
switch (command) {
case "add":
System.out.print("이름: ");
String name = scanner.nextLine();
System.out.print("전화번호: ");
String phoneNumber = scanner.nextLine();
manager.addContact(name, phoneNumber);
break;
case "remove":
System.out.print("삭제할 연락처 이름: ");
String nameToRemove = scanner.nextLine();
manager.removeContact(nameToRemove);
break;
case "search":
System.out.print("검색할 이름: ");
String nameToSearch = scanner.nextLine();
manager.searchContact(nameToSearch);
break;
case "list":
manager.listContacts();
break;
case "exit":
System.out.println("\uD83D\uDEAA 종료합니다.");
scanner.close();
return;
default:
System.out.println("⚠ 알 수 없는 명령입니다. 다시 시도하세요.");
break;
}
}
}
}
개선할 수 있는 부분:
- 연락처 정렬 기능: 현재 list 기능은 단순히 추가된 순서대로 출력됩니다. 이름 오름차순으로 정렬하여 출력하도록 Comparator와 Collections.sort() 또는 스트림의 sorted()를 활용해볼 수 있습니다.
- 중복 연락처 처리: 동일한 이름의 연락처가 추가되는 것을 방지하거나, 추가 시 사용자에게 경고하는 기능을 추가할 수 있습니다.
감사합니다 :)