✏️ Keyword

  • 정적 팩터리 메서드 (static factory method)
  • 불변 클래스 (immutable class)

✏️ 핵심 정리

  • 정적 팩토리 메서드란?
    • 객체 생성하기 위한 메서드
    • public static Integer valueOf(int value) { return new Integer(value); }
  • 정적 팩터리 메서드 장점
    • 메서드 이름을 통한 가독성 향상
    • 호출할 때마다 인스턴스를 새로 생성하지 않아도 됨
    • 반환 타입의 하위 타입 클래스를 반환 가능
    • 입력 매개변수에 따라 다양한 타입의 객체 반환 가능
    • 정적팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 됨
  • 정적 팩터리 메서드 단점
    • 클래스의 인스턴스를 생성하기 위해 정적 팩토리 메서드만 제공하면 하위 클래스 생성 불가
    • 정적팩터리메서드는 프레그래머가 찾기 어려움

✏️ My Think

정적 팩터리 메서드의 매력을 재발견

💡 계층별로 입출력 모델을 구분하는 컨벤션
현재 개발 컨벤션으로 계층별로 입출력 모델을 명확히 구분하고 있다.
Controller에서는 Request/Response를 사용하고, Service에서는 Input/Output을, Repository와 Mapper에서는 Domain을 사용하는 방식이다.
이러한 구조 속에서 계층 간 데이터 교환을 위해 이미 정적 팩터리 메서드를 자주 사용하고 있었다.
이번 아이템을 통해서야 내가 사용하던 메서드가 정적 팩터리 메서드였다는 사실을 깨달았고, 그 장점들을 명확히 인지하게 되었다.
특히, 정적 팩터리 메서드의 명명 방식을 적용하면 코드의 가독성과 유지보수성이 높아질 수 있다는 점에서 큰 가치를 느꼈다.

💡 무작정 public 생성자를 제공하는 습관 고치기
’무작정 public 생성자를 제공하는 습관을 고치자’는 말이 특히 와닿았다.
프로젝트의 압박 속에서 코드를 마치 공장에서 찍어내듯이 작성하던 나의 모습을 반성하게 되었다.
정말로 현재 상황에서 필요한가를 한 번 더 생각하고, 생성자 대신 정적 팩터리 메서드를 사용함으로써 얻을 수 있는 장점을 깨닫게 되었다.

이번 아이템을 통해 정적 팩터리 메서드와 public 생성자의 장단점을 명확히 이해하게 되었고, 앞으로는 이유를 가지고 적절한 방식을 선택할 수 있는 개발자가 되고 싶다.

💡 static의 의미와 활용
정적 팩터리 메서드를 사용하면서 자연스럽게 static 키워드에 대해 다시 생각하게 되었다.

static이란 무엇이며, 언제 붙이고 언제 붙일 수 없는가에 대해 깊이 고민해 보았다.

Java와 같은 객체 지향 언어에서 static만큼 강력한 무기가 또 있을까 하는 생각이 들었고, 그동안 static에 대한 개념을 충분히 이해하지 못해 사용을 꺼렸던 나 자신을 돌아보게 되었다.


✏️ Book

1. 정적 팩토리 메서드 개념

  • 정적 팩토리 메서드 란?
    • “ 객체를 생성하기 위한 메서드 ” 로, 보통 클래스의 정적 메서드(static)로 정의됨
    • 이를 통해 객체 생성을 제어하고, 생성자에 비해 더 많은 유연성을 제공
  • 정적 팩토리 메서드 예시
    • 예시 1: Java의 Integer 클래스는 정적 팩토리 메서드 valueOf를 제공함

      public class Integer {
      	private final int value;
        private Integer(int value) { this.value = value; }
      
        // 정적 팩토리 메서드 valueOf
        public static Integer valueOf(int value) { return new Integer(value); }
      }
      // 사용 예 : Integer intValue = Integer.valueOf(42);
    • 예시 2: 사용자 정의 클래스의 정적팩토리 메서드 사용 예시

      public class Person { 
      	private String name; 
      	private int age; 
      	
      	private Person(String name, int age) { 
      		this.name = name; 
      		this.age = age; 
      	}
      	
      	// 사용자 정의 Person 클래스에서 정적 팩토리 메서드 사용 예시 
      	public static Person createWithNameAndAge(String name, int age) { 
      		return new Person(name, age); 
      	} 
      	
      	public static Person createWithName(String name) { 
      		return new Person(name, 0);
      	} 
      } 
      
      // 사용 예
      // Person person1 = Person.createWithNameAndAge("Alice", 30); 
      // Person person2 = Person.createWithName("Bob"); 

