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

이펙티브 자바 아이템 1 - 생성자 대신 정적 팩터리 메서드를 고려하라 - 완벽 공략

by 개발인생 2022. 8. 26.
반응형

아이템 1 - 생성자 대신 정적 팩터리 메서드를 고려하라 - 완벽 공략

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

열거 타입 = Enumeration

Enum 이란 상수 목록을 담을 수 있는 데이터 타입이다.

public enum OrderStatus {
   ORDERED, SHIPPED, DELIVERED
}

가령 주문의 상태를 나타내는 필드가 있다고 가정해보면

public class Order {

   private boolean prime;
   private boolean urgent;

   private OrderStatus status;

   public static Order primeOrder() {
      Order order = new Order();
      order.prime = true;

      return order;
   }

   public static Order urgentOrder() {
      Order order = new Order();
      order.urgent = true;
      return order;
   }
}

위와 같이 클래스를 구성할 수 있을 것이다.

하지만 Enum을 사용하지 않게된다면

public class Order {

   private boolean prime;
   private boolean urgent;

   // 0 - 주문 확인 중
   // 1 - 제품 준비 중
   // 2 - 배송 중
   // 3 - 배송 완료
   private int status;

   public static Order primeOrder() {
      Order order = new Order();
      order.prime = true;

      return order;
   }

   public static Order urgentOrder() {
      Order order = new Order();
      order.urgent = true;
      return order;
   }
}

위와 같이 주석을 추가하여 상태에 대한 자세한 설명이 필요했을 것이다.

심지어 status 라는 값에 원하는 값이 아닌 다른 값이 들어갈 가능성이 생긴다.

하지만 Enum 을 사용하면 status 에 저장될 값을 제한 할 수 있다.

이걸 Type - Safety 를 보장한다 말한다.

뿐만 아니라 Enum 인스턴스는 JVM 내에 하나만 존재하는 것이 보장되므로 싱글톤 패턴 구현시 유용하게 사용되기도 한다.

질문 1) 특정 enum 타입이 가질 수 있는 모든 값을 순회하며 출력하라.

public class Main {
   public static void main(String[] args) {
      Arrays.asList(OrderStatus.values()).forEach(System.out::println);
   }
}

질문 2) enum 은 자바 클래스처럼 생성자, 메소드, 필드를 가질 수 있는가?

public enum OrderStatus {
   ORDERED(0), SHIPPED(1), DELIVERED(2);

   private int statusIntValue;

   OrderStatus(int statusIntValue) {
      this.statusIntValue = statusIntValue;
   }
}

enum 의 생성자는 private 이기 떄문에 외부에서 접근할 수 없다.

질문 3) enum 의 값은 == 연산자로 동일성을 비교할 수 있는가?

public class Main {
   public static void main(String[] args) {
      if (order.status == OrderStatus.ORDERED) {

      }
   }
}

이런식으로 == 비교가 가능하다.

== 비교는 NullPointException 이 발생하지 않기때문에 equals 를 사용한 비교보다 안전하다.

과제) enum 을 key 로 사용하는 Map 을 정의하세요. 또는 enum 을 담고 있는 Set 을 만들어 보세요.

public class Item01Main {
   public static void main(String[] args) {
      Map<OrderStatus, String> enumMap = new EnumMap<>(OrderStatus.class);

      EnumSet<OrderStatus> enumSet = EnumSet.allOf(OrderStatus.class);
   }
}

EnumMap 을 사용하면 성능상 이점을 가져올 수 있다.

HashMap 은 hash 값을 계산하여 table 을 제어하는 형식으로 데이터를 관리하지만

EnumMap 은 열거형 상수가 정의된 순서를 가지고, 배열의 index만 가져오면 되기때문에 대부분의 상황에서 성능이 더 좋다.

또한, HashMap 은 일정 이상의 자료가 저장되면 resizing 을 하지만

EnumMap 은 시작부터 데이터의 사이즈가 enum 으로 제한되기 때문에 더 빠른 해시 계산과 같은 몇가지 추가 성능 최적화를

수행할 수 있습니다.

HashSetHashMap 과 같이 map 의 value가 있다, 없다를 표현하는 지시자 같은 값이 들어가게 된다.

반면 EnumSet 은 값이 있다 없다만 표시하면 되니 10101011 와 같은 비트 백터 로 구현이 가능하다.


플라이웨이트 패턴

같은 객체가 자주 요쳥되는 상황에서는 플라이웨이트 패턴을 사용할 수 있다.

여기서 중요한건 같은 객체자주 사용된다 이다.

같은 객체가 자주 사용되니 어딘가에 저장해두던가 캐싱을 사용할 수 있다.

플라이웨이트 패턴은 객체를 재사용하는 방법이다.

플라이웨이트 패턴은 객체를 가볍게 만들어 메모리 사용을 줄이는 패턴이다.

객체 안에서 자주 변경되는 속성과 변경되지 않는 속성을 분리해서

