Developer.

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

📂 목차


참고: 망나니 개발자

📚 본문

이번에는 자바의 코드를 좋게 만드는 과정을 보려고 한다.

Effective Java

생성 관련

Static Factory Method

장점

  • 이름을 가지며 의도 표현 가능
  • 호출 시 항상 새 인스턴스를 만들 필요가 없음
  • 하위 클래스 반환 가능
  • 매개변수에 따라 다른 클래스 객체 반환 가능
  • 클래스가 존재하지 않아도 작성 가능

단점

  • 하위 클래스로 상속하려면 public/protected 생성자가 필요
  • 직관적이지 않아서 찾기가 어려움

예시

public class Person {
    private String name;
    private int age;

    private Person(String name, int age){
        this.name = name;
        this.age = age;
    }
    
    // Static Factory Method
    public static Person of(String name, int age) {
        return new Person(name, age);
    }
}

심화 예시

public abstract class Shape {
    public abstract void draw();

    // Static Factory Method
    public static Shape create(String type) {
        return switch (type.toLowerCase()) {
            case "circle": return new Circle();
            case "square": return new Square();
            default: throw new IllegalArgumentException("Unknown Type");
        };
    }
}

class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

class Square extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a square");
    }
}
Builder Pattern

생성자의 매개변수가 많아짐에 따라 코드 이해가 힘들고, Setter 를 사용하면 객체 일관성이 깨지며 Open-Closed 원칙 위배이다.

이때 Builder 패턴을 사용하면 이 문제를 쉽게 해결할 수 있다.

예시

public class Person {
    private final String name;
    private final int age;
    private final String email;
    private final String phone;

    // private 생성자
    private Person(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.email = builder.email;
        this.phone = builder.phone;
    }

    // Builder 클래스
    public static class Builder {
        private final String name; // 필수
        private int age = 0;       // 선택, 기본값 0
        private String email = ""; // 선택, 기본값 ""
        private String phone = ""; // 선택, 기본값 ""

        public Builder(String name) {
            this.name = name;
        }

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

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

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

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

이를 Static Factory Method 와 함께하면 다음과 같이 짤 수도 있다.

public class Person {
    private final String name;
    private final int age;
    private final String email;
    private final String phone;

    private Person(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.email = builder.email;
        this.phone = builder.phone;
    }

    // Static Factory Method
    public static Person builder(String name) {
        return new Builder(name).build();
    }

    public static class Builder {
        private final String name;
        private int age = 0;
        private String email = "";
        private String phone = "";

        public Builder(String name) {
            this.name = name;
        }

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

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

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

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

final 이라 값을 변경할 수 없지만 다음과 같이 새로 생성하는 것은 쉽게 된다:

Person p1 = new Person.Builder("Alice").age(30).build();
Person p2 = new Person.Builder(p1.getName())
                 .age(35) // 새 나이로 새 객체 생성
                 .build();
인스턴스가 불필요한 클래스
  • 유틸성 클래스
  • Validator 관련
  • Exception 관련

전부 생성자를 private 로 명시해버린다.

try-with-resources를 활용

try 안에 특정 변수를 선언하여서 해당 블럭이 종료되면 그 변수를 자동 메모리 할당 해제해주는
즉, AutoClosable 인터페이스가 구현이 되어 있다면, try 블록이 종료되면 자동으로 메모리를 회수해준다.

try (ResourceType resource = new ResourceType()) {
    // 리소스 사용
} catch (Exception e) {
    e.printStackTrace();
}

AutoClosable 인터페이스는 따로 구현하지 않아도 자바에서 거의 다 지원해준다.

실제 예시를 보자.

public class BufferedReaderExample {
    public static void main(String[] args) {
        String filePath = "example.txt"; // 읽을 파일 경로

        // try-with-resources 사용
        try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
            String line;
            while ((line = br.readLine()) != null) { // 한 줄씩 읽기
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace(); // 예외 발생 시 출력
        }
    }
}
public class ConsoleInputExample {
    public static void main(String[] args) {
        System.out.println("이름을 입력하세요: ");

        // try-with-resources로 BufferedReader 사용
        try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
            String name = br.readLine(); // 한 줄 입력 받기
            System.out.println("입력한 이름: " + name);

            System.out.println("나이를 입력하세요: ");
            String ageInput = br.readLine();
            int age = Integer.parseInt(ageInput); // 문자열을 정수로 변환
            System.out.println("입력한 나이: " + age);

        } catch (IOException e) {
            e.printStackTrace();
        } catch (NumberFormatException e) {
            System.out.println("나이는 숫자로 입력해야 합니다.");
        }
    }
}

Class & Interface

클래스와 멤버의 접근 권한을 최소화

접근 제한자 활용 및 은닉화 잘되어 있게 설계를 해야한다.

예시를 보자.

public class Person {
    // public 필드: 외부에서 직접 접근 가능
    public String name;
    public int age;
}

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        p.name = "";  // 잘못된 이름 넣을 수 있음
        p.age = -5;   // 말이 안 되는 나이 넣을 수 있음
    }
}

위처럼 해버리면 아무 변수나 막 넣을 수 있고,
그 값이 정말 신뢰할 수 있는지도 모른다.

public class Person {
    // private 필드: 외부에서 직접 접근 불가
    private String name;
    private int age;

