본문 바로가기
개발 공부/Java

이펙티브 자바 아이템 2 - 생성자에 매개변수가 많다면 빌더를 고려하라 - 완벽 공략

by 개발인생 2022. 9. 7.
반응형

아이템 2 - 생성자에 매개변수가 많다면 빌더를 고려하라 - 완벽 공략

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

자바 빈

(주로 GUI에서) 재사용 가능한 소프트웨어 컴포넌트이다.

GUI에서 이루어지는 프로퍼티 값을 읽어오거나 값을 저장하거나 하는 등 모든 것들을 정의한게 자바 빈 스펙이다.

자바 빈이 지켜야할 규약에는 다음과 같다.

  • 아규먼트 없는 기본 생성자

아규먼트 없는 기본 생성자를 사용하는 이유는 자바 빈을 사용하는 곳에서 객체를 만들기 쉽게 하기 위해서이다.

자바 빈은 리플렉션을 통해서 객체를 만들고 값을 주입하기 때문에

생성자에 아규먼트가 있으면 리플렉션으로 객체를 만들기가 까다로워진다.

  • getter와 setter 메소드 이름 규약

자바 빈즈에 사용하는 곳에서 자바 빈에 저장된 값을 사용하기 위해 특별한 네이밍 규칙을 사용한다.

여러 곳에서 공통적으로 사용하기 위해 정한 규약이다.

  • Serializable 인터페이스 구현

Serializable 을 구현하는 이유는 직렬화, 역직렬화 때문이다.

직렬화란 객체에 있는 모든 값들을 어딘가에 그 상태 그대로 저장가능한 상태로 만든다는 뜻이다.

직렬화, 역직렬화는 값을 어딘가에 저장해 두었다가(직렬화) 다시 꺼내서 쓸 수 있도록(역직렬화) 하기 위해서이다.

하지만 오늘날은 자바빈 스펙 중에서 getter와 setter 메소드 이름 규약 가 주로 사용된다.

특정 프로퍼티에 접근하는 여러 프레임워크의 일관적인 접근 방법을 위해 getter와 setter 메소드 이름 규약 이 필요하다.


객체 얼리기 (freezing)

객체 얼리기는 자바스크립트에 있는 기능이다.

const person = {
    'name': 'jun'
}

person.name = "hyun"

console.log(person.name)

자바스크립트에서는 선언한 프로퍼티를 조작할 수 있다.


public class Person {

   private String name;
   private int birthYear;
   private List<String> kids;

   public Person(String name, int birthYear) {
      this.name = name;
      this.birthYear = birthYear;
      this.kids = new ArrayList<>();
   }
}

마치 자바에서 final로 선언한 레퍼런스 객체의 값을 조작할 수 있는 것과 비슷하다.

   public static void main(String[] args) {
      Person person = new Person("junHyun", 1993);
      person.name = "lee";
   }
   public static void main(String[] args) {
      final Person person = new Person("junHyun", 1993);

      person = new Person("hearan", 1993); // 오류 발생
   }

final 키워드를 붙이면 인스턴스 변수가 다른 값으로 할당되는 걸 막을 수 있다.

자바스크립트에서는 이러한 객체의 변동을 막기위해 freeze라는 함수를 제공한다.

const person = {
    'name': 'jun'
}

Object.freeze(person)

person.name = "hyun" // 오류 발생

console.log(person.name)

이걸 자바에서 구현하려면 해당 객체가 변동불가능한 상태인지에 대한 플래그 값과

그 값을 체크하는 메서드를 만들어야한다.

public class Person {

   private String name;
   private int birthYear;
   private List<String> kids;
   private boolean isFreeze;

   public void setName(String name) {
      checkIfObjectIsFrozen();
      .... 나머지 코드 작성
   }

   private boolean checkIfObjectIsFrozen() {
      if (this.isFreeze)
         .... 나머지 코드 작성
   }

   public Person(String name, int birthYear) {
      this.name = name;
      this.birthYear = birthYear;
      this.kids = new ArrayList<>();
   }
}

이 방법은 변경이 가능한 객체인데 중간에 객체를 얼려 변경이 불가능한 객체로 만든다는 뜻이다.

언제 객체가 얼려졌는지 알기 어렵고, 예상하지 못한 사이드 이펙트가 발생할 여지가 크다.

떄문에 잘 사용되지 않는다.

