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

이펙티브 자바 아이템 3 - 생성자나 열거 타입으로 싱글턴임을 보증하라 - 완벽 공략

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

이펙티브 자바 아이템 3 - 생성자나 열거 타입으로 싱글턴임을 보증하라 - 완벽 공략

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

메서드 참조

메소드 하나만 호출하는 람다 표현식을 줄여쓰는 방법이다.

스태틱 메소드 레퍼런스

public class Person {
   LocalDate birthday;

   public Person() {

   }

   public Person(LocalDate birthday) {
      this.birthday = birthday;
   }

   public int getAge() {
      return LocalDate.now().getYear() - birthday.getYear();
   }

   public static int compareByAge(Person a, Person b) {
      return a.birthday.compareTo(b.birthday);
   }
}

Person 이라는 클래스가 있다.

   public static void main(String[] args) {
      List<Person> people = new ArrayList<>();
      people.add(new Person(LocalDate.of(1982, 7, 15)));
      people.add(new Person(LocalDate.of(2011, 3, 2)));
      people.add(new Person(LocalDate.of(2013, 1, 28)));

      people.sort(new Comparator<Person>() {
         @Override
         public int compare(Person a, Person b) {
            return a.birthday.compareTo(b.birthday);
         }
      });
   }

자바 8 이전에는 위와 같이 내부 클래스를 사용해 Comparator 클래스를 만들어 사용했다.

      people.sort(new Comparator<Person>() {
         @Override
         public int compare(Person a, Person b) {
            return a.birthday.compareTo(b.birthday);
         }
      });

여기서 사용하는 Comparator 클래스를 익명 내부 클래스라고 한다.

people.sort((p1, p2) -> p1.birthday.compareTo(p2.birthday));

자바 8 이후로는 위와 같이 사용할 수 있게 되었다.

이 방법을 람다 익스프레션 이라고 한다.

람다 익스프레션 에서 간단히 메서드 호출을 한번 하게되는 경우가 있다.

즉, 람다 익스프레션 안에서 하는 일이 어떠한 메서드 하나를 호출하는 일이라면

메서드 하나를 호출하는 일 을 메서드 레퍼런스를 통해 간추려서 사용할 수 있다.

메서드를 참조하는 방법이다.

일종의 람다 익스프레션을 만드는 일이라고 생각하면 좋다.

   public static int compareByAge(Person a, Person b) {
      return a.birthday.compareTo(b.birthday);
   }

Person 클래스의 compareByAge 메서드를 사용하여 메서드 레퍼런스를 사용하면

   public static void main(String[] args) {
      List<Person> people = new ArrayList<>();
      people.add(new Person(LocalDate.of(1982, 7, 15)));
      people.add(new Person(LocalDate.of(2011, 3, 2)));
      people.add(new Person(LocalDate.of(2013, 1, 28)));

      people.sort(Person::compareByAge);
   }

Person::compareByAge 이 부분을 메서드 레퍼런스라고 한다.

여기서는 static 메서드를 참조했다.

static 메서드를 참조하기 때문에 클래스 이름을 통해 참조했다.

Comparator 가 제공하는 메서드와 매칭이 되기때문에 사용할 수 있다.

    int compare(T o1, T o2); // Comparator 가 제공해야하는 compare 2개의 인자를 받아 int를 리턴

인스턴스 메서드 레퍼런스

인스턴스에 있는 메서드를 레퍼런스하려면

인스턴스를 생성해 사용하면 된다.

   public int compareByAge(Person a, Person b) {
      return a.birthday.compareTo(b.birthday);
   }

Person 클래스 내부의 compareByAge 메서드를 위와 같이 인스턴스 메소드로 변경하고

   public static void main(String[] args) {
      List<Person> people = new ArrayList<>();
      people.add(new Person(LocalDate.of(1982, 7, 15)));
      people.add(new Person(LocalDate.of(2011, 3, 2)));
      people.add(new Person(LocalDate.of(2013, 1, 28)));

      Person person = new Person(null);

      people.sort(person::compareByAge);
   }

위와같이 인스턴스를 통해 메서드를 레퍼런스할 수 있다.

임의 객체의 인스턴스 메소드 레퍼런스

   public static void main(String[] args) {
      List<Person> people = new ArrayList<>();
      people.add(new Person(LocalDate.of(1982, 7, 15)));
      people.add(new Person(LocalDate.of(2011, 3, 2)));
      people.add(new Person(LocalDate.of(2013, 1, 28)));

      people.sort(Person::compareByAge);
   }

위와같이 임의 객체의 인스턴스 메서드를 레퍼런스 할 수 있다.

컴파일러에서는 빨간줄이 뜨지만 호환 가능한 상태가 아니라 그렇다.

   public int compareByAge(Person b) {
      return this.birthday.compareTo(b.birthday);
      }