    // public getter/setter 제공: 필요한 경우만
    public String getName() {
        return name;
    }

    public void setName(String name) {
        // 간단한 검증 가능
        if (name != null && !name.isEmpty()) {
            this.name = name;
        }
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if (age > 0) { // 유효성 체크
            this.age = age;
        }
    }
}

이렇게 작성하면 함수 내에서 유효성 검증도 가능하게 되며 직접 멤버 변수로 접근을 못하게 할 수 있다.

변경 가능성을 최소화

불변의 객체(위에서 봤던 Builder 패턴 처럼)를 생성하여, 생성된 시점에 파괴되는 시점까지 동일한 값을 유지하도록 하게 최종적인, 가장 구체적인 클래스는 final 로 선언하거나, 정적 팩토리 메서드를 사용하여 더욱 유연하게 불변 객체를 생성 가능하다.

모르겠다면 위에 쓴 Builder 패턴을 보고 오자.

상속보다는 조합을 사용

상속은 좋지만, 상위 클래스의 구현이 하위 클래스로 노출되어 캡슐화를 깨뜨림.

  • 상속은 is-a 의 관계의 경우에만 사용하도록
  • 그 외에는 강하게 결합(strictly coupling) 되는 상속은 되도록 X
// 상위 클래스 구현이 그대로 하위로 노출됨
public class Vehicle {
    public int speed;
    public void accelerate() {
        speed += 10;
    }
}

public class Car extends Vehicle {
    public void turboBoost() {
        // 상위 클래스 필드 직접 접근
        speed += 50; // 하위 클래스가 상위 구현에 강하게 의존
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.accelerate(); // Vehicle 메서드 사용
        car.turboBoost(); // Vehicle 내부 구현 직접 활용
    }
}

위는 하위 클래스가 상위 클래스의 직접 멤버 변수를 참조하는 것을 볼 수 있다.
이렇게 되면 캡슐화가 제대로 된게 아니며, 강한 결합이 생긴다.

// Vehicle은 내부 상태를 은닉
public class Vehicle {
    private int speed;

    public void accelerate() {
        speed += 10;
    }

    public int getSpeed() {
        return speed;
    }
}

// Car는 Vehicle을 필드로 가지고 조합 사용
public class Car {
    private final Vehicle vehicle = new Vehicle();

    public void accelerate() {
        vehicle.accelerate();
    }

    public void turboBoost() {
        // vehicle 내부 구현에 직접 접근하지 않고, 메서드만 사용
        for (int i = 0; i < 5; i++) {
            vehicle.accelerate();
        }
    }

    public int getSpeed() {
        return vehicle.getSpeed();
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.accelerate();
        car.turboBoost();
        System.out.println(car.getSpeed());
    }
}

위와 같이 가지는, 조합하는 형태로 바꾼다.

상속을 고려하여 설계 및 문서화하기
  • 상속된 메서드는 공개
  • hook 을 이용했다면 그 hook 도 protected 로 공개
public class Vehicle {
    public int speed;

    private void accelerate() { 
        speed += 10;
    }
}

public class Car extends Vehicle {
    public void turboBoost() {
        speed += 50;
    }
}

위는 얼핏 보면 잘 설계한 것 같지만, 상속된 메서드에 대해 private 때문에 공개를 하지 않고 있다. 밑과 같이 바꾸자.

public class Vehicle {
    private int speed;

    // 상속된 메서드는 공개
    public void accelerate() {
        speed += 10;
    }