한가지 주의해야할 건 가변객체와 불변객체이다.

public class Person {

   private String name;
   private int birthYear;
   private List<String> kids;

   public Person(String name, int birthYear) {
      this.name = name;
      this.birthYear = birthYear;
      this.kids = new ArrayList<>();
   }

   public static void main(String[] args) {
      Person person = new Person("junHyun", 1993);
      person.name = "hong";
   }
}

가변 객체란 언제든지 변경이 가능한 객치이다.

final 을 붙이지 않은 프로퍼티들은 언제든지 변경이 가능하다.

public class Person {

   private final String name;
   private final int birthYear;
   private final List<String> kids;

   public Person(String name, int birthYear) {
      this.name = name;
      this.birthYear = birthYear;
      this.kids = new ArrayList<>();
   }

   public static void main(String[] args) {
      Person person = new Person("junHyun", 1993);
      person.kids.add("gaon");
   }
}

final 키워드를 붙여 불변으로 만들 수 있다.

하지만 레퍼런스값들은 해당 프로퍼티에 새로운 값을 할당하지 못한다는 뜻이지

해당 프로퍼티의 레퍼런스의 안에 있는 값 자체가 불변객체가 되지는 않는다.

때문에 위의 코드에서 kids 프로퍼티에 새로운 List 를 할당할 수는 없지만

kids 프로퍼티에 있는 List 에 값을 추가할 수는 있게 된다.

public final class Person {

   private final String name;
   private final int birthYear;
   private final List<String> kids;

   public Person(String name, int birthYear) {
      this.name = name;
      this.birthYear = birthYear;
      this.kids = new ArrayList<>();
   }

   public static void main(String[] args) {
      Person person = new Person("junHyun", 1993);
      person.kids.add("gaon");
   }
}

클래스에 final 키워드룰 붙여 상속을 막을수도 있다.


빌더 패턴

디자인 패턴에서의 빌더 패턴의 목적은 복잡한 객체를 만드는 프로세스를 별도의 클래스로 분리시키는 것이다.

원래 코드의 양을 분리를 통해 줄일 수 있고,

단일 책임의 원칙을 적용해 객체를 생성하는 과정을 별도의 클래스로 분리할 수 있는 장점이있다.

주요한 구성요소로는

  • Builder 인터페이스
  • Builder 인터페이스의 구현체
  • Director (자주 만들어지는 형태의 객체의 생성을 위임하는 클래스)

중요한 건 디자인 패턴의 목적이지 모양이 아니다.

비슷한 모양이더라도 다른 디자인 패턴의 이름을 가진 경우도 있다.


IllegalArgumentException

IllegalArgumentException 은 잘못된 인자를 넘겨 받았을 때 사용할 수 있는 기본 런타임 예외이다.

자바에서 제공해주는 unchecked exception 중의 하나이다.

즉, RuntimeException 을 상속받은 예외 중 하나이다.

IllegalArgumentException 을 발생시킬 때는 최소한 어떤 아규먼트가 왜 잘못되었는지를 같이 포함시키는 게 좋다.

질문1) checked exception과 unchecked exception의 차이?

  • checked exception

checked exception은 컴파일 타임에 체크를 해주기 때문에

다시 checked exception을 던지거나 예외를 try/catch 로 잡아 처리를 해야한다.

이렇게하지 않으면 에러가 발생해 코드를 작성할 수 없다.

  • unchecked exception

unchecked exception은 굳이 unchecked exception을 던지거나 예외를 try/catch 로 잡아 처리를 할 필요가 없다.

checked exception 은 예외를 던져서 받은 클라이언트가 예외를 받고 복구가 가능한 상황에서

예외를 던져서 받은 클라이언트가 복구가 불가능한 상황에서는 unchecked exception 을 사용한다.

트랜젝션과 예외는 아무런 상관이 없다.

질문2) 간혹 메소드 선언부에 unchecked exception을 선언하는 이유는?

public class Order {

   public void updateDeliveryDate(LocalDate deliveryDate) throws IllegalArgumentException {
      if (deliveryDate.isBefore(LocalDate.now())) {
         // TODO 과거로 배송을 하는 경우
         throw new IllegalArgumentException("delivery date can't be earlier than " + LocalDate.now());
      }
   }
}

위와 같이 unchecked exception 을 메서드 선언부에 선언할 수 있다.

