📂 목차
- Effective Java
- 기타 가져갈 것들
참고: 망나니 개발자
📚 본문
이번에는 자바의 코드를 좋게 만드는 과정을 보려고 한다.
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
- 배열은 공변이지만, 제너릭인 리스트는 불공변임
만약A
가B
의 하위 타입이면A[]
역시B[]
의 하위타입이 되는걸 공변 관계에 있다고 한다.
하지만List
는Generic
을 쓰기에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
반환은 신중히 반환Optional
은null
일 수도 있기 때문에 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
패키지에 대해서 눈여겨 보기 - 정확한 계산을 위해서는
float
나double
을 피하기.- 정확한 계산을 하고 싶다면, 소수점 추적이 필요하다면
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
와 함께 써넣으면 좋다.
- DRY – Don’t Repeat Yourself
- YAGNI – You Aren’t Gonna Need It
- KISS – Keep It Simple, Stupid
- SRP - Single Responsibility Principle
- OCP - Open-Closed Principle
- LSP - Liskov Substitution Principle
- ISP - Interfaced Segregation Principle
- DIP - Dependency Inversion Principle
- POJO - Plain Old Java Object
- VO - Value Object
- DTO - Data Transfer Object
- DAO - Data Access Object