    // Hook 제공, 하위 클래스에서 필요 시 오버라이드 가능
    protected void onSpeedChange() {
        // 기본 동작은 아무것도 하지 않음
    }

    public int getSpeed() {
        return speed;
    }

    public void changeSpeed(int delta) {
        speed += delta;
        onSpeedChange(); // Hook 호출
    }
}

public class Car extends Vehicle {
    @Override
    protected void onSpeedChange() {
        System.out.println("차의 속도가 변경되었습니다: " + getSpeed());
    }

    public void turboBoost() {
        changeSpeed(50); // 내부 구현에 직접 접근하지 않고 메서드 사용
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.accelerate();     // 상속된 메서드 사용
        car.turboBoost();     // Hook 활용
    }
}
추상 클래스보다는 인터페이스를
  • 추상 클래스는 최상단의 조상 클래스이다. 이는 계층 구조에 혼란을 줄 수 있음
  • 하지만 인터페이스는 유연하고 mix-in도 가능함
  • Java8 부터는 default 메서드도 있어서 인터페이스 구현이 명확한 부분은 개발하여 제공 가능
// 최상위 조상으로만 사용되어 계층 구조가 복잡
public abstract class Animal {
    public abstract void makeSound();
}

public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("멍멍");
    }
}

public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("야옹");
    }
}

추상 클래스가 계층 구조의 최상위에 위치하여 확장성에 제한을 시킨다.
코드의 유연성을 해치며, 다중 상속이 불가능 하도록 한다. 인터페이스로 바꾸자.

// 인터페이스 정의
public interface Soundable {
    void makeSound();

    // Java8 부터 default 메서드 제공 가능
    default void greet() {
        System.out.println("안녕하세요!");
    }
}

// 개 클래스
public class Dog implements Soundable {
    @Override
    public void makeSound() {
        System.out.println("멍멍");
    }
}

// 고양이 클래스
public class Cat implements Soundable {
    @Override
    public void makeSound() {
        System.out.println("야옹");
    }
}

// mix-in 예시: Flying 기능 추가
public interface Flyable {
    void fly();
}

public class Bird implements Soundable, Flyable {
    @Override
    public void makeSound() {
        System.out.println("짹짹");
    }

    @Override
    public void fly() {
        System.out.println("날아간다!");
    }
}
태그 달린 클래스보다는 계층 구조 이용
  • 어떤 객체가 동일 클래스 안에서 여러 타입을 구분하기 위해 추가 필드를 사용하는 것, 그 필드를 Tag 라고 함.
  • 이는 새 class 추가 시 기존 클래스 수정이 필요하게 됨
  • 이보다는 계층 extends 와 같은 걸 이용해 구조화를 함.
public class Shape {
    public static final int CIRCLE = 1;
    public static final int SQUARE = 2;

    private int type; // Tag 필드
    private int size;

    public Shape(int type, int size) {
        this.type = type;
        this.size = size;
    }

    public void draw() {
        if (type == CIRCLE) {
            System.out.println("Drawing a circle of size " + size);
        } else if (type == SQUARE) {
            System.out.println("Drawing a square of size " + size);
        }
    }
}

멤버 변수에 type 이라는 태그를 만들었다. 이는 새로운 Shape 가 추가될 때마다 확장성에 굉장히 예민하게 동작하며, 기존 클래스를 수정해야 한다.

또한 타입 별 동작이 조건문으로 분기하여 코드가 복잡하고, 유지보수가 어렵다.

// 상위 추상 클래스
public abstract class Shape {
    protected int size;

    public Shape(int size) {
        this.size = size;
    }

    public abstract void draw();
}

// 하위 클래스별 구현
public class Circle extends Shape {
    public Circle(int size) {
        super(size);
    }

    @Override
    public void draw() {
        System.out.println("Drawing a circle of size " + size);
    }
}

public class Square extends Shape {
    public Square(int size) {
        super(size);
    }

    @Override
    public void draw() {
        System.out.println("Drawing a square of size " + size);
    }
}

이를 계층으로 해결한다.

Nested Class 는 Static 으로

이는 이전에 살펴보았다.

Generic

Raw 타입 사용하지 말기

다시 말해서 Generic 타입을 생략하지 않아야 한다.

