🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [5]일차
🚀5일차에서는 static키워드를 사용하여 필드와 메소드에 대한 접근법을 배우고, 좋은 객체란 무엇인지
좋은 객체를 만들기 위해서는 고려해야할 사항이 무엇인지 배웠습니다.
또한 로또 번호 프로그램과 같은 실습을 진행하여, 객체 간의 커뮤니케이션도 배울 수 있었습니다.
개인적으로 저는 배운 내용과 관련하여 활용할 수 있는 실습을 진행하였습니다!
<메소드와 필드>
❓메소드
<메소드 이름>
- 앞에서 설명한 식별자 규칙과 같고,
- 메소드 이름은 소문자로 시작하는 것이 관례입니다
- 카멜 표기법을 사용하는 것이 관례입니다.
- 언더바는 사용을 지양해야합니다.
<클래스 메소드 vs 인스턴스 메소드>
static이 붙지않은 메소드 = 인스턴스 메소드
인스턴스 별로 다르게 동작해야한다 = 인스턴스메소드를 사용
static이 붙은 메소드 = 클래스 메소드
static메소드는 객체 생성이나 유틸리티 관련해서 사용될 때가 있다.
되도록이면 인스턴스 메소드를 사용한다. (static사용을 지양한다.)
❓필드
: 클래스가 가지는 속성을 의미한다.
필드는 어떤 키워드와 함께 사용되느냐에 따라 사용방법이 달라진다.
필드를 다른언어에서는 멤버변수라고 쓰기도하지만, 자바는 필드 용어를 사용
API보시면 Field Summary 처럼 정의되어있음
static이 사용되는 필드는 static 필드
static이 사용되지 않는 필드는 “인스턴스 필드” 라고한다.
<필드 선언 방법>
[접근제한자] [static] [final] 데이터타입 필드명 [=초기값];
- 대괄호 안 내용은 생략이 가능하다는 의미
- 필드명은 식별자 규칙을 따른다.
- 메소드 안에 있는 변수들은 초기화하지 않으면 에러가 발생하지만
- 필드는 초기화하지 않아도 사용할 수 있다. 참조형일 경우에는 null (String타입) boolean형일 경우에는 false 기본형은 모두 0으로 초기화된다.
- 필드는 첫번째 글자는 소문자로 시작하는 것이 관례이다.
- 타입은 기본형으로 (int, boolean … )등이 올 수 있고,
- 참조타입으로 (class, 인터페이스, 배열)등이 올 수 있다.
<매개변수(parameter) vs 전달인자(argument)>
매개변수는 말그대로 변수를 의미하고, 전달해주는 변수를 의미
System.out.println(i);
위의 코드처럼 i를 전달하는것이 매개변수
public static void test(int i){
System.out.println(i);
}
test(i);
이 메소드를 호출할때 test(i) ← 로 전달하는 것이 전달인자
매개변수는 메소드의 정의부분에 나열되어 있는 변수들을 의미하며,
전달인자는 메소드 를 호출할때 전달되는 실제 값을 의미한다.
▶️실습 - 자판기 프로그램 VendingMachine
<VendingMachine 클래스>
// 1번메뉴조건, 2번메뉴조건 등에 따라 다르게 표현하고 싶을때는 조건문을 활용
// 들어온 메뉴의 종류에 따라 String을 리턴하는 메소드
public String pushProductButton(int menuId){
String result = "메뉴없음";
switch(menuId){
case (1):
result = "콜라를 주문하셨습니다.";
break;
case (2):
result = "사이다를 주문하셨습니다.";
break;
case (3):
result = "환타를 주문하셨습니다.";
break;
case (4):
result = "녹차를 주문하셨습니다.";
break;
case (5):
result = "이프로를 주문하셨습니다.";
break;
case (6):
result = "보리차를 주문하셨습니다.";
break;
case (7):
result = "제로콜라를 주문하셨습니다.";
break;
}
return result;
}
- 원하는 메뉴를 입력받아 버튼을 누르면 그에 맞는 출력문이 나오도록 기능
🚀이 switch문을 main에 있는 배열값으로부터 문자열로써 출력하는 코드 개선도 가능할 것 같았다.
<VendingMachineMain 클래스>
VendingMachine vm = new VendingMachine();
Scanner sc = new Scanner(System.in);
String[] allMenu = {"콜라", "사이다", "환타", "녹차", "이프로", "보리차", "제로콜라"};
int menuCount = 1;
System.out.println("-----메뉴판-----");
for(int i = 0; i < allMenu.length; i++){
System.out.println(menuCount++ + ":" + allMenu[i]);
}
System.out.print("\n메뉴를 입력해주세요 : ");
int menuId = sc.nextInt();
System.out.println(vm.pushProductButton(menuId));
- 메뉴를 사용자에게 입력 받고 그에 따른 호출을 진행
< static >
static은 무분별한 사용은 하지 않아야한다.
예를들어)
Math클래스만 봐도 이미 자바는 프로그래밍이 동작하는데 필요한 객체들을 만들어놓았다.
💡이러한 것들을 "API"라고 한다.
Math.random();
Math는 클래스이름, random()은 메소드이름
Math클래스에는 수학적인 기능의 메소드가 많기때문에 필요에 따라 가져다 쓸 수 있다.
보통 클래스를 쓰기위해서는 객체를 인스턴스화 해야하지만 사용이 가능한데,
하지만 Math클래스는 random()메소드를 바로 사용할 수 있게하였다.
바로 static이라는 키워드가 이러한 일을 가능하게끔 만들어줍니다.
static으로 선언하게되면)
💡new()로 인스턴스화 하지 않아도 프로그램이 실행될때부터 “이미 메모리상에 올라가기 때문”
💡main메소드 또한 static키워드 덕분에 프로그램 실행 시 main메소드부터 실행할 수 있도록 함
Math의 random()은
: 0.0부터 1.0까지의 랜덤한 숫자를 반환해주는 것이 이 메소드의 행위이자, 기능이자, 목적이다.
비슷한 클래스로 Random클래스는 util패키지로서 Random한 기능에 특화되어 편하게 쓰라고 제공해주는 클래스
<static 예제>
public class Test {
public void test(){
System.out.println("test");
}
public static void staticTest(){
System.out.println("staticTest");
}
public static void main(String[] args) {
Test ex = new Test();
// 1) static을 써주지 않은 예, = 객체 생성 후 호출해야함
ex.test();
// 2) static을 써준 예, = 객체를 생성하지 않아도 바로 접근 가능
staticTest();
}
}
이러한 클래스가 있고,
내부클래스이지만 static이 지정되지 않으면 객체를 생성해야만 사용할 수 있다.
내부클래스이지만 static이 지정되어있으면 객체 생성 없이 바로 접근하여 사용할 수 있다.
public class Test2 {
public static void main(String[] args) {
Test.staticTest();
}
}
이처럼 static으로 정의된 메소드의 경우
다른 클래스여도 객체 생성 없이 바로 접근이 가능하다.
<static정보는 어디에 저장될까?>
java 7전까지는 힙이 아닌 영역에 저장 (non-heap)
java 8이후로는 힙에 저장된다.
❓메모리 관련 요약
- new연산자를 사용할때마다 메모리에 인스턴스가 생성된다
- 인스턴스는 더 이상 참조되는 것이 없을때, 메모리가 부족할때 등 가비지 컬렉터가 수거해가서 “가비지 컬렉션” 된다.
- static한 필드는 “클래스가 로딩될때” “딱 한번” 메모리에 올라가고 초기화
- 인스턴스 메소드(static이 없는)는 인스턴스를 생성하고나서 레퍼런스 변수를 이용해 사용이 가능하다.
- 클래스 메소드는 클래스명.메소드명() 으로 사용이 가능하다. (static이 붙은)
- 메소드 안에 선언된 변수들은 메소드안에서 메소드가 실행될때 메모리에 생성되었다가, 메소드를 벗어나면(메소드가 종료되면) 메모리에서 사라진다.
❓static필드와 non-static필드의 차이점
static한 필드와, static하지 않은 필드를 활용하기 위해 클래스를 하나 만든다.
<StaticField 클래스>
public class StaticField {
public static int COUNT = 30;
public int nonStaticField = 10;
}
<StaticFieldTest 클래스> - StaticField클래스를 실행할 main메소드
public class StaticFieldTest {
public static void main(String[] args) {
StaticField s1 = new StaticField();
StaticField s2 = new StaticField();
StaticField s3 = new StaticField();
s1.nonStaticField = 20;
s2.nonStaticField = 40;
s3.nonStaticField = 60;
System.out.println(s1.nonStaticField);
System.out.println(s2.nonStaticField);
System.out.println(s3.nonStaticField);
System.out.println("----------------------------");
s1.COUNT = 100;
s2.COUNT = 200;
s3.COUNT = 300;
System.out.println(s1.COUNT);
System.out.println(s2.COUNT);
System.out.println(s3.COUNT);
System.out.println(StaticField.COUNT);
}
}
실행결과를 보면, nonStaticField는 각 생성된 객체마다(s1, s2, s3) 인스턴스 필드를 가질 수 있게되어,
각기 다른 값을 가지게된다.
하지만 static으로 선언된 필드는 s1.COUNT = 100; 으로 지정했지만
마지막 s3.COUNT = 300; 으로 지정함에따라
static키워드로 인해 static int COUNT는 공유 필드처럼 사용되었으므로
마지막에 수정한 값이 s1, s2, s3 객체에 모두 적용이 된다.
따라서 StaticField.COUNT를 출력해보면 그대로 300이 출력되는 것을 확인할 수 있다.
위 예제에서 StaticField 클래스에
String name;
public static String group; // --인스턴스화할때 생성되는 것이 아니라 프로그램 실행 시 "미리 한 번만 생성"
// 따라서 값이 공유된다.
필드를 추가하고,
s1.name = "김부각";
s1.group = "멋쟁이사자처럼";
s2.name = "장조림";
s1.group = "멋쟁이사자처럼";
s3.name = "이발소";
s1.group = "멋쟁이사자처럼";
main메소드에서 이처럼 테스트해보았을때, group과 같은 필드는 계속 똑같이 쓰임을 확인할 수 있다.
값이 객체별로 바뀌는 name만 인스턴스필드로 두고
값이 객체에 상관없는 group은 static 필드 (=클래스 필드)로 두면 좋을 것이다.
System.out.println("[" + s1.name + "]의 그룹 : (객체 생성 후 필드 호출 : " + s1.group + "), (클래스에 직접 접근하여 필드 호출 : " + StaticField.group + ")");
System.out.println("[" + s2.name + "]의 그룹 : (객체 생성 후 필드 호출 : " + s2.group + "), (클래스에 직접 접근하여 필드 호출 : " + StaticField.group + ")");
System.out.println("[" + s3.name + "]의 그룹 : (객체 생성 후 필드 호출 : " + s3.group + "), (클래스에 직접 접근하여 필드 호출 : " + StaticField.group + ")");
이러한 방식으로 호출 방법에 따른 필드가 어떤 값을 가지는지 모두 확인할 수 있다.
이처럼 동일한 값을 가지고 지속적으로 호출되는 경우 static 필드를 사용하는 것이 좋을 수 있다.
<좋은 객체>
❓응집도와 결합도
⭐좋은 객체는 응집도가 높고 결합도(Coupling) 낮다.
좋은 객체는
- 관련된 것들을 잘 모아서 가지고 있고,
객체 간의 사용에서 💡A 객체에서 수정을 하는 내용이, B 객체에서 내용이 바뀌게 된다면,
B는 A한테 의지하고 있다는 것이다. 이러한 것들을 결합도가 높다고 얘기한다. - 역할과 책임에 충실하면서 다른 객체와 잘 협력하여 동작하는 객체
나쁜 객체는
- 여러가지 역할을 한가지 객체에게 부여
- 이름과는 맞지 않는 속성과 기능을 가짐
- 제대로 동작하지 않은 객체
- 다른 객체와도 동작이 매끄럽지 않은 객체
⭐서로의 값이 바뀌어도 영향을 주지 않도록 해야하는 것이 결합도가 낮게 설계하는 방법이다.
서로의 의존성을 낮춰야한다. 의존하는 객체 사이의 의존성을 낮추는 것이다.
❓다형성 - Polymorphism
- 객체지향에서의 핵심용어
- 프로그램 언어의 각 요소들이 다양한 자료형에 속하는 것이 허가되는 성질
- 어떤 것이 정의되었을때 다양한 형태로 있을 수 있는 것이 다형성
- 단적인 예시로 println()메소드가 다형성의 대표적 사례이다.
- 여러가지 자료형으로 [메소드] 오버로딩이 되어있고, 데이터타입 별로 println() 메소드를 똑같이 사용할 수 있게된다.
- ex. 웹 페이지 :
클라이언트와 서버가 존재할때, 클라이언트는 URL을 쓰고 접속
→ 서버에 접속 클라이언트가 입력하는 주소에 따라서 각각 다른 도메인 서버에 접속 가능
서버는 요청에 따라 응답을 하게됨 응답한 내용을 브라우저에 보여주게됨
서버는 응답을 다 똑같이 하지만, 구글, 네이버, 유튜브 등 어떤 URL에 접속했는지에 따라 웹페이지의 화면이 다르다.
이러한 것도 다형성의 예시라고 볼 수 있다.
<다형성 - 메소드 오버로딩(Overloading)>
- 메소드의 이름은 같고, 매개변수의 갯수나 타입이 다른 함수를 정의
- 리턴값만 다른 오버로딩을 작성 불가능
💡반댓말인 단형성 : 프로그램 언어의 각 요소가 한가지의 형태만 가지는 성질
❓패키지
- 관련있는 것들끼리 묶어서 제공을 한다.
- 폴더와 같은 기능
- 패키지 이름은 보통 도메인이름 (주소)을 거꾸로 적은 후에 프로젝트 이름 등을 붙여서 만든다.
com.naver.mypage kr.co.likelion.project1 net.likelion.bootcamp.backend.project1 |
이렇게 이름을 정할 수 있음
도메인을 이용하는 이러한 방식으로 이름을 짓는 이유 = 중복되지 않게 하기 위해서이다.
<패키지가 정의된 클래스 컴파일 하기>
javac -d 경로명 *.java
➡️리눅스의 -d옵션을 사용한다.
패키지를 import하지 않을 경우
com.day05.exam.Hello h = new com.day05.exam.Hello();
처럼 직접 패키지를 가져와서 쓸 수 있다.
<protected 필드>
기본패키지까지 허용되고, 다른 패키지라도 “나의 자손은 접근가능”
반면 private 필드는 상속을 하든, 뭘하든 접근이 불가능하다.
- static한 메소드는 static한 필드만 접근 가능하다.
- 인스턴스 메소드 인스턴스가 만들어진 후에 접근이 가능할테니,
인스턴스가 만들어지면 이미 만들어진 static필드와 인스턴스 필드를 둘다 사용 가능하다.
❓UML표기법
Unified Modeling Language의 약자로 즉, 모델을 만드는 표준언어
먼저 클래스를 정의할때 명세를 작성한다.
클래스이름 | MathBean | ||
메소드이름 | 매개변수 | 리턴 타입 | 설명 |
printClassName | 없음 (X 로 표시) | X | 클래스 이름을 화면에 출력 |
printNumber | int x | X | 매개변수로 받은 정수를 출력 |
getOne | X | int | 숫자 1을 리턴 |
plus | int x, int y | int | 매개변수로 정수 2개를 입력받아 그 합을 리턴 |
이후 명세로 정의된 클래스에 따라 UML표기법을 만드는데,
MathBean | ||
+ | getOne() | :int |
+ | plus(int, int) | :int |
+ | printClassName() | :void |
+ | printNumber(int) | :void |
이 UML표기법을 보고 MathBean클래스를 정의할 수 있어야한다.
- 맨 위의 이름 : 클래스 이름
- +: (플러스)표시 : 접근제한자를 의미 (public을 의미하고있음)
-:(마이너스)표시 : private 접근제한자를 의미 - 가운데는 메소드의 이름
- :int : 등은 반환 타입을 의미
MathBean클래스의 경우 필드가 없으므로, 본인의 데이터를 조작할 순 없고
사용자가 호출하는 메소드만 수행하는 기능을 함
<소스코드, 클래스 파일 자체는 정적>
- 동적인 것들은 “실행되면서 생성되는 것들”
- 클래스 정보 자체는 “정적”이다.
❓가비지 컬렉터
메모리 관리자체를 프로그래머가 직접 해줄 필요가 없다. 따라서 가비지 컬렉터가 자동으로 메모리 관리를 하게된다.
우리의 메모리 공간은 = [스택 메모리 공간 + 힙 메모리 공간]이 존재하는데, 여기서 쓰이지 않는 필드나 인스턴스들은
가비지 컬렉터가 청소하게된다. (=가비지 컬렉션)
👀여기서 ‘스택’자료구조는 밑이 막히고 위가 뚫려있는, 탑처럼 쌓아가는 FILO 구조이다. First In Last Out
❓추상화
객체를 필요한 것들만 남기면서 정의해나가는 과정
❓캡슐화
- 관련된 것을 모아서 가지고 있는 것
- 잘모아서 가지고 있을수록 응집도(Cohesion)가 높다고 표현
⚠️주의 : 똑같은 객체가 같은 패키지에 2개이상 생성될 수 없음
만약 펜에 관한 클래스를 만든다.
{펜의 이름, 펜의 가격, 펜의 색깔, 펜의 제조사} 등
이러한 정보들이 다른 클래스에 흩어져있는 것이 아니라
Pen이라는 클래스 안에 다가 이 펜에 대한 정보를 담아내는 것
관련된 정보끼리 잘 모아서 가지고 있는 이것이 캡슐화이다.
public class Pen{
String name; // 펜의 이름
int price; // 펜의 가격
String ProductCompany; // 펜의 제조사
}
이렇게 잘 모아서 가지고 있을수록 “응집도가 높다”
❓정보 은닉(객체지향프로그래밍(OOP)의 핵심)
: 객체 지향 언어적 요소를 활용하여 객체에 대한 구체적인 정보를 노출시키지 않도록 하는 기법
보통 (캡슐화 == 정보 은닉) 으로 생각할 수 있지만, 같은 것이 아니라 정보 은닉 기법 중 하나가 캡슐화 이다.
정보은닉의 예시)
- 세세한 부분들을 은닉하여 코드를 보기 쉽게 만들어 줍니다.
- 예를 들어, 자동차를 운전하면서 바퀴가 어떻게 굴러가는지, 어떻게 시동이 켜지는지는 알 필요가 없는 것과 같다.
▶️실습 - 로또 번호 프로그램
- 자바의 "추상화"에 관련된 실습
<LottoBall클래스>
public class LottoBall {
int number;
public LottoBall(int number) {
this.number = number;
}
}
- 로또의 번호를 가진 볼들을 관리할 클래스 작성
- 생성자 활용 : LottoBall 이 생성될때 번호를 갖고 생성
<LottoMachine클래스>
- 1번설계방법 - 프로그램이 실행될때부터 로또 머신 안에 1~45번호가 들어가서 생성되도록 함
이방법의 경우 생성자에 볼을 채우도록 하는 생성자를 만들어주면됨 - 2번설계방법 - Setter메소드를 활용하여 setBall과같은 메소드를 통해 그때그때 볼을 채움
이번 예제에서는 1번 설계방법으로 진행할 것이다.
public LottoMachine(){
// 1~45번까지의 숫자를 가진 볼을 "로또 머신(배열)에 넣어준다"
for(int i = 0; i < lottoBalls.length; i++){
lottoBalls[i] = new LottoBall(i+1);
}
}
// 로또 번호를 섞는 메소드
// 두 개를 뽑아 계속해서 바꿔주는 과정을 100번 하면 섞일 것이다.
public void mix(){
for(int i = 0; i < 100; i++){
int x = (int)(Math.random() * lottoBalls.length); // 랜덤한 숫자 o1
int y = (int)(Math.random() * lottoBalls.length); // 랜덤한 숫자 o2
// 로또배열에 Ball 객체가 들어있음
// Ball을 받을 "그릇"을 만듦 = temp
// func : 두 개의 정수를 바꿔주는 실질적 기능
LottoBall temp = lottoBalls[x];
lottoBalls[x] = lottoBalls[y];
lottoBalls[y] = temp;
}
}
- temp로는 직접적인 값을 교체하는 것이 아닌 배열에 담긴 “주소값”을 변경하는 것
- mix() : 로또 번호를 두 개를 뽑아 두 개를 섞는 방식을 100번 하면, 번호가 무작위로 섞일 것
- Math.random() * lottoBalls.length : Math.random() * 100 을 하면 0.0부터 100.0까지 출력이 되듯
lottoBalls.length를 지정하면, 0.0~45.0까지의 숫자가 랜덤하게 뽑힌다.
여기서 (int)형변환을 통해 0~45까지 뽑히게 만들어준다.
<LottoMachine 클래스>
// 로또 번호를 뽑는다. (반환타입은 LottoBall 클래스)
public LottoBall getBall(){
LottoBall ball; // 볼을 담을 공간
int index;
// 일단 한번은 무조건 뽑아야하므로 do-while문 사용
do{
// 랜덤한 인덱스를 뽑아, 그 로또머신에 해당하는 인덱스에 있는 값을 ball에 넣음
index = (int)(Math.random() * lottoBalls.length);
ball = lottoBalls[index];
}while(ball == null); // 뽑았는데 ball이 null이면 빠져나오게 설계
// 뽑은 후에는 이 해당하는 값이 담긴 로또머신의 값은 null로 바꿔서 중복사용이 되지 않도록 함
lottoBalls[index] = null;
// 볼을 반환해줌
return ball;
}
- 로또번호를 뽑는 기능을 추가한다.
- do-while문 : 일단 한번은 무조건 뽑도록 한다.
- index : LottoMachine배열에 담긴 LottoBall 객체를 접근하기 위해선 배열의 인덱스를 알아야한다.
이 인덱스를 랜덤하게 받아와 그에 해당하는 값을 ball에 넣고 - lottoBalls[index] = null; : 담긴 값은 null처리를 통해 LottoMachine배열을 비워준다.
- 이렇게 뽑은 볼을 리턴한다.
<LottoUser 클래스>
public static void main(String[] args) {
// 로또 머신 안에 1~45값이 담김
LottoMachine lotto = new LottoMachine();
// 볼 섞기
lotto.mix();
// 6개의 로또 번호를 뽑는다.
// LottoBall 타입으로 ball을 받는다.
for(int i = 0; i < 6; i++){
LottoBall ball = lotto.getBall();
System.out.println(ball.number);
}
}
- 로또 번호 프로그램을 실행할 LottoUser클래스
- new LottoMachine() : LottoMachine의 인스턴스를 만들어준다.
- lotto.mix() : 생성된 인스턴스로 LottoMachine클래스에있는 mix()메소드에 접근하여 호출한다. (실행한다)
- for반복문 : 6개의 볼을 LottoBall 타입으로 입력받아 LottoBall의 필드인 number(숫자)를 출력한다.
🚀실습 - 레벨업 게임
<게임 설계>
먼저 클래스를 설계하기 전 게임에서 어떠한 것들을 할지 생각나는 대로 적어보았다.
To-do | Complete |
- 회원가입 시스템 구현 | X |
- 로그인 시스템 구현 | X |
- 메인화면으로 들어가기 위해서는 로그인 필요 | X |
- 메인화면에서 게임진행 후 사용자 입력에 따른 레벨 상승 | X |
- 사용자 입력은 문제를 맞추는 경우 레벨이 오르는 방식으로 진행 |
X |
- 유저의 기록에 따른 랭킹 시스템 구현 | X |
<GameUser클래스>
public class GameUser {
public static String userName;
private String userId;
private String userPw;
int userLevel = 0;
/**
* 유저의 이름을 가져옵니다.
* @return userName 유저의 이름
*/
public String getUserName() {
return userName;
}
/**
* 유저의 레벨을 가져옵니다.
* @return userLevel 유저의 레벨
*/
public int getUserLevel() {
return userLevel;
}
/**
* 유저의 이름을 수정합니다.
* @param userName 입력된 유저의 이름
*/
public void setUserName(String userName) {
this.userName = userName;
}
/**
* 유저의 아이디를 수정합니다.
* @param userId 입력된 유저의 아이디
*/
public void setUserId(String userId) {
this.userId = userId;
}
/**
* 유저의 패스워드를 수정합니다.
* @param userPw 입력된 유저의 패스워드
*/
public void setUserPw(String userPw) {
this.userPw = userPw;
}
/**
* 유저의 레벨을 수정합니다.
*/
public void setUserLevel() {
this.userLevel++;
}
/**
* 유저의 레벨을 초기화합니다.
*/
public void setUserLevel(int fail) {
this.userLevel = 0;
}
}
- 유저의 정보를 담당하는 클래스이다.
- 유저의 아이디, 패스워드, 닉네임, 레벨을 관리
- public static String userName : 유저의 닉네임은 랭킹 출력 시 필요하기때문에 public과 static을 사용하였다.
- int userLevel = 0; : 게임을 진행하지 않았을 초반 레벨은 0으로 초기화해주었다.
<GameSystem>
- 게임의 "문제 제작"과 "서버 랭킹", "연산"과 관련된 역할을 하는 클래스
1) randNumber() : 게임의 문제 출제를 담당하는 메소드
public String randNumber(){
int x = (int)(Math.random()*10)+1;
int y = (int)(Math.random()*10)+1;
int op = (int)(Math.random()*4)+1;
// 큰 값을 앞으로 배치하는 시스템
if(x < y){
int temp = x;
x = y;
y = temp;
}
setX(x);
setY(y);
setOp(op);
opSelector(op);
screenExam = (x + " " + getOpString() + " " + y + " = ");
return screenExam;
}
- if(x > y) : 게임의 가독성과 난이도를 위해 큰 값을 앞으로 작은 값을 뒤로 배치하였다.
- setX()... : Setter메소드를 통해 랜덤하게 뽑힌 값들을 업데이트 해준다.
- screenExam : 화면에 현재 어떤 숫자가 뽑혔고, 어떤 연산을 할 것인지의 "퀴즈"를 String타입으로 전달한다.
- 이처럼 main메소드에 있는 게임화면에 랜덤하게 뽑힌 퀴즈를 screenExam변수를 통해 출력하게 된다.
2) opSelector() : 랜덤하게 뽑힌 연산자를 화면에 출력하기 위한 문자열로 바꾸는 작업과 실제 연산을 수행
public void opSelector(int op){
switch(op){
case 1:
opString = "+";
setOpString(opString);
result = x + y;
break;
case 2:
opString = "-";
setOpString(opString);
result = x - y;
break;
case 3:
opString = "*";
setOpString(opString);
result = x * y;
break;
case 4:
opString = "/";
setOpString(opString);
result = x / y;
break;
}
}
<GameManager 클래스>
- 게임의 실행과 각 객체의 호출, 사용자에게 보여지는 UI를 담당
public static void main(String[] args) throws Exception{
user = new GameUser();
br = new BufferedReader(new InputStreamReader(System.in));
game = new GameSystem();
// 1) 초기 페이지 출력
pageRegister();
// 2) 입력에 따른 회원가입 페이지 출력
int commandRun = Integer.parseInt(br.readLine());
while(true){
if(commandRun == 1) {
pageRegister(commandRun);
break;
}
else if(commandRun == 2){
System.out.println("게임을 종료합니다.");
System.exit(0);
}
else{
System.out.println("올바르지 않은 선택입니다. 다시 선택해주세요.");
}
}
// 3) 처음 게임 화면은 "게임을 계속하겠냐"는 문구가 나오지 않도록한다.
initGame();
}
- pageRegister() : 초기 페이지를 출력하는 메소드
- commandRun : 초기 페이지의 출력 후에 게임접속 및 게임종료를 사용자에게 입력받음
1 : 회원가입 페이지로 이동 >> pageRegister(commandRun)
2 : System.exit(0) >> 게임을 그대로 종료
그외 : else로 다시 선택할 수 있도록 while문의 처음으로 돌아간다. - initGame() : 처음의 게임 화면을 출력한다.
- pageRegister() : 회원가입을 진행하는 메소드
outter: while(true){
System.out.print("아이디를 입력해주세요 : ");
user.setUserId(br.readLine());
System.out.print("비밀번호를 입력해주세요 : ");
user.setUserPw(br.readLine());
System.out.print("닉네임 입력해주세요 : ");
user.setUserName(br.readLine());
System.out.println("\n이대로 가입하시겠습니까?");
System.out.println("1:가입 | 2:다시 작성");
int commandRegister = Integer.parseInt(br.readLine());
switch(commandRegister){
case 1:
System.out.println("가입이 완료되었습니다.\n");
break outter;
case 2:
user.setUserId(null);
user.setUserPw(null);
user.setUserName(null);
break;
}
}
- 아이디, 비밀번호, 닉네임을 입력받아 Setter메소드에 전달하여 필드를 업데이트한다.
- [다시 작성] : 기능을 구현하기 위해 while(true)반복문 안에 사용한다.
입력했던 아이디, 비밀번호, 닉네임을 null로 초기화하고 while 반복문 처음으로 돌아간다. - 가입 : 가입이 되면 switch문과 label을 이용하여 빠져나간다. (한번에 빠져나가기 위함)
- initGame() : 첫 게임 시작화면을 담당
// 초기화면 : "게임 반복 문구" 미출력
private static void initGame() throws Exception{
// 3) 문제 출제
screenMain();
// 4) 정답 입력받기
System.out.println("문제의 정답은?");
int userResult = Integer.parseInt(br.readLine());
// 5) 정답 체크
isCorrect(userResult);
processGame();
}
- screenMain() : 이 메소드에서 GameSystem클래스에서 만들었던 "퀴즈"를 화면에 출력한다.
- isCorrect(userResult) : 유저에게 입력받은 답과 GameSystem클래스에서 만든 결과값이 일치하는지 확인하는 메소드
- processGame() : 정답을 체크 완료했으면, 게임을 반복할 것인지의 출력이 포함된 ProcessGame() 메소드를 호출
- ProcessGame() : 첫 게임 시작 이후의 모든 화면을 담당
// 게임진행화면 : "게임 반복 문구" 출력
private static void processGame() throws Exception{
while(true){
System.out.println("게임을 계속하시겠습니까?");
System.out.println("1:예 | 2:아니오");
int repeatCommand = Integer.parseInt(br.readLine());
// 2가 입력되면 게임 종료
if(repeatCommand == 2) {
int finalLevel = user.getUserLevel();
System.out.println("최종 레벨 : " + finalLevel);
GameSystem.setServerRanking(finalLevel); // static한 메소드이므로 직접 접근
System.out.println("로비로 이동합니다.\n");
// 메인페이지 이동
pageMain();
}
// 3) 문제 출제
screenMain();
// 4) 정답 입력받기
System.out.print("문제의 정답은? ");
int userResult = Integer.parseInt(br.readLine());
// 5) 정답 체크
isCorrect(userResult);
}
}
- repeatCommand : 게임의 반복을 묻는 명령을 사용자에게 입력받는다.
- 게임종료로직 : 기존에는 게임종료로직으로 System.exit(0)만을 입력하여 프로그램을 종료시켰다면,
이번에는 "메인 로비"를 만들어 그곳으로 이동하게 하고 싶었다.
user.getUserLevel() : 이동할때는 최종 레벨을 저장하고, 사용자에게 보여준다. 또한 랭킹에 적용될 수 있도록
전달인자(아규먼트)로 보내준다. - pageMain() : 메인 로비로 이동한다.
- 그 외 메소드 반복
- isCorrect() : 정답을 판정하는 메소드
static void isCorrect(int result) throws Exception {
int answer = game.getResult();
if(answer == result){
System.out.print("정답입니다. [레벨업!]---");
user.setUserLevel();
System.out.println("[" + user.getUserName() + "]의 현재 레벨 : " + user.getUserLevel() + "\n");
}
else{
System.out.println("틀렸습니다. 레벨이 초기화됩니다.\n");
user.setUserLevel(0);
processGame();
}
}
- game.getResult() : GameSystem클래스에서 opSelector메소드를 통해 만들었던 문제의 결과값을 가져온다.
- 정답 : 정답 판정하면, 레벨업을하여 GameUser클래스의 user필드를 업데이트하고,
닉네임과 레벨 정보를 가져와 출력한다. - 오답 : 오답 판정하면, 레벨을 초기화 시키고, 게임 진행부분으로 되돌아간다.
<게임 실행>
1) [회원가입 성공 ➡️ 문제 풀이 성공 ➡️ 내 레벨 확인 ➡️ 서버 랭킹 확인 ➡️ 게임 종료] 순으로 실행해보았다.
모든 게임을 성공적 끝낼 수 있었다.
2) [회원가입 성공 ➡️ 문제 풀이 성공 ➡️ 문제 풀이 실패 ➡️ 게임 종료] 순으로 실행해보았다.
중간에 게임에 실패하게되면, 레벨이 초기화되는 것을 확인할 수 있다.
3) [회원가입 다시작성 ➡️ 게임 종료] 순으로 실행해보았다.
이후 게임 종료 부분은 위의 실행결과와 동일하다.
4) [메인페이지에서 바로 게임 종료] 를 실행해보았다.
🚀 트러블슈팅
장황하게 코드를 늘어썼지만 각 화면출력(screenMain), 회원가입(pageRegister), 문제정답여부(isCorrect) ... 등의
메소드를 다른 클래스로 분리하여 그 클래스를 호출하는 방식으로 클래스의 분리를 하였다면 어떨까 싶었다.
main메소드에서 담당하는 일이 많다보니, 다른 클래스에 비해 main메소드가 포함된 클래스는 코드 줄이 많이 넘어가고 있는 것이 보였다. 🥲
To-do | Complete |
- 회원가입 시스템 구현 | O |
- 로그인 시스템 구현 | X |
- 메인화면으로 들어가기 위해서는 로그인 필요 | X |
- 메인화면에서 게임진행 후 사용자 입력에 따른 레벨 상승 | O |
- 사용자 입력은 문제를 맞추는 경우 레벨이 오르는 방식으로 진행 |
O |
- 유저의 기록에 따른 랭킹 시스템 구현 | O |
모든 기능을 구현하지 못했지만 (ex.로그인...), 그냥 생각나는대로 써내려간 기능들을 하나하나씩 구현하다보니
간단한 기능임에도 객체들끼리 많이 연관되는 것을 확인할 수 있었다.
레벨에 맞게 난이도를 상승하는 스테이지 추가와 최종 결과 페이지 등도 구현하고 싶었지만 아이디어에 그쳐버렸다.🤣
가장 힘들었던 기능은 "랭킹 시스템" 구현이었는데, 처음에는 유저의 이름을 private 하게 지정해놨다보니,
이처럼 랭킹을 출력할때마다 {[null]의 레벨 : 레벨값} 으로 출력이 되었다.
또 랭킹을 어떻게 뿌려줄까에 대한 고민도 했었다.
main메소드에서 [ ]의 레벨 : 정도까지만 출력을 하고,
이름과 레벨값을 GameSystem에 String으로 받아와 출력만 해줘야하나 생각도 들었다.
그래도 랭킹만큼은 다른 유저들의 랭킹도 점점 쌓여가는 방식으로 구현하고 싶었다.
그렇게 생각난 것이 StringBuilder의 사용이었다.
물론 로그인도 구현되지 않고, 회원가입을 해도 데이터베이스를 구축하지 않았기에 프로그램이 종료되면 모든 유저의 정보가 삭제되고 다음 실행때도 이미 초기화 된 상태이다.
따라서 StringBuilder를 사용해도 결국 "한 명의 유저 랭킹점수"밖에 출력할 수 밖에 없다는걸 느꼈다.
다음에는 각 메소드를 클래스로 분리하여 만들어봐야겠다고 생각이 들었다!
💡 5일차에는 필드와 메소드에 대해 더 자세히 알아보고,
중요한 static키워드와 캡슐화,추상화,정보은닉 등의 개념을 익혔다.📖
로또 번호 프로그램 실습말고도 [레벨업 게임]을 기획해보고 구현해보며 메소드 사용에 익숙해지고자 노력할 수 있었다!🚀
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_7일차_'String클래스와 추상클래스' (0) | 2024.12.10 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_6일차_'상속과 메소드 오버라이딩' (1) | 2024.12.09 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_4일차_'객체지향프로그래밍' (1) | 2024.12.05 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_3일차_'배열과 객체' (1) | 2024.12.04 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_2일차_'자바 기초와 문법' (0) | 2024.12.03 |