2. 정적 팩토리 메서드 장점 (생성자 보다 좋은점)

  • 장점 1. 메서드 이름을 통한 가독성 향상

    • 생성자는 이름 가질 수 없음 ↔ 정적 팩토리 메서드는 createWithName()과 같은 이름을 통해 어떤 역할을 하는지 의미 전달 가능
    • 생성자는 1개의 시그니처로는 1개의 생성자만 만들 수 있음 ↔ 정적 팩토리 메서드는 제약이 없음
  • 장점 2. 호출할 때마다 인스턴스를 새로 생성하지 않아도 됨

    • 정적 팩토리 메서드를 통해 객체 생성 제어 하면, 동일한 요청에 대해 동일한 객체를 반환 가능 → 객체 생명주기 철저히 관리 가능 ( = 불필요한 객체 생성 방지, 메모리 사용 최적화 가능 )

    • 인스턴스 캐싱하여 재사용 가능

      • ex ) Integer.valueOf 메서드는 -128 ~ 127 범위 정수를 캐싱함 → 해당 범위 내에서 반복된 요청에 대해 새로운 객체를 생성하지 않고, 이미 생성된 객체를 반환 ( 재사용)

        public class Integer { 
        	private static final Integer[] cache = new Integer[256]; 
        	
        	static { 
        		for (int i = 0; i < cache.length; i++) { 
        			cache[i] = new Integer(i - 128); 
        		} 
        	} 
        	
        	public static Integer valueOf(int value) { 
        		if (value >= -128 && value <= 127) { 
        			return cache[value + 128]; 
        		} 
        		return new Integer(value); 
        	} 
        }
    • 인스턴스를 통제하여 싱글톤 구현 가능

      • 정적 팩토리 메서드 사용하여 클래스의 인스턴스를 하나만 유지하는 싱글톤 패턴 구현 가능 → 특정 클래스의 인스턴스가 단 하나만 존재하도록 보장 할 수 있음

        public class Singleton { 
        	private static final Singleton INSTANCE = new Singleton(); 
        	private Singleton() { } // 인스턴스화를 막기 위해 생성자 private 선언
        	public static Singleton getInstance() { return INSTANCE; } 
        }
  • 장점 3. 반환 타입의 하위 타입 클래스를 반환 가능

    • 하위 클래스 반환 가능하면 장점

      • 반환 타입을 유연하게 설정할 수 있음
      • API 만들 때 구현클래스를 공개하지 않고도 해당 객체를 반환할 수 있음
    • 예시 코드 (1) Parent 클래스의 정적 팩토리 메서드 create

      • create 메소드의 반환 타입은 Parent 이지만 조건에 따라 하위 클래스인 ChildAChildB 인스턴스를 반환하고 있음 → 즉, 반환 타입을 유연하게 설정할 수 있다!
      class Parent {
          // 정적 팩토리 메서드
          public static Parent create(boolean condition) {
              if (condition) {
                  return new ChildA();
              } else {
                  return new ChildB();
              }
          }
      }
      
      class ChildA extends Parent {
          @Override
          public String toString() {
              return "ChildA instance";
          }
      }
      
      class ChildB extends Parent {
          @Override
          public String toString() {
              return "ChildB instance";
          }
      }
      
      public class Main {
          public static void main(String[] args) {
              Parent p1 = Parent.create(true);
              Parent p2 = Parent.create(false);
      
              System.out.println(p1); // ChildA instance
              System.out.println(p2); // ChildB instance
          }
      }
    • 예시 코드 (2) 인터페이스를 정적패터리 메서드의 반환 타입으로 사용 예시

      • 인터페이스를 정적팩토리 메서드의 반환타입으로 사용하는 인터페이스 기반 프레임워크 를 만드는 핵심기술은 Client 코드가 구체적인 구현 클래스 대신 인터페이스를 사용하도록 유도하는 디자인 패턴을 말함
      • 이러한 구조는 Client 코드가 구체적인 구현 클래스가 아닌 인터페이스에 의존하므로 코드 유연성, 확장성, 유지보수성 등 많은 장점을 가지며, 다양한 구현체를 사용할 수 있는 프레임워크나 라이브러리 설계에 매우 유용함
      interface DatabaseConnection {
          void connect();
      }
      
      // 구현 클래스 
      class MySQLConnection implements DatabaseConnection {
          @Override
          public void connect() {
              System.out.println("Connecting to MySQL database");
          }
      }
      
      class PostgreSQLConnection implements DatabaseConnection {
          @Override
          public void connect() {
              System.out.println("Connecting to PostgreSQL database");
          }
      }
      
      // 정적 팩토리 메서드를 가진 클래스
      class DatabaseConnectionFactory {
          public static DatabaseConnection createConnection(String type) {
              if (type.equalsIgnoreCase("MYSQL")) {
                  return new MySQLConnection();
              } else if (type.equalsIgnoreCase("POSTGRESQL")) {
                  return new PostgreSQLConnection();
              }
              throw new IllegalArgumentException("Unknown database type");
          }
      }
      
      // 클라이언트 코드
      public class Main {
          public static void main(String[] args) {
              DatabaseConnection mySqlConn = DatabaseConnectionFactory.createConnection("MYSQL");
              mySqlConn.connect();  // Output: Connecting to MySQL database
      
              DatabaseConnection postgreSqlConn = DatabaseConnectionFactory.createConnection("POSTGRESQL");
              postgreSqlConn.connect();  // Output: Connecting to PostgreSQL database
          }
      }
      • 자바 8 부터는 인터페이스에도 정적 메서드를 선언할 수 있기 때문에 아래와 같이 변경 가능

        interface DatabaseConnection {
            void connect();
        
            static DatabaseConnection createConnection(String type) {
                switch (type.toUpperCase()) {
                    case "MYSQL":
                        return new MySQLConnection();
                    case "POSTGRESQL":
                        return new PostgreSQLConnection();
                    default:
                        throw new IllegalArgumentException("Unknown database type: " + type);
                }
            }
        }
  • 장점 4. 입력 매개변수에 따라 다양한 타입의 객체 반환 가능

    • Client 코드가 구체 구현 클래스에 의존하지 않고, 인터페이스나 상위 클래스를 통해 다양한 구현을 사용할 수 있게 하는 강력한 기능임

    • 예시) EnumSet

      • EnumSet은 추상클래스이며 RegularEnumSet과 JumboEnumSet 2가지 구체 구현을 제공함
      • EnumSet 클래스의 정적팩토리 메서드 of 는 입력 매개변수에 따라 적절한 타입의 구현을 반환함
      import java.util.EnumSet;
      
      enum Days {
          MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
      }
      
      public class EnumSetExample {
          public static void main(String[] args) {
              // 적은 수의 열거형 값을 가진 집합을 생성할 때는 RegularEnumSet의 인스턴스를 반환함
              EnumSet<Days> workDays = EnumSet.of(Days.MONDAY, Days.TUESDAY, Days.WEDNESDAY);
              System.out.println("Work Days: " + workDays);
      
              // 모든 열거형 값을 포함하는 집합을 생성할 때는 필요에 따라 JumboEnumSet의 인스턴스를 반환함
              EnumSet<Days> allDays = EnumSet.allOf(Days.class);
              System.out.println("All Days: " + allDays);
          }
      }
      
  • 장점 5. 정적팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 됨

    • 예시 : Animal 예제

      • 정적 팩터리 메서드를 정의할 때 해당 메서드가 반환할 구체 클래스가 아직 작성되지 않았더라도 메서드를 정의하고 사용할 수 있음

      • AnimalFactory 클래스의 정적 팩터리 메서드를 정의하는 시점에 → 구체적인 반환 클래스( Dog, Cat 클래스)가 아직 존재하지 않았더라도, 나중에 해당 클래스 작성하고 정적 팩토리 메서드를 통해 사용할 수 있음

      • 이러한 방식은 개발 과정에서 많은 유연성을 제공하며, 나중에 구현 클리스가 추가, 변경 되더라도 클라이언트 코드에 미치는 영향을 최소화 할 수 있음

      • 예제 코드

        // 인터페이스 정의 - 이후 구현 클래스들이 따라야할 규약을 정의
        public interface Animal {
            void speak();
        }
        
        public class AnimalFactory {
            // 정적 팩터리 메서드 정의 - Animal 인터페이스를 구현한 클래스의 인스턴스를 반환하게 됨
            public static Animal createAnimal(String type) {
                switch (type) {
                    case "dog":
                        return new Dog(); // Dog 클래스는 아직 작성되지 않음
                    case "cat":
                        return new Cat(); // Cat 클래스는 아직 작성되지 않음
                    default:
                        throw new IllegalArgumentException("Unknown animal type");
                }
            }
        }
        
        // 구현 클래스 작성
        public class Dog implements Animal {
            @Override
            public void speak() {
                System.out.println("Woof!");
            }
        }
        
        public class Cat implements Animal {
            @Override
            public void speak() {
                System.out.println("Meow!");
            }
        }
        
        // 클라이언트 코드
        public class Main {
            public static void main(String[] args) {
                Animal dog = AnimalFactory.createAnimal("dog");
                dog.speak(); // Outputs: Woof!
        
                Animal cat = AnimalFactory.createAnimal("cat");
                cat.speak(); // Outputs: Meow!
            }
        }
        
    • 예제 코드 : 서비스 제공자 프레임워크 JDBC

      • DriverManager 클래스의 getConnection 메서드는 정적 팩터리 메서드이며, 등록된 driver 중 적절한 드라이버를 선택하여 Connection 객체를 반환함
      • Driver 인터페이스와 이를 구현한 MySQLDriver 클래스는 서비스 제공자 역할을 함
      • DriverManager 는 이런 드라이버들을 등록, 관리하는 역할을 함
      • 즉, 정적 팩터리 메서드는 서비스 제공자 프레임워크(JDBC)가 다양한 구현체를 유연하게 관리, 제공할 수 있도록 지원함
      • 클라이언트는 특정 구현체에 종속되지 않고, 필요에 따라 적절한 구현체를 동적으로 선택, 사용할 수 있음
      // JDBC Driver 인터페이스
      public interface Driver {
          Connection connect(String url, Properties info) throws SQLException;
      }
      
      // DriverManager (정적 팩터리 메서드 사용)
      public class DriverManager {
          private static final List<Driver> registeredDrivers = new ArrayList<>();
      	  
          public static void registerDriver(Driver driver) { // driver 등록
              registeredDrivers.add(driver);
          }
      
          // connection 얻기 위한 정적 팩토리 메서드
          public static Connection getConnection(String url, Properties info) throws SQLException {
              for (Driver driver : registeredDrivers) {
                  Connection connection = driver.connect(url, info);
                  if (connection != null) {
                      return connection;
                  }
              }
              throw new SQLException("No suitable driver found for " + url);
          }
      }
      
      // Service Provider (Driver 구현체)
      public class MySQLDriver implements Driver {
          @Override
          public Connection connect(String url, Properties info) throws SQLException {
              if (url.startsWith("jdbc:mysql:")) {
                  // Create and return MySQL Connection
                  return new MySQLConnection(url, info);
              }
              return null;
          }
      }
      
      // 클라이언트 코드
      public class Main {
          public static void main(String[] args) {
              try {
                  // MySQL driver 등록
                  DriverManager.registerDriver(new MySQLDriver());
                  
                  // 정적팩토리 메서드 사용하여 connection 얻기
                  Properties info = new Properties();
                  Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", info);
                  
                  // connection 사용
                  System.out.println("Connected to the database!");
      
              } catch (SQLException e) {
                  e.printStackTrace();
              }
          }
      }
      