Object 는 Raw Type 과 마찬가지로 모든 타입을 포용할 수 있지만, 컴파일러에게 모든 타입을 허용하겠다는 의사를 전달했다는 것과 Raw 타입과는 다르다.

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List list = new ArrayList(); // Raw 타입
        list.add("Hello");
        list.add(123); // 다른 타입도 추가 가능

        for (Object obj : list) {
            System.out.println(obj);
        }
    }
}

제너릭 타입을 지정하지 않으면 모든 타입이 추가 가능하여
값의 다양성이 높아져 ClassCastException 에러가 발생할 우려가 있다.

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(); // 제네릭 타입 지정
        list.add("Hello");
        // list.add(123); // 컴파일 단계에서 오류 발생

        for (String str : list) {
            System.out.println(str);
        }
    }
}

제너릭 타입을 명시하여 타입 안정성을 확보하고, 컴파일러가 타입을 체크하도록 한다.

Array 보다는 List
  • 배열은 공변이지만, 제너릭인 리스트는 불공변임
    만약 AB 의 하위 타입이면 A[] 역시 B[] 의 하위타입이 되는걸 공변 관계에 있다고 한다.
    하지만 ListGeneric 을 쓰기에 Generic 에서는 이러한 공변관계가 성립하지 않아 이를 컴파일 시점에서 잡아낼 수 있다.
  • 배열은 실체화 됨.
class Animal {}
class Dog extends Animal {}

public class Main {
    public static void main(String[] args) {
        Dog[] dogs = new Dog[2];
        Animal[] animals = dogs; // 배열은 공변

        animals[0] = new Animal(); // 런타임 오류 발생 (ArrayStoreException)
    }
}

이는 컴파일 시 오류가 없지만, 런타임에 ArrayStoreException 이 발생할 수 있다. 타입 안전성도 보장되지 않으므로 다음과 같이 변경한다.

import java.util.ArrayList;
import java.util.List;

class Animal {}
class Dog extends Animal {}

public class Main {
    public static void main(String[] args) {
        List<Dog> dogs = new ArrayList<>();
        // List<Animal> animals = dogs; // 컴파일 오류 발생, 불공변

        dogs.add(new Dog());
        // dogs.add(new Animal()); // 컴파일 오류, 타입 안전성 확보
    }
}
한정적 와일드 카드를 사용해 API 유연성 높이기
  • ? extends E, ? super E 등을 사용
  • Pecs(Producer-extends, Consumer-super) 공식:
    매개변수화 타입이 생산자 = extends
    매개변수화 타입이 소비자 = super (한 번만 나오면 그냥 super 쓰자)
// static 와 void 사이의 <E> 는 이 제너릭 타입을 쓰겠다 라는 의미
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);

한 번만 제너릭이 나오면 두번째(와일드카드)가 좋음

Enumerate

  • 필요한 원소가 컴파일 시점때 다 알 수 있으면 열거가 좋음
  • switch 문에서는 좀 길어져서 유지보수가 힘든데,
    이때는 중첩 열거 타입이나 내부 메소드를 추가하여 해결할 수 있다.
public class Direction {
    public static final int NORTH = 0;
    public static final int SOUTH = 1;
    public static final int EAST  = 2;
    public static final int WEST  = 3;
}

public class Main {
    public static void main(String[] args) {
        int dir = Direction.NORTH;

        switch (dir) {
            case 0: System.out.println("북쪽"); break;
            case 1: System.out.println("남쪽"); break;
            case 2: System.out.println("동쪽"); break;
            case 3: System.out.println("서쪽"); break;
        }
    }
}

이는 상수값으로 방향을 표현하며 의미가 불분명하다. 새로운 방향을 추가하면 또 기존 코드를 수정해야 한다.

public enum Direction {
    NORTH("북쪽"),
    SOUTH("남쪽"),
    EAST("동쪽"),
    WEST("서쪽");

    private final String label;

    Direction(String label) {
        this.label = label;
    }

    public String getLabel() {
        return label;
    }

    // 내부 메서드 활용 가능
    public void printDirection() {
        System.out.println("방향: " + label);
    }
}

public class Main {
    public static void main(String[] args) {
        Direction dir = Direction.NORTH;

        // switch 대신 내부 메서드 사용
        dir.printDirection();

        // switch 사용 시에도 enum 사용 가능
        switch (dir) {
            case NORTH -> System.out.println("북쪽으로 이동");
            case SOUTH -> System.out.println("남쪽으로 이동");
            case EAST  -> System.out.println("동쪽으로 이동");
            case WEST  -> System.out.println("서쪽으로 이동");
        }
    }
}

