🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [8]일차
🚀 8일차에서는 instanceof에 대해서 더 자세히 복습하고, 인터페이스의 개념 및 사용법을 공부할 수 있었다.
예외처리에서는 단순히 에러가 발생했을때 고치는 것이 아닌 프로그램의 지속성을 유지시키기 위해 예외를 처리하는 방식을 배울 수 있었다.
final 키워드에 대해서도 다시 복습해보며 7일차와 더불어서 개념을 다질 수 있는 시간이었다!
<instanceof>
❓instanceof
- 객체 타입을 확인하는 연산자
- 형변환 가능 여부를 확인하며, true/false로 반환
- 주로 상속관계에서 부모객체인지 자식객체인지 확인하는데 사용
- 사용방법 : [ 객체 instaceof 클래스 ] 를 선언함으로써 사용
public static void main(String[] args){
Parent parent = new Parent();
Child child = new Child();
System.out.println( parent instanceof Parent ); // true
System.out.println( child instanceof Parent ); // true
System.out.println( parent instanceof Child ); // false
System.out.println( child instanceof Child ); // true
}
instanceof는 해당 클래스가 객체에 해당하는지 (그릇이 맞는지) 확인해주는 것
- parent instanceof Parent : Parent클래스의 parent이므로 true
- child instanceof Parent : Parent클래스를 상속받는 Child클래스의 child이므로 true
- parent instanceof Child : Child클래스의 parent는 구문상 맞지 않다(부모의 참조변수)
- child instanceof Child : Child클래스의 child이므로 true
<instanceof 예제>
main함수에 이렇게 선언이 되어있을때,
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.i = 10;
MyClass myClass2 = new MyClass();
myClass2.i = 20;
// i의 값을 업데이트하고나서
myClass.i = 20;
System.out.println("myClass와 myClass2의 equals() 비교결과 : " + myClass.equals(myClass2));
Person kim = new Person("김자바", 20, "강남구 역삼동");
Person kim2 = new Person("김자바", 20, "강서구 염창동");
}
class Person{
//...
// 이름과 나이가 같으면 true를 리턴하도록 함
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person person)) return false;
return age == person.age && Objects.equals(name, person.name);
}
//...
}
class MyClass{
// ...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyClass myClass = (MyClass) o;
return i == myClass.i;
}
// ...
}
- Person에서는 instanceof 가 쓰인 equals()메소드를 오버라이딩하였고,
- MyClass에는 getClass를 통해서 equals()메소드를 오버라이딩하였다.
1. MyClass클래스에는 필드 int i 가 선언되어있다.
2. 처음에는 equals()메소드가 오버라이딩되지 않았기때문에 myClass.equals(myClass2)
3. 처럼 false가 발생하고, myClass와 myClass2를 단순 출력해보면 이처럼 주소값이 나온다.
= 주소값이 서로 다르기때문에 다르다고 판단하여 false를 리턴
4. 이제 myClass와 myClass2를 toString()메소드를 오버라이딩하여 문자열로 표현
5. ⭐[1) getClass 사용] equals()의 기준을 세우기 위해 메소드를 오버라이딩 (MyClass클래스)
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyClass myClass = (MyClass) o;
return i == myClass.i;
}
- 여기서 o == null 이면 애초에 Object타입으로 받아들인 o가 빈 값으로 바로 false를 리턴하거나 (OR연산)
getClass ← 는 현재의 클래스의 클래스타입을 가져온다.
여기서는 MyClass에 작성되었기때문에 MyClass의 타입을 가져오고
(MyClass) o.getClass()를 통해 현재 Object타입으로 받아들인 o의 클래스 타입을 가져온다.
myClass2(MyClass 인스턴스)를 보냈기때문에 myClass와 myClass2의 클래스타입은 일치한다고 볼 수 있다. - 따라서 myClass2가 Object타입인 o로 매개변수가 들어왔기때문에
(MyClass) o : 형변환 해준다. - 이 메소드 안에서 MyClass타입의 myClass 참조변수로 새롭게 지정을 해준 후
(이 메소드가 종료되면 이 새롭게 지정된 참조변수도 사라진다)
💡지역변수 이기때문이다 ↔ 클래스변수나 인스턴스변수와는 다른 범위를 가진다) - 그렇게 새롭게 지정한 myClass의 i를 하게 되면 myClass2의 i를 실질적으로 가리키게되며,
i는 기존의 MyClass의 i이므로 myClass의 i를 가리키게된다. - 따라서 return하는 것은 (myClass.i == myClass2.i) 처럼 i값을 비교하여 리턴하게되는 기준을 세우는 것이다.
6. ⭐[2) instanceof 사용] equals()의 기준을 세우기 위해 메소드를 오버라이딩 (Person클래스)
// 이름과 나이가 같으면 true를 리턴하도록 함
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person person)) return false;
return age == person.age && Objects.equals(name, person.name);
}
- 여기서는 getClass대신 instanceof가 쓰였는데 두 방법 모두 사용이 가능하다.
- 받아들인 Object타입의 o 매개변수가 o가 Person 타입이라면,
그 Person를 person 참조 변수로 사용하겠다는 의미이다.
현재 들어온 o는 kim2로 Person클래스가 맞으므로, 이 구문에는 해당하지 않는다 - return문에서는 age가 현재 Person클래스의 kim의 age를 나타내고
person.age는 새롭게 형변환한 Person클래스의 kim2의 age를 나타낸다. - && AND연산을 통해 Objects들의 kim의 이름과 형변환한 kim2의 이름을 equals()로 비교한다.
- age와 name둘다 true이면 AND연산을 통해 true를 반환하여 equals()기준을 세우게 된다.
<인터페이스>
- 클래스가 구현해야할 메소드의 형식(시그니처)를 정의
- 실제 구현체는 없이 (메소드의 이름, 매개변수, 반환타입만) 정의
- implements 이라는 키워드로 사용 ( public interface MyInterface )
- 인터페이스를 구현하는 클래스는 인터페이스 메소드를 “반드시 구현”해야함 (강제성)
- ⭐인터페이스의 메소드에는 abstract같은 키워드를 붙여주지 않아도된다.
인터페이스의 메소드는 없는 것이 기본이기 때문이다.
이처럼 인터페이스는 추상메소드만을 가진다. (default, static도 자바 8이후로는 허용) - public static final 과 같은 상수도 사용이 가능 (인터페이스명.메소드() 처럼 직접접근이 가능하다)
- 다중 구현이 가능 : 하나의 클래스가 여러 인터페이스를 구현할 수 있다.
- 상속보다 “느슨한 결합”을 제공, 표준화된 사용방식 제공
▶️날 수 있는 물체와 인터페이스
[나비, 새, 비행기, 헬기]는 모두 “날다”라는 행위가 있어야한다. 하지만 그 방식이 표준화되어서 제공되고 있진 않기때문에
나는 방식이 각자 다를 것이다. 인터페이스는 껍데기(날다())를 제공하게 된다.
상속으로는 되지 않는 이유가 이 [나비, 새, 비행기, 헬기] 그룹이 공통적으로 묶일(=일반화할만한)것이 없다.
즉 인터페이스는 기능만 정의해서 묶어놓은 것
인터페이스는 이처럼 “기능의 통일화”, “사용자의 편의성”을 제공한다.
public interface Flyable {
void fly();
}
public class Airplane implements Flyable{
@Override
public void fly() {
System.out.println("비행기가 납니다.");
}
}
이처럼 implements를 사용하여 인터페이스를 구현할 수 있다.
- 오버로딩 불가능 :
> 인터페이스를 구현하는 클래스에서는 인터페이스의 메소드를 구현할때
> 오버로딩처럼 그 메소드의 다른 매개변수를 받을 수 없다.
> 인터페이스에서 선언한 매개변수 개수와 이름 그대로 사용해야한다. - 인터페이스 객체 생성 불가능 :
> (Flyable fly = new Flyable() ← (X)) - 인터페이스 타입으로 사용 가능 :
> (Flyable fly = new Airplane() ← (O))
이 경우 만약 Airplane클래스에서 따로 “상륙하다()”라는 기능을 구현했으면
Flyable에는 없는 메소드이므로 그 메소드는 사용이 불가하다.
> (fly.상륙하다() ← (X)) - 장점 :
> 기능의 통일화로 나비, 드론, 비행기가 각각 어떻게 동작하는지 “사용자”는 알 필요가 없다.
> 사용자는 단순히 인터페이스를 통해서 “날다()”라는 기능을 사용
▶️회원가입 시스템
회원가입 시 클라이언트와 서버가 정보를 주고받을때
서버의 비즈니스 레이어에서는 데이터를 저장하는 담당하는 데이터 레이어와 연결되어있는데
비즈니스 레이어에서 ex. save() 처럼 저장하는 로직을 담당하는 메소드를 호출할 것이고,
데이터 레이어에서는 이 save()메소드를 전달하며 연결되어있을 것이다.
다만 데이터 레이어에서 객체를 바꾸게되면 비즈니스 레이어도 객체를 바꾸어야되는 경우가 있는데
이런 경우를 결합도가 높다고 표현한다.
이 비즈니스 레이어와 데이터 레이어 사이에 "인터페이스"를 두어
비즈니스 레이어에서는 이 인터페이스를 구현하여 save()로직을 사용하게 된다.
이렇게 되면 직접 이용하는 것이 아닌 "결합도가 낮아진 상태"인 것이다.
좋은 소프트웨어의 기준은 [응집도는 높아야하고, 유연성도 높아야하지만, 결합도는 낮아야한다]
인터페이스를 이용하면 소프트웨어의 결합도를 낮출 수 있다.
▶️TV 리모컨
다른 예시로 TV 리모컨이 있는데
이 리모컨들이 각자 다른 방식으로 Power버튼이 구현이 되어있으면 사용자가 사용하기 힘들것이다.
TV 리모컨을 교체하게되면 객체가 교체가 되어서 사용자는 인스턴스부터 메소드까지 대거 교체를 해야할 수도 있다.
TV 리모컨 인터페이스로 [TV가 가져야하는 기본 기능들]을 정의해놓는다.
public interface TVInterface {
public void togglePower();
public void channelUp();
public void channelDown();
public void volumnUp();
public void volumnDown();
public void setChannel(int channel);
}
다른 TV를 구매했을때 이 TVInterface를 구현하기만 한다면 그대로 [TV가 가져야하는 기본 기능들]을 사용할 수 있다.
public class MyNewTV implements TVInterface{
private int channel;
private int volumn;
boolean power;
@Override
public void togglePower() {
power = !power;
if(power) {
System.out.println("환영합니다! ^ㅁ^ [TV] 전원 ON");
}
else{
System.out.println("---TV를 종료합니다---");
}
}
@Override
public void channelUp() {
this.channel++;
System.out.println("채널을 올립니다. " + channel + "번");
}
@Override
public void channelDown() {
this.channel--;
System.out.println("채널을 내립니다. " + channel + "번");
}
@Override
public void volumnUp() {
this.volumn++;
System.out.println("볼륨을 올립니다. " + volumn + "번");
}
@Override
public void volumnDown() {
this.volumn--;
System.out.println("볼륨을 올립니다. " + volumn + "번");
}
@Override
public void setChannel(int channel) {
this.channel = channel;
System.out.println("채널을 옮깁니다. " + channel + "번");
}
}
@Override로 인터페이스의 메소드를 구현한 것을 확인할 수 있다.
기존 interface는 구현체 { } 가 없으면 에러가 발생했지만,
Java 8이후부터는 default, static으로는 메소드의 구현체를 작성할 수 있다.
<클래스와 인터페이스 비교>
구분 | 클래스 | 인터페이스 |
객체생성 | new키워드로 인스턴스 생성 | 직접적인 객체 생성 불가능 |
멤버 | 필드, 메서드, 생성자 등 | 상수, 추상 메소드 |
다중 상속 | 불가능 | 다중 구현이 가능 |
목적 | 구현 (기능, 로직) 제공 | 메소드 기준(규약) 제공 |
<인터페이스의 다중상속>
메소드는 오버라이딩되면 무조건 자식의 메소드를 쓴다는 특징때문에
InterA에 methodA(), methodB()가 있고,
public interface InterA {
int I = 10; // 사실 static final한 I이다.
public void methodA();
public void methodB();
}
InterB에는 methodB()가 있을때,
public interface InterB {
int I = 20; // 사실 static final한 I이다.
public void methodB();
}
ImplementABC 클래스에서 implements로 InterA, InterB를 구현하고있고,
// 인터페이스의 다중상속을 보여주기 위한 예제
public class ImplementABC implements InterA, InterB, InterC{
@Override
public void methodA() {
}
@Override
public void methodB() {
}
@Override
public void methodC() {
}
}
methodA(), methodB()를 작성하는데,
methodB()가 InterA의 것인지 InterB의 것인지는 중요치 않다는 것이다.
어차피 메소드가 오버라이딩 된 자식클래스 ImplementABC의 methodB()가 실행되기 때문이다.
<인터페이스의 상수>
// 인터페이스 다중상속 실행 예제
public class InterExam {
public static void main(String[] args) {
ImplementABC abc = new ImplementABC();
abc.methodB();
InterA interA = abc;
interA.methodB();
InterB interB = abc;
interB.methodB();
InterC interC = abc;
interC.methodC();
// 각 인터페이스는 각자의 공간에서 I를 가지게된다.
// 참조변수로 접근한 것이 아닌 "인터페이스"로 직접 접근해서 사용한 예제
System.out.println(InterA.I); // 10 출력
System.out.println(InterB.I); // 20 출력
System.out.println(InterC.I); // 30 출력
}
}
<인터페이스의 상수에 대하여>
- 상수는 대문자로 쓰는 것이 관례
- 대문자로 써주기때문에 대소문자로 구분하기 어렵기떄문에 언더바를 사용하여 구분을 한다.
> OIL_PRICE, BOOK_PRICE
>실제 기름값이 계속 바뀌므로 그러한 기능들을 상수로 지정해놓고 프로그램이 실행될때 그러한 값을 상수로 전달 - 자바가 정의한 상수
> Math.PI; MAX_VALUE 등 - 인터페이스에는 일반 변수가 들어가지 못한다.
> int I = 10; 으로 표시되어있어도 [static final] 이 포함되어있는 것 - 메소드에서 abstract가 생략된다.
[public void methodB()] > [ [public [abstract] void methodB()]
<인터페이스 간의 상속>
// 인터페이스끼리의 상속을 보여주기 위한 예제
public interface ExtendsABC extends InterA, InterB, InterC {
// 이 클래스만의 메소드
public void methodABC();
}
public class ImplementExtends implements ExtendsABC {
@Override
public void methodA() { }
@Override
public void methodB() { }
@Override
public void methodC() { }
@Override
public void methodABC() { }
}
이처럼 ExtendsABC 인터페이스는 InterA, InterB, InterC의 인터페이스를 상속받았고,
자신만의 메소드인 methodABC()를 가진다. (껍데기만)
그렇다면 이 ExtendsABC 인터페이스를 구현해야하는 ImplementExtends 클래스에서는
InterA, InterB, InterC 의 메소드를 오버라이딩해야할 뿐만아니라
ExtendsABC 인터페이스의 methodABC() 또한 구현해야해서
총 4개의 메소드를 구현해야하는 강제성을 지닌다.
이처럼 상속이 가능한 이유는
- 인터페이스는 구현체가 없기때문에 가능
- 중복이 되지 않기때문이다.
<인터페이스와 instanceof>
인터페이스도 타입의 역할을 할 수 있어서 instanceof 에서도 true / false반환값을 가질 수 있다.
// 인터페이스의 instanceof 예제
// class ImplementExtends implements ExtendsABC
ImplementExtends aaa = new ImplementExtends();
System.out.println(aaa instanceof ImplementExtends); // true
System.out.println(aaa instanceof InterA); // true -> InterA를 상속받는 ExtendsABC를 구현하고 있는 aaa이므로 타입 일치
System.out.println(aaa instanceof InterB); // true -> InterB를 상속받는 ExtendsABC를 구현하고 있는 aaa이므로 타입 일치
System.out.println(aaa instanceof ExtendsABC); // true -> ExtendsABC 인터페이스를 구현하고 있는 aaa이므로 타입 일치
// class ImplementABC implements InterA, InterB, InterC
// 인터페이스를 다중상속받는 ImplementABC의 참조변수 abc는
// InterA, InterB, InterC를 상속받는 ExtendsABC 인터페이스의 클래스가 아니다.
ImplementABC abc = new ImplementABC();
System.out.println(abc instanceof ExtendsABC); // false
<인터페이스의 목적 / 장점>
- 기능의 통일화
- 객체와 객체 사이 인터페이스를 제공함으로써 사용자가 객체에 직접 접근하지않고 인터페이스를 통해 접근가능
> “결합도를 낮출 수 있다” - 실제 인스턴스가 바뀌어도 사용방법 크게 달라지지 않음
- 표준화 : 특정 기능을 제공해야하는 클래스들이 동일한 메소드 형태를 갖출 수 있게 표준화된 규격을 제공함
- 다형성 지원 : 인터페이스 타입으로 다양한 구현체 처리 가능
- 느슨한 결합 : 의존성을 인터페이스로 추상화하여 유지보수성과 확장성을 향상
❓인터페이스의 default / static 메소드
// 상수는 대문자로 쓰는 것이 관례
public static final int CONSTANT = 10;
public int CONSTANT = 10; // 이렇게 써줄 수 있다.
- 명시적으로 입력하지 않아도 인터페이스에서는 static final이 포함
- 변수는 인스턴스가 되어서야 값이 만들어질 수 있기때문에
인스턴스가 만들어지지 않는 인터페이스는 static으로 선언하여
프로그램 실행 시 부터 값을 가질 수 있게함
메소드의 경우에는
void doSomethind(); // 인터페이스의 일반 메소드
public default void doDefault(){
System.out.println("인터페이스의 default()메소드");
}
public static void staticMethod(){
System.out.println("인터페이스의 static 메소드");
}
❓default()
- 가지고는 있지만 강제하진 않음. ex. Object의 equals()와 toString()메소드와 같은 개념
- 오버라이딩해서 사용하게할 목적으로 정의한 메소드
- 💡인터페이스 내 default 메소드는 “선택적으로” 재정의 가능 (오버라이딩 가능)
- 💡기존 코드를 깨뜨리지 않고, 인터페이스에 새 메소드를 추가할 수 있게함
- 간단히 표현하면 “필요하면 쓰고, 아님 쓰지않으셔도 됩니다.”
❓static()
- 클래스와 상관없이 이 인터페이스가 가지고 있는 경우를 말한다.
- 그 객체가 가져야하는 기능과는 별개로 추가로 이 기능을 가지게 하는 것
- 즉 static한 것 = 별개로 사용이 될 수 있음 = 따로 쓰일 수 있음
- 인스턴스로 만들지 않아도 인터페이스명으로 접근해서 사용
(인터페이스명.메소드명()) - 오버라이딩 불가능 : 구현 클래스에서 재정의 불가능
- 💡공통적으로 사용되는 “유틸리티 메소드”를 인터페이스에 “제공할때 유용”
<인터페이스의 default - TV기능 추가>
만약 세계의 각 TV에서 쓰이는 TV인터페이스의 메소드들을 다 구현했는데,
새롭게 추가한 기능이 있을때 그 메소드들을 강제하면
모든 TV사용자가 메소드들을 바꿔야하므로
새롭게 추가한 기능을 default 메소드로 선언하여서
그 기능이 필요한 사람들이 직접 오버라이딩해서 쓸 수 있도록 하면
에러가 나지않고, 신기능을 사용할 사람은 알아서 사용하게끔 만들 수 있다는 것이다.
🚀실습 - 컴퓨터 실행 프로그램
<인터페이스 정의>
public interface ComputerInterface {
public void powerOn();
public void runMainboard();
public void runGraphicCard();
public void showMonitor();
public default void connectKeyboard() {
System.out.print("[default] 연결 : 키보드를 연결중... === ");
}
public static void checkSound(){
System.out.println("\n\n[static] 스피커 : 소리가 정상적으로 출력됩니다.");
}
}
- powerOn(), runMainboard(), runGraphicCard(), showMonitor()와 같은 메소드들은 인터페이스를 구현하는 객체에서
구현하도록 강제한다. - connectKeyboard() : default메소드로써 이 메소드를 사용할 객체에서만 오버라이딩해서 사용할 수 있게한다.
- checkSound() : static메소드로써 이 메소드를 구현하지 않아도 사용자가 직접 접근해서 메소드를 사용할 수 있다.
<Desktop> - 인터페이스를 구현할 객체
// @@**@@
public class Desktop implements ComputerInterface{
@Override
public void powerOn() {
System.out.println("[데스크탑]의 전원을 켭니다.");
}
@Override
public void runMainboard() {
System.out.println("[데스크탑]의 메인보드가 동작합니다.");
}
@Override
public void runGraphicCard() {
System.out.println("[데스크탑]의 그래픽카드가 실행됩니다.");
}
@Override
public void showMonitor() {
System.out.println("[데스크탑]탑의 모니터가 켜집니다.");
}
@Override
public void connectKeyboard() {
ComputerInterface.super.connectKeyboard();
System.out.println("USB : [데스크탑]에 키보드를 연결합니다.");
}
}
<Laptop> - 인터페이스를 구현할 객체
// @@**@@
public class Laptop implements ComputerInterface{
@Override
public void powerOn() {
System.out.println("[노트북]의 전원을 켭니다.");
}
@Override
public void runMainboard() {
System.out.println("[노트북]의 메인보드가 동작합니다.");
}
@Override
public void runGraphicCard() {
System.out.println("[노트북]의 그래픽카드가 실행됩니다.");
}
@Override
public void showMonitor() {
System.out.println("[노트북]의 모니터가 켜집니다.");
}
}
<ComputerUser> - 인터페이스의 동작을 확인할 main메소드
public static void main(String[] args) {
ComputerInterface computer = new Desktop();
ComputerInterface laptop = new Laptop();
computer.powerOn();
computer.runMainboard();
computer.runGraphicCard();
computer.showMonitor();
computer.connectKeyboard(); // 인터페이스의 default 메소드를 재정의
ComputerInterface.checkSound(); // 인터페이스의 static 메소드
System.out.println("\n--------------------------");
laptop.powerOn();
laptop.runMainboard();
laptop.runGraphicCard();
laptop.showMonitor();
laptop.connectKeyboard(); // 인터페이스의 default 메소드를 재정의
ComputerInterface.checkSound(); // 인터페이스의 static 메소드
}
- 데스크탑에서는 인터페이스의 default메소드인 connectKeyboard()를 오버라이딩하여 호출이 가능
- 또한 checkSound()는 static이기때문에 인터페이스명으로 직접 접근해서 사용하고 있는 것을 확인할 수 있다.
- 노트북에서는 default메소드를 구현하지 않았고, static메소드를 직접 접근해서 사용한 것을 확인할 수 있다.
🚀실습 - 패스트푸드 키오스크
<인터페이스 정의>
public interface Orderable {
// 음식 주문 메소드
void order();
// 음식 결제 방법 메소드
void selectPayment();
// 음식 준비 메소드
void prepare();
// 음식 서빙 메소드
default void deliver(){
System.out.println("홀 서빙 : [일반 직원] 배정, 곧 서빙을 시작합니다.");
}
// 영수증 출력
static void printReceipt(){
System.out.println("===== [영수증] =====");
System.out.println("- 음식을 주문완료");
System.out.println("- 음식을 준비완료");
System.out.println("- 준비된 음식서빙");
System.out.print("- 결제방법 : ");
}
}
- 음식주문, 음식결제방법, 음식준비는 추상메소드로 정의만 한다.
- 음식서빙메소드는 default로 인터페이스를 구현할 객체에서 선택적으로 오버라이딩하도록 한다.
- 영수증출력메소드는 static으로 인터페이스명으로 접근가능하도록 한다.
<Chicken> - 인터페이스를 구현한 객체
public class Chicken implements Orderable{
private String chickenPayment;
private String type;
public Chicken(String type) {
this.type = type;
}
public String getUserPayment() {
return chickenPayment;
}
public void setUserPayment(String userPayment) {
this.chickenPayment = userPayment;
}
@Override
public void order() {
System.out.println("[" + type + " 치킨]이 주문되었습니다.");
}
@Override
public void selectPayment() {
Scanner sc = new Scanner(System.in);
while(true){
System.out.println("[" + type + " 치킨]을 결제할 방식을 입력해주세요.");
System.out.println("[신용카드] [현금] [포인트]");
String tempPayment = sc.nextLine();
if(tempPayment.equals("신용카드")){
setUserPayment(tempPayment);
break;
} else if(tempPayment.equals("현금")){
setUserPayment(tempPayment);
break;
} else if(tempPayment.equals("포인트")){
setUserPayment(tempPayment);
break;
} else {
System.out.println("다시 입력해주세요.");
}
}
}
@Override
public void prepare() {
System.out.println("[" + type + " 치킨]을 조리중입니다.");
}
@Override
public void deliver() {
System.out.println("[" + type + " 치킨] 준비 완료 : [사장님] 배정, 곧 서빙을 시작합니다.");
}
}
- selectPayment() : 지불방법을 물어보는 while문을 작성
올바르지 않은 값이 입력되면 while문의 처음으로 돌아감
올바른 값이 입력되면 setUserPayment()라는 Setter메소드를 통해 chickenPayment를 업데이트함 - 나머지의 메소드는 인터페이스의 추상메소드와 default메소드를 오버라이딩함
<Pizza> - 인터페이스를 구현한 객체 하나를 더 만든다.
public class Pizza implements Orderable {
private String pizzaPayment;
private String type;
// ... 치킨과 동일
// ...
@Override
public void prepare() {
System.out.println("[" + type + " 피자]를 조리중입니다.");
}
@Override
public void deliver() {
System.out.println("[" + type + " 피자] 준비 완료 : [주방장] 배정, 곧 서빙을 시작합니다.");
}
}
<Customer>
static BufferedReader br;
public static void main(String[] args) throws IOException {
br = new BufferedReader(new InputStreamReader(System.in));
printMenu();
int menuSelect = Integer.parseInt(br.readLine());
if(menuSelect == 1){
printChickenMenu();
int userChicken = Integer.parseInt(br.readLine());
menuChicken(userChicken);
}
else if(menuSelect == 2){
printPizzanMenu();
int userPizza = Integer.parseInt(br.readLine());
menuPizza(userPizza);
}
}
- BufferedReader로 사용자에게 입력을 받음
- printMenu() : 현재 패스트푸드점의 메뉴를 보여줌
- printChickenMenu() : switch문으로 치킨을 선택할 경우 치킨 종류 메뉴판을 출력
- menuChicken() : 메소드 호출로 손님에게 입력받아, 해당 메뉴를 선택할 수 있게함
- printPizzaMenu() : 피자 종류 메뉴판을 출력
- menuPizza() : 메소드 호출로 손님에게 입력받아, 해당 메뉴를 선택할 수 있게함
private static void menuChicken(int menuSelect) {
switch(menuSelect){
case 1:
makeChicken("양념");
break;
case 2:
makeChicken("간장");
break;
case 3:
makeChicken("파닭");
break;
default:
System.out.println("올바르지 않은 메뉴입니다.");
System.out.println("키오스크를 종료합니다.");
System.exit(0);
}
}
private static void makeChicken(String type) {
Chicken chicken = new Chicken(type);
chicken.order();
chicken.selectPayment();
chicken.prepare();
chicken.deliver(); // default
Orderable.printReceipt();
System.out.println("[" + chicken.getUserPayment() + "]");// static
System.out.println("===================");
}
- menuChicken() 메소드에서 사용자가 메뉴를 선택하면 그 메뉴의 문자열값을 입력받아
makeChicken() 메소드의 매개변수로 전달한다. - 전달된 메뉴의 타입을 토대로 인스턴스를 생성하고, 인터페이스가 구현한 메소드에 맞게 출력한다.
🚀 먼저 음식 주문하기 라는 실습예제가 주어졌을때, 패스트푸드점을 한번 만들어봐야겠다고 생각이 들었다.
사용자에게 입력을 받는 것은 "키오스크"로 만드는 것이 적합하다고 느꼈고 사용자에게 선택을 받아 메뉴별로 출력할 수 있는 메소드를 만드는 것도 관건이었다.
처음에는 main메소드 내에 길게 써내려가다보니 코드가 장황하게 길어지는 것 같아
메뉴판을 출력할 메소드, 사용자가 메뉴를 골랐을때의 그 메뉴별 맛을 보여줄 메소드 등을 나누어 코드를 작성하였다.
각 기능을 담당하는 메소드 별로 알맞게 동작하는 것을 보고 메소드의 호출과 정의 방식을 더 잘 알아갈 수 있었다.
<인터페이스와 추상메소드의 차이점>
- 객체와 상관없이 공통된 기능을 쭉 묶어놓은 것들이 인터페이스
- 추상클래스는 클래스와 비슷한 느낌으로 추상메소드를 구현해놓은 것
<private 생성자의 인스턴스 생성 - getInstance() 활용>
public class Son {
private Son() { }
public static Son getInstance(){
return new Son();
}
}
public class SonTest {
public static void main(String[] args) {
// Son son = new Son(); // 인스턴스 생성 불가
// son참조변수에 이러한 방식으로 new키워드를 사용하지 않고
// 인스턴스를 넣어줄 수 있음
Son son = Son.getInstance();
}
}
- getInstance()메소드를 만들어서 인스턴스를 생성한다.
- 비슷한 사례로 Calendar 클래스
👀<Calendar>
자바에는 Calendar라는 날짜를 확인할 수 있는 캘린더 기능을 제공하는 클래스가 있다.
1. Constructor (생성자)를 보면 protected로 되어있음을 확인할 수 있다.
(= 패키지가 같거나 상속받는 자손클래스에서만 사용가능)
2. Calendar는 추상클래스이기에 new라는 키워드를 제공하지 않음
Calendar calendar = Calendar.getInstance();
3. Calendar는 날짜가 추상화된 클래스(=객체)
자식클래스로 GregorianCalendar (그레고리안 캘린더)의 인스턴스가 사용되고 있는데,
언제든지 교체될 수 있다. 하지만 교체된다하더라도 Calendar클래스는 영향이 없을 것이다.
<final> - Detail
❓final 클래스
- 자바에서 다른 클래스가 그것을 상속받을수 없게함
- 해당 클래스는 “최종적”이며 “변경할 수 없다”는 것을 의미
<final 클래스 필요성>
- 불변성 보장 : 클래스가 일단 생성되면 그 상태가 변경되지 않도록 함
- 상속 방지 : 특정 클래스의 설계와 구현이 그대로 유지되어야할때 사용
- 불변클래스 생성 : 불변 객체 생성 후 그 상태가 변경되지 않으므로 프로그램의 신뢰성 증가
- 성능 최적화 : final클래스 안에있는 모든 메소드가 상속되거나 오버라이딩 될 수 없어 성능이 증가
- 메소드 오버라이딩 방지 : final클래스 안의 모든 메소드는 “자동으로 final”
public final class SecurityConfig {
private static final String ENCRYPTION_KEY = "ComplexKey123!";
private SecurityConfig() {
// 생성자를 private으로 선언하여 외부에서 인스턴스화 방지
}
public static String getEncryptionKey() {
return ENCRYPTION_KEY;
}
}
// Main.java 파일
public class Main {
public static void main(String[] args) {
String encryptionKey = SecurityConfig.getEncryptionKey();
System.out.println("암호화 키: " + encryptionKey);
}
}
ENCRYPTION_KEY 이 키 값은 private로 선언하여 외부에서 접근할 수 없도록 함
private SecurityConfig() 처럼 생성자를 private로 선언하여 외부에서 인스턴스로 만들지 못함
final클래스 사용으로 보안 관련 중요한 설정이 “외부에서 변경되는 것을 방지하는 방법”을 보여준 예제이다.
<JDK에서의 final클래스>
- 변경되어서는 안되는 중요한 클래스들이 포함
java.lang.String
- 문자열 표현시 불변성 유지 = 한 번 생성된 String객체의 내용은 변경될 수 없음
- 이 클래스를 상속받아 변경하는 것이 불가능
java.lang.Math
- 수학적 연산과 함수를 제공하는 유틸리티 클래스
- 이 클래스의 메소드들이 정적 메소드로만 이루어져있음 (static) = 인스턴스 생성 및 상속이 될 수 없음
java.lang.System
- 시스템 관련 기능 제공 (시스템의 입력, 출력 및 오류 출력을 관리하는 정적 메소드들)
- 상속을 통한 변경이 금지
java.util.Collections의 내부 클래스들
- Collections 클래스의 내부에는 여러 final로 선언된 내부클래스들이 존재
- 이들은 불변 컬렉션을 생성하는데 사용 ex. Collections.UnmodifiableList
❓final필드
- 한번 초기화하면 그 값을 변경할 수 없는 필드
- 상수를 정의하거나 객체의 불변성을 보장하는데 사용
<final 필드의 필요성>
- 상수 정의 : 변경되지 않는 고정된 값을 가진 상수 정의 = 수학적 상수, 설정 값 등 프로그램에 걸쳐 일관된 값들
- 불변객체 생성 : 객체의 핵심 속성이 한 번 설정된 후엔 변경되지 않도록함 = 객체의 예측가능성과 신뢰성을 높임
- 스레드 안전성 : 멀티스레딩에서 스레드 간의 안전한 읽기 작업을 보장
- 메모리 가시성 보장 : 한번 쓰여지고 이후엔 변경되지 않아서 값이 모든 스레드에게 일관되게 보여짐
- 객체의 핵심 속성 보호 : 설정값, 중요한 데이터를 final필드로 선언하여 해당값의 “무결성 유지 가능
// final로 선언된 필드의 예제
public class Pen {
private String name;
private int price;
// 제조사를 변경할 수 없도록 함 (final 상수)
private final String company = "모나미";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public static void main(String[] args) {
Pen pen = new Pen();
// pen.company = "한국볼펜사"; // final로써 불가능, final이 빠지면 가능
pen.name = "제트제트";
pen.price = 1500;
System.out.println(pen.company + "의 볼펜 [" + pen.getName() + "]의 가격 = {" + pen.getPrice() + "}");
}
}
<JDK에서의 final 필드>
java.lang.Math 클래스의 상수
- Math.PI = 원주율 파이의 값
- Math.E = 자연로그의 밑 e의 값
- 이 값들은 변경될 수 없고 전역적으로 일관된 값을 제공
java.lang.Collections의 불변 컬렉션
- Collections.EMPTY_LIST
- Collections.EMPTY_MAP
- Collections.EMPTY_SET
- 각각 빈 리스트, 맵, 세트를 나타내는 불변의 컬렉션으로 수정할 수 없고, 불필요한 객체생성을 방지함
java.lang.System 클래스의 입출력 필드
- System.in
- System.out
- System.err
- 각각 표준 입력 스트림, 표준 출력 스트림, 표준 에러 출력 스트림을 나타냄
- 프로그램 실행 중에 이 필드들의 “참조가 변경되지 않음을 보장
❓final 메소드
- 한번 선언되면 하위클래스에서 오버라이딩 불가능 (재정의 불가능)
- 클래스의 핵심적인 기능을 안정적으로 유지하기 위해 사용
<final 메소드의 필요성>
- 메소드의 무결성 보장 : 특정 메소드의 기능이 그대로 유지 = “기본동작을 정의하는” 핵심 메소드들이 그대로 유지
- 오버라이딩 방지 : 부모클래스의 “중요한 메소드가 자식클래스에서 예기치 않게 변경되는 것을 방지”
- 보안 강화 : 보안이 중요한 메소드 변경하거나 악용하는 것을 방지 = 프로그램의 보안 수준을 향상
- 예측 가능한 동작 : 개발자가 이 메소드가 항상 동일한 방식으로 동작한다는 것을 인지 = 의도치 않은 오류 감소
<JDK에서의 final메소드>
java.lang.Object 클래스의 getClass메소드
- 객체가 속한 클래스의 Class객체를 반환
- 다른 클래스에서 재정의가 불가능
- 모든 객체가 속한 클래스의 “정확한 정보를 제공"
<예외처리>
- (=Exception Handling)
- 프로그램 실행 중 예상치 못한 상황에 대비하여
“프로그램의 정상적인 흐름을 유지”하고 “예외사항을 안전하게 처리”하는 프로그래밍 기법 - 프로그램의 안정성과 신뢰성을 높임
예외를 만났을때 프로그램이 종료되면 안되므로, 예외처리를 해야한다.
코드 작성 시 예외가 발생할 것을 “미리 짐작해서” 예외가 발생했을때 처리할 코드를 미리 작성
❓try-catch-finally
- try 블록안에 “예외가 발생가능한 문장”
- catch 블록에는 “예외가 발생시 처리할 문장”
> 어떤 예외가 발생할것같은지를 작성해줌 - NullPointerException)
> catch는 여러개의 블록이 가능하다. - finally 블록에는 "예외발생여부에 관계없이 실행되는 코드를 포함"
- Exception :
모든 예외의 최상위 클래스이자 "조상"
<예외처리 구조>
1) try-catch 사용 - Exception
public static void main(String[] args) {
try{
System.out.println(args[0]);
}catch(Exception e){
System.out.println("적절한 예외처리");
}
// 예외처리가 잘 처리된 후 실행될 문장
System.out.println("안녕하세요");
System.out.println("자바입니다.");
System.out.println("반갑습니다.");
}
2) try-catch - 예외 작성
public static void main(String[] args) {
int[] testArr = {0, 1, 2, 3};
try{
// 예외가 발생하면, 예외가 발생한 지점부터 try블럭의 코드는 실행되지 않음
// ArrayIndexOutOfBoundsException 발생 후 [처리 완료]
System.out.println(testArr[4]);
// ArithmeticException 발생 [catch문에 작성 전이므로 예외 발생]
int i = testArr[3] / testArr[0]; // 3을 0으로 나누는 값이 가능한지 확인
// 결과 : 예외 발생 시에는 이 문장이 실행되지 않음
System.out.println("1) [try블럭]예외 발생 시 이 문장이 실행되는지 확인");
System.out.println("2) [try블럭] 예외 발생 시 이 문장이 실행되는지 확인");
}catch(ArrayIndexOutOfBoundsException e){
System.out.println(e);
}
System.out.println("3) [예외처리 후] 다음 문장 실행");
System.out.println("4) [예외처리 후] 다음 문장 실행");
}
3) catch블록에 ArthmeticException 예외 추가
public static void main(String[] args) {
int[] testArr = {0, 1, 2, 3};
try{
// 예외가 발생하면, 예외가 발생한 지점부터 try블럭의 코드는 실행되지 않음
// 1) ArrayIndexOutOfBoundsException 발생 후 [처리 완료]
System.out.println(testArr[3]);
// 2) ArithmeticException 발생 [catch문에 작성 전이므로 예외 발생]
int i = testArr[3] / testArr[0]; // 3을 0으로 나누는 값이 가능한지 확인
// 결과 : 예외 발생 시에는 이 문장이 실행되지 않음
System.out.println("1) [try블럭]예외 발생 시 이 문장이 실행되는지 확인");
System.out.println("2) [try블럭] 예외 발생 시 이 문장이 실행되는지 확인");
}catch(ArrayIndexOutOfBoundsException e){
System.out.println(e);
}catch(ArithmeticException e){ // 위의 int i 에서의 예외 발생
System.out.println(e.getMessage());
}catch(Exception e){ // 모든 예외를 여기서 처리 가능
System.out.println(e);
}
System.out.println("3) [예외처리 후] 다음 문장 실행");
System.out.println("4) [예외처리 후] 다음 문장 실행");
}
4) Exception 하나만으로 예외 처리
public static void main(String[] args) {
int[] testArr = {0, 1, 2, 3};
try{
// 예외가 발생하면, 예외가 발생한 지점부터 try블럭의 코드는 실행되지 않음
// 1) ArrayIndexOutOfBoundsException 발생 후 [처리 완료]
System.out.println(testArr[3]);
// 2) ArithmeticException 발생 [catch문에 작성 전이므로 예외 발생]
int i = testArr[3] / testArr[0]; // 3을 0으로 나누는 값이 가능한지 확인
// 결과 : 예외 발생 시에는 이 문장이 실행되지 않음
System.out.println("1) [try블럭]예외 발생 시 이 문장이 실행되는지 확인");
System.out.println("2) [try블럭] 예외 발생 시 이 문장이 실행되는지 확인");
}catch(Exception e){ // 모든 예외를 여기서 처리 가능
System.out.println(e);
}
System.out.println("3) [예외처리 후] 다음 문장 실행");
System.out.println("4) [예외처리 후] 다음 문장 실행");
}
- 각각 처리하지 않고 한번에 처리하고 싶을때는 Exception을 사용
5) try-catch의 finally 사용
public static void main(String[] args) {
int[] testArr = {0, 1, 2, 3};
try{
// 예외가 발생하면, 예외가 발생한 지점부터 try블럭의 코드는 실행되지 않음
// 1) ArrayIndexOutOfBoundsException 발생 후 [처리 완료]
System.out.println(testArr[3]);
// 2) ArithmeticException 발생 [catch문에 작성 전이므로 예외 발생]
int i = testArr[3] / testArr[0]; // 3을 0으로 나누는 값이 가능한지 확인
// 결과 : 예외 발생 시에는 이 문장이 실행되지 않음
System.out.println("1) [try블럭]예외 발생 시 이 문장이 실행되는지 확인");
System.out.println("2) [try블럭] 예외 발생 시 이 문장이 실행되는지 확인");
}catch(ArrayIndexOutOfBoundsException e){
System.out.println(e);
}finally{
System.out.println("[finally] 반드시 실행되는 블록");
}
System.out.println("3) [예외처리 후] 다음 문장 실행");
System.out.println("4) [예외처리 후] 다음 문장 실행");
}
finally는 반드시 처리해야하는 코드들을 처리해주는 키워드
<메시지 출력>
- e.getMessage() : 메소드를 통해 에러 발생 메시지 출력
- e.printStackTrace(); : 메소드로 어느 부분에서 예외가 발생했는지 빨간 줄로 표시
catch(ArithmeticException e){ // 위의 int i 에서의 예외 발생
System.out.println(e.getMessage()); // 메시지 출력 (어느 문제가 발생했는지 출력)
e.getStackTrace(); // 메시지 출력 (어느 구문에서 발생했는지까지 출력)
}
<예외처리의 중요성>
- 프로그램이 예외상황에서도 중단되지 않고 계속실행될수 있도록 함
- 적절한 메시지를 제공하여 사용자나 개발자가 문제를 이해하고 대처할 수 있도록 함
- 프로그램의 안전성과 신뢰성보장
- 오류의 조기발견 및 대응
- 사용자 경험 개선
<예외처리를 하지 않으면>
- 프로그램의 비정상적 중단
→ 사용자에게 혼란을 주고, 프로그램 신뢰성을 저하, 중요한 작업이 완료되지 못하는 상황 발생 - 데이터손실
→ 예를들어 파일 작업 중 예외발생 시 파일 손상 위험, DB작업 중 예외발생 시 일관성 없는 상태 - 보안 취약점
- 사용자 경험 저하
→ 사용자는 프로그램의 오류상황을 이해하기 어려운 기술적인 메시지에 직면 - 유지보수의 어려움
→ 정확한 원인 파악이 어려우며, 문제해결과정을 복잡하고 시간이 많이 소모
💡 8일차는 크게 인터페이스, 예외처리에 대해서만 배웠지만 각 개념이 매우 중요하기때문에 자세히 배울 수 있었다.
이전에는 예외처리같은 경우 오류가 발생하면 그 오류를 수정하고 넘어갔다면, 예외처리를 배우고 나서는 예외처리가 왜 중요한지, 왜 필요한지에 대해 알 수 있었다.
인터페이스 또한 추상클래스와 비슷한 부분이 많아 구분하기 애매모호했는데 비교를 해보며 인터페이스의 사용방법이나 필요성을 알고나니 인터페이스 또한 중요한 역할을 하는 것임을 느낄 수 있었다.
관련 실습을 더 풀어보며 익숙해지기 위해 노력해야겠다고 생각했다!🚀
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_11일차_'데이터베이스' (1) | 2024.12.16 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_10일차_'내부클래스와 제네릭' (2) | 2024.12.13 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_7일차_'String클래스와 추상클래스' (0) | 2024.12.10 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_6일차_'상속과 메소드 오버라이딩' (1) | 2024.12.09 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_5일차_'메소드, 필드, static' (3) | 2024.12.06 |