3. 정적 팩토리 메서드 단점

  • 단점 1. 클래스의 인스턴스를 생성하기 위해 정적 팩토리 메서드만 제공하면 하위 클래스 생성 불가

    • 정적 팩터리 메서드만 제공하고, 생성자를 private으로 만들면 해당 클래스를 상속하여 하위 클래스를 만들 수 없음

    • 상속하려면 public 이나 protected 생성자가 필요하기 때문 → 생성자가 private이면 외부 클래스에서 인스턴스를 생성할 수 없고, 하위 클래스에서도 상위 클래스의 생성자를 호출 할 수 없기 때문

    • 예시 코드 : 정적 팩토리 메서드만 제공 (private 생성자)

      • Animal 클래스는 private 생성자와 정적 팩터리 메서드만을 제공함

      • Dog 클래스는 Animal 상속 불가능 : Dog 클래스는 Animal의 생성자를 호출 할 수 없기 때문

        // 정적 팩터리 메서드만 제공하는 클래스
        public class Animal {
            // private 생성자: 외부에서 인스턴스 생성 불가
            private Animal() { }
        
            // 정적 팩터리 메서드
            public static Animal createAnimal() {
                return new Animal();
            }
        }
        
        // 하위 클래스 만들려는 시도 - 컴파일 오류 발생
        public class Dog extends Animal {
            // Animal의 private 생성자 때문에 Dog는 Animal 상속 불가능
        }
        
    • 예시 코드 : 생성자를 protected로 변경한다면 ?

      • Animal 클래스 생성자를 protected로 변경하면 Dog 클래스는 Animal을 상속하고 Animal 생성자를 호출할 수 있음
      • 생성자를 protected로 변경하면 상속은 가능해지지만, 이는 정적 팩터리 메서드만 제공하는 경우의 제한사항을 벗어나게 됨!!
      • 생성자를 public이나 protected로 선언하면 클라이언트가 직접 생성자를 호출하여 객체를 생성할 수 있게 되면서 객체 생성을 통제할 수 없게 됨 (정적 팩터리 메서드의 장점을 잃게 됨)
    public class Animal {
        protected Animal() { } // protected 생성자: 하위 클래스에서 접근 가능
        public static Animal createAnimal() { // 정적 팩토리 메서드
            return new Animal();
        }
    }
    
    public class Dog extends Animal {
        public Dog() {
            super(); // Animal의 protected 생성자 호출 가능
        }
    }
    
    
  • 단점 2. 정적팩터리메서드는 프레그래머가 찾기 어려움

    • Animal 클래스에서 createCat()이나 createDog() 같은 정적 팩러리를 통해 객체를 생성하도록 구현할 경우, 다른 개발자가 해당 메서드 존재를 알지 못한다면 Animal 클래스를 인스턴스화하는 방법을 찾기 어려울 수 있음
      • public static Animal createCate() {return new Animal("Cate"); }
    • 해결 방안
      • 클래스의 정적 팩터리 메서드를 잘 문서화 해야 함 (Java doc을 통해 설명을 명확히 제공하여 프로그래머가 쉽게 찾을 수 있도록 함)
        • 명명 규칙 통일 : of(), from(), getInstance() 등의 표준화된 이름을 사용하는 것이 좋음