Lambda & Stream

  • 익명 클래스보다는 람다 사용을 하되 남용하진 말고 코드 줄도 길어지지 않는 선에서 사용한다.
  • 람다보다는 메소드 참조를 이용하여 코드를 더 간결하게 작성한다.
  • Stream 은 주의해서 사용해야 한다. 장점도 많지만, 과용하면 유지보수가 힘들며, 메소드 이름을 잘 지어주어야 한다.
  • Stream 에서는 부작용(Side Effect)이 없는 함수를 이용한다.
    ex) forEach 내에서 값을 set 하는 건 부작용 우려가 있음. 순회하는 동안 데이터를 변경했기 때문에 다른 스레드에 영향을 줌
  • 병렬 스트림은 주의!!!
    • int, long 형 범위가 병렬화 효과가 가장 좋음(데이터가 연속적이고 손쉽게 나눌 수 있기 때문)
    • int, long 이 참조 지역성이 뛰어남
    • collect 메서드는 합치는 비용 때문에 병렬화에 적합하지 않음
    • Stream 은 처리할 데이터가 수십만은 되어야 성능 향상이 됨

잘못된 예시

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(Arrays.asList("Alice", "Bob", "Charlie"));

        // 불필요하게 긴 람다
        names.forEach(name -> {
            if (name.startsWith("A")) {
                System.out.println(name.toUpperCase());
            } else {
                System.out.println(name.toLowerCase());
            }
        });

        // forEach 내에서 외부 상태 변경 (부작용 발생)
        int[] count = {0};
        names.forEach(n -> count[0]++); // 공유 변수 변경 → 동시성 문제 발생 가능
    }
}

잘된 예시

import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

        // 간단한 경우 메서드 참조 활용
        names.forEach(System.out::println);

        // Stream 연산에서 부작용 없는 함수 사용
        List<String> upperNames = names.stream()
                                       .filter(n -> n.startsWith("A"))
                                       .map(String::toUpperCase) // 순수 함수
                                       .toList();

        System.out.println(upperNames);

        // 병렬 스트림은 신중히
        long count = IntStream.rangeClosed(1, 1_000_000)
                              .parallel()
                              .filter(i -> i % 2 == 0)
                              .count();
        System.out.println("짝수 개수: " + count);
    }
}

Method

  • 메서드 시그니처를 신중히 설계
    • 편의 메서드 많이 만들지 말기
    • 이름 신중히 짓기
    • 매개변수가 boolean 이 있다면 Enum 을 사용하는 것을 고려
    • 빌더 패턴을 메서드 호출에 응용
  • Overloading 은 신중히 사용
    • Overloading 은 가변 인수를 쓰면서 오버로딩하면 컴파일러가 헷갈릴 수 있으며, 특히 매개변수 수가 같다면 어떤 메서드가 선택될 지 혼란을 줄 수 있음
  • 가변인수는 신중히 사용
    • Overriding 할 때 컴파일러에게 혼동을 줄 수 있음
  • Optional 반환은 신중히 반환
    • Optionalnull 일 수도 있기 때문에 API 사용자에게 명확히 알려주어야 한다.
    • 특히 이를 받은 사용자는 다음 행동을 할 수 있다.
      • 기본값 정함
      • 예외 던짐
      • 항상 값이 있다고 가정하고 꺼냄
      • 값의 여부를 boolean 으로 받음(isPresent 활용)

잘못된 예시

// boolean 매개변수 → 의미 불명확
public void setMode(boolean flag) {
    if (flag) {
        System.out.println("Dark mode ON");
    } else {
        System.out.println("Dark mode OFF");
    }
}

// 오버로딩이 헷갈림
public void print(String s) {}
public void print(Object o) {} // String도 Object라 애매

// Optional 반환을 남용
public Optional<String> findName() {
    return Optional.ofNullable(null); // 무의미한 Optional
}

잘된 예시

// Enum 활용 → 의미 명확
public enum Mode { DARK, LIGHT }

public void setMode(Mode mode) {
    switch (mode) {
        case DARK -> System.out.println("Dark mode ON");
        case LIGHT -> System.out.println("Light mode OFF");
    }
}

// 오버로딩 대신 명확한 메서드명
public void printString(String s) {}
public void printObject(Object o) {}

// Optional은 null 가능성이 있을 때만
public Optional<String> findNameById(int id) {
    if (id == 1) return Optional.of("Alice");
    else return Optional.empty();
}

