Effective Java 1 | Object

공부의 기록·2021년 12월 13일
0

Java (Effective Java)

목록 보기
2/12
post-thumbnail

오브젝트

본 글은 2021년 12월 13일 에 기록되었다.

본 내용은 자바의 기본 전반을 알고 있는 사람 을 전제로 하는 것 같다.
따라서 본인이 Java 의 자료형, 클래스, 인터페이스, try-catch 를 잘 모른다면 별도의 포스트를 보고 오는 것을 추천한다.

오브젝트란?

객체(컴퓨터 과학) | 위키백과, 우리 모두의 백과사전
클래스에서 정의한 것을 토대로 메모리에 할당된 것
프로그램에서 사용되는 데이터 또는 식별자(변수명)에 의해 참조되는 공간


아이템


아이템 1 | 정적 팩토리 메서드

정적 팩토리 메서드가 생성자보다 좋은 이유는 다음과 같다.

  1. 이름을 가질 수 있다.
    1.1. 하나의 시그니처 는 하나의 생성자를 만들 수 있다.
    1.2. 따라서 하나의 시그니처가 여러 생성자를 만들어야 한다면 정적 팩토리 메서드 가 좋은 선택이 될 수 있다.

  2. 호출될 때마다 객체를 새로 생성하지 않아도 된다.
    2.1. 불변 클래스로써 존재하며 여러 번 객체를 생성하지 않아도 된다.
    2.2. 이러한 클래스를 인스턴스 통제 클래스 라고하며 다음 클래스들의 근간이 될 수 있다.
    2.2.1. static 이라는 점에서 singleton 과 유사하나 목적 및 구조 상으로 차이 가 있다.
    2.2.2. 역시나 factory method 와 유사하나 구조 상으로 차이 가 있다.
    2.2.3. 역시나 플라웨이트 패턴의 근간이 된다.
    2.3. 이러한 경우에 Enum 을 사용하면 인스턴스가 하나만 만들어짐을 보장한다.

  3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.

  4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

  5. 정적 팩터리 메서드를 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

정적 팩토리 메서드가 나쁜 이유는 다음과 같다.

  1. 상속을 하려면 public 이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스를 만들 수 없다.
  2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.

명명법

이름예시설명
fromDate d=Date.from( 매개변수 );형변환 메서드
ofSet<Rank> faceCards=EnumSet.of( 매개변수 1, ... , 매개변수 n);형변환 메서드
valueOfBigInteger prime=BigInteger.valueOf(Integer.MAX_VALUE);from 과 of 의 조금 더 자세한 버전
instnace
getInstance
StackWalker luke=StackWlker.getInstance(ontions);(매개변수를 받는다면)
매개변수로 명시한 인스턴스를 반환
같은 인스턴스 임을 보장하지는 않는다.
create
newInstanc
Object newArray=Array.newInstance(classObject, arrayLen);(instance, getInstance 와 같지만)
매번 새로운 인스턴스를 생성 반환
getTypeFileStore fs=Files.getFileStore(path);본 클래스와 관계없는
다른 클래스의 객체를 반환
newTypeBufferedReader bf=Files.newBufferedReader(path);본 클래스와 관계없는
다른 클래스의 객체를 반환
typeList<Complaint> litany=Collections.list(legacyLitany);getType 과 newType 간결한 버전

아이템 2 | 빌더 패턴

매개변수가 많은 객체를 생성하는 경우, 다음과 같은 선택지가 있을 수 있다.

  1. 그냥 객체 생성
  2. JavaBeans 패턴
  3. Builder 패턴

일반 객체 생성의 한계
많은 수의 멤버변수를 가지고 있고 이를 외부에서 받아서 객체가 생성되는 경우,
생성자가 불필요하게 많이 생기는 불편함이 있다.

public class Main{
    public static main(String[] args){
      User user1=new User("고유값","이메일","비밀번호");
      User user2=new User("고유값","이메일","비밀번호",23);
    }
}
class User {
    private String user_serial;
    private String user_email;
    private String user_password;
    private int user_age;
    private String user_phone;
    // 필수값(NOT NULL) 만 포함된 생성자
    // 그 외의 경우의 수를 포함한 생성자
}

JavaBeans 패턴의 한계
JavaBeans 패턴을 사용하면 생성자 숫자를 적게 사용할 수 있다.
하지만 그 값들이 들어가는 중간에 일관성이 깨지는 단점이 있다.

public class Main{
    public static main(String[] args){
      User user=new User();
      user.setSerial("고유값");
      user.setEmail("이메일");
      user.setPassword("비밀번호");
      user.setAge(23);
    }
}
class User {
    private String serial;
    private String email;
    private String password;
    private int age;
    private String phone;
    User() {} // 기본 생성자
    