변경되지 않는 속성을 Flyweight Factory 에 모아두고 Flyweight Factory 에서 꺼내 쓰는 방법이다.

Flyweight Factory 는 개념적 용어이다.

간단히말해 변경되지 않는 속성을 어딘가에 모아두고 그 어딘가에 서 꺼내 쓰는 방법이다.

public class Character {

   private char value;
   private String color;
   private String fontFamily;
   private int fontSize;

   public Character(char value, String color, String fontFamily, int fontSize) {
      this.value = value;
      this.color = color;
      this.fontFamily = fontFamily;
      this.fontSize = fontSize;
   }
}

Character 라는 클래스가 있다고 하자.

여기서 fontFamilyfontSize 는 주로 변경하지 않는 값이다.

public class Character {

   private char value;
   private String color;
   private Font font;

   public Character(char value, String color, Font font) {
      this.value = value;
      this.color = color;
      this.font = font;
   } 
}
public class Font {

   private String fontFamily;
   private int fontSize;

   public Font(String fontFamily, int fontSize) {
      this.fontFamily = fontFamily;
      this.fontSize = fontSize;
   }
}

fontFamilyfontSizeFont 라는 클래스로 값을 따로 모아두었다.

public class FontFactory {

   private Map<String, Font> cache = new HashMap<>();

   public Font getFont(String font) {
      if (cache.containsKey(font)) {
         return cache.get(font);
      } else {
         String[] split = font.split(":");
         Font newFont = new Font(split[0], Integer.parseInt(split[1]));
         cache.put(font, newFont);
         return newFont;
      }
   }
}

FontFactory 를 만들어 자주 사용하는 값들을 캐싱하는 로직을 추가한다.

public class HelloWorld {
   public static void main(String[] args) {
      FontFactory fontFactory = new FontFactory();

      Character c1 = new Character('h', "white", fontFactory.getFont("nanum:12"));
      Character c2 = new Character('h', "white", fontFactory.getFont("nanum:12"));
      Character c3 = new Character('h', "white", fontFactory.getFont("nanum:12"));
   }
}

위와 같이 FontFactory 를 통해 자주 사용하는 객체를 캐싱해 사용할 수 있다.

public class FontFactory {

   private static Map<String, Font> cache = new HashMap<>();

   public static Font getFont(String font) {
      if (cache.containsKey(font)) {
         return cache.get(font);
      } else {
         String[] split = font.split(":");
         Font newFont = new Font(split[0], Integer.parseInt(split[1]));
         cache.put(font, newFont);
         return newFont;
      }
   }
}

또한 정적 팩토리 메서드 를 사용하여 캐싱된 객체를 받을 수 있다.

플라이웨이트 패턴을 사용하면 메모리 사용량을 줄일 수 있다.


인터페이스와 정적 메서드

자바 8과 9에서 주요 인터페이스의 변화

자바 8 이후로는 인터페이스에 메서드를 정의할 수 있다.

이때 default 라는 키워드를 메서드에 붙여야한다.

이를 기본 메서드 라고 한다.

기본 메서드 는 인스턴스에서만 사용할 수 있다.

public interface HelloService {
   String hello();

   default String hi() {
      return "Hi";   
   }
}

자바 9버전에서는 인터페이스에서 private static 메서드를 정의할 수 있다.

public interface HelloService {
    String hello();

    default String hi() {
        return "Hi";
    }

    private static HelloService of(String lang) {
        if (lang.equals("ko")) {
            return new KoreanHelloService();
        } else {
            return new EnglishHelloService();
        }
    }
}

이러한 인터페이스의 변화로 인해 8버전, 9버전 이후로는

인터페이스들의 기능들이 풍부해졌다.

그렇기 때문에 인스턴스화 불가 클래스 를 만들 이유가 많이 줄어들었다.

그러나 인터페이스는 프라이빗한 필드를 가질 수 없기 때문에

프리이빗한 필드를 사용하는 Helper나 유틸리티 클래스를 만들어야 하는 경우도 있다.

질문1) 내림차순으로 정렬하는 Comparator 를 만들고 List를 정렬하라.

public class Item01Main {
   public static void main(String[] args) {
      List<Integer> numbers = new ArrayList<>();
      numbers.add(100);
      numbers.add(20);
      numbers.add(33);
      numbers.add(3);

      System.out.println(numbers);

      Comparator<Integer> desc = new Comparator<>() {
         @Override
         public int compare(Integer t1, Integer t2) {
            return t2 - t1;
         }
      };

      Collections.sort(numbers, desc);

      System.out.println(numbers);
   }
}

질문2) 질문1에서 만든 Comparator를 사용해서 오름차순으로 정렬하라.

public class Item01Main {
   public static void main(String[] args) {
      List<Integer> numbers = new ArrayList<>();
      numbers.add(100);
      numbers.add(20);
      numbers.add(33);
      numbers.add(3);

      System.out.println(numbers);

      Comparator<Integer> desc = new Comparator<>() {
         @Override
         public int compare(Integer t1, Integer t2) {
            return t2 - t1;
         }
      };

      Collections.sort(numbers, desc.reversed());

      System.out.println(numbers);
   }
}