public static void main(String[] args) {
    // Optional 활용 예시
    String name = new Main().findNameById(1)
                            .orElse("기본 이름");
    System.out.println(name);
}

일반적 프로그래밍 원칙

  • 라이브러리 익히고 사용: 특히 java.lang, java.util, java.io 와 그 하위 패키지, Collection, Stream 패키지에 대해서 눈여겨 보기
  • 정확한 계산을 위해서는 floatdouble 을 피하기.
    • 정확한 계산을 하고 싶다면, 소수점 추적이 필요하다면
      BigDecimal 을 사용하는 것이 더 좋다.
  • 박싱된 기본 타입 보다는 기본 타입을 사용하기
  • 다른 타입이 적절하다면 문자열 사용을 피하기
  • 문자열 연결은 느리니 주의하기 => StringBuilder 를 사용
  • 객체는 인터페이스를 사용해 참조(Open-Closed Principle)
  • 최적화는 신중하게 하기

예외

  • 예외는 진짜 예외 상황에만 적용
  • 복구 가능한 상황에는 검사 예외 사용
  • 프로그래밍 오류에는 런타임 예외 사용
  • 메소드가 던지는 모든 예외를 문서화 하기
  • 예외의 상세 메시지에 실패 관련 정보를 담기
  • 가능한 실패를 Atomic 하게 만들기
public class Calculator {
    // 단순 조건에도 예외를 던짐 (남용)
    public int divide(int a, int b) throws Exception {
        if (b == 0) {
            throw new Exception("0으로 나눌 수 없음");
        }
        return a / b;
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        try {
            int result = calc.divide(10, 0);
        } catch (Exception e) { // 광범위한 Exception
            e.printStackTrace();
        }
    }
}
// 복구 가능한 상황 → 검사 예외
class FileFormatException extends Exception {
    public FileFormatException(String message) {
        super(message);
    }
}

// 프로그래밍 오류 → 런타임 예외
class NegativeNumberException extends RuntimeException {
    public NegativeNumberException(String message) {
        super(message);
    }
}

public class Calculator {
    /**
     * @throws FileFormatException 잘못된 입력 형식일 경우
     * @throws NegativeNumberException 음수 입력 시
     */
    public int parseAndAdd(String input) throws FileFormatException {
        try {
            int num = Integer.parseInt(input);
            if (num < 0) {
                throw new NegativeNumberException("음수는 허용되지 않음: " + num);
            }
            return num + 10;
        } catch (NumberFormatException e) {
            throw new FileFormatException("잘못된 숫자 형식: " + input);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        try {
            int result = calc.parseAndAdd("abc");
        } catch (FileFormatException e) {
            System.err.println("입력 오류 → " + e.getMessage());
        }
    }
}

직렬화

  • 직렬화 대안을 찾기: 직렬화는 상당히 우험함. Json 이나 프로토콜 버퍼와 같은 대안을 사용하는 것이 좋음.
    신뢰할 수 없는 데이터는 역직렬화 하지 않으며, 필터를 통해 먼저 검사하기
  • Serializable 을 구현할지는 신중히 결정하기
    • 버그와 보안 구멍이 생길 우려
    • Serializable 을 구현 시 릴리즈 한 뒤 수정이 힘듦
    • 해당 클래스의 신 버전을 릴리즈할 때 테스트 할 것들이 늘어남

기타 가져갈 것들

  • import 문에 와일드 카드 생략하지 않기
  • 패키지명은 도메인 역순 사용
  • 모든 생성자는 첫 줄에 super() 또는 this()를 호출해야 함
  • 명시하지 않으면 자동으로 super() 호출
  • 인터페이스의 메서드에도 default, static 이 올 수 있다.

과제하며 배운점

다음을 java docs 의 Marker 와 함께 써넣으면 좋다.

  1. DRY – Don’t Repeat Yourself
  2. YAGNI – You Aren’t Gonna Need It
  3. KISS – Keep It Simple, Stupid
  4. SRP - Single Responsibility Principle
  5. OCP - Open-Closed Principle
  6. LSP - Liskov Substitution Principle
  7. ISP - Interfaced Segregation Principle
  8. DIP - Dependency Inversion Principle
  9. POJO - Plain Old Java Object
  10. VO - Value Object
  11. DTO - Data Transfer Object
  12. DAO - Data Access Object