위와같이 Person 클래스의 메서드를 변경한다.

임의 객체에 대한 인스턴스 메서드 레퍼런스는 첫번째 인자가 자기자신 이 된다.

때문에 메서드의 인자가 하나만 있어도 된다.

임의 객체의 인스턴스 메서드를 레퍼런스 인 경우에만 첫번째 인자가 자기자신 이 된다.

생성자 레퍼런스

   public static void main(String[] args) {
      List<LocalDate> dates = new ArrayList<>();
      dates.add(LocalDate.of(1993, 9, 27));
      dates.add(LocalDate.of(1993, 9, 3));
      dates.add(LocalDate.of(2022, 6, 28));

      dates.stream().map(Person::new).collect(Collectors.toList());
   }

메서드 레퍼런스는 메서드 호출을 한번하는 람다를 간추리는 것이기 때문에

위와같이 생성자를 레퍼런스 할 수 있다.

   public Person() {

   }

   public Person(LocalDate birthday) {
      this.birthday = birthday;
   }

만약 생성자가 2개가 있을 때 인자가 없는 생성자를 사용하고 싶으면 어떻게 해야할까?

함수형 인터페이스를 살펴보면서 알아보자.


함수형 인터페이스

자바 8부터 사용할 수 있는 기술이다.

자바는 함수형 인터페이스라는 기본 인터페이스를 제공한다.

함수형 인터페이스는 타겟 타입 을 지정할 수 있다.

   public static void main(String[] args) {
      List<LocalDate> dates = new ArrayList<>();
      dates.add(LocalDate.of(1982, 7, 15));
      dates.add(LocalDate.of(2011, 3, 2));
      dates.add(LocalDate.of(2013, 1, 28));

      List<Integer> before2000 = dates.stream()
            .filter(d -> d.isBefore(LocalDate.of(2000, 1, 1)))
            .map(LocalDate::getYear)
            .collect(Collectors.toList());
   }

람다 익스프레션, 함수 레퍼런스 부분을 변수로 뺴보면 둘에 대한 타겟 타입이 정의 되어있다.

      Predicate<LocalDate> localDatePredicate = d -> d.isBefore(LocalDate.of(2000, 1, 1));
      Function<LocalDate, Integer> getYear = LocalDate::getYear;

      List<Integer> before2000 = dates.stream()
      .filter(localDatePredicate)
      .map(getYear)
      .collect(Collectors.toList());

여기서 Predicate , Function 이 함수형 인터페이스이다.

@FunctionalInterface
public interface MyFunction {

   String valueOf(Integer integer);
}

위와 같이 함수형 인터페이스를 정의할수도 있다.

인터페이스 안에 메서드 선언이 하나만 있으면 된다.

인터페이스 안에 static 메서드를 만들어도 되지만 구현이 비어있는 선언은 오직 하나만 있어야한다.

이러한 인터페이스에만 @FunctionalInterface 를 붙일 수 있다.

@FunctionalInterface 가 없어도 함수형 인터페이스로 간주가 된다.

중요한건 구현이 비어있는 선언은 오직 하나만 있어야한다 는 점이다.

      Function<Integer, String> intToString; // <input, output>
      Supplier<Integer> integerSupplier; // <output>
      Consumer<Integer> integerConsumer;
      Predicate<Integer> integerPredicate;

자바가 기본으로 제공하는 함수형 인터페이스가 굉장히 많지만 위의 4개를 알면 다른 인터페이스를 알기가 수월해진다.

Function<Integer, String> 2개의 제너릭타입이 있는데

1번째는 input, 두번째는 output 이다.

여기서는 int를 받고 String을 리턴하는 함수다.

Function<Integer, String> intToString = integer -> integer.toString();
Function<Integer, String> intToString = Object::toString;

위와같이 정의해 사용할 수 있다.

Supplier 는 output만 있는 경우이다.

인자가 없이 리턴만 하는 경우다.

Supplier 이라면 Person을 리턴하는 메서드가 된다.

Supplier<Person> integerSupplier = Person::new;

위와같이 하면 기본 생성자를 참조할 수 있게된다.

Function<LocalDate, Person> personFunction = Person::new;

위는 LocalDate를 이용해 Person을 사용하는 생성자를 참조한다.

Consumer 같은 경우는 받는 건 있지만 리턴이 없는 경우다.

대표적인 예로는 System.out.println 이 있다.

Consumer<Integer> integerConsumer = System.out::println;

Predicate 은 Integer 를 받아서 Boolean 을 리턴한다.

무조건 Boolean 을 리턴하기 때문에 리턴타입은 정의하지 않는다.

나머지들은 위의 4가지 함수형 인터페이스에서 파생된 것들이다.


객체 직렬화

