이 글은 이전의 연락처 관리 시스템 글에 이어, ArrayList 대신 HashSet을 사용하여 회원 관리 프로그램을 구현하는 과정을 상세히 학습하고자 작성되었습니다.
HashSet은 중복을 허용하지 않는다는 특징을 가지고 있어, 회원 ID와 같이 고유해야 하는 데이터를 관리하는 데 매우 적합합니다.
자바를 활용하여 콘솔 기반의 간단한 회원 관리 시스템을 만들어보겠습니다.
📚 과제 내용 및 목표
만들어야 할 프로그램은 다음과 같은 기능을 수행합니다.
- 회원 ID 추가 (add): 새로운 회원 ID를 입력받아 저장합니다. (중복된 ID는 등록 불가)
- 회원 ID 삭제 (remove): ID를 기준으로 특정 회원을 목록에서 제거합니다.
- 회원 목록 출력 (list): 현재 저장된 모든 회원 ID를 출력합니다.
- 회원 ID 조회 (check): ID를 기준으로 회원이 존재하는지 확인하고 그 정보를 출력합니다.
- 프로그램 종료 (exit)
핵심 조건: 중복 없는 데이터 저장을 위해 HashSet을 사용합니다.
출력 예시:

💡 Step 1: 프로그램의 뼈대 잡기
객체 지향 프로그래밍의 원칙을 적용하여 프로그램의 역할을 명확히 분리하는 것으로 시작합니다. 각 역할에 맞는 클래스를 정의하고, 기본적인 구조를 잡아봅니다.
User 클래스: 회원 정보 정의
가장 먼저 할 일은 하나의 회원이 어떤 정보를 가질 것인지 정의하는 것입니다. 각 회원은 고유한 ID를 가지므로, 이 ID를 필드로 포함하는 User 클래스를 만듭니다.
// User.java
import java.util.Objects; // Objects.hash() 사용을 위한 임포트 추가
class User {
private int id; // 회원 ID. 캡슐화를 위해 private로 선언
// 생성자: ID를 초기화합니다.
public User(int id) {
this.id = id;
}
// 회원 ID를 반환하는 getter 메소드
public int getId() {
return id;
}
}
- private int id;: 회원 ID를 저장하는 필드입니다. private으로 선언하여 외부 직접 접근을 막고 캡슐화를 합니다.
- 생성자: User 객체를 생성할 때 id 값을 받아 초기화합니다.
- getId() 메소드: 캡슐화된 id 값을 외부에서 읽을 수 있도록 해주는 getter 메소드입니다.
- User 클래스는 HashSet에 저장될 객체이므로, HashSet이 객체의 중복 여부를 올바르게 판단하고 효율적으로 동작하도록 equals()와 hashCode() 메소드를 반드시 오버라이드해야 합니다. 이 메소드들의 자세한 구현은 Step 2에서 다룹니다.
UserApp 클래스: 프로그램의 시작점 (main 메소드)
UserApp 클래스는 프로그램의 진입점(Entry Point) 역할을 합니다. main 메소드를 포함하며, 사용자로부터 명령을 입력받고 적절한 UserManager 메소드를 호출합니다.
// UserApp.java
import java.util.Scanner; // 사용자 입력을 위한 Scanner 클래스 임포트
public class UserApp {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in); // 사용자 입력을 받기 위한 Scanner 객체 생성
UserManager manager = new UserManager(); // 회원 관리를 담당할 UserManager 객체 생성
// 무한 루프를 통해 사용자의 명령을 계속해서 받습니다.
while (true) {
System.out.println("\n\uD83D\uDC65 회원 관리 시스템 (add, remove, list, check, exit)");
System.out.print("명령 입력: ");
String command = scanner.nextLine().trim().toLowerCase(); // 사용자 입력 처리: 공백 제거 및 소문자 변환
// 입력된 명령에 따라 적절한 기능을 수행합니다.
switch (command) {
case "add":
System.out.print("추가할 회원 ID: ");
int addId = Integer.parseInt(scanner.nextLine()); // 문자열을 정수로 변환
manager.addUser(addId); // UserManager의 addUser 호출
break;
case "remove":
System.out.print("삭제할 회원 ID: ");
int removeID = Integer.parseInt(scanner.nextLine());
manager.removeUser(removeID); // UserManager의 removeUser 호출
break;
case "list":
manager.listUsers(); // UserManager의 listUsers 호출
break;
case "check":
System.out.print("검색할 회원 ID: ");
int checkId = Integer.parseInt(scanner.nextLine());
manager.checkUser(checkId); // UserManager의 checkUser 호출
break;
case "exit":
System.out.println("🚪 프로그램 종료.");
scanner.close(); // Scanner 자원 해제
return; // main 메소드 종료로 프로그램 완전 종료
default:
System.out.println("⚠️ 알 수 없는 명령입니다. 다시 시도하세요.");
break;
}
}
}
}
- Scanner와 UserManager 객체 생성: while 루프 바깥에서 한 번만 생성하여 프로그램이 실행되는 동안 상태(회원 목록)를 유지하고 불필요한 객체 생성을 막습니다.
- trim().toLowerCase(): 사용자 입력의 앞뒤 공백을 제거하고, 모든 문자를 소문자로 변환하여 대소문자나 불필요한 공백에 관계없이 명령어를 정확히 인식하도록 합니다.
- Integer.parseInt(): 사용자로부터 입력받는 ID는 문자열 형태이므로, int 타입으로 변환하기 위해 사용합니다. (이 부분의 예외 처리는 Step 3에서 다룰 예정입니다.)
- exit와 return: exit 명령 시 Scanner 자원을 닫고 main 메소드를 return하여 프로그램이 완전히 종료되도록 합니다.
UserManager 클래스: 핵심 기능 관리
UserManager 클래스는 실제 회원 목록(HashSet)을 관리하고, 회원 추가/삭제/조회/출력 등 모든 핵심 비즈니스 로직을 담당합니다. 처음에는 각 메소드의 뼈대만 만들고, 다음 Step 2 에서 기능을 구현할 예정입니다.
// UserManager.java
import java.util.HashSet; // HashSet 사용을 위한 임포트
class UserManager {
// 회원들을 저장할 HashSet 필드를 선언합니다.
// private로 선언하여 외부에서 직접 접근을 막고 캡슐화를 유지합니다.
private HashSet<User> users;
// 생성자: UserManager 객체 생성 시 users HashSet을 초기화합니다.
public UserManager() {
this.users = new HashSet<>();
}
// 아래 메소드들은 각 기능이 구현될 자리입니다.
public void addUser(int userID) {
System.out.println("🚧 addUser() 구현중...");
}
public void removeUser(int userID) {
System.out.println("🚧 removeUser() 구현중...");
}
public void listUsers() {
System.out.println("🚧 listUsers() 구현중...");
}
public void checkUser(int userID) {
System.out.println("🚧 checkUser() 구현중...");
}
}
- HashSet 필드 선언 및 초기화: users는 UserManager의 핵심 데이터이므로 private으로 선언하고, 생성자에서 new HashSet<>()를 통해 반드시 초기화해줍니다. 이렇게 해야 NullPointerException 없이 add 등의 메소드를 사용할 수 있습니다. 특히 HashSet은 중복된 요소를 저장하지 않는다는 특성이 있어, 고유한 회원 ID를 관리하는 데 매우 적합합니다.
💡 Step 2: 핵심 기능 구현
이제 UserManager 클래스와 User 클래스에 정의된 메소드들을 하나씩 구현하여 프로그램에 실제 기능을 추가하고, HashSet의 동작 원리를 적용해봅니다.
User 클래스 : equals(), hashCode(), toString() 메소드 구현
// User.java
class User {
private int id; // 회원 ID. 캡슐화를 위해 private로 선언
public User(int id) {
this.id = id;
}
public int getId() {
return id;
}
// equals(Object obj) 메소드를 오버라이드합니다.
// HashSet은 이 메소드를 사용하여 객체의 중복 여부를 판단합니다.
@Override
public boolean equals(Object obj) {
if (this == obj) return true; // 동일한 객체를 참조하면 true
if (obj == null || getClass() != obj.getClass()) return false; // null이거나 타입이 다르면 false
User user = (User) obj; // 안전하게 User 타입으로 다운캐스팅
return id == user.id; // int 타입의 id 값을 직접 비교
}
// hashCode()를 오버라이드합니다. 이 메소드는 객체의 해시 코드를 반환합니다.
@Override
public int hashCode() {
return Objects.hash(id);
}
// 객체를 문자열로 표현할 때 사용되는 toString() 메소드 오버라이드
@Override
public String toString() {
return String.valueOf(id);
}
}
- equals() 오버라이드: HashSet은 새로운 요소를 추가하거나 기존 요소를 검색/삭제할 때 equals() 메소드를 사용하여 객체의 동등성을 비교합니다. 동일한 ID를 가진 User 객체를 중복으로 간주하고 싶으므로, id 필드를 기준으로 구현합니다. 특히 getClass() != obj.getClass() 체크를 통해 엄격하게 같은 타입의 객체만 동등하다고 판단합니다.
- hashCode() 오버라이드: equals() 메소드를 오버라이드했다면, 반드시 hashCode() 메소드도 오버라이드해야 합니다. HashSet은 내부적으로 해시 테이블을 사용하므로, id가 같은 객체는 항상 같은 해시 코드를 반환하도록 하여 효율적인 검색 및 중복 방지를 가능하게 합니다. Objects.hash(id)는 이를 안전하게 구현하는 편리한 방법입니다.
- toString() 오버라이드: User 객체를 System.out.println() 등으로 쉽게 출력할 수 있도록 id 값을 문자열로 반환합니다.
UserManager 클래스 : 핵심 기능 메소드 구현
이제 UserManager 클래스의 addUser, removeUser, listUsers, checkUser 메소드를 HashSet의 특징을 활용하여 구현합니다.
// UserManager.java
import java.util.HashSet;
class UserManager {
private HashSet<User> users;
public UserManager() {
this.users = new HashSet<>();
}
// 새로운 회원 ID를 추가합니다. ID가 이미 존재하면 추가되지 않습니다.
public void addUser(int userID) {
User newUser = new User(userID);
// HashSet의 add() 메소드는 요소 추가 성공 시 true, 실패 시 false를 반환합니다.
System.out.println(users.add(newUser) ? "✅ 회원이 추가되었습니다." : "⚠️ 회원 ID " + userID + "는 이미 존재합니다.");
}
// 특정 회원 ID를 가진 회원을 삭제합니다.
public void removeUser(int userID) {
User userToRemove = new User(userID);
// HashSet의 remove() 메소드는 요소 제거 성공 시 true, 실패 시 false를 반환합니다.
System.out.println(users.remove(userToRemove) ? "✅ 회원이 삭제되었습니다." : "⚠️ 존재하지 않는 회원 ID입니다.");
}
// 현재 등록된 모든 회원 목록을 출력합니다.
public void listUsers() {
if (users.isEmpty()) {
System.out.println("❌ 회원이 존재하지 않습니다.");
} else {
System.out.println("📋 현재 회원 목록: ");
// 향상된 for 루프를 사용하여 HashSet의 각 User 객체를 순회합니다. HashSet은 저장 순서를 보장하지는 않습니다.
for (User user : users) {
System.out.println(user); // User 클래스의 toString() 메소드 자동 호출. id만 출력
}
}
}
// 특정 회원 ID를 가진 회원이 존재하는지 확인합니다.
public void checkUser(int userID) {
User searchUser = new User(userID);
// HashSet의 contains() 메소드를 사용하여 존재 여부를 확인합니다.
System.out.println(users.contains(searchUser) ? "✅ 회원이 존재합니다." : "⚠️ 회원이 존재하지 않습니다.");
}
}
- addUser(int userID): users.add(newUser)를 통해 User 객체를 HashSet에 추가합니다. User 클래스의 equals()와 hashCode() 덕분에 중복을 자동으로 방지하며, 성공/실패 메시지를 출력합니다.
- removeUser(int userID): users.remove(userToRemove)를 통해 User 객체를 HashSet에서 제거합니다. 마찬가지로 equals()와 hashCode()를 사용하여 일치하는 객체를 찾고, 성공/실패 메시지를 출력합니다.
- listUsers(): users.isEmpty()로 목록이 비어있는지 확인하고, 비어있지 않으면 향상된 for 루프를 사용하여 HashSet의 모든 User 객체를 순회하며 출력합니다.
- checkUser(int userID): users.contains(searchUser)를 통해 특정 User 객체의 존재 여부를 확인하고, 그 결과를 메시지로 출력합니다.
💡 Step 3: 예외 처리
현재 프로그램은 사용자 입력이 항상 올바르다고 가정합니다. 예를 들어, 회원 ID를 입력해야 할 때 숫자가 아닌 문자를 입력하면 NumberFormatException이 발생하여 프로그램이 예기치 않게 종료될 수 있습니다.
이러한 예외 상황을 안정적으로 처리하고, 잘못된 입력이 들어왔을 때 사용자에게 명확한 피드백을 제공하도록, try-catch 구문을 사용하여 예외 처리를 합니다.
// UserApp.java
import java.util.Scanner;
public class UserApp {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
UserManager manager = new UserManager();
while (true) {
System.out.println("\n\uD83D\uDC65 회원 관리 시스템 (add, remove, list, check, exit)");
System.out.print("명령 입력: ");
String command = scanner.nextLine().trim().toLowerCase();
switch (command) {
case "add":
System.out.print("추가할 회원 ID: ");
// --- 예외 처리 시작 ---
try {
int addId = Integer.parseInt(scanner.nextLine());
manager.addUser(addId);
} catch (NumberFormatException e) {
System.out.println("⚠️ 오류: 회원 ID는 숫자 형식으로 입력해주세요.");
}
// --- 예외 처리 끝 ---
break;
case "remove":
// --- 동일하게 예외 처리 --- ...
- try-catch 블록: Integer.parseInt() 호출 부분을 try 블록으로 감쌉니다. 만약 이 코드에서 NumberFormatException이 발생하면, 프로그램이 비정상 종료되는 대신 catch 블록 안의 코드가 실행됩니다. add, remove, check 명령 모두 동일한 방식으로 적용하여 안전성을 높일 수 있습니다.
- 사용자 피드백: catch 블록에서는 사용자에게 "회원 ID는 숫자 형식으로 입력해주세요."와 같이 구체적이고 친절한 오류 메시지를 출력하여 무엇이 문제인지, 어떻게 해결해야 하는지 알려줍니다. 이는 사용자 경험을 크게 향상시킵니다.
그런데 이렇게 하면, 동일한 try-catch 구문을 case마다 구현을 해주어야 합니다. 반복되는 구문을 처리해주기 위한 메소드를 만들어 코드의 재사용성과 가독성을 높였습니다.
// UserApp.java
import java.util.Scanner;
public class UserApp {
// 사용자로부터 유효한 정수를 입력받는 헬퍼 메소드
private static int getIntInput(Scanner scanner, String prompt) {
while (true) {
System.out.print(prompt);
String input = scanner.nextLine().trim();
try {
return Integer.parseInt(input);
} catch (NumberFormatException e) {
System.out.println("⚠️ 오류: 입력은 숫자 형식이어야 합니다. 다시 시도해주세요.");
}
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
UserManager manager = new UserManager();
while (true) {
System.out.println("\n\uD83D\uDC65 회원 관리 시스템 (add, remove, list, check, exit)");
System.out.print("명령 입력: ");
String command = scanner.nextLine().trim().toLowerCase();
switch (command) {
case "add":
int addId = getIntInput(scanner, "추가할 회원 ID: ");
manager.addUser(addId);
break;
case "remove":
int removeID = getIntInput(scanner, "삭제할 회원 ID: ");
manager.removeUser(removeID);
break;
case "list":
manager.listUsers();
break;
case "check":
int checkId = getIntInput(scanner, "검색할 회원 ID: ");
manager.checkUser(checkId);
break;
case "exit":
System.out.println("🚪 프로그램 종료.");
scanner.close();
return; // 프로그램 종료
default:
System.out.println("⚠️ 알 수 없는 명령입니다. 다시 시도하세요.");
break;
}
}
}
}
최종 코드
import java.util.HashSet;
import java.util.Objects;
import java.util.Scanner;
// --- User 클래스 ---
class User {
private int id; // 회원 ID. 캡슐화를 위해 private로 선언
public User(int id) {
this.id = id;
}
public int getId() {
return id;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true; // 동일한 객체면 true
if (obj == null || getClass() != obj.getClass()) return false; // null이거나 타입이 다르면 false
User user = (User) obj; // User 타입으로 다운캐스팅
return id == user.id; // id 값이 같으면 true
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return String.valueOf(id);
}
}
// --- UserManager 클래스 ---
class UserManager {
private HashSet<User> users;
public UserManager() {
this.users = new HashSet<>();
}
public void addUser(int userID) {
User newUser = new User(userID);
System.out.println(users.add(newUser) ? "✅ 회원이 추가되었습니다." : "⚠️ 회원 ID " + userID + "는 이미 존재합니다.");
}
public void removeUser(int userID) {
User userToRemove = new User(userID);
System.out.println(users.remove(userToRemove) ? "✅ 회원이 삭제되었습니다." : "⚠️ 존재하지 않는 회원 ID입니다.");
}
public void listUsers() {
if (users.isEmpty()) {
System.out.println("❌ 회원이 존재하지 않습니다.");
} else {
System.out.println("📋 현재 회원 목록: ");
for (User user : users) {
System.out.println(user);
}
}
}
public void checkUser(int userID) {
User searchUser = new User(userID);
System.out.println(users.contains(searchUser)? "✅ 회원이 존재합니다." : "⚠️ 회원이 존재하지 않습니다.");
}
}
// --- UserApp 클래스 ---
public class UserApp {
private static int getIntInput(Scanner scanner, String prompt) {
while (true) {
System.out.print(prompt);
String input = scanner.nextLine().trim();
try {
return Integer.parseInt(input);
} catch (NumberFormatException e) {
System.out.println("⚠️ 오류: 입력은 숫자 형식이어야 합니다. 다시 시도해주세요.");
}
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
UserManager manager = new UserManager();
while (true) {
System.out.println("\n\uD83D\uDC65 회원 관리 시스템 (add, remove, list, check, exit)");
System.out.print("명령 입력: ");
String command = scanner.nextLine().trim().toLowerCase();
switch (command) {
case "add":
int addId = getIntInput(scanner, "추가할 회원 ID: ");
manager.addUser(addId);
break;
case "remove":
int removeID = getIntInput(scanner, "삭제할 회원 ID: ");
manager.removeUser(removeID);
break;
case "list":
manager.listUsers();
break;
case "check":
int checkId = getIntInput(scanner, "검색할 회원 ID: ");
manager.checkUser(checkId);
break;
case "exit":
System.out.println("🚪 프로그램 종료.");
scanner.close();
return; // 프로그램 종료
default:
System.out.println("⚠️ 알 수 없는 명령입니다. 다시 시도하세요.");
break;
}
}
}
}
개인 공부를 위해 정리한 글입니다 :)
틀린 내용이나 개선점이 있으면 댓글 남겨주세요.