4. 정적 팩터리 메서드 명명 방식

  • from : 매개변수 1개를 받아 해당 타입의 인스턴스 반환하는 형변환 메서드 Date d = Date.from(instant);
  • of : 여러 매개변수 받아 적합한 타입 인스턴스 반환하는 집계 메서드 Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
  • 매개변수로 명시한 인스턴스를 반환
    • getInstance : 같은 인스턴스 임을 보장하지 않음
    • create 또는 newInstance : 매번 새로운 인스턴스 생성해 반환함을 보장함 Obejct newArray = Array.newInstance(classObject, arrayLen);
  • 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드 정의할 경우
    • newType : Type은 팩터리 메서드가 반환할 객체 타입을 의미함 BufferedReader br = Files.newBufferedReader(path);List<Complaint> litany = Collection.list(legacyLitany);

✏️ Learn More

1. 불변 클래스

  • 정적 팩토리 메서드를 사용하여 객체 생성을 제어하면, 동일한 요청에 대해 동일한 객체를 반환하는 방식으로 객체의 생명주기를 철저히 관리할 수 있음

  • 불변 클래스 (immutable class)

    • 불변 클래스 란?

      • 한번 설정된 필드 값이 이후에 변경되지 않는 클래스
      • 객체의 상태가 한번 설정되면 그 객체는 불변이어야 한다는 것을 의미함
    • 불변 클래스 특징

      • 상태 불변성 : 객체가 생성된 후 그 상태가 한 번 설정되면 변경 불가능함
      • 쓰레드 안전성 : 불변 객체는 여러 쓰레드에서 동시 접근해도 상태가 변하지 않기 때문에 쓰레드 안전성을 자연스럽게 보장함
      • 캐싱과 재사용 가능성 : 동일한 상태를 가지는 불변 객체를 사용하거나 캐싱하여 성능 최적화 가능
    • 불변 클래스 구현 방법

      • 클래스와 모든 필드를 final로 선언
      • 필드 값을 변경할 수 있는 메서드 제공 금지
      • 필드를 참조하는 객체가 변경 가능하다면, 이를 방어적으로 복사하여 사용함
      • 모든 필드를 생성자를 통해서만 초기화 함
    • 예시

      public final class ImmutablePerson {
      	private final String name;
      	private final Date birthDate;
      	private fianl int age;
      
      	public ImmutablePerson(String name, Date birthDate, int age) {
      		this.name = name;
      		this.birthDate = new Date(birthDate.getTime()); //방어적 복사 : 전달된 Date 객체의 복사본을 사용
      	  this.age = age;
      	}
      
      	public String getName() { return name; }
      	public int getAge() { return age; }
      
      	public Date getBirthDate() {
      		return new Date(birthDate.getTime()); //방어적 복사: 내부 Date 객체의 복사본을 반환
      	}
      
      	public ImmutablePerson withNewAge(int newAge) {
      		// 방어적 복사 (필요시)
      		return new ImmutablePerson(this.name, this.Date, newAge);
      	}
      }
      
  • ✅ 변경 가능한 객체를 방어적 복사 없이 사용하면 왜 문제가 발생할까?

    • 문제가 되는 이유

      • Date 객체는 변경가능(mutable)한 객체이므로, 외부에서 Date 객체를 전달받아 그대로 사용하면 해당 Date객체가 외부에서 변경될 수 있음
      • Person 객체가 불변객체 처럼 보이지만 실제로는 birthDate 필드가 외부에서 변경될 수 있기 때문에 불변성이 깨짐
    • 변경 가능한 객체를 불변클래스에 포함할 때 방어적 복사의 필요성

      • 방어적 복사 활용시 외부에서 전달된 객체를 그대로 사용하지 않고, 복사본을 만들어 사용하므로 불변 클래스의 내부 상태에 영향 미치지 않음

      • 방어적 복사 미적용 예시

        public final class MutablePerson {
        	private final String name;
        	private final Date birthDate;
        
        	public MutablePerson(String name, Date birthDate) {
                this.name = name;
                this.birthDate = birthDate; // 문제 발생 가능 지점
            }
        
            public Date getBirthDate() {
                return birthDate; // 문제 발생 가능 지점
            }
        }
        
    • 사용 예시

      • mutablePerson 객체는 불변 객체처럼 보이지만 실제로는 birthDate 필드가 외부에서 변경될 수 있으므로 불변성이 깨짐
      • immutablePerson 객체에서는 birthDate 필드가 방어적 복사를 통해 초기화되고 반환됨 따라서 외부에서 Date객체를 변경하더라도 immutablePerson 객체의 상태는 변경되지 않았음
      Date birthDate = new Date();
      
      // 방어적 복사 미적용
      MutablePerson person = new MutablePerson("Alice", birthDate);
      // 방어적 복사 적용
      ImmutablePerson immutablePerson = new ImmutablePerson("Alice", birthDate);
      
      // 외부에서 Date 객체를 변경하면?
      birthDate.setTime(birthDate.getTime() + 1000);
      
      // person 객체의 내부 상태 결과
      System.out.println(person.getBirthDate()); // person 객체의 내부 상태가 외부에서 변경됨
      System.out.println(immutablePerson.getBirthDate()); // person 객체의 내부 상태는 변경되지 않음
      
  • ✅ withNewAge 메서드를 사용하면 왜 객체 자체의 상태를 변경되지 않았다고 하는가?

    • withNewAge 메서드 동작

      • withNewAge 메서드는 기존 객체의 필드를 수정하는 대신, 기존 객체의 상태를 기반으로 새로운 객체를 생성함
      • 즉, 원래 객체는 여전히 불변 상태를 유지하며, 새로운 객체만 새로운 상태를 가진다.
    • 사용 예시

      • Person 객체는 그대로 유지되고, 나이만 변경된 새로운 olderPerson 객체가 생성됨

        ImmutablePerson person = new ImmutablePerson("Alice", 30);
        // person은 나이가 그대로 30
        
        ImmutablePerson olderPerson = person.withNewAge(31);
        // 새로운 나이를 가진 새로운 객체를 생성함
        
    • 방어적 복사 (withNewAge 메서드) 필요성

      • 현재 객체 상태를 복사하면서 일부 속성만 변경하여 새로운 객체를 생성할 때 사용됨
      • 불변 객체의 특성을 유지하면서 상태를 일부 변경할 때 유용함

