반응형

ITEM 8 "finalizer 와 cleaner 사용을 피하라"

 

 

 

이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 해설 영상을 보는 것을 추천한다.

처음에 글을 읽자마자 이해가 안 되었는데 영상을 통해 이해 되는 부분이 많았다. 무조건 시청하는 것을 추천한다.

아래 동영상은 finalizer 를 오버라이딩 했을 때 부모 객체의 필드를 접근할 수 있는 공격 예제이다. 단점 4에서 설명한다.

 

자바는 두 가지 객체 소멸자를 제공한다. finalizer 와 cleaner.

finalizer 와 cleaner 는 해당 객체가 JVM 에서 Garbage Collection 을 해야 할 대상이 될 때 호출되는 메서드이며, 모두 GC 로 인해 자원이 회수가 되어야만 소멸을 할 수 있다.

 

자바 9에서는 finalizer 를 deprecated API 로 지정하고 cleaner 를 사용 대안으로 제시하고 있지만, 둘 다 엄청난 단점들을 가지고 있어 두 소멸자를 의지하여 코딩하면 안 된다. C++ 에서 destructor(소멸자) 와 같은 개념으로 오해하기 쉽지만 전혀 다른 개념이다. 정말 자원 반납을 하고 싶다면 다음 장에서 언급할 try-with-resource 나 try-finally 를 통해 해야한다.

 

단점 1

finalizer 와 cleaner 가 언제 실행이 될지 알 수 없다. 스코프를 벗어난 지역변수라던가, 값이 할당되지 않은(null 인) 변수들이 GC 의 대상이 된다는 것은 얼핏 알고 있지만 GC 의 대상이 될 뿐 언제 GC 가 회수해 가는지 누구도 알지 못 한다. 전적으로 GC 알고리즘에 달려 있으며, 구현체마다 천차만별이다. 자원 해제하는데 시간이 얼마나 걸릴지 모른다는 것은 타이밍이 중요한 작업에서는 hell(지옥)이다.

 

finalizer 스레드는 다른 애플리케이션 스레드보다 우선 순위가 낮아서 실행될 기회를 제대로 얻지 못 할 수도 있다. 한편, cleaner 는 백그라운드에서 실행하며 자신을 수행할 스레드를 제어할 수 있다는 면이 있지만 GC 통제하에 있다 보니 즉각 수행되리라는 보장이 없다.

 

즉시 실행되지 않을 뿐 아니라 아예 실행하지 않을 수도 있다. 자바 언어 명세서에서도 수행 여부조차 보장하지 않는 것을 볼 수 있다. 따라서, Finalizer나 Cleaner로 저장소 상태를 변경하는 일을 하지 말아야 된다. 데이터베이스 같은 자원의 락을 그것들로 반환하는 작업을 한다면 전체 분산 시스템이 멈춰 버릴 수도 있다.

 

System.gc 나 System.runFinalization 메서드에 속지 말아야 한다. 실행될 가능성을 높여줄 수는 있으나 보장하지 않는다.

사실 실행을 보장하기 위해 System.runFinalizersOnExit 와 Runtime.runFinalizersOnExit 메서드를 제공하려고 시도했지만 심각한 결함 때문에 수십 년간 지탄받아 왔다.

 

단점 2

finalizer 동작 중 발생한 예외는 무시된다. 처리할 작업이 있다고 하더라도 바로 종료된다. 종료될 때 마무리가 덜 된 상태로 남을 수 있고 다른 스레드가 훼손된 객체를 사용한다면 어떻게 동작할지 예측이 불가하다. 스택 추적 내역을 확인할 수도 없다.

 

단점 3

심각한 성능문제가 있다. AutoCloseable 객체를 생성하고 try-with-resource 로 자신을 닫게 했다면 12ns 가 걸리지만 finalizer 를 사용하면 550ns 가 걸렸다고 한다. cleaner 도 마찬가지이다. 

 

단점 4

원래 객체 생성이 안 되는데 상속 받아 하위 클래스에서 객체를 생성하고 finalize 를 오버라이딩해 상위 클래스의 기능을 사용할 수 있다. (finalize attack) finalize 는 객체가 소멸될 때 GC 에 의해 발생하므로 강제로 exception 을 발생시켜 해제함과 동시에 GC 가 회수해 가야 된다는 조건이 있긴 하다. 객체 생성을 막기 위해 생성자에서 예외를 던졌는데 finalize 를 통해 우회한 것이다. 사실 하위 클래스를 만들 권한이 있다면, 이미 심각한 수준의 보안 문제를 야기한 것이라고 생각하지만 Trigger 과정에서 공격의 범위가 넓어지는 것이니 보안 문제라고 볼 수 있다.

 

public class Account {
    private String accountName;
    public Account(String accountName) {
        this.accountName = accountName;
        if (this.accountName.equals("러시아")) {
            throw new IllegalArgumentException("돌아가");
        }
    }
    public void transfer(int amount, String to) {
        System.out.printf("transfer %d from %s to %s.", amount, this.accountName, to);
    }
}
public class BrokenAccount extends Account {
    public BrokenAccount(String name) {
        super(name);
    }
    @Override
    protected void finalize() throws Throwable {
        this.transfer(10000, "gold-egg");
    }
}

