Developer.

[멋사 백엔드 19기] TIL 8일차

📂 목차


📚 본문

Abstract Class

사실 추상 클래스는 잘못 구현되었다고 실제 James Gosling 이 말한 바 있다.

문제는 다음과 같다:

  • 다중 상속 불가: 다중 상속을 막은 것에서 부터 공통 기능과 계약(메서드 시그니처)을 섞어야 하는 경우, abstract class 는 불편함.
// 계약만 정의
interface Drivable {
    void drive();
}

// 공통 기능 포함
abstract class Vehicle {
    String model;

    public Vehicle(String model) {
        this.model = model;
    }

    // 공통 기능
    public void printModel() {
        System.out.println("Model: " + model);
    }

    // 추상 메서드 (계약)
    public abstract void startEngine();
}
// Car는 Vehicle을 상속해야 하고 Drivable도 구현해야 함
class Car extends Vehicle implements Drivable {

    public Car(String model) {
        super(model);
    }

    @Override
    public void startEngine() {
        System.out.println("Car engine started!");
    }

    @Override
    public void drive() {
        System.out.println("Car is driving!");
    }
}

// 만약 다른 클래스도 상속해야 하면 abstract class 상속은 하나만 가능
class SportsCar extends Car /*extends SomeOtherClass*/ { 
    // 자바는 다중 상속을 막아서, Vehicle + SomeOtherClass 동시에 상속 불가
}

이는 굉장히 유연하지 못함.

  • 인터페이스와 혼동: default 가 있는데도 이는 추상 클래스의 목적을 인터페이스가 흡수하게 된 것.

  • 기능 제한: 생성자, 필드, 메서드 구현 모두 가능하지만, 서브 클래스가 이미 다른 클래스를 상속하고 있다면 abstract class 를 못쓰는 구조적 제약이 있음

이러한 이유로 abstract class 는 진짜 최상단 부모가 아니라면 쓰지 않는 것이 좋다.

Abstract Class 의 접근제한자

abstract class 를 선언할 때에는 public, default 로만 선언할 수 있다. 당연히 두 접근제한자는 클래스에 적용되는 접근제한자 규약이랑 똑같다.

  • public: 모든 패키지 접근
  • default: 같은 패키지 내에 접근

protected 가 없는 이유는 당연하다. 상속을 위한 기능인데 당연히 상속된 것들은 다 접근 가능해야 할 것이다.

메서드의 접근제한자는 private 를 제외하고 다 가능하다.
이 이유는 서브클래스에서 이를 접근할 수 없게 되므로 구현하지 못하게 된다.

Constructor 에서의 접근제한자는 private 까지 가능하지만, 호출이 불가하기 때문에 거의 쓰이지 않는다.

추상 클래스 구현 시 유의할 점

자바는 단일 상속만 지원하기 때문에 이미 다른 클래스를 상속받고 있는 경우 Abstract Class 를 추가로 상속할 수 없다. 따라서 공통 기능 + 계약을 묶어서 abstract class 로 만들면 유연성이 떨어진다.

가능하면 공통 기능은 default 메서드가 있는 인터페이스로 대체한다.

abstract class 는 진짜 상속 계층의 최상단 부모로서 common state나 field 가 필요할 때만 사용하는 것으로 한다.

또한 추상 클래스는 static, final 이 붙을 수 있지만, 해당 개념의 의미를 정확히 파악하여 이게 정말로 초기화가 가능한지, 접근이 가능한지를 따지면 어디에 static 이 붙을 수 있고 안 붙을 수 있는지 파악 할 수 있다.

java.lang.Throwable

이제 자바에서 처리되는 유사 Trap 을 본다.

java.lang.Throwable
├── java.lang.Error                // 주로 JVM 레벨 문제
│   ├── VirtualMachineError
│   │   ├── OutOfMemoryError
│   │   └── StackOverflowError
│   ├── AssertionError
│   └── LinkageError
└── java.lang.Exception      // 프로그램에서 처리 가능한 예외
    ├── java.lang.RuntimeException // Unchecked Exception
    │   ├── NullPointerException
    │   ├── IndexOutOfBoundsException
    │   │   ├── ArrayIndexOutOfBoundsException
    │   │   └── StringIndexOutOfBoundsException
    │   ├── ArithmeticException
    │   ├── ClassCastException
    │   ├── IllegalArgumentException
    │   │   └── NumberFormatException
    │   ├── IllegalStateException
    │   ├── UnsupportedOperationException
    │   └── ConcurrentModificationException
    ├── java.io.IOException          // Checked Exception (입출력 관련)
    │   ├── FileNotFoundException
    │   ├── EOFException
    │   ├── InterruptedIOException
    │   └── ObjectStreamException
    │       ├── InvalidClassException
    │       ├── NotSerializableException
    │       └── OptionalDataException
    ├── java.sql.SQLException
    ├── ClassNotFoundException
    ├── NoSuchMethodException
    ├── NoSuchFieldException
    ├── InstantiationException
    └── ReflectiveOperationException
        ├── IllegalAccessException
        └── InvocationTargetException

우리가 마주하는 대부분의 예외는 java.lang.RuntimeException, java.io.IOException 이다.

여기서 Error 는 보통 처리하지 않고, Exception 을 try-catch 문으로 처리를 한다.

Checked Exception

Checked Exception 은 반드시 try-catch 문 혹은 throws 선언이 필요하다.

필수

  • try-catch
  • throws 문

이런게 없다면 컴파일이 실행되지 않는다.

Unchecked Exception

보통 처리하지 않으며, 선택적으로 try-catch 를 사용하여 에러 메시지를 띄울 수 있다.

import java.io.*;