2. static 블록 (정적 초기화 블록)

  • static 블록이란?
    • 클래스 초기화 블록이라 불림
    • 클래스가 처음 로드될 때 실행되는 코드 블록 (클래스가 JVM에 의해 로드될 때 static 블록이 실행됨)
    • static 블록은 클래스가 로드될 때 한 번 실행되며, 주로 클래스 레벨 변수의 초기화에 사용됨
  • 코드 예시 및 분석
    • Integer의 cache 배열은 클래스 로드시 초기화 되며 -128 ~ 127까지의 Integer 객체가 미리 생성되어 배열에 저장됨
    • 이후 valueof 정적팩토리메서드를 통해 -128에서 127 범위 값은 항상 같은 Integer 객체를 반환 → 메모리 사용 최적화하고, 성능 향상 시킴
public class Integer {
		// static 필드 선언 : 정적 필드 cache는 클래스 로드시 초기화 되며, cache 배열은 256개의 Integer 객체를 보관함
    private static final Integer[] cache = new Integer[256];

		// static 초기화 블록 : 클래스가 처음 로드 될 때 실행되며, for문을 통해 cache 배열의 각 요소를 초기화 함
    static {
        for (int i = 0; i < cache.length; i++) {
            cache[i] = new Integer(i - 128);
        }
    }