class AccountTest {
    @Test
    void 오_마이_갓() {
        Account account = null;
        try {
            account = new BrokenAccount("러시아");
        } catch (Exception exception) {
            System.out.println("No?");
        }
        System.gc();
        Thread.sleep(3000L);
    }
}

 

정말 finalize 를 사용해야 한다면 위와 같은 보안 문제를 막기 위해 final 로 선언하자. 

 

그렇다면, 어떻게 자원을 해제해야 하는 것일까?

그저 자원을 해제할 클래스에 AutoCloseable 을 구현해주고 클라이언트에서 close 메서드를 호출하면 된다.

 

public class Resource implements AutoCloseable {
	private boolean closed;
    @Override
    public void close() throws RuntimeException {
        if (this.closed) {
        	throw new IllegalStateException();
        }
        closed = true;
        System.out.println("close");
    }
    public void hello() {
        System.out.println("hello");
    }
    // 안정망
    @override
    protected final void finalize() throws Throwable {
    	if(!this.closed) close();
    }
}

public class Runner {
    public static void main(String[] args) {
        Resource resource1 = null;
        // 첫 번째 방법. try-catch
        try{
            resource1 = new Resource();
            resource1.hello();
        } finally {
            if(resource1 != null) {
                resource1.close();
            }
        }

        // 두번째 방법. try-with-resource
        // scope 를 벗어나면 자동으로 close 메서드 호출
        try(Resource resource2 = new Resource()) {
            resource2.hello();
        }
    }
}

 

그럼 Cleaner 와 Finalizer 는 언제 필요할까?

첫 번째. 안전망 역할.

클라이언트가 실수로 close 메서드를 호출하지 않아 자원 반납을 안 할 수 있다. cleaner 와 finalizer 가 즉시 호출되지는 않지만 늦게라도 자원 회수를 해주는 것이 안전하기 때문에 사용된다. 바로 위 코드에서 finalize 를 오버라이딩해 close 를 삽입하여 안전망을 설치하는 것을 볼 수 있다.

 

두 번째. 네이티브 피어와 연결된 객체를 해제할 때 사용.

네이티브 오브젝트는 순수 자바코드로만 프로그래밍되지 않은 오브젝트를 말한다. 간혹 다른 언어나 어셈블리어를 통해 기능을 구현할 때가 있는데 그 기능들의 메서드를 통해 위임받아 기능을 수행하는 자바 오브젝트를 네이티브 피어라고 한다. 이 네티이브 피어는 가비지 컬렉터에서 그 존재를 알지 못하기 때문에 자바 피어를 회수할 때 네이티브 객체까지 회수하지 못한다. 명시적으로 close 메서드를 사용하는 것이 best 이나 cleaner 와 finalizer 로도 자원 회수가 가능하다.

권장은 역시 close 메서드!

 

cleaner 사용 방법

위에서 finalizer 와 관련된 코드를 살펴보았지만 cleaner 사용방법은 확인하지 않았다.

AutoClosable 구현체 내부에 Cleaner 와 Cleanable 를 생성 후 생성자에서 해당 객체와 청소할 객체를 연결시켜주면 끝이다. 주의할 점은 청소할 객체에 AutoClosable 구현 객체를 주입하면 안 된다. 순환참조가 발생한다. 

 

public class Resource implements AutoCloseable {
    private boolean closed;
    private static final Cleaner CLEANER = Cleaner.create();
    private final Cleaner.Cleanable cleanable;
    private final ResourceCleaner resourceCleaner;

    public Resource(){
        this.resourceCleaner = new ResourceCleaner();
        this.cleanable = CLEANER.register(this, resourceCleaner);
    }
    private static class ResourceCleaner implements Runnable {
        // 절대로 Resource 객체를 이 클래스에 주입하면 안 된다.

        @Override
        public void run() {
            System.out.println("Clean");
        }
    }
    @Override
    public void close() throws RuntimeException {
        if (this.closed) {
            throw new IllegalStateException();
        }
        closed = True;
        cleanable.clean();
    }
}
반응형
반응형

ITEM 7 "다 쓴 객체 참조를 해제하라"

 

 

이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 해설 영상을 보는 것을 추천한다.

열심히 문어체로 정리하고 작성하지만, 구어체를 따라 올 전달력은 없는 것 같다. 열심히 예를 들어 설명해주셔서 덕분에 이해가 잘 됐다.

 

자바에는 GC (Garbage Collector) 가 있기 때문에 메모리 관리에 대해 신경을 쓰지 않아도 될 것이라고 생각하기 쉽지만, 사실 그렇지 않다. GC 가 언제 메모리를 회수해 가는지 정책(?) 을 잘 알아야 메모리 누수 문제에 해방될 수 있다.

 