서비스 제공자 프레임워크

서비스 제공자 프레임워크 또는 서비스 제공자 인터페이스 패턴이라 불리는 것은 개념적인 이야기다.

즉, 다양한 구현 방법과 변형이 존재할 수 있다.

목적이 중요한 것 이지 구현 형태가 중요한게 아니다.

목적은 확장 가능한 애플리케이션 을 만드는 방법을 제공하는 것이다.

확장이 가능하다는 건 코드는 그대로 유지되면서 외적인 요인을 변경했을 때 애플리케이션의 동작을 다르게 동작할 수 있게 만들 수 있는 것 을 말한다.

예제는 의존 객체 프레임워크(Spring) 을 사용한다.

주요 구성요소는 다음과 같다.

서비스 제공자 인터페이스 (SPI)와 서비스 제공자 (서비스 구현체)

public interface HelloService {
   String hello();

   default String hi() {
      return "Hi";
   }

   private static HelloService of(String lang) {
      if (lang.equals("ko")) {
         return new KoreanHelloService();
      } else {
         return new EnglishHelloService();
      }
   }
}

어떤 서비스를 확장 가능하게 만들 것이냐 를 서비스 제공자 인터페이스라고 부른다.

다양한 형태로 구현체가 만들어질 수 있는 인터페이스이다.

예제코드에서 HelloService 는 서비스 제공자 인터페이스이다.

간단히 서비스 인터페이스라고 생각해도 된다.

서비스 제공자 인터페이스의 구현체는 같은 프로젝트에 있거나 다른 프로젝트에 있어도 된다.

서비스 제공자 등록 API (서비스 인터페이스의 구현체를 등록하는 방법)

서비스 구현체를 등록하는 방법을 제공한다.

@Configuration
public class AppConfig {

   @Bean
   public HelloService helloService() {
      return new KoreanHelloService();
   }
}

Spring 의 경우 @Configuration 가 있는 클래스 안에 @Bean 을 통해 서비스 구현체를 등록하게 된다.

이게 바로 서비스 제공자 등록 API 라고 할 수 있다.

서비스 접근 API (서비스의 클라이언트가 서비스 인터페이스의 인스턴스를 가져올 때 사용하는 API)

서비스 접근 API는 등록된 서비스를 가져오는 방법 이다.

public class HelloWorld {
   public static void main(String[] args) {
      ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
      HelloService helloService = applicationContext.getBean(HelloService.class);
      System.out.println(helloService);
   }
}

Spring 에서는 ApplicationContext 를 정의하고 getBean 메서드를 통해 서비스를 가져와 사용한다.

다른 방법으로는 @Autowired 를 통해 빈을 주입받아 사용하는 방법도 있다.

여기까지가 Spring(의존 객체 프레임워크) 를 사용했을 때 서비스 제공자 프레임워크의 관점에서본 각각의 역할이다.

자바에서는 ServiceLoader 라는 서비스 제공 프레임워크의 구현체가 있다.


리플렉션

서비스 구현체 인터페이스가 없더라도 리플렉션 을 사용해 특정한 구현체의 인스턴스를 만들 수 있다.

리플렉션이란 클래스 로더를 통해 읽어온 클래스 정보를 사용하는 기술 이다.

클래스 정보는 JVM 에 있는 클래스 로더가 읽어들여 해당하는 정보를 메모리에 저장해둔다.

이때 읽어온 클래스의 정보가 곧 거울에 비친 모습이라 생각하면 된다.

가령 클래스 로더가 읽어들인 클래스에 애노테이션 여부를 확인하고 기능을 추가하는 작업이 가능해진다.

혹은 특정한 네이밍 패턴에 해당하는 메서드나 필드를 찾는 작업도 가능해진다.

public class HelloWorld {
   public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
      Class<?> aClass = Class.forName("effective.code.item01.KoreanHelloService");
      Constructor<?> constructor = aClass.getConstructor();
      HelloService helloService = (HelloService) constructor.newInstance();
      System.out.println(helloService.hello());
   }
}

다음과 같이 클래스의 풀네임을 이용해 클래스의 인스턴스를 만들 수 있다.

클래스의 인스턴스를 이용해 불러온 서비스의 구현체의 생성자를 가져와 서비스의 인스턴스를 만들 수 있다.

public class HelloWorld {
   public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
      Class<?> aClass = Class.forName("effective.code.item01.KoreanHelloService");

      Method[] methods = aClass.getDeclaredMethods();
   }
}

위와 같이 해당 서비스의 정보를 알 수 있다.

필드의 값을 변경하거나 인스턴스의 메서드를 호출하는 것도 가능하다.

접근 지시자와 관련이 없기때문에 private 이 붙어있어도 사용하는게 가능하다.

리플렉션을 사용하면 문자열만 가지고도 해당하는 타입의 인스턴스를 만들 수 있게된다.

반응형

댓글