들어가며
자바 콘솔 기반의 미니 커머스 프로젝트를 되돌아보며 코드 품질 개선 작업을 진행했습니다.
처음엔 요구사항에 있는 기능 구현을 중심으로 작업하다 보니 의식의 흐름으로 쓰여진 코드 덩어리가 되었습니다.
특히 반복되는 코드들이 많고 절차적 형태로 쓰였거나 유지보수가 매우 어려운 코드로 가득했습니다.
해서 이를 개선하기 위해 어떤 고민을 했고, 어떤 객체 지향 원칙을 적용했는지에 대한 학습 과정을 기록합니다.
1. ScannerSystem을 통한 안정성 확보
프로젝트 초기 버전은 모든 기능 메서드 내부에 Scanner를 이용한 입력 로직과 try-catch 예외 처리 구문이 반복되었습니다. 이는 코드를 읽기 어렵게 만들 뿐만 아니라, 새로운 입력 처리 방식이 필요할 때마다 수정해야 할 곳이 너무 많다는 문제를 가지고 있었습니다.
🔥 문제 인식: 중복 코드와 낮은 응집도
변경 전 상품 선택이나 메뉴 이동 시 숫자를 입력받는 코드입니다.
public int selectProduct(Scanner sc) {
while (true) {
try {
System.out.print("상품 번호를 입력하세요: ");
int num = sc.nextInt();
sc.nextLine(); // 줄바꿈 제거
return num;
} catch (InputMismatchException e) {
System.out.println("잘못된 입력입니다. 숫자를 입력해주세요.");
sc.nextLine(); // 줄바꿈 제거
}
}
}
이 코드가 숫자 입력이 필요한 모든 곳에 반복적으로 작성되어 있었습니다. '숫자 입력 오류 메시지' 문자열 하나만 수정하려 해도 10군데를 모두 찾아다녀야 했습니다.
🛠 해결 방법: 입력 책임의 캡슐화
이 문제를 해결하기 위해 ScannerSystem이라는 클래스를 만들고, 입력 처리와 예외 처리의 책임을 오직 이 클래스에만 부여했습니다. 이는 SRP(단일 책임 원칙)를 따르는 방식입니다.
public Integer getValidatedInput(int min, int max) {
int userInput;
//입력 예외처리
while (true) {
try {
userInput = scanner.nextInt();
scanner.nextLine(); //줄바꿈 제거
//입력값 유효성 검증
if (userInput < min || userInput > max) {
System.out.print("잘못된 입력입니다. 다시 입력해 주세요: ");
continue;
}
break;
} catch (InputMismatchException e) {
System.out.print("숫자를 입력해 주세요 :");
scanner.nextLine(); //줄바꿈 제거
continue;
}
}
return userInput;
}
// 사용처에서는 이렇게 간결해집니다.
int userInput = ScannerSystem.getValidatedInput(1,3);
💡 학습 인사이트
가장 기본적인 입력 처리조차도 객체 지향적 관점에서 재사용 가능하고 오류가 없도록 설계해야 함을 깨달았습니다. 코드를 개선한 후, 핵심 로직에만 집중할 수 있게 되었습니다.
2. 필터링 로직 개선
필터링된 목록에서 상품을 선택할 때, 사용자가 보는 번호와 시스템이 접근하는 데이터 인덱스가 불일치하여 오류가 발생했습니다.
🔥 문제 인식: 인덱스 불일치
1. 사용자 선택 인덱스의 오해: 사용자는 화면에 필터링되어 보여지는 목록을 보고 상품 번호를 입력합니다.
예를 들어, 원본 상품이 5개인데 필터링 후 3개만 보인다면, 사용자는 3개 목록을 기준으로 1, 2, 3 중 하나를 입력합니다.
2. getProduct 내부 로직의 오류: 사용자의 입력 값으로 상품 데이터를 접근합니다.
예를들어 필터링되어진 목록의 1(사과),2(키위) 중 2번으로 데이터를 접근했다면 원본 상품의 1(사과),2(바나나),3(토마토),4(키위)의 데이터에서는 2(바나나)를 가져오게 됩니다. 또한 화면에서 보여지지 않는 4번을 입력해도 상품이 있는 것으로 처리하게 됩니다.
//변경 전 코드
public Product getProduct(int idx) {
if (products.size() < idx || idx < 1) {
return null;
}
return products.get(idx - 1);
}
🛠 해결 방법: 필터링 결과만을 이용한 인덱스 처리
문제는 필터링된 결과와 인덱스를 일치시키지 못한 데 있었으며, 변경된 코드는 필터링 조건에 맞는 데이터에서 값을 가져오도록 함수를 생성하여 문제를 해결했습니다.
- 스트림을 이용한 필터링: 메서드 내부에서 products.stream().filter(condition).toList()를 사용하여, 사용자가 입력한 인덱스는 오직 필터링된 상품 목록내에서만 유효하게 처리하도록 했습니다.
Product product = null;
//필터링 조건 추가
public boolean showProduct() {
//... 생략
if (filter == 1) {
productInfoMessage = getCategory(curCategory).showProductsInfo();
pickMax = getCategory(curCategory).getProductsSize();
} else if (filter == 2) {
productInfoMessage = getCategory(curCategory).showProductsInfoUnder();
pickMax = getCategory(curCategory).getProductsSizeUnder();
} else if (filter == 3) {
productInfoMessage = getCategory(curCategory).showProductsInfoOver();
pickMax = getCategory(curCategory).getProductsSizeOver();
//... 생략
}
//필터링 조건에 맞는 데이터 접근
public int getProductsSize() {
return products.size();
}
public int getProductsSizeOver() {
//필터걸기
List<Product> filtered = products.stream()
.filter(condition)
.filter(product -> product.getPrice() > 1000000)
.toList();
return filtered.size();
}
💡 학습 인사이트
데이터 처리 로직은 사용자에게 제공되는 정보의 맥락을 따라야 합니다. 즉, 사용자가 보고 있는 필터링된 목록을 기준으로 모든 처리가 이루어져야 합니다. 가시적 목록을 생성하고 데이터 처리 기준을 함께 변경해 문제를 해결할 수 있었습니다.
더 나아가 개선을 해본다면 고유 ID를 사용하여 이러한 맥락의 불일치 문제를 근본적으로 해결해보고 싶습니다.
3. UI 로직과 비즈니스 로직의 분리
초기에는 메인 로직을 담고 있는 start() 함수 내부에 메뉴 출력, 사용자 선택, 그리고 이에 따른 비즈니스 로직 호출이 모두 섞여 있었습니다. 기능을 수정한다고 했을 때 어느 부분을 찾아봐야할지 start()를 처음부터 읽어봐야했습니다.
🔥 문제 인식: 낮은 응집도와 OCP 위반
하나의 메서드가 너무 많은 책임을 지니고 있었고, 기능 확장에 대한 OCP(개방-폐쇄 원칙)이 지켜지지 못했습니다.
또한 사용자에게 보여주는 로직과 흐름 제어 로직이 분리되지 않고 섞여 있다는 점에서 많은 문제가 있었습니다.
🛠 해결 방법: 메뉴 관리 객체 도입
// [1] 프레젠테이션 로직 (UI 출력)
System.out.println("\n[ 실시간 커머스 플랫폼 메인 ]");
int count = 1;
for (Category category : categories) {
System.out.println(count++ + ". " + category.getName());
}
// 장바구니 상태에 따라 메뉴가 바뀜 (조건부 출력)
if (cart.getCartItemAmount() > 0) {
System.out.println("4. 장바구니 확인 (총 " + cart.getCartItemAmount() + "개)");
System.out.println("5. 주문 취소");
}
System.out.println("6. 관리자 모드");
System.out.println("0. 프로그램 종료");
System.out.print("번호를 선택하세요: ");
// [2] I/O 로직 및 유효성 검사 (흐름 제어와 혼재)
int inputMenu;
try {
// 입력값 유효성 검사 로직이 여기에 직접 구현됨
inputMenu = scanner.nextInt();
scanner.nextLine(); // 버퍼 비우기
} catch (Exception e) {
System.out.println("잘못된 입력입니다. 숫자를 입력해주세요.");
scanner.nextLine();
continue;
}
// [3] 흐름 제어 로직 (거대하고 복잡한 분기)
if (inputMenu == 0) {
System.out.println("커머스 플랫폼을 종료합니다.");
break;
} else if (inputMenu == 6) {
//... 생략
// 관리자 모드 호출 및 인증 로직 (여기서 다시 수십 줄의 코드가 중첩됨)
break;
} else if (inputMenu >= 1 && inputMenu <= categories.size()) {
//...생략
// [4] 다음 단계 로직 호출 (ex 1: 전자제품 상품 목록 보기 2: 의류 상품 목록 보기 )
//.. 여기에서도 수십 줄의 코드가 쓰여짐
} else {
System.out.println("선택할 수 없는 번호입니다.");
}
모든 로직이 담겨있던 while (true) 루프 대신, 이제는 mainMenu() 메서드를 호출하는 역할만 남았습니다.
public void start() {
// 1. 상품 데이터 저장 (비즈니스 초기화)
createData();
// 2. 메뉴 객체 생성 및 목록 저장 (흐름 제어 초기화)
menuCreate();
while (true) {
// 메인 메뉴 선택 단계로 진입
if (!mainMenu())
break; // mainMenu()에서 프로그램 종료를 선택하면 break
}
}
또한 start()에 있었던 출력 및 입력 처리의 모든 책임을 캡슐화했습니다.
while (true) {
//카테고리 필터 선택 단계
if (!showFilter())
break;
//상품 선택 단계
if (!showProduct())
continue;
//장바구니 추가 여부 선택
if (!selectAddCartStep())
continue;
}
}
💡 학습 인사이트
메뉴 로직을 캡슐화하고 분리함으로써, UI 로직과 핵심 로직이 명확히 분리되었습니다. 이는 코드를 읽기 쉽게 만들 뿐만 아니라, 새로운 메뉴 항목을 추가하거나 기존 메뉴를 변경할 때 다른 코드에 영향을 주지 않는 유연한 구조를 만들 수 있었습니다.
🌟 마치며
리팩토링 과정을 거치면서 객체지향 설계의 중요성을 체감할 수 있었습니다.
지금은 매우 많은 삽질을 거쳐 수정되었지만 다음 프로젝트를 한다면 개선된 코드를 기반으로 효율적인 작업에 도움이 될 수 있을 것 같습니다.
'트러블슈팅' 카테고리의 다른 글
| [JPA] JPA 테이블 생성 (0) | 2025.12.05 |
|---|---|
| 트러블 슈팅/ 데이터 무결성 (0) | 2025.11.18 |
| 트러블 슈팅/ 퍼블릭 IP로 서버 접속하기 (0) | 2025.11.13 |
| 트러블 슈팅/ 의존성 설정 (0) | 2025.10.27 |
| 트러블슈팅/ 캡슐화 (0) | 2025.09.23 |