public class Stack {

    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        this.ensureCapacity();
        this.elements[size++] = e;
    }

    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        return this.elements[--size]; // 여기가 문제!
    }

    /**
     * 원소를 위한 공간을 적어도 하나 이상 확보한다.
     * 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다.
     */
    private void ensureCapacity() {
        if (this.elements.length == size) {
            this.elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

 

위 스택을 구현한 코드는 메모리 누수 문제가 있다.

스택에 아이템을 계속 쌓았다가 다시 빼냈다고 가정해보자. 스택이 차지하는 메모리가 줄어들 것이라고 예상이 되지만, 줄어들지 않는다. pop 메서드에서 this.element 를 리턴하고 있는데 배열의 인덱스만 줄어들었을 뿐 배열의 크기는 조정이 된 것이 없기 때문에 계속해서 메모리만 할당이 되고 해제가 되지 않는 것이다.

 

그렇다고 해도 GC 가 주기적으로 검사하여 해제를 할 수 있을 것 같은데 무엇이 문제일까?

GC 는 scope 중심으로 scope 가 끝나는 지점에 더 이상 사용이 되지 않는 변수들을 해제한다. 하지만 위와 같이 직접 메모리를 관리하는 경우, 명시적으로 null 을 삽입해주어야 해제 대상이 된다. 위 스택 코드를 보면 element 배열에 있는 값을 그대로 두고 인덱스만 조정하기 때문에 값은 그대로 있게 되고 해제 대상이 되지 않는 것을 볼 수 있다. (객체들의 다 쓴 레퍼런스를 그대로 가지고 있다라고 표현한다.)

 

public Object pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }

    Object value = this.elements[--size];
    this.elements[size] = null;
    return value;
}

 

위와 같이 스택에서 꺼낼 때 그 자리를 null 로 설정해주면 GC 가 발생할 때 레퍼런스가 정리된다. 실수로 null 처리한 참조를 사용하여 NullPointerException 이 발생할 수 있지만, null 이 처리되지 않고 인식하지 못 한 상태에서 값이 사용되는 것보단 낫다. 프로그래밍 에러는 언제든지 빨리 포착하는 것이 유익하다.

 

하지만, 모든 필요 없는 객체를 null 로 만들 필요는 없다. 그 레퍼런스를 가리키는 변수를 스코프 안에서만 사용한다면 문제가 없다. ("변수를 가능한 가장 최소의 scope 안에서 처리하라" 라는 규칙만 지킨다면 자연스럽게 해결할 수 있다.)

보통은 위와 같은 규칙을 지키면 문제가 없고, 스택 코드처럼 메모리를 직접 관리하는 코드만 메모리 누수에 주의하면 된다.

 

캐시 역시 메모리 누수를 일으키는 주범이다. 재사용할 목적으로 객체의 레퍼런스를 캐시에 넣어두지만, 언제 이 캐시가 유효한지 정확히 정의하기 어렵기 때문에 객체를 다 쓴 뒤에도 비우는 것을 잊기 쉽다. 외부에서 키를 참조하는 동안만 엔트리가 살아 있다고 가정하면 WeakHashMap 을 사용하는 것도 나쁘지 않다. 캐시의 키에 대한 레퍼런스가 캐시 밖에 서 필요 없어지만 자동으로 제거되는 원리이다.

 

그런데 보통은 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 사용한다. 백그라운드 스레드를 통해 쓰지 않는 엔트리를 청소하거나 캐시에 새 엔트리를 추가할 때 엔트리를 검사하여 청소하는 방법이 있다. LinkedHashMap 은 removeEldestEntry 메서드가 후자의 방식으로 처리한다.

 

리스너와 콜백도 메모리 누수를 일으킬 수 있다. 콜백을 등록하기만 하고 명확히 해지하지 않는다면 계속 쌓이기만 할 것이다. 이럴 때 콜백을 약한 참조로 저장하면 GC 가 수거할 수 있다. WeakHashMap 에 키로 저장하는 방법도 한 예이다.

 

Weak 레퍼런스에 대한 자세한 내용은 아래 링크를 참조하자.

https://web.archive.org/web/20061130103858/http://weblogs.java.net/blog/enicholas/archive/2006/05/understanding_w.html

 

"다 쓴 객체 참조를 살려두면 GC 는 그 객체 뿐만 아니라 그 객체를 참조하는 모든 객체를 회수해가지 못 한다.

GC 의 편리성에 속아 성능을 잃지 말자."

반응형
반응형

ITEM 6 "불필요한 객체 생성을 피하라"

 

 

이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 해설 영상을 보는 것을 추천한다.

열심히 문어체로 정리하고 작성하지만, 구어체를 따라 올 전달력은 없는 것 같다. 열심히 예를 들어 설명해주셔서 덕분에 이해가 잘 됐다.

 

객체를 새로 만드는 대신 하나를 재사용하는 것이 적절할 때가 있다. 불변 객체의 경우 언제든 재사용할 수 있고 가변 객체라고 하더라도 사용 중에 변경이 되지 않을 것임을 안다면 재사용할 수 있다. 재사용할 수 있음에도 불구하고 매번 같은 기능의 객체를 생성하는 것은 불필요하며 성능에 제약을 가져다 준다. 불필요한 객체를 생성하는 예를 살펴보자.

 

new String() 을 사용하지 말자.

자바의 문자열 String 을 new 로 생성하면 매번 새로운 객체를 만들게 된다.

가급적 String s = "gold-egg"; 와 같이 String 객체를 생성하는 것이 올바르다. 문자열이 같다면 리터럴 자체를 재사용한다.

 

String s1 = new String("gold-egg");
String s2 = new String("gold-egg");
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // true

