2025. 1. 14. 11:35ㆍDevelop/Java
계산기 과제 해설 세션을 들으며 내가 배우고 깨달았 던 점을 정리해보았다!
1. 반복문 밖에서 객체 생성의 중요성
코드를 작성하는 과정에서 나의 문제점은 계산 결과가 리스트에 지속적으로 쌓여야 하는데 그렇지 않았다는 점이다. 그래서 내가 추론한 과정은 이러하다.
App클래스에서 계산을 수행할 때 while 무한반복문을 돌며 계속 객체를 새로 생성하는 것 같다. -> 각기 다른 객체가 만들어지고 객체마다 메모리 주소가 다른 ArrayList를 참조하고 있을 것이다. -> 때문에 한 개의 ArrayList에 여러객체의 계산 결과값이 공유되어 있지 않다. -> 그렇다면 이 ArrayList를 인스턴스멤버가 아닌 클래스멤버로 바꾸자! -> static 키워드를 이용하자.
였다.
그래서 나는 ArrayList를 static 키워드를 이용하였고 이 후 정상적으로 리스트에 값이 쌓였다.
//객체 간의 계산결과를 공유하기 위해서 static 키워드 사용
static private final List<Integer> resultArray = new ArrayList<Integer>();
인스턴스 멤버?
- 인스턴스 멤버는 객체를 생성해야 사용할 수 있습니다.
- 또한 객체의 인스턴스 필드는 각각의 인스턴스마다 고유하게 값을 가질 수 있습니다.
- 그렇다면 객체가 인스턴스화할 때마다 객체의 메서드들은 인스턴스에 포함되어 매번 생성이 될까요?
- 그렇지 않습니다. 매번 저장한다면 중복 저장으로 인해 메모리 효율이 매우 떨어지기 때문에 메서드는 메서드 영역에 두고서 모든 인스턴스들이 공유해서 사용합니다.
- 대신 무조건 객체를 생성 즉, 인스턴스를 통해서만 메서드가 사용될 수 있도록 제한을 걸어둔 것입니다.
클래스 멤버?
클래스는 Java의 클래스 로더에 의해 메서드 영역에 저장되고 사용됩니다.
- 이때 클래스 멤버란 메서드 영역의 클래스와 같은 위치에 고정적으로 위치하고 있는 멤버를 의미합니다.
- 따라서 클래스 멤버는 객체의 생성 필요 없이 바로 사용이 가능합니다.
클래스 멤버 선언
- 필드와 메서드를 클래스 멤버로 만들기 위해서는 static 키워드를 사용하면 됩니다.
- 일반적으로 인스턴스마다 모두 가지고 있을 필요 없는 공용적인 데이터를 저장하는 필드는 클래스 멤버로 선언하는 것이 좋습니다.
- 또한 인스턴스 필드를 사용하지 않고 실행되는 메서드가 존재한다면 static 키워드를 사용하여 클래스 메서드로 선언하는 것이 좋습니다.
그런데 해설코드를 보니 static 키워드를 이용하지 않았다! 따라서 내가 한 추론이 틀린 것인가? 하고 다시 static 키워드를 없애고 코드를 돌려보니 또 값이 쌓이지 않는 문제가 발생했다.
그래서 해설코드를 보며 다음과 같이 또 추론해보았다.
해설코드에서는 전체적인 실행흐름을 담당하는 App클래스와 계산을 수행하는 메서드와 그 외 다양한 메서드가 있는 Calculator2클래스가 같은 패키지에 있다. 하지만 내 코드는 각기 클래스가 다른 패키지에 있었고 혹시 클래스가 서로 다른 패키지에 있기 때문에 ArrayList가 공유가 되지 않는것인가?
이 추론은 틀렸다!
코드가 다른 패키지에 작성되었다고 해도, 같은 JVM 내에서 클래스 로딩과 메모리 관리는 동일하게 이루어진다. 하지만, 계산을 수행하는 코드에서 Calculator2 객체를 매번 새로 생성하고 있다면, 각 객체는 독립적인 resultArray를 가지므로 결과가 새 객체의 리스트에만 저장되고, 이전 객체의 리스트와는 공유되지 않는다.
그럼 내가 작성한 코드는 계산기 객체를 매번 새로 생성하고 있었던 것이고 해설 코드는 그렇지 않았던 것일까?
package org.example.calculator3;
import java.util.List;
import java.util.Scanner;
public class App {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
Calculator2 calculator = new Calculator2();
while (true) {
System.out.print("첫 번째 숫자를 입력하세요: ");
int firstNumber = sc.nextInt();
if (firstNumber < 0) {
System.out.println("음수는 입력할 수 없습니다. 프로그램을 재시작합니다.");
continue;
}
System.out.print("두 번째 숫자를 입력하세요: ");
int secondNumber = sc.nextInt();
if (secondNumber < 0) {
System.out.println("음수는 입력할 수 없습니다. 프로그램을 재시작합니다.");
continue;
}
System.out.print("사칙연산 기호를 입력하세요 (+, -, *, /): ");
char operator = sc.next().charAt(0);
double result = calculator.calculate(firstNumber, secondNumber, operator);
System.out.println("결과: " + result);
System.out.println("더 계산하시겠습니까? (exit 입력 시 종료)");
if ("exit".equals(sc.next())) {
break;
}
System.out.println("가장 먼저 저장된 연산 결과를 삭제하시겠습니까? (yes 입력 시 삭제)");
if ("yes".equals(sc.next())) {
calculator.removeResult();
}
System.out.println("저장된 연산결과를 조회하시겠습니까? (yes 입력 시 조회)");
if ("yes".equals(sc.next())) {
List<Double> resultList = calculator.getResultList();
System.out.println("저장된 연산결과는 : " + resultList.toString());
}
}
}
}
그렇다!
중요한 점은 바로 while 무한반복문 밖에서 객체를 생성하고 있는 점이다. 따라서 반복문 밖에서 객체를 한번만 생성하고 생성한 객체의 메서드를 가지고 계산을 수행 후 result변수에 재할당하고 있는 것이었다.
반면 내 코드는 어떻게 작성했냐면,
package org.example.calculator;
import org.example.calculator2.Calculator2;
import java.util.ArrayList;
import java.util.Scanner;
public class App {
public static void main(String[] args) {
while(true) {
Scanner sc = new Scanner(System.in);
//숫자 입력
int firstNumber;
System.out.println("계산할 첫번째 숫자을 입력하세요.(0을 포함한 양의 정수): ");
while (true) {
try {
firstNumber = sc.nextInt();
if (firstNumber < 0) {
System.out.println("0을 포함한 양수를 입력하세요.");
System.out.println("계산할 첫번째 숫자을 입력하세요.(0을 포함한 양의 정수): ");
sc.nextLine();
continue;
}
break;
} catch (Exception e) {
System.out.println("정수를 입력하세요.");
System.out.println("계산할 첫번째 숫자을 입력하세요.(0을 포함한 양의 정수): ");
sc.nextLine();
}
}
int secondNumber;
System.out.println("계산할 두번째 숫자을 입력하세요.(0을 포함한 양의 정수): ");
while (true) {
try {
secondNumber = sc.nextInt();
if (secondNumber < 0) {
System.out.println("0을 포함한 양수를 입력하세요.");
System.out.println("계산할 두번째 숫자을 입력하세요.(0을 포함한 양의 정수): ");
sc.nextLine();
continue;
}
break;
} catch (Exception e) {
System.out.println("정수를 입력하세요.");
System.out.println("계산할 두번째 숫자을 입력하세요.(0을 포함한 양의 정수): ");
sc.nextLine();
}
}
//연산자 입력
System.out.println("계산할 연산자를 입력하세요.: ");
char operator;
while (true) {
try {
String inputOperator = sc.next();
if (inputOperator.isEmpty()) {
System.out.println("계산할 연산자를 입력하세요.: ");
sc.nextLine();
continue;
}
operator = inputOperator.charAt(0);
if (operator != '+' && operator != '-' && operator != '*' && operator != '/') {
System.out.println("입력할 수 있는 연산자는 오직 +,-,*,/ 입니다.");
System.out.println("계산할 연산자를 입력하세요.: ");
sc.nextLine();
continue;
}
break;
}
catch (Exception e) {
System.out.println("유효하지 않은 입력입니다. 입력할 수 있는 연산자는 오직 +,-,*,/ 입니다.");
System.out.println("계산할 연산자를 입력하세요.: ");
}
}
//계산 수행
Calculator2 doCalculate = new Calculator2();
doCalculate.calculate(firstNumber, secondNumber, operator);
doCalculate.printResult();
//계산 결과 기록
System.out.println("현재까지 기록된 계산결과입니다.");
doCalculate.fullGetter();
//계산 결과 삭제
System.out.println("계산결과를 삭제하고 싶다면 remove를 입력하세요.\n계산결과를 수정하고 싶다면 edit을 입력하세요.\n그렇지 않다면 아무 키나 입력하세요: " );
String removeAnswer = sc.next();
if(removeAnswer.equals("remove")) {
while(true) {
System.out.println("현재까지 기록된 계산결과입니다.");
doCalculate.fullGetter();
if(doCalculate.getter().isEmpty()) {
System.out.println("기록된 계산결과가 없습니다. 삭제를 종료합니다.");
break;
}
System.out.println("가장 첫번째 결과부터 삭제됩니다.\n삭제를 원하면 1을 입력하세요. 삭제를 종료하고 싶다면 0을 입력하세요.: ");
int removeInt = sc.nextInt();
if(removeInt == 1) {
doCalculate.removeResult();
} else if (removeInt == 0) {
break;
}
}
//계산결과 수정
} else if (removeAnswer.equals("edit")) {
while(true) {
System.out.println("현재까지 기록된 계산결과입니다.");
doCalculate.fullGetter();
System.out.println("수정하고 싶은 결과의 인덱스를 입력하세요.(0부터 시작): ");
int editIndex = sc.nextInt();
System.out.println("어떤 값으로 수정하시겠습니까? 값을 입력하세요.: ");
int editValue = sc.nextInt();
doCalculate.setter(editIndex, editValue);
System.out.println("수정된 계산 결과입니다.");
doCalculate.fullGetter();
System.out.println("계속 수정을 원할 경우 1을 입력하세요. 수정을 종료하고 싶다면 0을 입력하세요.: ");
int editInt = sc.nextInt();
if(editInt == 1) {
continue;
} else if (editInt == 0) {
break;
}
}
}
//종료 여부 결정
System.out.println("계산을 이어서 수행하고 싶은 경우 continue 입력, 종료하고 싶은 경우 exit을 입력하세요.: ");
String answer = sc.next();
if(answer.equals("exit")){
break;
} else if (answer.equals("continue")) {
continue;
}
}
}
}
바로 반복문 안에서 new 키워드를 이용해 객체를 매번 생성하고 있었던 것이다.
이렇게 될 경우 내가 했었던 것 처럼 static 키워드를 이용해 ArrayList를 클래스변수로 선언하여 공용적인 데이터 필드로 사용할 수 있도록 하거나, 반복문 밖에서 객체를 한번만 생성해야한다.
그렇지만 ArrayList 자료구조가 가지고 있는 특성 상 이 자료구조를 사용하겠다는 것은,
static을 선언하지 않아도 객체가 메서드를 여러번 수행할 때 마다 그 리턴 값을 ArrayList 자료구조로 모아서 관리하겠다는 의미인데, static을 사용해서 또 따로 이 필드를 클래스변수로 관리하는 것은 불필요해보인다.
또한 static 키워드를 사용하기 전 원래 이 ArrayList에 이렇게 직접적으로 접근하는 것 부터가 잘못되었다. 직접 접근해서 add 하는 것이 아니라 getter와 setter를 이용해서 값을 조회하고 수정하도록 해야한다. 그리고 왠만해선 필드에 static 키워드를 사용하는 것이 좋지 않다고 튜터님께서 말했다.
static에 관한 개념은 이번 주 목요일에 한번 더 자세하게 다룰 예정이니 그 때 가서 다시 블로그에 정리해보도록 하겠다!
2. 항상 null safe 하게
항상 값을 입력받을 때 null값이 들어올 수 있다는 것을 명심해야 한다.
코드에서 Scanner를 이용해서 값을 자주 받아오는데 이때에 null값이 들어오게 된다면, 들어온 값과 어떠한 값을 비교할 때 null error가 발생하게 될것이다.
다음은 내가 작성했던 코드다.
//계산 결과 삭제
System.out.println("계산결과를 삭제하고 싶다면 remove를 입력하세요.\n계산결과를 수정하고 싶다면 edit을 입력하세요.\n그렇지 않다면 아무 키나 입력하세요: " );
String removeAnswer = sc.next();
if(removeAnswer.equals("remove")) {
while(true) {
System.out.println("현재까지 기록된 계산결과입니다.");
doCalculate.fullGetter();
if(doCalculate.getter().isEmpty()) {
System.out.println("기록된 계산결과가 없습니다. 삭제를 종료합니다.");
break;
}
System.out.println("가장 첫번째 결과부터 삭제됩니다.\n삭제를 원하면 1을 입력하세요. 삭제를 종료하고 싶다면 0을 입력하세요.: ");
int removeInt = sc.nextInt();
if(removeInt == 1) {
doCalculate.removeResult();
} else if (removeInt == 0) {
break;
}
}
sc.next()를 이용해 removeAnswer 변수에 값을 담고 해당 변수와 "remove"라는 값과 .equals()를 이용해 값을 비교하였다.
다음은 해설 코드 일부분이다.
System.out.println("더 계산하시겠습니까? (exit 입력 시 종료)");
if ("exit".equals(sc.next())) {
break;
}
스캐너로 입력받은 문자열은 절대로 null이 될 수 없다. 그러나 우리가 리터럴로 적어준 "exit"이란 문자열은 더욱 절대로 null이 될 수 없을 것이다. 그래서 .equals()로 비교할 때는 왠만하면 앞에 오는게 더 확실하게 null이 아닌 것을 적어주는 것이 더 좋다. 물론 스캐너로 입력받은 것도 내부적으로 절대 null이 될 수 없게 코딩되어 있다. 그렇지만 nullpointerexception이 나는 것을 미연에 방지하기 위해서 위와같이 코딩하는 것을 습관으로 하는 것이 좋다!
'Develop > Java' 카테고리의 다른 글
| 계산기 과제 트러블슈팅 (2) | 2025.01.09 |
|---|