    void setSerial(String serial) {
      this.serial=serial;
    }
    void setEmail(String email) {
      this.email=email;
    }
    void setPassword(String password) {
      this.password=password;
    }
    void setAge(int age) {
      this.age=age;
    }
    void setPhone(String phone) {
      this.phone=phone;
    }
}

Builder 패턴 (생성자 + JavaBeans 패턴)

빌더 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다.
코드가 장황해지기 때문에 매개변수가 4개 이상은 되어야 값어치를 한다.
API는 시간이 지날수록 매개변수가 많아지는 경향이 있으므로 코드가 더 커질 가능성이 있다.

Builder 패턴은 생성자보다 간결하고 JavaBeans 보다 안전하다.

public class Main{
    public static void main(String[] args){
      UserBuilder user=new UserBuilder.User("시리얼","이메일","비밀번호").setAge(나이-숫자형).setPhone("010-0000-0000");
    }
}
// 불변 클래스
class UserBuilder {
    private final String serial;
    private final String email;
    private final String password;
    private final int age;
    private final String phone;
    
    public static class User {
        private final String serial;
        private final String email;
        private final String password;
        
        private int age=0;
        private String phone=null;
        
        public User(String serial, String email, String password) {
          this.serial=serial;
          this.email=email;
          this.password=password;
        }
        
        public User setAge(int value){
          age=value;
          return this;
        }
        
        public User setPhone(String value){
          phone=value;
          return this;
        }
    }
    
    private UserBuilder(User user){
      serial=user.serial;
      email=user.email;
      password=user.password;
      age=user.age;
      phone=user.phone;
    }
}

아이템 3 | private constructor, enum 과 싱글턴 보증

원제목 | private constructor 과 enum 으로 싱글턴 보증

특정한 객체를 한 개만 생성해야 하는 경우 는 다음 과 같다.

  1. 함수 와 같은 무상태 객체 의 경우.
  2. 설계상 유일해야 하는 시스템 컴포넌트 의 경우

이러한 경우 싱글톤 패턴 이 좋은 선택지가 될 수 있다.
이를 구현하는 방법은 다음과 같다.

  1. Private Constructor 과 Public Member Variable 를 이용
    1.1. 장점
    1.1.1. Singleton 임을 API 차원에서 알 수 있다.
    1.1.2. 간결하다.
    1.2. 단점
    1.2.1. 직렬화 노동 소요가 발생한다. (4번 참고)

  2. Static Factory 메서드에서 발전
    2.1. 장점
    2.1.1. API 를 바꾸지 않고도 싱글턴이 아니게 바꿀 수 있다.
    2.1.2. 제네릭 싱글턴 패턴으로도 만들 수 있다.
    2.1.3. Static Factory 메서드의 참조를 Suplier 로 사용할 수 있다. (아이템 43,44 후술)
    2.2. 단점
    2.2.1. 직렬화 노동 소요가 발생한다. (4번 참고)

  3. Enum 을 이용
    베스트 초이스

  4. 직렬화 노동 소요

Private Constructor 과 Public Member Variables 를 이용

// 1단계 : private 생성자
public class DBConnection {
    private DBConnection() { ... }
}

// 2단계 : public 멤버변수로 객체 생성
public class DBConnection {
    public static final DBConnection conn=new DBConnection();
    private DBConnection() { ... }
}

// 3단계 : 리플랙션 API ( AccessibleObject.setAccesible ) 방어
// p.s. 해당 메서드는 private 생성자 호출을 할 수 있게 만들어 준다.
public class DBConnection throws Exception {
    public static final DBConnection conn=new DBConnection();
    
    private DBConnection() {
      if(conn==null){
        // 객체 생성
      } else {
        // 에러 던지기
        throw Exception;
      }
    }
}

Static Factory Method 를 이용

public class DBConnectin {
    priavet static final DBConnection conn=new DBConnection();
    
    private static DBConnection() {
      // 리턴
    }
    public static DBConnection getInstance() {
      return INSTANCE;
    }
    
    public void  leaveTheBuilding() { ... }
}

Enum 를 이용
public 필드 방식과 비슷하지만
더 간단하고 추가적인 노력 없이 직렬화 할 수 있다.

심지어 아주 복잡한 직렬화 상황이나 리플랙션 공격에서도 제 2의 인스턴스가 생기는 일을 완벽히 막아준다.

단, SingletonEnum 이외의 클래스를 상속 해야 한다면, 이 방법은 사용할 수 없다.
단, Enum 이 다른 인터페이스를 구현하도록 선언 할 수는 있다.

public class User {
    DBConnection;
    