		// 정적 팩토리 메서드 : 정수값을 입력 받아 -128 ~ 127 사이 값은 캐시된 Integer 객체를 반환함
		// 그외 값에 대해서는 새로운 Intger 객체를 생성하여 반환함
    public static Integer valueOf(int value) {
        if (value >= -128 && value <= 127) {
            return cache[value + 128];
        }
        return new Integer(value);
    }
}

3. 플라이웨이트 패턴 (Flyweight pattern)

  • 플라이웨이트 패턴이란?
    • 객체를 가능한 공유 하여 메모리 사용을 줄이려는 목적을 가지고 있음
    • 주로 동일한 객체가 많이 생성될 때 사용됨 → 객체 생성 비용 높은 경우 성능 향상 가능,되고, 객체 생성 비용이 높거나 메모리 사용이 문제가 될 때 사용함
  • 플라이 웨이트 패턴 장단점
    • 장점
      • 공유 가능한 객체 재사용함으로써 메모리 사용 감소
      • 객체 생성 비용이 높은 경우, 성능 향상시킬 수 있음
    • 단점
      • 공유상태와 비공유 상태를 분리하여 관리해야 하므로 코드 복잡성 증가
      • 객체의 비공유 상태를 외부에서 관리하므로 추가적인 코드가 필요함
  • 구현 방법
    • Client는 FlyweightFactory를 통해 Flyweight 객체를 얻어서 사용함
    • A와 B라는 2개의 Flyweight 객체가 생성되며, A는 한번만 생성되어 재사용 됨
// Flyweight : 공통 인터페이스 정의
interface Flyweight {
    void operation(String extrinsicState);
}

// ConcreteFlyweight : Flyweight 인터페이스를 구현한 클래스, intrinsicState(공유 가능한 상태)를 저장
class ConcreteFlyweight implements Flyweight {

    private final String intrinsicState;
    public ConcreteFlyweight(String intrinsicState) {
        this.intrinsicState = intrinsicState;
    }

    @Override
    public void operation(String extrinsicState) {
		    // 공유 상태(Intrinsic State) : 여러 객체 간에 공유되는 상태, 변경되지 않으며 동일한 값을 가지는 상태
		    // 비공유 상태(Extrinsic State) : 각 객체마다 다를 수 있는 상태로, 객체의 외부에서 관리
        System.out.println("Intrinsic State: " + intrinsicState + ", Extrinsic State: " + extrinsicState);
    }
}

// FlyweightFactor : Flyweight 객체들을 관리함, 요청시 이미 존재하는 객체를 반환하거나 새로 생성하여 반환함
class FlyweightFactory {
    private final Map<String, Flyweight> flyweights = new HashMap<>();

    public Flyweight getFlyweight(String key) {
        if (!flyweights.containsKey(key)) {
            flyweights.put(key, new ConcreteFlyweight(key));
        }
        return flyweights.get(key);
    }
}

