[JAVA] 이펙티브 자바 (ing..)

eunniverse·2024년 3월 2일
0

나홀로스터디

목록 보기
1/5

01. 생성자 대신 정적 팩토리 메서드를 고려하라

장점

  • 이름을 가질 수 있다.
    • BigInteger(int, int, Random) vs BigInteger.probablePrime
      • 후자(정적 팩토리 메서드)가 의미 전달이 더 쉽다.
      • 정적 팩토리 메서드는 이름을 가질 수 있으므로 의미를 알지 못하는 제약을 가지지 않는다.
  • 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
    • 불변클래스 (immutable class) : 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.
    • 인스턴스 통제 클래스 : 반복되는 요청에 같은 객체를 반환하는 식으로 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있는 클래스
  • 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
    • 하위타입 : 상속 또는 구현을 의미
      • ex. List list = new ArrayList<>();
    • 예시 - JAVA Collection Untitled
    • JAVA 8 이전에는 인터페이스에서 정적 메서드 지원이 되지 않아서 동반 클래스가 필요하였다.
  • 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
    • 매개변수에 따라 Enum 하위 클래스 인스턴스를 반환한다.

      Untitled

  • 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
    • 예시 : JDBC (서비스 제공자 프레임워크의 대표 예시)
      • 핵심 컴포넌트

        • service interface : 구현체의 동작 정의 (JDBC Connection)
        • provider registration API : 제공자가 구현체를 등록할 때 사용 (DriverManager.registerDriver)
        • service access API : 클라이언트가 서비스의 인스턴스를 얻을 때 사용하는 서비스 접근 API (DriverManager.getConnection)
      • 예시
        - registerDriver : JDBC Driver 등록

        ```java
        /**
             * Registers the given driver with the {@code DriverManager}.
             * A newly-loaded driver class should call
             * the method {@code registerDriver} to make itself
             * known to the {@code DriverManager}. If the driver is currently
             * registered, no action is taken.
             *
             * @param driver the new JDBC Driver that is to be registered with the
             *               {@code DriverManager}
             * @exception SQLException if a database access error occurs
             * @exception NullPointerException if {@code driver} is null
             */
            public static void registerDriver(java.sql.Driver driver)
                throws SQLException {
                registerDriver(driver, null);
            }
        
            /**
             * Registers the given driver with the {@code DriverManager}.
             * A newly-loaded driver class should call
             * the method {@code registerDriver} to make itself
             * known to the {@code DriverManager}. If the driver is currently
             * registered, no action is taken.
             *
             * @param driver the new JDBC Driver that is to be registered with the
             *               {@code DriverManager}
             * @param da     the {@code DriverAction} implementation to be used when
             *               {@code DriverManager#deregisterDriver} is called
             * @exception SQLException if a database access error occurs
             * @exception NullPointerException if {@code driver} is null
             * @since 1.8
             */
            public static void registerDriver(java.sql.Driver driver,
                    DriverAction da)
                throws SQLException {
                /* 
        				 * Register the driver if it has not already been added to our list 
        				 */
                if (driver != null) {
                    registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
                } else {
                    // This is for compatibility with the original DriverManager
                    throw new NullPointerException();
                }
        
                println("registerDriver: " + driver);
            }
        ```
        
        - getConnection : DB 연결
            - registerDriver 를 통해 연결된 드라이버를 순회하며 Connection 을 맺는다.
            - getConnection() 메서드 작성 시점에는 Driver 가 존재하지 않았지만, 실행 시점에 인스턴스를 할당 받는 구조로 동작한다.
        //  Worker method called by the public getConnection() methods.
            private static Connection getConnection(
                String url, java.util.Properties info, Class<?> caller) throws SQLException {
                /*
                 * When callerCl is null, we should check the application's
                 * (which is invoking this class indirectly)
                 * classloader, so that the JDBC driver class outside rt.jar
                 * can be loaded from here.
                 */
                ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
                if (callerCL == null || callerCL == ClassLoader.getPlatformClassLoader()) {
                    callerCL = Thread.currentThread().getContextClassLoader();
                }
        
                if (url == null) {
                    throw new SQLException("The url cannot be null", "08001");
                }
        
                println("DriverManager.getConnection(\"" + url + "\")");
        
                ensureDriversInitialized();
        
                // Walk through the loaded registeredDrivers attempting to make a connection.
                // Remember the first exception that gets raised so we can reraise it.
                SQLException reason = null;
        
                for (DriverInfo aDriver : registeredDrivers) {
                    // If the caller does not have permission to load the driver then
                    // skip it.
                    if (isDriverAllowed(aDriver.driver, callerCL)) {
                        try {
                            println("    trying " + aDriver.driver.getClass().getName());
                            Connection con = aDriver.driver.connect(url, info);
                            if (con != null) {
                                // Success!
                                println("getConnection returning " + 
        																aDriver.driver.getClass().getName());
                                return (con);
                            }
                        } catch (SQLException ex) {
                            if (reason == null) {
                                reason = ex;
                            }
                        }
        
                    } else {
                        println("    skipping: " + aDriver.getClass().getName());
                    }
        
                }
        
                // if we got here nobody could connect.
                if (reason != null)    {
                    println("getConnection failed: " + reason);
                    throw reason;
                }
        
                println("getConnection: no suitable driver found for "+ url);
                throw new SQLException("No suitable driver found for "+ url, "08001");
            }

