이 블로그는 개인의 공부 목적으로 작성된 블로그입니다. 왜곡된 정보가 포함되어 있을 수 있습니다.
3. private 생성자나 열거 타입으로 싱글턴임을 보증하라
싱글톤은 인스턴스를 하나만 가질수 있는 클래스이다. 따라서 유일성이 보장되야하는 객체나 무상태(특정 클라이언트에 의존하는 필드가 없는 것) 객체에 싱글톤을 활용할 수 있다. 요새에는 스프링에서 빈과 같은 싱글톤 패턴을 지원하고 있어서 이에 대해 크게 신경쓰지 않고 있었다. 그러나 그 이전에 자바에서 싱글톤 패턴을 구현해야한다면 어떻게 해야할까?
이전 아이템 1에서 우리는 정적 팩터리 메서드를 활용하면 싱글톤으로 인스턴스를 관리할 수 있음을 확인했다. 책에서는 다음과 같은 방법으로 싱글톤 객체 구현을 추천하고 있다.
1. private 생성자 + public static final
public class Elvis {
public static final Elvis INSTANCE=new Elvis();
private Elvis(){
}
public void printSelf(){
System.out.println("this = " + this);
}
}
생성자가 private 접근 제한자로 선언되어 있어 Elvis INSTANCE를 생성할때, 오직 한번 생성된다.
이방법은 아래의 제시된 두번째 방법에 비해 더 코드상으로 싱글톤임을 더 명확하게 표현하고 간결하다는 장점이 있다.
이경우 싱글톤이 보장되지 않는경우가 딱 한가지 존재하는데, 리플렉션를 활용하여 생성자를 가져오는 경우 가능하다
리플렉션이란 JVM이 클래스 정보를 JVM 메모리에 저장할때 저장되어 있는 클래스 정보를 제공하는 API이다. 다음은 리플렉션을 활용하여 싱글톤을 보장하지 않는 예외의 경우이다.
package item1.item3;
import item1.item3.Elvis;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ElvisTest {
@Test
public void testSingletonInstance() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Elvis instance1 = Elvis.INSTANCE;
Class<?> classType = Elvis.class;
Constructor<?> constructor = classType.getDeclaredConstructor();
constructor.setAccessible(true);
Elvis instance2 = (Elvis) constructor.newInstance();
assertEquals(instance1, instance2);
}
}
constructor.setAccessible(true) 에 의해 private 생성자도 접근이 가능해진다. 그러나 이러한 리플렉션사용은 지양한다고 한다.(사용해야하는 경우가 딱히 떠오르지 않는다)
2. private 생성자 + private static final + 정적 펙터리 메소드
두번째 방법의 1번의 방법에 정적 펙터리 메소드를 사용하는 아이템 1(https://bluesparrow.tistory.com/25)에서 제시 되었던 방법이다.
public class Elvis2 {
private static final Elvis2 INSTANCE=new Elvis2();
private Elvis2(){
}
public static Elvis2 getInstance(){
return INSTANCE;
}
public void printSelf(){
System.out.println("this = " + this);
}
}
getInstance()에 의한 객체 접근만을 허용함으로 싱글톤이 보장된다. 또한 1번째 방법과 비교하여 몇가지 장점이 존재하는데
- 필요시 싱글톤이 아니도록 구현할 수 있다. (생성자를 열어두고, 싱글톤을 사용하고 싶으면 해당 정적 팩터리 메소드를 호출하면 된다.)
- 필요시 정적 팩터리를 제네릭 싱글턴 팩터리로 바꿀수 있다.
- 정적 팩터리 메서드 참조를 공급자로 사용할 수 있다.
아래는 2번째 장점으로 제시된 제네릭 싱글턴 팩터리를 사용한 예시이다.
public class ElvisTest {
public static UnaryOperator<Objects>IDENTITY_FN=(t)->t;
@SuppressWarnings("unchecked")
public static <T>UnaryOperator<T> identityFunction(){
return(UnaryOperator<T>) IDENTITY_FN;
}
@Test
public void GenericTest() {
UnaryOperator<Elvis2> instance1=identityFunction();
UnaryOperator<Elvis2> instance2=identityFunction();
assertEquals(instance1, instance2);
}
}
상수 IDENTIY_FN을 통해 동일한 객체 반환을 지원하여 싱글톤을 보장하고 있다. 그러나 1번 방법에도 동일하게 적용가능한데 어떠한 이유로 2번 방법에 사용했을때 이득인지 아직 모르겠다.
결국 책에서도 2번에 제시된 장점을 사용하지 않을경우 1번의 방법을 권장하고 있다.
3. 열거형 타입
public enum Elvis3 {
INSTANCE;
public void printSelf(){
System.out.println("this = " + this);
}
}
열거형 타입을 선언하는 방법으로 가장 간결한 방법이다. 리플렉션으로 부터 안전하고, 직렬화에도 크게 문제가 없다고 한다.
그러나 열거형을 상속이 불가능하기 때문에 이경우 주의해야한다.
4. 인스턴스화를 막으려거든 private 생성자를 사용하라
인스턴스를 생성하지 않고 정적 메서드, 정적 필드만 있는 클래스를 만드는 경우가 있다.(일반적으로 유틸관련 기능을 구현할때) 실제로 우리가 많이 많이 사용하는 클래스중에 이러한 클래스가 있는데 바로 java.util.Arrays 이다. sort와 같은 배열 유틸 함수를 static의 형태로 지원한다.
package item1.item4;
import static java.util.Arrays.sort;
public class Util {
public static void main(String[] args) {
int[] arr=new int[10];
for(int i=0;i<10;i++){
arr[i]=10-i;
}
java.util.Arrays.sort(arr);
sort(arr);
}
}
실제로 우리가 사용했던 sort 함수가 사실은 java.util.Arrays에 static으로 선언되어 있는 함수인 것이다.
public static void sort(int[] a) {
DualPivotQuicksort.sort(a, 0, 0, a.length);
}
또한 특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드를 모은 클래스도 있다 java.util.Collections이 그 예시이다.
import java.util.ArrayList;
import java.util.Collections;
public class Util2 {
public static void main(String[] args) {
ArrayList<Integer> arr=new ArrayList<Integer>();
for(int i=0;i<10;i++){
arr.add(10-i);
}
System.out.println(Collections.max(arr));
System.out.println(java.util.Collections.max(arr));
}
}
이외에도 final 클래스 관련 메서드를 모은 클래스의 경우에도 이에 해당한다(final은 상속이 불가능하기 때문이다. 그렇다면 final 클래스를 인스턴스를 생성해서 사용해도 되는것 아닌가?)
결국에 인스턴스를 생성할 필요없는 경우에 대해서 정적 메서드나 필드로 관리가 되는 것인데 그렇다면 이러한 클래스의 인스턴스 생성을 금지해야한다.
이경우 private 생성자를 추가하여 클래스의 인스턴스화를 막을 수 있다. 아이템 3번 에서 싱글톤을 위해 private 생성자를 설정한 것과 동일한 방식이다. 당연하겠지만 priavate 생성자를 사용하면 상속을 예방할 수도 있다.
5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
아이템 4번에서 우리는 유틸리티 클래스의 경우 인스턴스화 하지 않고 정적 메소드의 형태로 사용하는 것이 효과적이라는 것을 알고 있다. 그러나 무조건적인 정적 메소드 사용은 주의 해야한다. 다음예시를 보자
public class SpellChecker {
private static final Lexicon dictionary=new Lexicon();
private SpellChecker(){
}
public static SpellChecker INSTANCE=new SpellChecker();
public static boolean isValid(String word){
//....
return true;
}
public static List<String> suggestions(String typ0){
//....
return null;
}
}
Spellchecker 클래스에서 Spellchecker 인스턴스를 싱글톤으로 관리 되고 있는 것을 확인 할 수 있다. 별 문제가 없어 보이지만 Spellchecker 클래스는 dictionary 라는 필드에 의존적인 것을 확인할 수 있다. 따라서 만약 dictionary가 변경될 경우 유연하게 대처할수 없다. 따라서 이렇게 의존적인 자원을 가지고 있는 인스턴스의 경우 정적으로 관리하는 것이 옳바르지 못하다.(대부분의 경우 우리가 구현하는 클래스의 경우 특정 클래스에 의존적인 경우가 대부분이다.) 책에서는 인스턴스를 생성할때 필요한 자원을 넘겨주어 인스턴스를 생성하는 방식을 소개하고 있다.(spring의 DI와 동일하다) 다음 코드를 보자
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary){
this.dictionary= Objects.requireNonNull(dictionary);
}
public boolean isValid(String word){
return dictionary.isValidWord(word);
}
public List<String> suggestions(String typ0){
//....
return null;
}
}
SpellChecker의 모든 필드와 메서드를 static을 제거하고 생성자에 dictionary을 받아 해당 필드로 초기화한다.
class SpellCheckerTest {
// Mock Lexicon 클래스를 구현하여 테스트에 사용
private static class MockLexicon extends Lexicon {
@Override
public boolean isValidWord(String word) {
// 테스트에 필요한 동작 구현
return Arrays.asList("apple", "banana", "orange").contains(word);
}
}
@Test
void isValid() {
// MockLexicon을 사용하여 SpellChecker 객체 생성
SpellChecker spellChecker = new SpellChecker(new MockLexicon());
// isValid 메서드 테스트
assertTrue(spellChecker.isValid("apple"));
assertTrue(spellChecker.isValid("banana"));
assertTrue(spellChecker.isValid("orange"));
assertFalse(spellChecker.isValid("pear"));
assertFalse(spellChecker.isValid("grape"));
}
}
SpellChecker의 필드 dictionary는 객체 생성 시점에 생성자의 파라미터 값으로 초기화 되기 떄문에 변화에 유능하고, 다형성을 활용할 수 있다.
5. 불필요한 객체 생성을 피하라
불필요하다면 객체 생성을 피하거나 하나의 객체를 재사용하는게 좋다. 별 생각없이 코딩을 한다면 다음과 같은 실수를 범할 수 있다.
String dummy = new String("dummy");
System.out.println(dummy);
책에서 위와 같은 방법을 강력하게 주의하고 있다. 별문제가 없어보이겠지만 String에는 두가지의 선언 방식이 존재한다.
- String str = new String("") 으로 인스턴스 생성
- Sting str= "" 으로 상수풀에 인스턴스 생성 또는 이미 상수풀에 인스턴스가 존재한다면 pointing
1번의 경우 동일한 문자열에 대해서 계속해서 다른 주소로 인스턴스가 생성된다. 그러나 2번의 경우 상수풀에 존재하는 인스턴스를 가리키기 때문에 인스턴스가 계속 생성되지 않아 불필요한 메모리 낭비를 줄일 수 있다
Boolean의 경우에도 Boolean(string) 대신 Boolean.valueOf(String)으로 선언하는 것을 권장한다.(무슨 이유에서 인지 intellij 에서 Boolean(string)을 막고 있는 것이 확인되었다.자바 9이후에서는 deprecated API로 지정되었다.)
위와 같이 불변하는 인스턴스임이 보장되는 경우 재사용해서 사용하는 것을 권장한다.
책에서는 정규표현식에 사용되는 matches함수를 사용할때 matches함수내에 사용되는 Pattern 인스턴스를 캐싱한다면(재사용한다면) 더좋은 성능을 보인다고 한다.
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
public static boolean matches(String regex, CharSequence input) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}
public static Pattern compile(String regex) {
return new Pattern(regex, 0);
}
위코드는 matches의 실행 순서(Pattern과 String 클래스에서 긁어옴)로 matches->matches(boolean)->compile)순으로 실행되면 매번 matches가 실행될 때마다 Pattern 인스턴스가 생성되는 것을 알 수 있다.
public class RomanNumerals {
private static final Pattern ROMAN=Pattern.compile("something");
static boolean isRomanNumerals(String str){
return ROMAN.matcher(str).matches();
}
}
따라서 우리는 위와같이 Pattern 인스턴스를 캐싱하여 재사용한다면 보다 높은 성능을 기대할 수 있다. (정규표현식은 불편하게 사용되기 때문에 사용가능)
또한 박싱된 기본 타입의 사용 특히 오토 박싱에 대해서 책에서 경고하고 있다.
public static long sum(){
Long sum=0L;
for(long i=0;i<=Integer.MIN_VALUE;i++){
sum+=i;
}
return sum;
}
위 코드는 Long 과 long사이에서 오토 박싱이 되고 있다(long ->Long)
이경우 불필요한 객체가 계속 생성되게 된다. 더 안타까운것(?)은 오토 박싱 떄문에 해당 코드의 문제를 찾기 어렵다는 것이다. 따라서 박싱된 기본 타입보다는 기본타입을 지향하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자
'Java' 카테고리의 다른 글
[이펙티브 자바] 아이템 47, 48, 49 (2) | 2024.04.27 |
---|---|
[이펙티브 자바] 아이템 39, 40, 41 (1) | 2024.03.24 |
[이펙티브 자바] 아이템 10, 11 (1) | 2024.01.14 |
[이펙티브 자바] 아이템 7,8,9 (1) | 2024.01.13 |
[이펙티브 자바] 아이템 1, 2 (0) | 2024.01.04 |