    public void leaveTheBuilding() { ... }
}

아이템 4 | Static Utility Class

원제목 | 인스턴스화를 막으려거든 private 생성자를 사용해라

static 메서드와 static 필드만을 담은 클래스를 만들고 싶을 때가 있다.
일반적으로는 남용해서는 안되는 방법이지만 Math나 Array처럼 특정한 자료형에 대한 메서드들을 모아놓거나, Collection 처럼 특정 인터페이스를 구현하는 객체를 생성해주는 static method(factory)를 모아놓을 수 있다.

이러한 클래스를 만들 때, 객체 생성을 방지하고 싶을 때가 있다.
그런 순간에 아래의 두 방법을 모두 사용하여 효과적인 방지 대책을 세울 수 있다.

  1. private 생성자 생성
  2. 해당 생성자 안에 AssertionError() 던지기
public class DBConnection {
    private DBConnection() {
      throw new AssertionError();
    }
}

아이템 5 | Dependency Injection

원제목 | 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라.

Node.JS 백엔드 프레임워크 Nest.JS 나 Java 백앤드 프레임워크인 Spring 등에서 들어볼 수 있는 개념이다. 그 본질은 객체의 생성 책임을 떠넘기는 것 에 있다.

이 DI 에 대해서 본 도서에서는 맞춤법 기능을 예시로 들고있다.
맞춤법 기능을 구현하는 데에는 2가지 예시가 있다.

  1. 아이템 4 | Static Utility Class
  2. 아이템 3 | Singleton

이를 해결하기 위한 방법으로는 DI 를 사용하면 된다.
그리고 이를 변형시킨 방법이 Factory Method 패턴 이 있다.
구체적인 예시를 보고싶으면 jdk 1.8의 Supplier<T> 를 체크해보자. 이 친구는 한정적 와일드 카드 타입 < ? extends T > 까지 사용하고 있어 좋은 예시가 될 수 있을 것이다.

// 아이템 4
// Static Utility Class 를 잘못 사용한 예 : 유연하지 않고 테스트하기 어렵다.
// p.s. 사전을 교체할 수 없기 때문에 유연하지 않다라고 표현하는 것이다.
public class SpellChecker {
    private static final Lexicon dictionary = ... ;
    
    // 객체 생성 방지
    private SpellChecker() {}
    
    public static boolean isValid(String word) { ... }
    public static List<STring> suggestions(String typo) { ... }
}
// 아이템 3
// Singleton 를 잘못 사용한 예 : 유연하지 않고 테스트하기 어렵다.
// p.s. 사전을 교체할 수 없기 때문에 유연하지 않다라고 표현하는 것이다.
public class SpellChecker() {
    private final Lexicon dictionary = ... ;
    public static SpellChecker INSTANCE=new SpellChecker(...);
    
    // 싱글 객체 생성
    private SpellChecker (...) {}
    
    public boolean isValid(String word) { ... }
    public List<string> suggestions(String typo) { ... }
}
// 아이템 5
// DI 를 사용하여 유연성 확보
public class SpellChecker {
    private final Lexcon dictionary;
    
    public SpellChecker(Lexicon dictionary) {
      this.dictionary=Objects.requiredNonNull(dictionary);
    }
    
    public boolean isValid(String word) { ... }
    public List<String> suggestions(String typo) { ... }
}

아이템 6 | 객체와 캐싱

원제목 | 불필요한 객체 생성을 피해라

아래에서는 불필요한 객체 생성의 예시와 그에 대한 솔루션을 위주로 작성되어 있다.
불필요한 예시의 유형은 다음과 같다.

  1. String s=new String("text");
  2. text.matches("정규표현식");
  3. 오토박싱 Long 그리고 long

이 내용의 마지막에는 이런 내용이 적혀 있다.
이번 아이템을 객체 생성은 비싸니 피해야 한다 라고 오해하면 안된다. 이에 관련된 지침이 몇 가지가 있었다.

  1. 프로그램의 명확성, 간결성, 기능을 위한 객체의 추가 생성은 일반적으로 좋은 일이다.
  2. 아주 무거운 객체가 아닌 이상, 나만의 객체 풀 은 만들지 말자.
  3. 데이터베이스 연결의 경우, 비용이 매우 비싸니 재사용하는 것 이 좋다.

차후 서술할 아이템 50 | 방어적 복사 와 연계해서 생각해보자.


아이템 7 | 다쓴 객체 참조 해제


아이템 8 | finalizer , cleaner 기피


아이템 9 | try-finally > try-with-resources

profile
2022년 12월 9일 부터 노션 페이지에서 작성을 이어가고 있습니다.

0개의 댓글