굳이 선언을 안해도 되는데 선언을 하는 이유는

코드를 사용하는 클라이언트에게 명시적으로 알려주고 싶을 때 선언함을써 클라이언트에게 알려줄 수 있다.

하지만 너무 많은 unchecked exception 이 발생할 수 있을 때는 메서드 선언부에 선언함으로써

코드의 가독성이 떨어지기 때문에 보통 checked exception 을 표기하게 된다.

질문3) checked exception은 왜 사용할까?

unchecked exception 은 원하면 try/catch 로 잡아서 처리할 수도 메소드 선언부에 선언할 수도 있다.

하지만 checked exception 은 강제하는 부분이 많다.

그래서 보통은 unchecked exception 계열의 exception 들을 많이 사용한다.

checked exception 은 해당 에러가 발생했을 때 클라이언트가 후속 조치를 해주기 바라는 경우 사용한다.

과제1) 자바의 모든 RuntimeException 클래스 이름 한번씩 읽어보기.

  • ArithmeticException - 정수를 0으로 나눴을 때
  • ArrayStoreException - 배열 유형이 허락하지 않는 객체를 배열에 저장하려했을 때
  • ArrayIndexOutOfBoundsException - 배열을 참조하는 인덱스가 잘못되었을 때
  • ClassCastException - 형변환을 적절하지 못하게 했을 때
  • NullPointerException - Null 객체를 참조했을 때
  • NegativeArraySizeException - 배열의 크기가 음수일 때
  • IndexOutOfBoundsException - 객체의 범위를 벗어난 인덱스를 사용했을 때
  • IllegalArgumentException - 메서드 유형이 일치하지 않는 매개변수를 전달했을 때
  • IllegalMonitorStateException - 스레드가 스레드에 속하지 ㅇ낳는 객체를 모니터하려고 기다릴 때
  • IllegalStateException - 적절하지 않은 떄에 메서드를 호출할 떄

과제2) 이 링크에 있는 글을 꼭 읽으세요.

https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html


가변인수

가변인수는 여러 인자를 받을 수 있는 가변적인 매개변수이다.

public class VarargsSamples {

   public void printNumbers(int... numbers) {
      System.out.println(numbers.getClass().getCanonicalName());
      System.out.println(numbers.getClass().getComponentType());

      Arrays.stream(numbers).forEach(System.out::println);
   }

   public static void main(String[] args) {
      VarargsSamples samples = new VarargsSamples();

      samples.printNumbers(1, 2, 3, 4);
   }
}
  • 가변인수는 메서드에 오직 하나만 선언할 수 있다.

    public class VarargsSamples {
    
     public void printNumbers(int... numbers, String... names) { // 에러 발생
        System.out.println(numbers.getClass().getCanonicalName());
        System.out.println(numbers.getClass().getComponentType());
    
        Arrays.stream(numbers).forEach(System.out::println);
     }
    
     public static void main(String[] args) {
        VarargsSamples samples = new VarargsSamples();
    
        samples.printNumbers(1, 2, 3, 4);
     }
    }
  • 가변인수는 매서드의 가장 마지막 매개 변수가 되어야 한다.

public class VarargsSamples {

   public void printNumbers(int... numbers, String names) { // 에러 발생
      System.out.println(numbers.getClass().getCanonicalName());
      System.out.println(numbers.getClass().getComponentType());

      Arrays.stream(numbers).forEach(System.out::println);
   }

   public static void main(String[] args) {
      VarargsSamples samples = new VarargsSamples();

      samples.printNumbers(1, 2, 3, 4);
   }
}

가변인수는 항상 파라미터가 여러개 있을 때 가장 마지막에 하나만 위치해야한다.

public class VarargsSamples {

   public void printNumbers(String names, int... numbers) { // 가변인수는 마지막에 하나만 존재해야한다.
      System.out.println(numbers.getClass().getCanonicalName()); // 배열로 들어온다.
      System.out.println(numbers.getClass().getComponentType()); // 배열 안에 있는 값들의 타입을 출력

      Arrays.stream(numbers).forEach(System.out::println);
   }

   public static void main(String[] args) {
      VarargsSamples samples = new VarargsSamples();

      samples.printNumbers(); // 가변인수를 아예 전달하지 않을수도 있다.
   }
}
반응형

댓글