객체 직렬화는 객체를 바이트 스트림으로 상호 변환하는 기술이다.

바이트스트림으로 변환한 객체를 파일로 저장하거나 네트워크를 통해 다른 시스템으로 전송할 수 있다.

이사를 가는 것과 비슷하다 생각하면 된다.

이사를 갈 때 집안의 짐들을 트럭에 실을 수 있게 짐을 포장하는 과정이다.

짐을 포장하는 과정을 직렬화, 이삿짐을 푸는 과정을 역 작렬화 라고 생각하면 된다.

보통 10 여년 전에는 자바 객체를 직렬화하거나 역직렬화를 하는 일이 많았지만

오늘날에는 JSON 을 많이 사용한다.

받아서 처리할 곳이 JVM 이라면 직렬화가 유용하지만

다른 시스템이라면 바이트스트림을 보내는 건 무의미하다.

public class Book implements Serializable {
   private String isbn;

   private String title;

   private LocalDate published;

   private String name;

   private int numberOfSold;

   public Book(String isbn, String title, String author, LocalDate published) {
      this.isbn = isbn;
      this.title = title;
      this.published = published;
   }

   @Override
   public String toString() {
      return "Book{" +
            "isbn='" + isbn + '\'' +
            ", title='" + title + '\'' +
            ", published=" + published +
            ", numberOfSold=" + numberOfSold +
            '}';
   }

   public String getIsbn() {
      return isbn;
   }

   public void setIsbn(String isbn) {
      this.isbn = isbn;
   }

   public String getTitle() {
      return title;
   }

   public void setTitle(String title) {
      this.title = title;
   }

   public LocalDate getPublished() {
      return published;
   }

   public void setPublished(LocalDate published) {
      this.published = published;
   }

   public int getNumberOfSold() {
      return numberOfSold;
   }

   public void setNumberOfSold(int numberOfSold) {
      this.numberOfSold = numberOfSold;
   }
}

Book 클래스를 만든다.

public class SerializationExample {
   private void serialize(Book book) {
      try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("book.obj"))) {
         out.writeObject(book);
      } catch (IOException e) {
         throw new RuntimeException(e);
      }
   }

   private Book deserialize() {
      try (ObjectInput in = new ObjectInputStream(new FileInputStream("book.obj"))) {
         return (Book) in.readObject();
      } catch (IOException | ClassNotFoundException e) {
         throw new RuntimeException(e);
      }
   }

   public static void main(String[] args) {
      Book book = new Book("12345", "이팩티브 자바 완벽 공략", "백기선",
                LocalDate.of(2022, 3, 21));
      book.setNumberOfSold(200);

      SerializationExample example = new SerializationExample();
      example.serialize(book);
      Book deserializedBook = example.deserialize();

      System.out.println(book);
      System.out.println(deserializedBook);
   }
}

Book 클래스에 대해 직렬화, 역직렬화를 하는 코드이다.

직렬화, 역직렬화를 하려면 Serializableimplements 헤야한다.

특정값을 직렬화하고 싶지 않다면 해당 필드에 transient 를 붙이면 된다.

   private transient int numberOfSold; // 직렬화에서 제외

transient 를 붙이면 역직렬화시 해당 값이 0이 된다.

public static 필드는 클래스에 할당되는 값이기 때문에 직렬화가 되지 않는다.

직렬화 후 역직렬화시 클래스가 바뀐다면 역직렬화가 될까?

SerializationExample example = new SerializationExample();
Book deserializedBook = example.deserialize();

System.out.println(deserializedBook);

직렬화 후 역직렬화 전에 Book 클래스를 변경한다면 역직렬화가 되지 않는다.

serialVersionUID 가 맞지 않아서 오류가 발생한다고 콘솔에 출력된다.

Serializable 을 구현한 클래스에 명시적으로 serialVersionUID 를 선언하지 않으면

JVM 이 런타임 중에 임의 적으로 serialVersionUID 를 만들어 준다.

클래스가 바뀌면 serialVersionUID 를 새로 만들어준다.

serialVersionUID 를 바꾸지 않으면 동일한 serialVersionUID 를 유지하기 때문에

클래스가 변경이 되어도 직렬화, 역직렬화가 된다.

만약, 필드가 달라졌다 하더라도 역직렬화를 하려면 같은 serialVersionUID 를 유지하면 된다.

private static final long serialVersionUID = 1L;

private static final long 타입으로 serialVersionUID 를 선언하면 된다.

값은 임의대로 바꾸어도 된다.

필드가 없어지더라도 serialVersionUID 가 동일하면 역직렬화가 가능하다.

역직렬화시 사라진 필드는 제외하고 읽어온다.

좀더 유연하게 직렬화, 역직렬화를 하려면 serialVersionUID 를 직접관리하면 된다.

반응형

댓글