// Client 코드
public class FlyweightPatternDemo {
    public static void main(String[] args) {
        FlyweightFactory factory = new FlyweightFactory();

        Flyweight flyweight1 = factory.getFlyweight("A");
        flyweight1.operation("First Call");

        Flyweight flyweight2 = factory.getFlyweight("B");
        flyweight2.operation("Second Call");

        Flyweight flyweight3 = factory.getFlyweight("A");
        flyweight3.operation("Third Call");

        // flyweight1과 flyweight3는 동일한 객체임
        System.out.println(flyweight1 == flyweight3); // true
    }
}
  • 구현 예시 (1) 동일한 글꼭 객체 공유

    • 그래픽 애플리케이션에서 화면에 많은 텍스트를 렌더링할 때 동일한 글꼴 사용하는 경우가 많음
    • 플라이웨이트 패턴 사용시 색상과 사이즈는 다르지만 동일한 글꼴일 경우 객체를 재사용하여 메모리 절약 가능
    interface Font {
        void display(String color, int size);
    }
    
    class ConcreteFont implements Font {
        private final String fontName;
    
        public ConcreteFont(String fontName) {
            this.fontName = fontName;
        }
    
        @Override
        public void display(String color, int size) {
            System.out.println("Font: " + fontName + ", Color: " + color + ", Size: " + size);
        }
    }
    
    class FontFactory {
        private final Map<String, Font> fontMap = new HashMap<>();
    
        public Font getFont(String fontName) {
            Font font = fontMap.get(fontName);
            if (font == null) {
                font = new ConcreteFont(fontName);
                fontMap.put(fontName, font);
            }
            return font;
        }
    }
    
    // 클라이언트는 다양한 색상과 크기로 텍스트를 표시함
    // 단, 동일한 글꼴 이름에 대해 동일한 Font 객체가 반환되어 재사용됨
    public class FlyweightPatternExample {
        public static void main(String[] args) {
            FontFactory fontFactory = new FontFactory();
    
            Font arial = fontFactory.getFont("Arial");
            arial.display("Red", 12);
    
            Font timesNewRoman = fontFactory.getFont("Times New Roman");
            timesNewRoman.display("Blue", 14);
    
            Font arialAgain = fontFactory.getFont("Arial");
            arialAgain.display("Green", 16);
    
            // arial과 arialAgain은 동일한 객체를 참조
            System.out.println(arial == arialAgain); // true
        }
    }
    
  • 구현 예시 (2) 체스 게임

    • 체스 게임의 말들은 대부분 동일한 외형과 색상을 가지며, 위치만 달라짐
    • 플라이웨이트 패턴을 통해 체스말을 효율적 관리할 수 있음
    interface ChessPiece {
        void place(int x, int y);
    }
    
    class ConcreteChessPiece implements ChessPiece {
        private final String type;
        private final String color;
    
        public ConcreteChessPiece(String type, String color) {
            this.type = type;
            this.color = color;
        }
    
        @Override
        public void place(int x, int y) {
            System.out.println("Placing " + color + " " + type + " at (" + x + ", " + y + ")");
        }
    }
    
    class ChessPieceFactory {
        private final Map<String, ChessPiece> chessPieceMap = new HashMap<>();
    
        public ChessPiece getChessPiece(String type, String color) {
            String key = type + "-" + color;
            ChessPiece chessPiece = chessPieceMap.get(key);
            if (chessPiece == null) {
                chessPiece = new ConcreteChessPiece(type, color);
                chessPieceMap.put(key, chessPiece);
            }
            return chessPiece;
        }
    }
    
    public class FlyweightPatternChess {
        public static void main(String[] args) {
            ChessPieceFactory factory = new ChessPieceFactory();
    
            ChessPiece whitePawn = factory.getChessPiece("Pawn", "White");
            whitePawn.place(0, 1);
    
            ChessPiece blackPawn = factory.getChessPiece("Pawn", "Black");
            blackPawn.place(0, 6);
    
            ChessPiece whitePawn2 = factory.getChessPiece("Pawn", "White");
            whitePawn2.place(1, 1);
    
            // whitePawn과 whitePawn2는 동일한 객체를 참조
            System.out.println(whitePawn == whitePawn2); // true
        }
    }
    

4. static을 언제 사용해야 하는가?