단점

  • 상속을 하려면 public 이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스를 만들 수 없다.
    • 컴포지션 사용 유도 및 불변 타입으로 만들기 때문에 장점이 될 수 있다.
  • 프로그래머가 찾기 어렵다.
    • 따라서 정적 팩터리 메서드에 흔히 사용하는 명명 방식이 있다.
      • 명명 방식
        • from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
        • of : 여러 매개변수를 받아 적합한 인스턴스를 반환하는 집계 메서드
        • valueOf : from 과 of 의 자세한 버전
        • instance / getInstance : 매개변수로 명시한 인스턴스를 반환하지만 같은 인스턴스임을 보장하지 않음
        • create / newInstance : 매번 새로운 인스턴스를 생성해 반환함을 보장
        • getType : getInstance 와 같으나 다른 클래스에 팩토리 메서드를 정의할 때 사용
        • newType : newInstance 와 같으나 다른 클래스에 팩토리 메서드를 정의할 때 사용
        • type : getType 과 newType 의 간결한 버전

02. 생성자에 매개변수가 많다면 빌더를 고려하라

점층적 생성자 패턴

public class User {

//회원 ID(필수)
private int id;

//회원 이름(필수)
private String userName;

//회원 별명(선택)
private String nickName;

//회원 사물함번호(선택)
private int lockerNumber;

//필수 매개변수를 받는 생성자
public User(int id, String userName){
    this(id,userName,null);
}

public User(int id, String userName, String nickName){
    this(id,userName,nickName,0);
}

public User(int id, String userName, String nickName, int lockerNumber){
    this.id = id;
    this.userName = userName;
    this.nickName = nickName;
    this.lockerNumber = lockerNumber;
}

@Override
public String toString() {
    return "User{" + "id=" + id + ", userName='" + userName + '\'' 
		+ ", nickName='" + nickName + '\'' + ", lockerNumber=" + lockerNumber +      '}';
  }
}
  • 특징
    • 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.

자바빈즈 패턴

public class NutritionFacts {
 
	private int servingSize = -1; // 필수
	private int servings = -1; // 필수
	private int calories = 0;
	private int fat = 0;
	private int sodium = 0;
	private int carbohydrate = 0;
 
	public NutritionFacts() { }
 
	public void setServingSize(int servingSize) {
		this.servingSize = servingSize;
	}
	public void setServings(int servings) {
		this.servings = servings;
	}
	public void setCalories(int calories) {
		this.calories = calories;
	}
	public void setFat(int fat) {
		this.fat = fat;
	}
	public void setSodium(int sodium) {
		this.sodium = sodium;
	}
	public void setCarbohydrate(int carbohydrate) {
		this.carbohydrate = carbohydrate;
	}
}