String s3 = "gold-egg";
String s4 = "gold-egg";
System.out.println(s3 == s4); // true
System.out.println(s3.equals(s4)); // true

 

static 팩토리 메서드를 사용하자.

Item 1 에서 살펴보았듯이 static 팩토리 메서드는 매번 새로운 객체를 만들지 않고 캐싱해 둔 객체를 리턴할 수 있다.

Boolean(String) 대신 Boolean.valueOf(String) 처럼 static 팩토리 메서드를 사용하자. 생성자는 매번 새로운 객체를 생성하기 때문에 위와 같이 불변객체라면 팩토리 메서드를 통해 캐싱된 객체를 사용하는 것이 바람직하다.

 

비싼 객체라면 재사용할 수 있는지 고려해야 한다.

객체 생성 시, 메모리나 시간이 오래 걸리는 객체를 비싼 객체라고 표현한다. 이런 비싼 객체가 반복해서 필요하다면 성능 문제가 발생할 수 있다. 캐싱하여 재사용할 수 있는지 검토해야 한다.

 

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 메서드를 통해 정규표현식이 매치되는지 확인하는데 내부적으로 정규표현식용 Pattern 인스턴스가 만들어진다. matches 메서드 인자 값이 Pattern 인스턴스 재료이다. 그런데 잘 보면 매치가 일어난 다음에 scope 가 끝나버려 바로 GC 의 대상이 되는 것을 볼 수 있다.

 

Pattern 인스턴스는 정규표현식을 표현하기 위해 내부적으로 FSM 유한 상태 기계를 만드는데 이 비용이 비싸다. Pattern 인스턴스가 불변이라면 클래스 초기화 과정에 직접 생성해 캐싱해두고, 필요할 때마다 재사용한다면 성능을 향상시킬 수 있다.

 

public class RomanNumber {

    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();
    }

}

 

개선 후 조금 더 빨라지고 코드도 더 명확해진 것을 볼 수 있다. 어떤 문자열을 매칭하는 것인지 몰랐지만 Pattern 을 바깥으로 끄집어 내면서 ROMAN 이라는 문자열을 통해 무엇을 비교하는지 명확해진 것이다. 그러나 만약 초기화 된 후 isRomanNumeral 메서드를 사용하지 않는다면 쓸데없이 객체를 생성한 꼴이 된다는 점도 알아야 한다. 처음 호출 할 때 초기화가 될 수 있도록 지연 초기화라는 방법을 생각할 수 있지만 코드는 더 복잡해지고 성능은 개선되지 않는 경우가 많아 비추천한다.

 

반대로 같은 객체라고 생각을 하지 못 하고 객체를 공유하여 사용해서 잘못된 side-effect 가 발생할 수도 있다.

 

public static void main(String[] args) {
    Map<Integer, String> apt = new HashMap<>();
    apt.put(101, "gold-egg");
    apt.put(102, "silver-egg");

    Set<Integer> keySet1 = apt.keySet();
    Set<Integer> keySet2 = apt.keySet();

    System.out.println(keySet1 == keySet2); // true

    keySet1.remove(101);
    System.out.println(keySet2.size()); // 1
    System.out.println(apt.size()); // 1
}

 

불변 객체라면 안전하게 재사용할 수 있지만 어댑터 패턴으로 생성된 뷰 객체들은 원본 객체들을 수정할 수 있기 때문에 주의가 필요하다. Map 인터페이스의 KeySet 메서드는 Map 뒤에 있는 Set 인터페이스의 뷰를 제공한다. key 들만 따로 모아놓은 Set 인터페이스인데 호출할 때마다 새로운 객체가 생성되는 것이 아니라 같은 객체를 리턴하게 된다.

 

리턴 받은 Set 객체를 변경하면 그 뒤에 있는 Map 객체도 변경하게 될 수 있다.

 

오토박싱은 불필요한 객체를 생성한다.

오토박싱과 오토 언박싱은 프리미티브 타입과 레퍼런스 타입을 섞어 사용할 때 자동으로 상호 변환해주는 기술이다.

Int <-> Integer, boolean <-> Boolean 등... 

 

public class AutoBoxingExample {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        Long sum = 0l;
        for (long i = 0 ; i <= Integer.MAX_VALUE ; i++) {
            sum += i;
        }
        System.out.println(sum);
        System.out.println(System.currentTimeMillis() - start);
    }
}

 

위 예제에서 sum 변수의 타입을 실수해서 Long(대문자) 로 만들었기 때문에 불필요한 Long 객체가 2의 31제곱만큼 만들어지게 된다. i 와 sum 둘 다 long 타입이었으면 별도의 객체를 만들지 않아도 되었을텐데 레퍼런스 타입 때문에 불필요한 객체가 많이 생기게 되는 것이다. 오토박싱은 프리미티브 타입과 레퍼런스 타입의 경계가 안 보여주지만 그렇다고 그 경계가 없어지진 않는다.

 

"그렇다고 객체 생성이 항상 비싸며 가급적 피해야 한다는 오해를 해서는 안 된다.

방어적인 복사를 해야 하는 경우에도 객체를 재사용하면 심각한 버그와 보안 문제가 발생할 수 있다.

