아이템 8 - finalizer 와 cleaner 사용을 피하라 - 완벽 공략
이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.
정적이 아닌 중첩 클래스는 자동으로 바깥 객체의 참조를 갖는다.
static 이 아닌 중첩 클래스는 자동으로 바깥 객체의 참조를 갖는다.
public class OuterClass {
class InnerClass {
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
InnerClass innerClass = outerClass.new InnerClass();
System.out.println(innerClass);
outerClass.printFiled();
}
private void printFiled() {
Field[] declaredFields = InnerClass.class.getDeclaredFields();
for(Field field : declaredFields) {
System.out.println("field type:" + field.getType());
System.out.println("field name:" + field.getName());
}
}
}
OuterClass 안에 InnerClass 가 있다.
InnerClass 는 static 이 붙지 않았다.
즉, 정적 클래스가 아니라는 뜻이다.
InnerClass innerClass = outerClass.new InnerClass();
InnerClass 의 인스턴스를 만들려면 outerClass 의 인스턴스를 먼저 만들어야한다.
위의 코드를 실행시켜보면 OuterClass 타입의 this$0 이라는 레퍼런스를 확인할 수 있다.
public class OuterClass {
private void hi() {
}
class InnerClass {
public void hello() {
OuterClass.this.hi();
}
}
}
InnerClass 에서 OuterClass 를 참조하는 방법은 OuterClass.this
를 사용하면 된다.
public class BigObject {
private List<Object> resource;
public BigObject(List<Object> resource) {
this.resource = resource;
}
public static class ResourceCleaner implements Runnable {
private List<Object> resourceToClean;
public ResourceCleaner(List<Object> resourceToClean) {
this.resourceToClean = resourceToClean;
}
@Override
public void run() {
resourceToClean = null;
System.out.println("cleaned up.");
}
}
}
정적이 아닌 중첩 클래스를 사용하면 외부 클래스에 대한 레퍼런스가 생기기 때문에
cleaner 를 등록할 때 static 으로 만들어야 한다.
중첩 레퍼런스가 생기면 자원이 정리가 되지 않는다.
물론 cleaner 를 다른 클래스로 만든다면 static 을 붙일 필요는 없다.
람다 역시 바깥 객체의 참조를 갖기 쉽다.
public class LambdaExample {
private int value = 10;
private Runnable instanceLambda = () -> {
System.out.println(value);
};
public static void main(String[] args) {
LambdaExample example = new LambdaExample();
Field[] declaredFields = example.instanceLambda.getClass().getDeclaredFields();
for (Field field : declaredFields) {
System.out.println("field type: " + field.getType());
System.out.println("field name: " + field.getName());
}
}
}
위의 클래스는 인스턴스 맴버 instanceLambda 에 람다가 정의되어있다.
Runnable 을 정의해 메서드 바깥의 값을 참조하게 되면
람다를 감싸고 있는 LambdaExample 클래스에 대한 레퍼런스가 람다 안에 들어가게 된다.
LambdaExample 클래스에 있는 어떠한 값을 cleaner 를 이용해 정리하는 작업을 위와 같이 작성하면 안된다.
정리할 객체에 대한 레퍼런스가 생기기 때문이다.
public class LambdaExample {
private Runnable instanceLambda = () -> {
};
public static void main(String[] args) {
LambdaExample example = new LambdaExample();
Field[] declaredFields = example.instanceLambda.getClass().getDeclaredFields();
for (Field field : declaredFields) {
System.out.println("field type: " + field.getType());
System.out.println("field name: " + field.getName());
}
}
}
바깥 객체를 참조하지 않으면 바깥 객체에 대한 레퍼런스가 생기지 않는다.
public class LambdaExample {
private static int value = 10;
private static Runnable instanceLambda = () -> {
System.out.println(value);
};
public static void main(String[] args) {
LambdaExample example = new LambdaExample();
Field[] declaredFields = example.instanceLambda.getClass().getDeclaredFields();
for (Field field : declaredFields) {
System.out.println("field type: " + field.getType());
System.out.println("field name: " + field.getName());
}
}
}
바깥 객체를 참조하더라도 static 이라면 레퍼런스가 생기지 않는다.
Finalizer 공격
public class Account {
private String accountId;
public Account(String accountId) {
this.accountId = accountId;
if (accountId.equals("푸틴")) {
throw new IllegalArgumentException("푸틴은 계정을 막습니다.");
}
}
public void transfer(BigDecimal amount, String to) {
System.out.printf("transfer %f from %s to %s\n", amount, accountId, to);
}
}
Account 클래스를 작성한다.
만약 계정의 아이디가 푸틴
인 경우에는 계정을 막도록 되어있다.
하지만 악의적인 사용자라면 계정이 막히더라도 계정을 사용할 수 있다.
class AccountTest {
@Test
void 일반_계정() {
Account account = new Account("junhyun");
account.transfer(BigDecimal.valueOf(10.4),"hello");
}
}
일반 계정이라면 오류가 발생하지 않는다.
class AccountTest {
@Test
void 푸틴_계정() {
Account account = new Account("푸틴");
account.transfer(BigDecimal.valueOf(10.4),"hello");
}
}
푸틴 의 계정이라면 오류가 발생한다.
Finalizer 를 이용하면 오류가 발생하더라도 transfer 메서드를 실행시킬 수 있다.
public class BrokenAccount extends Account{
public BrokenAccount(String accountId) {
super(accountId);
}
@Override
protected void finalize() throws Throwable {
this.transfer(BigDecimal.valueOf(1000000), "junhyun");
}
}
Account 클래스를 상속받아 finalize() 를 재정의 한다.
finalize()
재정의 하면서 Account 클래스의 transfer 를 사용하면된다.
class AccountTest {
@Test
void 푸틴_계정() throws InterruptedException {
Account account = null;
try {
account = new BrokenAccount("푸틴");
} catch (Exception e) {
System.out.println("이러면?");
}
System.gc();
Thread.sleep(3000L);
}
}
위의 코드에서는 new BrokenAccount("푸틴");
으로 생성시 발생하는 오류를 try - catch 블럭으로 잡는다.
그 뒤 GC 를 일으켜 오버라이딩한 finalize()
를 호출하게 한다.
이러한 Finalizer 공격 을 막으려면
public final class Account {
private String accountId;
public Account(String accountId) {
this.accountId = accountId;
if (accountId.equals("푸틴")) {
throw new IllegalArgumentException("푸틴은 계정을 막습니다.");
}
}
public void transfer(BigDecimal amount, String to) {
System.out.printf("transfer %f from %s to %s\n", amount, accountId, to);
}
}
상속을 허용하지 않도록 final 키워드를 클래스에 붙여준다.
만약 상속을 해야만 한다면
public final class Account {
private String accountId;
public Account(String accountId) {
this.accountId = accountId;
if (accountId.equals("푸틴")) {
throw new IllegalArgumentException("푸틴은 계정을 막습니다.");
}
}
public void transfer(BigDecimal amount, String to) {
System.out.printf("transfer %f from %s to %s\n", amount, accountId, to);
}
@Override
protected final void finalize() throws Throwable {
super.finalize();
}
}
아무것도 하지 않는 finalize()
를 만든 후 final 키워드를 붙여주면 된다.
final 키워드를 메서드 앞에 붙여주면 오버라이딩을 할 수 없는 메서드가 된다.
AutoClosable
public interface AutoCloseable {
/**
* Closes this resource, relinquishing any underlying resources.
* This method is invoked automatically on objects managed by the
* {@code try}-with-resources statement.
*
* <p>While this interface method is declared to throw {@code
* Exception}, implementers are <em>strongly</em> encouraged to
* declare concrete implementations of the {@code close} method to
* throw more specific exceptions, or to throw no exception at all
* if the close operation cannot fail.
*
* <p> Cases where the close operation may fail require careful
* attention by implementers. It is strongly advised to relinquish
* the underlying resources and to internally <em>mark</em> the
* resource as closed, prior to throwing the exception. The {@code
* close} method is unlikely to be invoked more than once and so
* this ensures that the resources are released in a timely manner.
* Furthermore it reduces problems that could arise when the resource
* wraps, or is wrapped, by another resource.
*
* <p><em>Implementers of this interface are also strongly advised
* to not have the {@code close} method throw {@link
* InterruptedException}.</em>
*
* This exception interacts with a thread's interrupted status,
* and runtime misbehavior is likely to occur if an {@code
* InterruptedException} is {@linkplain Throwable#addSuppressed
* suppressed}.
*
* More generally, if it would cause problems for an
* exception to be suppressed, the {@code AutoCloseable.close}
* method should not throw it.
*
* <p>Note that unlike the {@link java.io.Closeable#close close}
* method of {@link java.io.Closeable}, this {@code close} method
* is <em>not</em> required to be idempotent. In other words,
* calling this {@code close} method more than once may have some
* visible side effect, unlike {@code Closeable.close} which is
* required to have no effect if called more than once.
*
* However, implementers of this interface are strongly encouraged
* to make their {@code close} methods idempotent.
*
* @throws Exception if this resource cannot be closed
*/
void close() throws Exception;
}
AutoCloseable 은 인터페이스이다.
이 인터페이스를 사용하면 try-with-resource 를 사용할 수 있다.
try-with-resource 를 사용하면 자원 반납을 자동으로 해준다.
public class App {
public static void main(String[] args) {
try(AutoClosableIsGood good = new AutoClosableIsGood("")) {
// TODO 자원 반납 처리가 됨.
}
}
}
위의 코드처럼 try-with-resource 를 사용하면 된다.
public class App {
public static void main(String[] args) {
try(AutoClosableIsGood good = new AutoClosableIsGood("");
AutoClosableIsGood good1 = new AutoClosableIsGood("");
AutoClosableIsGood good2 = new AutoClosableIsGood("");) {
// TODO 자원 반납 처리가 됨.
}
}
}
자원을 반납할 객체를 여러개 사용할 수도 있다.
AutoCloseable 인터페이스는 void close() throws Exception;
메서드를 하나만 가지고있다.
그렇다고 함수형 인터페이스는 아니다.
void close() throws Exception;
에서는 throws Exception 이 붙어있지만
구현하고는 아무런 관련이 없다.
close() 에서 발생하는 Exception 을 처리하는 방법은 크게 세가지가 있다.
import java.io.IOException;
public class AutoClosableIsGood implements AutoCloseable {
private BufferedReader reader;
public AutoClosableIsGood(String path) {
try {
this.reader = new BufferedReader(new FileReader(path));
} catch (FileNotFoundException e) {
throw new IllegalArgumentException(path);
}
}
@Override
public void close() throws IOException {
reader.close();
}
}
첫번째 방법은 throws 를 사용하는 방법이다.
이 방법의 의도는 해당 코드를 사용하는 클라이언트 쪽에 책임을 전가하게 된다.
import java.io.IOException;
public class App {
public static void main(String[] args) {
try (AutoClosableIsGood good = new AutoClosableIsGood("")) {
// TODO 자원 반납 처리가 됨.
} catch (IOException e) {
e.printStackTrace();
}
}
}
해당 코드의 클라이언트는 위와같이 close() 메서드 호출시 던져지는 예외를 처리해야한다.
예외를 던져야만한다면 구체적인 예외 를 던지길 권장한다.
public class AutoClosableIsGood implements AutoCloseable {
private BufferedReader reader;
public AutoClosableIsGood(String path) {
try {
this.reader = new BufferedReader(new FileReader(path));
} catch (FileNotFoundException e) {
throw new IllegalArgumentException(path);
}
}
@Override
public void close() {
try {
reader.close();
} catch (IOException e) {
// logging
}
}
}
두번째 방법은 close() 메서드 내에서 예외를 처리하는 것이다.
이렇게하면 클라이언트는 예외처리에 대한 작업을 하지 않아도 된다.
public class AutoClosableIsGood implements AutoCloseable {
private BufferedReader reader;
public AutoClosableIsGood(String path) {
try {
this.reader = new BufferedReader(new FileReader(path));
} catch (FileNotFoundException e) {
throw new IllegalArgumentException(path);
}
}
@Override
public void close() {
try {
reader.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
세번째 방법으로는 예외를 변환하는 것이다.
위의 코드는 예외 발생시 해당 스레드를 종료시키게 된다.
그리고 가급적이면 close() 는 멱등성을 유지하도록 권장한다.
즉, 몇번을 실행하더라도 같은 결과를 내야한다.
AutoCloseable 을 상속한 다양한 인터페이스가 존재한다.
'개발 공부 > Java' 카테고리의 다른 글
이펙티브 자바 아이템 9 - try-finally 보다 try-with-resouces 를 사용하라 - 완벽 공략 (0) | 2022.10.14 |
---|---|
이펙티브 자바 아이템 9 - try-finally 보다 try-with-resouces 를사용하라 - 핵심 정리 (0) | 2022.10.14 |
이펙티브 자바 아이템 8 - finalizer와 cleaner 사용을 피하라 - 핵심 정리 (0) | 2022.10.12 |
이펙티브 자바 아이템 7 - 다 쓴 객체 참조를 해제하라 - 완벽 공략 (0) | 2022.10.07 |
이펙티브 자바 아이템 6 - 불필요한 객체 생성을 피하라 - 완벽 공략 (1) | 2022.09.23 |
댓글