public class main {
	public static void main(String[] args) {
		NutritionFacts cocalCola = new NutritionFacts();
		cocalCola.setServingSize(240);
		cocalCola.setServings(8);
		cocalCola.setCalories(100);
		cocalCola.setSodium(35);
		cocalCola.setCarbohydrate(27);
	}
}
  • 특징
    • 객체 하나를 만들려면 메서드를 여러 개 호출해야 한다.
    • 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓인다.
    • 클래스를 불변으로 만들 수 없다.

빌더 패턴

public class NutritionFacts {
 
	private int servingSize; // 필수
	private int servings; // 필수
	private int calories;
	private int fat;
	private int sodium;
	private int carbohydrate;
 
	public static class Builder {
    
		// 필수 매개변수
		private int servingSize;
		private int servings;
        
		// 선택 매개변수
		private int calories     = 0;
		private int fat          = 0;
		private int sodium       = 0;
		private int carbohydrate = 0;
 
		private Builder(int servingSize, int servings) {
			this.servingSize = servingSize;
			this.servings = servings;
		}
        
		public Builder servingSize(int servingSize) {
			this.servingSize = servingSize;
			return this;
		}
 
		public Builder servings(int servings) {
			this.servings = servings;
			return this;
		}
 
		public Builder calories(int calories) {
			this.calories = calories;
			return this;
		}
 
		public Builder fat(int fat) {
			this.fat = fat;
			return this;
		}
 
		public Builder sodium(int sodium) {
			this.sodium = sodium;
			return this;
		}
 
		public Builder carbohydrate(int carbohydrate) {
			this.carbohydrate = carbohydrate;
			return this;
		}
 
		public NutritionFacts build() {
			NutritionFacts nutritionFacts = new NutritionFacts();
			nutritionFacts.fat = this.fat;
			nutritionFacts.sodium = this.sodium;
			nutritionFacts.servings = this.servings;
			nutritionFacts.carbohydrate = this.carbohydrate;
			nutritionFacts.servingSize = this.servingSize;
			nutritionFacts.calories = this.calories;
			return nutritionFacts;
		}
	}
}

public class Main {
	public static void main(String[] args) {
			NutritionFacts cocalCola = new NutritionFacts.Builder(240, 8)
											.calories(100)
											.sodium(35)
											.carbohydrate(27)
											.build();
	}
}
  • 특징
    • 명명된 선택적 매개변수를 흉내 낸 것이다.
    • 계층적으로 설계된 클래스와 함께 쓰기 좋다.
    • 가변인수 매개변수를 여러 개 사용할 수 있다.