불필요한 객체 생성을 피해야 한다는 점을 리마인드해야 한다."

반응형
반응형

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

 

 

이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 해설 영상을 보는 것을 추천한다.

열심히 문어체로 정리하고 작성하지만, 구어체를 따라 올 전달력은 없는 것 같다. 열심히 예를 들어 설명해주셔서 덕분에 이해가 잘 됐다.

 

객체 지향 세계에서 객체들은 독립적이고 자신의 상태를 책임지기 때문에 다른 객체에서 값을 꺼내 사용하기 보다는 직접 그 객체에게 메세지를 보내 처리한다. 그래서 대상 객체에 메세지를 보내기 위해 대상 객체를 내포하고 있다.

 

class A {
	private B b;
}

 

A 에서 B 로 흐름이 이어지니 A -> B 로 의존성이 있다고 표현한다. 이번 장에서는 이런 의존성을 관계로 맺을 때 직접 객체를 명시하지 말고 DI(Dependency Injection) 를 사용하라고 권장하고 있다.

 

class A {
	// 직접 명시
	private final B b = new B();
    
    // DI
    private final C c;
    public A (C c) { this.c = c; }
}

 

직접 명시하게 될 경우 B 가 아닌 다른 객체를 테스트해보고 싶을 때 A 라는 클래스를 매번 수정해야 된다는 단점이 있다. 또한 final 지시자를 지우고 다른 메서드에서 b 라는 객체를 다른 객체로 바꾸더라도 멀티 쓰레드 환경에서는 적합하지 않다. 원래 있었던 객체를 다른 객체로 바꾸어도 동작을 보장해야 되는데 불안전하다.

 

Item 3 과 Item 4 에서 소개한 싱글톤 객체와 유틸 클래스는 한 번 로드가 되면 수정이 불가능하거나 static 한 변수를 사용하기 때문에 위와 같이 직접 명시를 사용한다. 앞에서 살펴보았듯이 유연하지 않으며 사실 두 클래스 모두 객체 지향적인 사고가 아니라는 것을 알 수 있다. 일반적이지 않으니 꼭 필요할 때만 사용해야 한다.

 

아래 방법처럼 DI 를 사용하면 많은 이점이 있다. 연관된 객체가 여러 개이든 상관 없이 그 추상 객체 하나만 주입 받으면 문제 없이 잘 동작한다. 또한 불변을 보장하여 여러 클라이언트가 의존 객체들을 안심하고 공유할 수 있다. 주입 당시 다른 객체들을 삽입할 수 있으므로 테스트에도 용이하다.

 

사실, 디자인 패턴의 꽃이라고 불리는 전략 패턴도 사실 이 DI (의존관계, 연관관계) 를 강조한 개념이다.

 

public class A {
    // 스프링 프레임워크 같은 DI 프레임워크에서 지원해주는 필드 주입
    @AutoWired
    private final B b;

    // 생성자 주입
    public A(B b) { this.b = b; }

    // Setter 주입
    public void setB(B b) { this.b = b; }

    // 일반 메서드 주입 (Setter 주입과 유사하다.)
    public void ilban(B b) { this.b = b; }

    // 실제 사용 예시
    public static void main(String[] args) {
        B b = new B();
        A a = new A(b);
    }
}

 

위와 같이 DI 는 생성자 주입, 필드 주입, 수정자 주입, 일반 메서드 주입 등 다양한 곳에서 주입할 수 있다. 필드 주입은 스프링 프레임워크와 같은 DI 프레임워크가 없으면 불가능하며 외부에서 접근이 불가능하다는 단점이 있다. 수정자 주입(Setter)은 주입 받는 객체가 변경될 가능성이 있는 경우에 사용되는데 객체가 변경될 가능성이 극히 드물다.

 

생성자 주입이 "객체의 불변성 확보", "테스트 코드 작성 용이", "필드 객체에 final 키워드 사용 가능" 이라는 이점들이 많아 제일 많이 사용된다. 생성자 주입을 응용하여 팩토리 메서드 패턴으로 활용할 수도 있다. 자바 8부터 제공해주는 Supplier<T> 함수형 인터페이스를 사용하여 지금 당장 객체를 만들어 주입하지 않더라도, 나중에 주입해줄 수 있게 생성자에 자원 팩토리를 넘겨줄 수도 있다. 타입에 한정적 와일드 카입을 사용한다면 (Supplier<? extends A>) A 클래스의 하위 클래스도 상황에 따라 생성할 수 있는 유연함도 갖추게 된다.

 

하지만 이러한 주입도 위 코드 실제 사용 예시 보면 주입 한 개당 여러 개의 구문을 작성해야 한다는 단점이 있다. 지금은 한 개 밖에 되지 않지만 의존 관계가 체인 형태로 구성되어 있고 갯수가 많아지면 복잡해지고 관리가 힘들 것이다.

현실적으로 대거, 주스, 스프링 같은 DI 프레임워크를 사용하여 개발할 가능성이 크다. ^^

반응형
반응형

Supplier 인터페이스

 

도입 시기 JAVA 8
분류 함수형 인터페이스
함수 원형 @FunctionalInterface
public interface Supplier<T>
필드 X
메서드 T get()
특징 T 타입을 반환하는 함수를 정의하고 get 메서드를 통해 결과를 리턴하는 메서드 하나만 가지고 있다.

 