정적 필드 : 각 인스턴스들이 공통적으로 같은 값이 유지되어야 하는 경우 필드에 static 을 붙이자
정적 메소드 : 인스턴스 변수를 사용하지 않는 메소드가 있다면 static 붙일것을 고려해보자

  • static(정적) 이란?

    • 정적 멤버는 클래스에 고정된 멤버로 “ 객체를 생성하지 않고도 변수나 함수를 사용 가능 ”
    • 정적 멤버는 객체(인스턴스)에 소속된 멤버가 아니라 클래스에 소속된 멤버이므로 클래스 멤버라고도 함
    • 정적 멤버는 클래스 로더가 클래스(바이트 코드)를 로딩해서 메소드 메모리 영역에 적재할 때 클래스별로 관리됨 → 클래스가 메모리로 로딩되면 정적멤버를 바로 사용할 수 있음 (장점)
  • 언제 사용해야 하는가?

    • 필드 : 인스턴스 필드 와 정적 필드 선언 기준

      • 객체마다 갖고 있어야 할 데이터라면 인스턴스 필드 → ex > Calculator 객체마다 색깔이 다르다면 색깔은 인스턴스 필드로 선언
      • 객체마다 갖고 있을 필요성이 없는 공용적인 데이터라면 정적 필드 → ex > pi는 Calculator 객체마다 갖고 있을 필요가 없는 변하지 않는 공용적 데이터이므로 정적 필드로 선언
    • 메소드 : 인스턴스 메서드와 정적 메서드 선언 기준

      • 인스턴스 필드를 이용해서 실행해야 한다면 인스턴스 메서드로 선언 → 인스턴스 필드인 color을 변경하는 메소드는 인스턴스 메소드로 선언
      • 인스턴스 필드를 이용하지 않는다면 정적 메서드로 선언 → 덧셈 기능은 인스턴스 필드를 이용하기 보다는 외부에서 주어진 매개값들을 가지고 수행하므로 정적 메서드로 선언
    • 예시

      public class Calculator {
      	String color;
      	static double pi = 3.14159; 
      	
      	void setColor(String color) { this.color = color; }
      	static int plous(int x, int y) { return x+y; }
      }
      
      // 정적 멤버 사용 방법
      double resultA = 10 * 10 * Calculator.pi; // 클래스.필드;
      int resultB = Calculator.plus(30,40);//클래스.메소드(매개갑,...);
      
      // 객체 참조 변수로도 정적 멤버에 접근 가능하지만 원칙적으로 클래스 이름으로 접근하는 것이 좋음
      Calculator myCal = new Calculator();
      myCal.plus(30,40); // 좋지 못한 예
  • 정적 초기화 블록 (static 블록)

    • 정적 필드는 필드 선언과 동시에 초기값을 주는 것이 보통이다. 인스턴스 필드는 생성자에서 초기화하지만, 정적 필드는 객체 생성 없이도 사용해야 하므로 생성자에서 초기화 작업을 할 수 없다.
    • 정적 필드의 복잡한 초기화 작업정적 블록을 이용함 정적 블록은 클래스가 메모리로 로딩될 때 자동으로 실행됨 정적 블록은 클래스 내부에 여러개 선언되어도 상관 없으며, 선언된 순서대로 실행됨
    • 예시
    public class Television {
    	// 정적 필드는 보통 필드 선언과 동시에 초기값 세팅
    	static class company = "Samsung"; 
    	static String model = "LCD";
    	static String info;
    	
    	//정적 필드의 복잡한 초기화 작업은 정적 블록 이용
    	static {
    		info = comapny + "-" + model; 
    	}
    }
  • 정적 메소드와 블록 선언시 주의점

    • 객체 없어도 실행된다는 특징 때문에 정적 메소드와 정적 블록 내부에 인스턴스 필드나 인스턴스 메소드를 사용할 수 없음
    • 객체 자신 참조인 this 키워드도 사용 불가능
    • 인스턴스 멤버 사용하고 싶다면 객체를 먼저 생성하고 참조 변수로 접근해야 함
    • main 메소드도 static 메소드이므로 객체 생성 없이 인스턴스 필드와 인스턴스 메소드를 main()메소드에서 바로 사용할 수 없음
    • 예시
    public class Car {
    	int speedA;
    	void runA(){...}
    	
    	static int speedB;
    	static void runB(){...}
    	
    	static {
    		// 인스턴스 필드와 메서드 사용
    		speedA = 10; // 컴파일 에러 
    		runA(); // 컴파일 에러
    		speedB = 10;
    		runB();
    	}
    	
    	static void MethodC {
    		// this. 키워드 사용 불가 
    		this.speedA = 10; // 컴파일에러
    		this.runA(); // 컴파일에러
    		speedB = 10;
    		runB();
    	}
    	
    	public static void main(Stirng[] args){
    		speed = 60; // 컴파일 에러
    		run(); // 컴파일 에러
    		
    		Car myCar = new Car();
    		myCar.speed = 60;
    		myCar.run();
    	}
    }
  • 요약

    • 각 인스턴스들이 공통적으로 같은 값이 유지되어야 하는 경우 정적 필드로 만든다!
      • 각 인스턴스들은 서로 독립적이므로 인스턴스 필드는 서로 다른 값을 유지하기 때문
      • 정적 필드는 클래스가 메모리 올라갈 때 이미 자동적으로 생성되므로, 인스턴스를 생성하지 않아도 사용할 수 있음
    • 정적 메서드는 인스턴스 변수를 사용할 수 없다!
      • 정적 메서드는 인스턴스 생성 없이 호출 가능한 반면, 인스턴스 변수는 인스턴스를 생성 해야지만 존재
      • 정적 메서드를 호출할 때 인스턴스가 항상 생성되어 있는 것이 아니기 때문에 인스턴스 필드 사용 불가능
      • 반대로 인스턴스 필드나 메서드에서는 정적멤버 항상 사용 가능 (인스턴스 변수가 존재한다는 것은 static 붙은 변수가 이미 메모리에 존재한다는 의미)
    • 객체의 속성을 사용하지 않는 메서드의 경우 static을 붙이는 것을 고려해보자
      • 메서드 내에서 인스턴스 변수가 필요하다면 정적메소드로 만들 수 없다.
      • 객체 속성을 사용하지 않는다는 의미는 객체가 갖고 있는 필드들을 사용하지 않는다는 의미 예를들어 Math 클래스의 add() 메소드의 경우 Math 객체와 무관하다. 이럴 경우 add메소드에 statix을 붙이자!
      • 즉, 인스턴스 변수가 필요하지 않다면 static을 붙이는 것이 좋다. → 메소드 호출 시간이 짧아지기 때문에 효율이 높아지기 때문
  • 참고 : https://vaert.tistory.com/101