public class ExceptionExample {
    public static void main(String[] args) {
        // Checked Exception 예시: 파일 읽기
        try {
            FileReader reader = new FileReader("nonexistent.txt");
            reader.read();
            reader.close();
        } catch (FileNotFoundException e) {
            System.out.println("파일이 존재하지 않습니다: " + e.getMessage());
        } catch (IOException e) {
            System.out.println("파일 입출력 오류 발생: " + e.getMessage());
        }

        // Unchecked Exception 예시: 0으로 나누기
        try {
            int a = 10 / 0;
        } catch (ArithmeticException e) {
            System.out.println("산술 오류 발생: " + e.getMessage());
        }
    }
}

Error

보통 프로그램에서 처리하지 않고 JVM 레벨에서 실행이 종료된다거나 처리된다.

필요시 catch 가 가능하긴 하지만 권장되지는 않는다.

실제로 Checked Exception 을 마주칠 일이 없는데 필자는 BufferedWriter 을 사용할 때, 해당 함수를 사용하는 다른 함수들한테 전부 throws IOException 시그니처를 적용시켜줘야 하여 번거로웠다. 이때는 try-catch 를 활용하여 catch 스코프에서 Unchecked Exception 으로 던져주는 것이 좋을 듯하다.

Validator 계층

실무에서 빠질 수 없다. servlet 을 통해 REST API를 사용하는 서버가 있다고 하자. 그러면 유저의 입력을 받아 서비스를 제공해주는 것이 목적일 터이다.

여기서 유저의 입력은 과연 우리의 서비스의 함수 시그니처에 맞는 인자를 전달할지 안할지 체크를 해야 한다. 이때 자주 쓰이는 게 Validation 이며(Validator 라고도 하고 다양함), util 패키지에 저장되어 사용하는 것이 대부분이다.

Validation 이 접근 하여 로직을 수행하는 곳은 다음과 같다.

  • controller
  • domain
  • (입력이 들어오는 곳 어디든?)

유틸이기에 다방면으로 사용해도 상관은 없다.

Interface

인터페이스도 Abstract Class 와 마찬가지로

  • public
  • default

를 사용 가능하다. 기능 목적은 똑같다.

Interface Field

인터페이스의 필드는 무조건 public static final 이어야 한다. 이는 별도로 명시하지 않아도 무조건 public static final 이 기본이 된다.

Interface Method

모든 인터페이스의 메서드는 기본적으로 public 이며, default, private 까지 사용 가능하다. static 도 당연 사용 가능하다.

  • default: 기본적으로 interface 에 선언되어 있고, 해당 메서드의 오버라이딩은 선택적
  • static: 오버라이딩은 못하지만 다른 곳에서 사용 가능

디자인 패턴

인터페이스와 상속을 더 배웠으니 객체 지향에서 마주칠 수 있는 주된 문제들에 대한 해결할 수 있는 개발 패턴을 본다.

Template Method Pattern

public abstract class Game {
    // 템플릿 메소드
    public final void play() {
        initialize();
        startPlay();
        endPlay();
    }

    // 추상 메소드들 (하위 클래스에서 구현)
    abstract void initialize();
    abstract void startPlay();
    abstract void endPlay();
}

중요한 것은 추상화된 클래스가 직접 로직을 제공하고 그 부품들은 각자 구현된 하위 클래스에서 구현하도록 하는 것이다.

이렇게 되면 하위 클래스는 굳이 상위 클래스의 전반적인 로직을 모르더라도 쪼개어진 기능들만 구현하면 제대로 동작하게 됨을 볼 수 있다.

Singleton Pattern

메모리 상에서 오로지 하나 생성하고, 더 이상의 생성을 제어하고 싶을 때 싱글톤 패턴을 사용한다.

public class Singleton {
    // 클래스 내부에서 단 하나의 인스턴스를 생성
    private static final Singleton instance = new Singleton();

    // private 생성자 → 외부에서 new Singleton() 불가능
    private Singleton() {
        System.out.println("Singleton 생성자 호출");
    }
}

Abstract Factory Pattern

// 1. 추상 팩토리 인터페이스
interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

// 2. 구체 팩토리 (Windows)
class WindowsFactory implements GUIFactory {
    @Override
    public Button createButton() {
        return new WindowsButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new WindowsCheckbox();
    }
}

// 3. 구체 팩토리 (Mac)
class MacFactory implements GUIFactory {
    @Override
    public Button createButton() {
        return new MacButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new MacCheckbox();
    }
}

Buidler Pattern

// Product 클래스
class Computer {
    private String cpu;
    private String gpu;
    private int ram;
    private int storage;

    private Computer(Builder builder) {
        this.cpu = builder.cpu;
        this.gpu = builder.gpu;
        this.ram = builder.ram;
        this.storage = builder.storage;
    }

    @Override
    public String toString() {
        return "Computer [CPU=" + cpu + ", GPU=" + gpu + ", RAM=" + ram + "GB, Storage=" + storage + "GB]";
    }

    // Builder 클래스
    public static class Builder {
        private String cpu;
        private String gpu;
        private int ram;
        private int storage;

        public Builder setCpu(String cpu) {
            this.cpu = cpu;
            return this;
        }

        public Builder setGpu(String gpu) {
            this.gpu = gpu;
            return this;
        }

        public Builder setRam(int ram) {
            this.ram = ram;
            return this;
        }

        public Builder setStorage(int storage) {
            this.storage = storage;
            return this;
        }

        public Computer build() {
            return new Computer(this);
        }
    }
}

더 얻어갈 것들

Reflect 패키지

참고: reflect 패키지

Object 와 Objects 활용

참고: Object 와 Objects 활용