
커머스 플랫폼에 관리자 모드를 추가하면서문제점을 만났다.
예외 처리를 어디서, 어떻게 해야 하는가 였다.
코드는 결국 동작했지만, 그 과정에서 생각보다 많은 것을 고민했다.(항상 Exception , 에러코드들을 관리하는 것은 복잡한 것 같다.)
요구사항을 처음 읽었을 때는 단순해 보였다.
비밀번호 입력 → 틀리면 카운트 증가 → 3회 되면 메인으로
그래서 처음에는 이렇게 짰다.
private void showAdminLogin(Scanner scanner) {
int failCount = 0;
while (failCount < 3) {
System.out.print("비밀번호: ");
String input = scanner.next();
if (input.equals("admin123")) {
showAdminMenu(scanner);
return;
}
failCount++;
System.out.println("틀렸습니다. (" + failCount + "회 실패)");
}
System.out.println("초과했습니다.");
}
동작은 했다. 그런데 문제가 생겼다.
failCount가 메서드 로컬 변수라 매번 0으로 초기화됐다.
즉, 3회 실패해서 메인으로 돌아간 다음 6번을 다시 누르면 카운트가 리셋됐다.
의도한 동작인지 아닌지가 불명확했는데, 요구사항을 다시 읽어보니
"메인 메뉴로 돌아가기" 가 포인트였다. 복귀 후 재시도는 허용하는 게 자연스럽다고 판단했다.
그렇다면 resetFailCount()는 메인으로 복귀하는 시점에 호출해야 했다.
private void showAdminLogin(Scanner scanner) {
admin.resetFailCount(); // 진입 시점에 초기화
while (!admin.isLocked()) {
System.out.print("비밀번호: ");
String input = scanner.next();
if (admin.authenticate(input)) {
showAdminMenu(scanner);
return;
}
if (admin.isLocked()) {
System.out.println("초과했습니다. 메인으로 돌아갑니다.");
} else {
System.out.printf("틀렸습니다. (%d회 실패)%n", admin.getFailCount());
}
}
}
흐름 제어에서 "언제 초기화하는가" 는 생각보다 중요한 설계 결정이다.
로컬 변수로 처리하면 간단해 보이지만, 상태가 객체에 있어야 할 때는 명시적으로 초기화 시점을 정해줘야 한다.
System.out.println으로 막기장바구니에 재고 없는 상품을 담으려 할 때, 처음에는 Cart.addItem() 안에서 이렇게 처리했다.
public void addItem(Product product) {
if (product.getStock() == 0) {
System.out.println("재고가 없어 추가할 수 없습니다."); // 그냥 출력하고 끝
return;
}
// ...
}
동작은 했다. 그런데 관리자 모드에서 음수 가격 입력 처리를 짜다가 의문이 생겼다.
// CommerceSystem 안에서
while (true) {
System.out.print("가격 입력: ");
price = scanner.nextInt();
if (price < 0) {
System.out.println("0 이상이어야 합니다.");
} else {
break;
}
}
Product.setPrice()에도 같은 검증이 있다.
public void setPrice(int price) {
if (price < 0) {
System.out.println("가격은 0 이상이어야 합니다.");
return;
}
this.price = price;
}
검증이 두 곳에 있다. 어디서 막아야 하는 걸까?
| 위치 | 장점 | 단점 |
|---|---|---|
도메인 객체 내부 (setPrice) | 어디서 호출해도 안전 | 출력 메시지가 도메인 안에 있는 게 어색함 |
흐름 제어 레이어 (CommerceSystem) | 입력 단계에서 차단, UX 제어 가능 | 도메인 객체를 믿고 쓸 수 없음 |
결론적으로 현재 구조에서는 둘 다 유지했다.
이유는 간단하다. 지금은 Exception을 던지지 않고 System.out.println으로 처리하고 있는데, 도메인 객체 내부 검증이 없으면 잘못된 값이 그냥 들어가버린다. 흐름 제어 레이어의 while 루프는 UX를 위한 것이고, 도메인 내부 검증은 방어선이다.
사실 이상적인 구조는 이렇다.
// 도메인에서 예외를 던지고
public void setPrice(int price) {
if (price < 0) throw new IllegalArgumentException("가격은 0 이상이어야 합니다.");
this.price = price;
}
// 호출부에서 잡아서 처리
try {
product.setPrice(newPrice);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
그런데 지금 프로젝트는 아직 Exception 설계를 제대로 도입하지 않았다.
주석에 // Exception 처리 - System.out.print로 대체 라고 적어두었는데,
이게 임시 처리임을 명시한 것이다.
현 단계에서 Exception을 도입하면 try-catch 구조가 전체로 퍼지는데,
그 설계를 아직 잡지 않은 상태에서 섣불리 도입하면 오히려 코드가 더 복잡해질 수 있다고 판단했다.
예외 처리는 "어디서 막는가" 의 문제가 아니라 "누가 책임지는가" 의 문제다.
도메인 객체는 자신의 규칙을 스스로 지켜야 하고,
흐름 제어 레이어는 사용자 경험을 책임진다.
지금은 두 역할이 섞여 있지만, 다음 단계에서는 Exception을 제대로 설계해서 분리할 예정이다.
| 문제 | 원인 | 해결 |
|---|---|---|
| 실패 카운트 초기화 시점 | 로컬 변수 vs 객체 상태 혼동 | Admin 객체가 상태를 소유, 진입 시점에 reset() 호출 |
| 예외 처리 이중화 | 도메인·흐름 레이어 책임 불분명 | 현 단계에서 System.out.println 임시 처리, Exception 설계는 다음 단계로 |
콘솔 커머스 프로젝트지만 이런 설계 결정들이 실제 서버 개발에서도 그대로 나온다는 걸 느꼈다.
다음에는 Exception 계층을 제대로 설계해서 도메인 규칙 위반을 명시적으로 처리해보려 한다.