03. private 생성자나 열거 타입으로 싱글턴 임을 보증하라

  • 싱글턴 패턴 : 인스턴스를 오직 하나만 생성할 수 있는 디자인 패턴
    • ex. DB 사용 시 설정하는 Config 클래스
    • 생성방식
      • private 생성자 + public static final 필드

        public class Elvis {
        	public static final Elvis INSTANCE = new Elvis();
        	
        	/**
        	 * public static final 필드인 Elvis.INSTANCE 초기화할 때 한번만 호출됨 
        	 **/
        	private Elvis() { ... }
        
        	public void leaveTheBuilding()
        }
      • 장점

        • 클래스가 싱글턴임이 명백히 보장된다.
        • 간결하다.
      • 예외

        • 리플렉션 api인 AccessibleObject.setAccessible 을 사용해 생성자 호출 가능

          Constructor<Elvis> constructor = (Constructor<Elvis>) elvis2.getClass().getDeclaredConstructor();
          constructor.setAccessible(true);
          
          Elvis elvis3 = constructor.newInstance();
          assertNotSame(elvis2, elvis3); // FAIL
        • 해결방법 : private 생성자에서 객체 null 체크하여 null이 아닐 경우 예외를 던지면 된다.

      • private 생성자 + public static 멤버인 정적 팩토리 메서드

        public class Elvis {
        	private static final Elvis INSTANCE = new Elvis();
        	private Elvis() { ... }
         
          // 항상 같은 객체를 제공함 --> 두번째 인스턴스는 만들어지지 않음.
          // 리플렉션을 사용한 예외는 적용되므로 예외처리가 필요.
        	public static Elvis getInstance() { return INSTANCE; }
        
        	public void leaveTheBuilding()
        }
      • 장점

        • 메서드를 수정하지 않아도 싱글톤이 아니게 변경할 수 있다.
          • 호출하는 스레드 별로 다른 인스턴스를 넘겨주게 할 수 있다.
        • 정적 팩토리 → 제네릭 싱글턴 팩토리로 만들 수 있다.
          • 제네릭 싱글톤 팩토리란?

            Untitled

        • 정적 팩토리 메서드 참조를 공급자(Supplier)로 사용할 수 있다.
          • Supplier
            - JAVA8에 추가된 함수형 인터페이스
            - 매개변수를 받지 않고 단순히 반환하는 추상메서드 존재
            - Lazy Evalution 가능 —> 불필요한 연산을 피한다

            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/62bb9e03-0126-4bd3-a8c7-3b791df2dacc/Untitled.png)
            
            - 참고 블로그 : https://m.blog.naver.com/zzang9ha/222087025042
            Supplier<Elvis> elvisSupplier = Elvis::getInstance;
            Elvis elvis = elvisSupplier.get();
        • 생성방식 문제점
          • 직렬화 후 역직렬화할 때 새로운 인스턴스를 만든다.
          • 역직렬화 : 기본 생성자 호출하지 않고 값을 복사해서 새로운 인스턴스 반환
            • 해결방안
              • readResolve() 메서드에서 싱글턴 인스턴스 반환

              • 모든 필드에 transient(직렬화 제외) 키워드 추가

                // 역질렬화시 반드시 호출되는 readResolve 메서드를 싱글턴을 리턴하도록 수정
                // 가짜 elvis 는 가비지 컬렉터로...
                private Obejct readResolve() {
                	return INSTANCE;
                }
      • 원소가 하나인 열거타입 선언하는 방식

        • 싱글턴을 만드는 가장 좋은 방법!!

          public enum Elvis {
          	INSTANCE;
          	
          	public void leaveTheBuilding() {}
          }