Intellij 에서 정의부분 캡처

 

사용 예제

public class Test {
    private int id;
    private String value;
    public Test(int id, String value) {
        this.id = id;
        this.value = value;
    }

    public int getId() {
        return id;
    }

    public String getValue() {
        return value;
    }
}

public class Example {

    public static void main(String[] args) {

        Supplier<Test> testSupplier = ()-> new Test(0, "Hello");
        Test result = testSupplier.get();
        System.out.println(result.getId() + ", " + result.getValue());
    }
}

 

Why? 어디에 쓰이는가?

함수형 인터페이스를 왜 사용하는 것인가에 대한 질문과 동일하다.

함수형 인터페이스는 1개의 추상 메서드를 가지고 있는 인터페이스를 의미한다. 자바의 람다 표현식이 이 함수 인터페이스로만 사용 가능하다. (람다 함수는 익명 함수로 함수를 간단하게 만드는 표현식이다.)

 

함수를 변수화할 수 있다는 의미이다. 변수로 다룰 경우 직접 계산하지 않아도 된다는 장점이 있다. (Lazy Evaluation)

 

다음과 같이 여러 변수를 체크하는 함수가 있는데 "func(T a, T b, T c)", c 라는 변수가 고비용이 발생하는 함수를 거쳐 반환이 된다고 가정하자. func 함수에서는 a 와 b 를 체크해서 c 라는 값을 계산할지 결정한다고 하면 c 라는 변수를 실제로 사용하지 않는 경우의 수도 있을 것이다. 보통 순차적으로 실행되기 때문에 c 라는 고비용 함수를 매번 거쳐야 되서 성능 이슈에 문제가 있을 수도 있다. 아래 예제를 살펴보자.

 

public class NumberWorld {
    private static void negativeTest(int number, String expression) {
        if (number < 0) {
            System.out.println("This Value is Negative Written by" + expression);
        } else {
            System.out.println("This Value is Positive");
        }

    }
    private static String expressionProcessing() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "gold-egg";
    }
    public static void main(String[] args) {
        negativeTest(-1, expressionProcessing());
        negativeTest(1, expressionProcessing());
        negativeTest(2, expressionProcessing());
    }
}

 

위 예제 negativeTest 함수는 number 가 양수일 때 고비용 함수 expressionProcessing 의 결과 값인 expression 을 사용하지 않아도 된다. 그럼에도 불구하고 매번 expressionProcessing 함수가 호출되는 것을 볼 수 있다. 실제 필요한 구간에만 실행하게 해준다면 더 빠른 속도로 처리할 수 있을 것이다. 이런 개념을 Lazy Evaluation 라고 한다.

 

Supplier 를 활용하여 Lazy Evaluation 을 적용하면 다음과 같이 작성할 수 있다.

 

public class NumberWorld {
    private static void negativeTest(int number, Supplier<String> expressionSupplier) {
        if (number < 0) {
            System.out.println("This Value is Negative Written by" + expressionSupplier.get());
        } else {
            System.out.println("This Value is Positive");
        }

    }
    private static String expressionProcessing() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "gold-egg";
    }
    public static void main(String[] args) {
        negativeTest(-1, () -> expressionProcessing());
        negativeTest(1, () -> expressionProcessing());
        negativeTest(2, () -> expressionProcessing());
    }
}

 

Supplier와 Lazy Evaluation 에 대해 더 자세히 알고 싶으면 아래 동영상 링크를 확인하자.

반응형
반응형

ITEM 4 "인스턴스화를 막으려거든 private 생성자를 사용하라"

 

 

이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 해설 영상을 보는 것을 추천한다.

열심히 문어체로 정리하고 작성하지만, 구어체를 따라 올 전달력은 없는 것 같다. 열심히 예를 들어 설명해주셔서 덕분에 이해가 잘 됐다.

 

이번 장에서는 인스턴스를 막기 위해 private 생성자를 사용할 것을 권장하고 있다.

 

가끔 유틸 클래스처럼 정적 메서드와 정적 필드만 있는 클래스를 설계할 때가 있다. 유용한 메서드들을 한 곳에 모아둘 때 사용하곤 하는데 객체 생성 없이 사용할 수 있어 객체지향적 사고는 아니다. 이러한 클래스는 굳이 생성자가 필요하지 않기 때문에 생성자 생성을 제한하는 장치가 필요하다.

 

public abstract class UtilClass {
    public static String getName() { return "gold-egg"; }

    static class AnotherClass extends UtilClass {

    }

    public static void main(String[] args) {
        // abstract 추상 클래스에서는 인스턴스를 만들지 못 한다.
        UtilClass utilClass = new UtilClass();
        // 그 대신 상속해서 사용이 가능하다.
        AnotherClass anotherClass = new AnotherClass();
        // anotherClass.getName() 이 불가하다.

        UtilClass.getName();
    }
}

 

위 코드와 같이 생성자를 만들지 않으면 컴파일러가 자동으로 public 생성자를 만들어 준다. 사용자는 생성자가 자동 생성 된 것인지, 컴파일러가 만들어 준 것인지 알 수 없다. 이렇게 생성된 생성자를 사용할 경우, 잘못된 side effect 를 초래할 수 있다.

 

