Recording/멋쟁이사자처럼 BE 13기

[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_8일차_'인터페이스와 예외처리'

LEFT 2024. 12. 11. 18:11

🦁멋쟁이사자처럼 백엔드 부트캠프 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는 해당 클래스가 객체에 해당하는지 (그릇이 맞는지) 확인해주는 것

  1. parent instanceof Parent : Parent클래스의 parent이므로 true
  2. child instanceof Parent : Parent클래스를 상속받는 Child클래스의 child이므로 true
  3. parent instanceof Child : Child클래스의 parent는 구문상 맞지 않다(부모의 참조변수)
  4. 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로 되어있음을 확인할 수 있다.
(= 패키지가 같거나 상속받는 자손클래스에서만 사용가능)

Calendar API - Constructor

2. Calendar는 추상클래스이기에 new라는 키워드를 제공하지 않음

Calendar API - abstract

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일차는 크게 인터페이스, 예외처리에 대해서만 배웠지만 각 개념이 매우 중요하기때문에 자세히 배울 수 있었다.

이전에는 예외처리같은 경우 오류가 발생하면 그 오류를 수정하고 넘어갔다면, 예외처리를 배우고 나서는 예외처리가 왜 중요한지, 왜 필요한지에 대해 알 수 있었다. 

인터페이스 또한 추상클래스와 비슷한 부분이 많아 구분하기 애매모호했는데 비교를 해보며 인터페이스의 사용방법이나 필요성을 알고나니 인터페이스 또한 중요한 역할을 하는 것임을 느낄 수 있었다.

관련 실습을 더 풀어보며 익숙해지기 위해 노력해야겠다고 생각했다!🚀