04. 인스턴스화를 막으려거든 private 생성자를 사용하라

  • 인스턴스화
    • 클래스로부터 객체를 생성하는 것
  • 인스턴스화가 필요없는 케이스
    1. final 클래스
      1. final 클래스를 상속해서 하위 클래스에 메서드를 넣는 건 불가능하기 때문
      • java.lang.Math
        public final class Math {
        
            /**
             * Don't let anyone instantiate this class.
             */
            **private Math() {}**
        
            /**
             * The {@code double} value that is closer than any other to
             * <i>e</i>, the base of the natural logarithms.
             */
            public static final double E = 2.7182818284590452354;
        
            /**
             * The {@code double} value that is closer than any other to
             * <i>pi</i>, the ratio of the circumference of a circle to its
             * diameter.
             */
            public static final double PI = 3.14159265358979323846;
        
            /**
             * Constant by which to multiply an angular value in degrees to obtain an
             * angular value in radians.
             */
            private static final double DEGREES_TO_RADIANS = 0.017453292519943295;
        
            /**
             * Constant by which to multiply an angular value in radians to obtain an
             * angular value in degrees.
             */
            private static final double RADIANS_TO_DEGREES = 57.29577951308232;
        
            /**
             * Returns the trigonometric sine of an angle.  Special cases:
             * <ul><li>If the argument is NaN or an infinity, then the
             * result is NaN.
             * <li>If the argument is zero, then the result is a zero with the
             * same sign as the argument.</ul>
             *
             * <p>The computed result must be within 1 ulp of the exact result.
             * Results must be semi-monotonic.
             *
             * @param   a   an angle, in radians.
             * @return  the sine of the argument.
             */
            @HotSpotIntrinsicCandidate
            public static double sin(double a) {
                return StrictMath.sin(a); // default impl. delegates to StrictMath
            }
        
            /**
             * Returns the trigonometric cosine of an angle. Special cases:
             * <ul><li>If the argument is NaN or an infinity, then the
             * result is NaN.</ul>
             *
             * <p>The computed result must be within 1 ulp of the exact result.
             * Results must be semi-monotonic.
             *
             * @param   a   an angle, in radians.
             * @return  the cosine of the argument.
             */
            @HotSpotIntrinsicCandidate
            public static double cos(double a) {
                return StrictMath.cos(a); // default impl. delegates to StrictMath
            }
        
            /**
             * Returns the trigonometric tangent of an angle.  Special cases:
             * <ul><li>If the argument is NaN or an infinity, then the result
             * is NaN.
             * <li>If the argument is zero, then the result is a zero with the
             * same sign as the argument.</ul>
             *
             * <p>The computed result must be within 1 ulp of the exact result.
             * Results must be semi-monotonic.
             *
             * @param   a   an angle, in radians.
             * @return  the tangent of the argument.
             */
            @HotSpotIntrinsicCandidate
            public static double tan(double a) {
                return StrictMath.tan(a); // default impl. delegates to StrictMath
            }
        
            /**
             * Returns the arc sine of a value; the returned angle is in the
             * range -<i>pi</i>/2 through <i>pi</i>/2.  Special cases:
             * <ul><li>If the argument is NaN or its absolute value is greater
             * than 1, then the result is NaN.
             * <li>If the argument is zero, then the result is a zero with the
             * same sign as the argument.</ul>
             *
             * <p>The computed result must be within 1 ulp of the exact result.
             * Results must be semi-monotonic.
             *
             * @param   a   the value whose arc sine is to be returned.
             * @return  the arc sine of the argument.
             */
            public static double asin(double a) {
                return StrictMath.asin(a); // default impl. delegates to StrictMath
            }
        
            /**
             * Returns the arc cosine of a value; the returned angle is in the
             * range 0.0 through <i>pi</i>.  Special case:
             * <ul><li>If the argument is NaN or its absolute value is greater
             * than 1, then the result is NaN.</ul>
             *
             * <p>The computed result must be within 1 ulp of the exact result.
             * Results must be semi-monotonic.
             *
             * @param   a   the value whose arc cosine is to be returned.
             * @return  the arc cosine of the argument.
             */
            public static double acos(double a) {
                return StrictMath.acos(a); // default impl. delegates to StrictMath
            }
        }
    2. 정적 메서드와 정적 필드만 담은 클래스 (유틸리티 클래스)
      • java.util.Arrays
        public class Arrays {
        
            /**
             * The minimum array length below which a parallel sorting
             * algorithm will not further partition the sorting task. Using
             * smaller sizes typically results in memory contention across
             * tasks that makes parallel speedups unlikely.
             */
            private static final int MIN_ARRAY_SORT_GRAN = 1 << 13;
        
            // Suppresses default constructor, ensuring non-instantiability.
            // 기본 생성자를 억제하여 비인스턴스성을 보장합니다.
            private Arrays() {}
        
            /**
             * A comparator that implements the natural ordering of a group of
             * mutually comparable elements. May be used when a supplied
             * comparator is null. To simplify code-sharing within underlying
             * implementations, the compare method only declares type Object
             * for its second argument.
             *
             * Arrays class implementor's note: It is an empirical matter
             * whether ComparableTimSort offers any performance benefit over
             * TimSort used with this comparator.  If not, you are better off
             * deleting or bypassing ComparableTimSort.  There is currently no
             * empirical case for separating them for parallel sorting, so all
             * public Object parallelSort methods use the same comparator
             * based implementation.
             */
            static final class NaturalOrder implements Comparator<Object> {
                @SuppressWarnings("unchecked")
                public int compare(Object first, Object second) {
                    return ((Comparable<Object>)first).compareTo(second);
                }
                static final NaturalOrder INSTANCE = new NaturalOrder();
            }
        
            /**
             * Checks that {@code fromIndex} and {@code toIndex} are in
             * the range and throws an exception if they aren't.
             */
            static void rangeCheck(int arrayLength, int fromIndex, int toIndex) {
                if (fromIndex > toIndex) {
                    throw new IllegalArgumentException(
                            "fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")");
                }
                if (fromIndex < 0) {
                    throw new ArrayIndexOutOfBoundsException(fromIndex);
                }
                if (toIndex > arrayLength) {
                    throw new ArrayIndexOutOfBoundsException(toIndex);
                }
            }
        }
  • 인스턴스화가 필요없다면? —> 생성자를 만들지 않으면 되지 않는가?
    • 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만들어준다.
      - 매개변수를 받지 않는 public 생성자가 생성

      —> 추상 클래스로 만들면 되지 않는가?

    • 하위 클래스를 만들어 인스턴스화가 가능하다.

      결론 ⇒ private 생성자를 만든다.

    • 컴파일러가 기본생성자를 만드는 경우는 명시된 생성자가 없을 때 뿐이므로 인스턴스화를 막을 수 있다.

    • private이니 클래스 바깥에서 접근할 수 없다.

    • +) 상속이 불가능하다.

      • 모든 생성자는 상위 클래스의 생성자를 호출하므로 접근이 불가능하다.
    • 예시코드

      public class Utility {
      	// 기본 생성자가 만들어지는 것을 막는다. (인스턴스화 방지용)
      	private Utility() {
      			throw new AssertionError(); 
      	}
        // 코드 생략 .....
      }

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

  • 정적 유틸리티 클래스, 싱글턴을 잘못 사용한 예
    • 코드
      // 정적 유틸리티 클래스 잘못된 예
      public class SpellChecker {
      	private static final Lexicon dictionary = ...;
          
          private SpellChecker() {} // 인스턴스화 방지 (아이템 4 참고)
          
          public static boolean isVaild(String word) {...}
          public static List<String> suggestions(String typo) {...}
      }
      
      //사용은 이렇게!
      SpellChecker.isValid(word);
      // 싱글턴 클래스 잘못된 예
      public class SpellChecker {
      	private final Lexicon dictionary = ...;
          
          private SpellChecker() {} // 인스턴스화 방지 (아이템 4 참고)
          public static SpellChecker INSTANCE = new SpellChecker(...);
          
          public static boolean isVaild(String word) {...}
          public static List<String> suggestions(String typo) {...}
      }
      
      //사용은 이렇게!
      SpellChecker.INSTANCE.isValid(word);
      • 잘못된 이유?
        • 다른 종류의 사전을 사용할 경우, 대응이 어렵다.
        • final 한정자를 제거하고 교체하는 메서드를 추가할 수 있으나, 오류를 내기 쉬우며, 멀티 스레드 환경에서는 쓸 수 없다.
          • 멀티 스레드 환경에서 쓸 수 없는 이유?
            • 예시코드로 알아보자!!
              public class Main {
                  public static void main(String[] args) {
                      Runnable d = new CheckThread("으니");
              
              				// d2 생성 시 name static 변수에는 으니2로 세팅된다.
                      Runnable d2 = new CheckThread("으니2"); 
              
                      Thread t1 = new Thread(d);
                      Thread t2 = new Thread(d2);
              
                      // 따라서 결과는 으니2만 출력된다. (의도한바가 아님.)
                      t1.start();
                      t2.start();
                  }
              }
              
              // 멀티 쓰레드 테스트
              class CheckThread implements Runnable {
                  private static String name;
              
                  CheckThread(String name) {
                      setName(name);
                  }
              
                  public static void setName(String name) {
                      CheckThread.name = name;
                  }
                  
                  @Override
                  public void run() {
                      try {
              
                          for (int i = 0; i < 5; i++) {
                              System.out.println(name);
                              Thread.sleep(1000);
                          }
              
                      } catch (Exception e) {
                          System.out.println(e);
                      }
              
                      System.out.println("쓰레드 종료 : " + name);
                  }
              }
      • 사용하는 자원에 따라 동작이 달라지는 클래스는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다!
  • 어떻게 사용해야 되는가?
    • 의존 객체 주입 패턴
      • 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식
      • 코드
        public class SpellChecker {
        
            private final Lexicon dictionary; //final로 불변을 보장하자
        
        		//생성자에 필요한 자원을 넘겨준다
            public SpellChecker(Lexicon dictionary) { 
                this.dictionary = Objects.requireNonNull(dictionary);
            }
            
            public boolean isValid(String word) {...}
            public List<String> suggestions(String typo) {...}
        }
      • 장점
        • 자원이 몇 개든 의존 관계가 어떻든 상관없이 잘 작동한다.
        • 불변 보장 ⇒ 여러 클라이언트가 의존 객체들을 안심하고 공유할 수 있다.
        • 유연성과 테스트 용이성을 개선한다.
      • 단점
        • 의존성이 많은 경우 코드를 어지럽게 할 수 있다.
      • 변형 예시 패턴
        • 팩토리 메서드 패턴
          • 팩토리 ?
            • 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체
          • 코드
            package pattern.factory;
            
            // 로봇 추상 클래스
            public abstract class Robot {
            	public abstract String getName();
            }
            
            // 슈퍼로봇 구현체
            public class SuperRobot extends Robot {
            	@Override
            	public String getName() {
            		return "SuperRobot";
            	}
            }
            
            // 파워로봇 구현체
            public class PowerRobot extends Robot {
            	@Override
            	public String getName() {
            		return "PowerRobot";
            	}
            }
            
            // 로봇 공장
            public class RobotFactory {
            	@Override
            	Robot createRobot(String name) {
            		switch( name ){
            			case "super": return new SuperRobot();
            			case "power": return new PowerRobot();
            		}
            		return null;
            	}
            }
            
            public class FactoryMain {
            	public static void main(String[] args) {
            
            		RobotFactory rf = new RobotFactory();
            		Robot r = rf.createRobot("super");
            		Robot r2 = rf.createRobot("power");
            
            		System.out.println(r.getName());
            		System.out.println(r2.getName());
            	}
            }