보통 Spring 프레임워크처럼 abstract 추상화 클래스를 선언하여 인스턴스 생성을 방지한다. 추상화 클래스를 상속하여 하위 클래스에서 인스턴스를 만들 수 있기 때문에 완벽한 금지 방법은 아니다. 또한, abstract 클래스는 원래 상속을 권장하기 위해 만들어진 클래스이므로 오해할 가능성이 있다. 그래서 책에서는 private 생성자를 명시하여 인스턴스화를 막는 것을 권장하고 있다.

 

클래스가 정말 정적 멤버(static 변수와 static 함수) 로만 이루어져 있다면 설령 자식 클래스로 인스턴스를 만든다고 하더라도 해당 static 함수를 사용할 수가 없다. anotherClass.getName() 부분이 불가한 것을 볼 수 있다. 인스턴스를 생성하더라도 못 쓰는 것이다. 아마도 Spring 프레임워크에서도 이 이유 때문에 abstract 클래스만 선언하여 사용하는 것으로 추정된다.

 

그럼에도 불구하고 하위 클래스에서도 생성자 생성을 금지하고 싶고 생성자를 호출하지 않게 하려면 아래와 같이 private 생성자를 만들고 접근 시 AssertionError 예외를 발생시키라고 권고하고 있다.

 

public class Test {
	private Test() { throw new AssertionError(); }
}

 

반응형
반응형

ITEM 3 "private 생성자나 열거 타입으로 싱글톤임을 보장하라"

 

 

이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 해설 영상을 보는 것을 추천한다.

열심히 문어체로 정리하고 작성하지만, 구어체를 따라 올 전달력은 없는 것 같다. 열심히 예를 들어 설명해주셔서 덕분에 이해가 잘 됐다.

 

이번 장에서는 private 생성자나 열거 타입으로 싱글톤임을 보장해야 한다고 서술하고 있다.

 

싱글톤 패턴이란, 객체의 인스턴스가 오직 1개만 생성이 되는 패턴을 말한다.

최초 한 번만 메모리에 할당이 되고 그 인스턴스를 어디에서든지 참조할 수 있도록 할 수 있다. 어플리케이션 로딩 개의 인스턴스 생성만 필요한 경우에 패턴을 사용한다. 설정 파일이나 데이터베이스 연결 등이 예이다.

 

싱글톤 클래스를 사용할 , 생성된 객체를 공용으로 사용하고 별도로 생성하는 것을 금지한다고 약속할 있다. 하지만 사람이기 때문에 실수할 있다. 한 번만 생성해야 하는 객체를 여러 번 생성할 수 있다.

 

그래서 클래스 선언 외부에서 인스턴스를 생성하지 못하게 별도의 장치를 설정하는데 이번 장에서 소개할 private 생성자와 열거 타입이 주인공이다.

 

번째 방법은,

private 생성자를 사용하여 클래스 내부에 public static final 필드로 객체를 생성해두어 인스턴스를 반환하는 방법이다. final 필드이기 때문에 초기화 번만 호출된다.

 

// 선언부
public class Test {
    public static final Test INSTANCE = new test();
    private Test(){}
}

// 사용부
Test test1 = Test.INSTANCE;
Test test2 = Test.INSTANCE;

 

번째 방법은,

번째 방법과 유사한데 public static final 필드를 private static final 필드로 바꾸고, 정적 팩토리 메서드를 제공하는 것이다.

 

public class Test {
    private static final Test INSTANCE = new test();
    private Test() {}
    public static Test getInstance() { return INSTANCE; }
}

 

방법 모두 항상 같은 객체를 반환하므로 안전해보인다. 책에서는 Reflection API 통해 setAccessible 사용해 권한을 획득하고 private 생성자를 호출할 있다고 하는데 사실 협업 과정에서 저렇게까지 코딩하지는 않을 같다. ㅎㅎㅎ;;; 싱글톤처럼 전역적으로 사용하는 객체는 안전하게 제공해야 의무가 있기 때문에 책에서 제안하는 것처럼 생성자에 번째 생성자를 생성할 Exception 예외를 던지자.

 

// 생성자 내부에서 count 변수 체크 후 1 이상이면 예외 출력
static int count;
private Test(){
    count++;
    if (count != 1) {
        throw new IllegalStateException("this object should be singleton");
    }
}

 

번째 방법은 간결하고 해당 클래스가 싱글톤이라는 점을 명백히 알려준다.

public static final 이니 절대로 다른 객체를 참조할 없다.

 

번째 방법은 싱글톤이 아니게 변경할 있다는 장점이 있다. 팩터리 메서드에서 반환하던 싱글톤 객체를 다른 객체로 변경이 가능하다. 제네릭 싱글톤 팩터리로 만들어 있다. 마지막으로 정적 팩터리의 메서드 참조를 공급자로 사용할 있다. getInstance Supplier<객체> 사용하는 식이다.

이러한 장점들이 있지만 싱글톤이 아니게 변경될 있다는 유연한 구조 때문에 번째 방법의 장점을 사용하지 않을 것이라면 가급적 번째 방법을 권장하고 있다.
 