06. 불필요한 객체 생성을 피하라

  • 잘못된 예시
    String s = new String("으니");
    왜 잘못되었는가?
    • 실행될 때마다 String 인스턴스를 새로 만든다.

      ⇒ 쓸데없이 String 인스턴스가 수백 만 개 만들어질 수 있다!

    • 개선 버전

      /**
      * 하나의 String 인스턴스를 사용한다.
      * 생성자 대신 정적 팩터리 메서드를 사용하는 예시이다.
      * 생성자는 호출할 때마다 새로운 객체를 만들지만, 팩토리 메서드는 전혀 그렇지않다.
      **/
      String s = "으니";
    • new String 과 String 의 차이점 (참고링크)

      https://ict-nroo.tistory.com/18
      
      https://starkying.tistory.com/entry/what-is-java-string-pool
      static boolean isRomanNumeral(String s) {
      	return s.matches("^(?=.)M*(c[md]|D?C{0,3})"+"(X[CL]|L?X{0,3})(I[XV]|V?I{0.3})$"); 
      }

      왜 잘못되었는가?

    • String.matches 는 생성 비용이 아주 비싼 객체이다.

      ⇒  캐싱하여 재사용 하는 게 좋다!
      
      - 개선 버전
          
          ```java
          public class RomanNumerals {
          	private static final Pattern ROMAN = Pattern.compile(
          		"^(?=.)M*(c[md]|D?C{0,3})"+"(X[CL]|L?X{0,3})(I[XV]|V?I{0.3})$"
          		);
          
          	static boolean isRomanNumeral(String s) {
          		return ROMAN.matcher(s).matches();
          	}
          }
          ```
          
      - **개선점**
          1. 코드 의미가 명확하다.
          2. 성능이 좋아진다. 
              
               ⇒ 클래스 초기화 과정에서 직접 생성해 캐싱하고, 호출될 때 인스턴스를 재사용하기 때문이다.
              
      private static long sum() {
      	Long sum = 0L;
      	for(long i = 0; i <= Integer.MAX_VALUE; i++) {
      		sum += i; // sum += Long.valueOf(i); 와 같다.
      	}
      
      	return sum;
      }

      왜 잘못되었는가?

    • 오토박싱

      • 기본타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술 ⇒ 상기 코드에서 Long 으로 선언해서 불필요한 Long 인스턴스가 21억개가 만들어진다.
        • long 타입인 i가 Long 타입인 sum 에 더해질 때마다 발생
    • 개선버전

      private static long sum() {
      	long sum = 0L;
      	for(long i = 0; i <= Integer.MAX_VALUE; i++) {
      		sum += i;
      	}
      
      	return sum;
      }
      • primitive 타입을 사용하고, 의도치 않는 오토박싱이 숨어들지 않도록 주의해야한다.

07. 다 쓴 객체 참조를 해제하라

profile
dev 갓생으로의 기록

0개의 댓글