안전하게 싱글톤 객체를 만들기 위해 가지 경우를 생각해야 한다. Java 에서는Serializable 클래스로 직렬화를 제공하고 있는데 역직렬화할 때 가짜 객체를 생성한다. 가짜 객체 대신 진짜 객체를 반환하기 위해서 readResolve 함수를 재정의해야 한다. (모든 필드를 transient 선언해야 한다는 점도 잊지 말아야 한다.) 아래는 Java 에서 Serializable 클래스를 사용했을 직렬화/역직렬화 구조이다.

 

 

앞서 소개한 방법은 리플렉션이나 직렬화 문제 때문에 보완적인 코드들을 많이 생성해야 한다. 마지막 번째 방법, Enum 열거를 활용한 방법도 있다. 복잡한 직렬화 상황이나 리플렉션 구조에도 완벽히 방어할 있으며 간단한다. 하지만 상속은 못 한다는 단점이 있다.

 

// 선언부
public enum Test {
    INSTANCE;
    public String getName() {
        return "gold-egg";
    }
}

// 사용부
String name = Test.INSTANCE.getName();

 

반응형
반응형

오늘의 공부 영상은 아래 영상입니다. 원본 영상은 아래 링크를 참조해주세요~

 

 

TDDTest Driven Development 의 약자로 "테스트 주도 개발"이라고 한다. 작은 단위의 테스트를 먼저 작성하고 그 테스트를 통과하는 코드를 지속적으로 추가하면서 짧게 개발하는 애자일 방법론 중 하나이다.

 

TDD 사이클

 

TDD 사이클은 위 그림과 같다. INITAL TEST 단계부터 시작해서 REFACTOR CYCLE 까지 순차적으로 반복 수행한다.

INITAL TEST 단계에서는 실패하는 테스트 코드를 먼저 작성한다. 다음 단계 가기 전까지 실제 로직에 들어가는 코드를 작성하면 안 된다. CODE 단계에서는 테스트 코드를 성공시키기 위한 실제 로직을 작성한다. REFACTOR CYCLE 단계에서는 중복 코드 제거, 일반화 등 리펙토링을 수행한다.

 

먼저 실패하는 테스트 코드를 작성하는 것은 어렵게 느껴지지 않는다. 요구사항에 주어진 입력과 출력의 경계라던지, 요구사항에 주어진 규칙에 반대되는 코드들을 작성하면 된다. 그런데 막상 해보면 알고리즘 문제에서 주어지는 통합환경에서의 테스트 정도다.

 

이 정도의 테스트만 해도 될까? 더 해야 되는 테스트가 있을까? 그런 생각을 하던 찰나에 위 영상을 보게 되었다.

영상에서는 처음부터 TDD 를 의식하지 말고 아래와 같은 규칙을 만들면서 코딩 연습을 해보라고 권장하고 있다.

 

코드 컨벤션을 지키면서 프로그래밍한다.

(자바의 경우, Google Java Style Guide)

한 메서드에 오직 한 단계의 들여쓰기만 한다.

함수 또는 메서드가 한 가지 일만 할 수 있도록 작게 만든다.

else 예약어를 쓰지 않는다.

모든 원시값과 문자열을 포장한다.

한 줄에 점을 하나만 찍는다.

불필요하게 공백 라인을 만들지 않는다.

공백 라인을 띄우는 것도 의미있게 띄우자.

줄여쓰지 않는다.(축약 금지)

모든 엔티티를 작게 유지한다.

3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.

일급 컬렉션을 쓴다.

객체에서 데이터를 꺼내지 말고 객체에 메세지를 보내라.

Getter/Setter/Property 를 쓰지 않는다.

메서드(함수)에서 이상적인 인자 개수는 0개이다. 인자의 갯수를 3개 이하로 줄여라.

 

위 규칙들을 지키면서 코딩하게 되면, 자연스레 메서드와 클래스가 작은 단위로 쪼개지고 각 단위 별 테스트가 가능해진다. 마지막 문장은 클린 코드에서 본 문장이었는데 억지스러운 문장처럼 보이지만 실제로 된다. 저 내용 말고도 더 많은 내용들이 있겠지만, 오늘부터 위 내용을 반드시 지키면서 코딩해보려고 한다.

 

끝으로 영상 마지막 부분에 누가 강사님께 질문을 했는데 내가 직접 답변해보면서 마무리하고자 한다.

 

Q . TDD 로 개발하다보면 재사용성은 좋지만 같은 기능을 중복되서 작성하거나, 성능에 이슈를 가져오는 코드가 생길 수 있는데 어떻게 생각하시나요?

 

A . 객체지향으로 설계하다 보면 간혹 기능이 중복되는 코드들이 존재하는데 나중에 리팩토링 단계에서 합성이나 프록시 패턴으로 반복되는 구간을 함께 운영하면 좋을 것 같다. 테스트를 용이하게 작성하는 것이 제일 중요하다. 그 후 성능에 가져오는 코드들만 모아 다시 리팩토링하면 좋지 않을까 생각한다.

반응형

'독후감 > 교양' 카테고리의 다른 글

[아키텍처] 헥사고날 아키텍처  (0) 2022.08.08

+ Recent posts