'이펙티브'에 해당되는 글 70건

JAVA/Effective Java

규칙 74 - Serializable 인터페이스를 구현할 때는 신중하라.

클래스 선언부에 implements Serializable를 붙히면 간단하게 직렬화 가능 객체를 만들수 있다. 

그렇기 때문에 개발자 입장에서는 Serializable을 붙혀서 직렬화 기능을 만드는 것이 간단하다고 생각할 수 있다.

여기서 먼저 직렬화에 대해서 간단한 예제를 보고 가자.

import java.io.Serializable;

public class Student implements Serializable {

	private static final long serialVersionUID = 1L;
	
	public Student(String name, int number, int height) {
		this.name = name;
		this.number = number;
		this.height = height;
	}
	
	private String name;
	private int number;
	private int height;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getNumber() {
		return number;
	}

	public void setNumber(int number) {
		this.number = number;
	}

	public int getHeight() {
		return height;
	}

	public void setHeight(int height) {
		this.height = height;
	}

}

우선 객체의 Serializable 인터페이스를 붙힘으로써 객체 직렬화를 할 클래스를 만드는 준비가 된 것이다. 여기서 함께 사용된 UID는 스트림 고유 식별자로서 모든 직렬화 가능 클래스는 고유한 식별 번호를 가진다. 만약 붙혀주지 않으면 알아서 식별번호를 생성하는데 이 생성되는 식별번호는 클래스 이름, 해당 클래스의 상속 인터페이스, 멤버 변수 등을 영향을 받아 생성되게 된다. 그렇기 때문에 이런 속성이 하나라도 변경된다면 UID가 변경되기 때문에 나중에 호환성이 깨져 실행도중에 InvalidClassException이 발생할 수 있다.

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class Main {
	
	public static void main(String args[]) {
		Student student = new Student("Wedul", 12, 189);
		
		try (
		FileOutputStream fileOut =
		         new FileOutputStream("./dbsafer.obj");
		         ObjectOutputStream out = new ObjectOutputStream(fileOut);) {
			
			out.writeObject(student);
			out.close();
			fileOut.close();
			System.out.println("직렬화 성공.");
		} catch (Exception e) {
			System.out.println("직렬화 실패.");
		}
	}
	
}

다음과 같이 코드를 작성하면 성공적으로 클래스를 직렬화하고 파일로 만들어서 내보낸 것을 확인할 수 있다.

그리고 직렬화한 파일을 다시 deserializabable하여 역직렬화 할 수도 있다.

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class Main {
	
	public static void main(String args[]) {
		Student student = null;
		try (
		FileInputStream fileIn =
		         new FileInputStream("./dbsafer.obj");
		         ObjectInputStream in = new ObjectInputStream(fileIn);) {
			
			student = (Student) in.readObject();
			
			if (null != student) {
				System.out.println(student.toString());
			}
			
			in.close();
			fileIn.close();
			System.out.println("역직렬화 성공.");
		} catch (Exception e) {
			System.out.println("역직렬화 실패.");
		}
	}
	
}

결과화면

이렇듯 자바 직렬화는 “자바 직렬화 형태의 데이터 교환은 자바 시스템 간의 데이터 교환을 위해서 존재한다.”

 

 


 

그러면 여기서 다시 자바 직렬화시 고려해야 할 문제를 살펴보자.

위에서 보면 알겠지만 Serializable 구현된 클래스는 추후에 변경하기 어려워진다. 왜냐하면 직렬화를 진행하고 나면 생성되는 바이트 스트림 인코딩도 하나의 공개 API가 되기 때문이다. 그렇기 때문에 한번 직렬화를 제공하는 클래스의 경우 쉽게 변경할 수 없게된다. 쉽게 변경하게 될 경우 역직렬화가 실패하게 될 수 있다.

그리고 또 하나 문제는 직렬화를 하게되면 그 클래스 내부에 존재하는 private, package-private 객체도 공개가 되기 때문에 정보은닉의 기본 개념이 사라지게 되는 문제가 발생한다.

그리고 만약 클래스의 Serializable을 구현하지 않기로 하는경우 주의해야 할 것이 있다. 왜냐하면 그 클래스를 상속받는 클래스를 Serializable을 구현하려고자 하는 경우에 불가능할 수도 있기 떄문이다. 다시말하면 상위 클래스에 무인자 생성자가 없다면 직렬화 가능 하위 클래스를 만들수가 없다는 뜻이다. 

그렇기 때문에 계승을 고려해 설계한 직렬화 불가능 클래스에는 무인자 생성자를 제공해주어야 하위클래스가 직렬화를 할 수 있다는 것을 알아야 한다. 

마지막으로 내부 클래스 (inner class)의 경우에는 Serializable을 구현하면 안된다. 왜냐하면 내부 클래스는 바깥 객체에 대한 참조를 보관하고 바깥 유효범위의 지역 변수 값을 보관하기 위해 컴파일러가 자동으로 생성하는 인위생성 필드가 있기 때문이다. 그래서 내부 클래스의 기본 직렬화 형식을 정의할 수 없다. 

 

출처 : 조슈아  블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1)

JAVA/Effective Java

규칙 73 - 스레드 그룹은 피하라.

스레드 그룹(thread group)은 원래 applet을 격리시켜 보안문제 해결할 목적으로 만들어졌으나 성공하지 못했다.

그러면 이런 스레드 그룹은 왜 남아있는가? 아예 쓸곳이 없는가? 그렇지는 않다. 스레드 기본연산을 여러 스레드에 동시에 적용할 수 있도록 하는 기능을 가지고 있다. 하지만 대부분이 deprecated 되었다.

결론을 이야기하자면 이미 다 페기가 되어버린 기능이다. 그렇기 때문에 신경쓸 것 없이 사용하지 말아야한다.

 

 

출처 : 조슈아  블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1)

JAVA/Effective Java

규칙 72 - 스레드 스케줄러에 의존하지마라.

실행해야 할 스레드가 많을 경우 어떠한 스레드를 얼마나 오랫동안 실행할지 결정은 스레드 스케줄러가 진행한다.

운영체제마다 스레드 스케줄러는 다르기 때문에 아무리 운영체제에서 효율적으로 진행한다고 하더라고 이에 의존하여 프로그램을 제작해서는 안된다.

정확하고 좋은 스레드 프로그램은 의존하는것이 아니라 실행가능한 스레드의 개수가 프로세수 개수보다 넘지 않도록 제작하는 것이다.

그렇게되면 스레드 스케줄러가 순차적으로 스레드를 실행시켜줄뿐 정책에 신경쓰지않는다.

그렇다면 실행중인 스레드의 개수를 최대한 줄일 수 있는 방법은 무엇일까?

바로 사용하지 않는 스레드는 실행하지 않고 정지하거나 종료해야한다. 그래서 바로 직전에 공부했던 스레드 풀을 사용하여 적절하게 스레드를 관리하면 좋은 프로그램을 만들 수 있다.

그렇다면 오래 반환하지 않고 잘못된 스레드 방식으로 개발하는 코드를 알아보자.

public class FailLatch {
	private int count;
	public FailLatch (int count) {
		if (count < 0) {
			throw new IllegalArgumentException(count + " < 0");
		}
		this.count = count;
	}
	
	public void await() {
		while (true) {
			synchronized (this) {
				if (count == 0) {
					System.out.println("the end");
					return;
				}
					
			}
		}
	}
	
	public synchronized void countDown() {
		if (count != 0) {
			count--;
		}
	}

}

 위의 코드를 보면 count가 0일 때까지 스레드가 놀고있어야 하는 아주 좋지 않은 프로그램이 된다. 만약 그럼 저상태에서 대기상태에서 Thread.yield를 사용한다면 조금 나아지려나?

그렇지 않다. 왜냐하면 이는 일부 JVM에서는 성능이 향상되는 것처럼 보일 수 있으나, 무조건 좋아지지 않는다. 그렇기 때문에 병렬적으로 실행 가능한 스레드 수를 애초에 줄이는것이 중요하다.

결론을 이야기하자면 프로그램의 정확성을 스레드 스케줄러에 의존하지말고 쓰레드 수를 제한하고 일부 쓰레드가 무조건 낭비되구 있는것을 방지하자. 단 Thread.yield 등과 같이 임시방편으로 무엇을 해결하려 하지 말고 근본적 문제를 해결하라.

 

출처 : 조슈아  블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1)

 

JAVA/Effective Java

규칙 71 - 초기화 지연은 신중하게 하라

Lazy initialization(초기화 지연)은 필드 초기화를 실제로 그 값이 쓰일 때까지 미루는 것이다.


대부분 초기화 지연의 이유는 초기화의 비용이 증가하고 사용빈도가 특별한 경우에 사용하는 필드에 대해서 그렇게 적용한다.


만약 그렇지 않은 경우에도 초기화 지연을 사용하면 어떨까?

이럴 경우 클래스를 초기화하고 객체를 생성하는 비용은 줄이지만 필드 사용 비용은 증가시킨다.


그럼 동기화가 필요한 다중 스레드 환경에서는 초기화 지연은 어떻게 구현해야할까? 생각만 해도 어렵다. 


몇가지 방법을 살펴보자.

우선 초기화 지연을 사용하지 않고 진행하는 일반적인 초기화는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
 
public class TestClass {
    // 일반적인 초기화 기법
    // 클래스가 처음 로드될 때 바로 초기화
    private final int val = getValue();
    
    private int getValue() {
        return 0;
    }
}
 
cs


이 상태에서 동기화를 적용시키면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
 
public class TestClass {
    // 일반적인 초기화 기법
    // 클래스가 처음 로드될 때 바로 초기화
    private int val;
    
    synchronized int getValue() {
        return 0;
    }
}
 
cs


그렇다면 정적 필드의 초기화를 지연시키고 싶다면 어떻게 해야할까?

이럴때는 Lazy initialization holder class 숙어를 적용하면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestClass {
    
    private static class ValueHolder {
        static final int value = getValue(); 
    }
    
    static int getData() {
        return ValueHolder.value;
    }
    
    private static int getValue() {
        return 0;
    }
}
 
cs


처음 getData()가 호출되는 순간에 getValue()가 호출되면서 초기화가 진행된다. 이럴경우 synchronized를 붙혀주지 않아도된다. 왜냐하면 최신 VM은 클래스를 초기화하기위한 필드 접근은 동기화를 진행한다.


그리고 필드를 검사를 진행할 때 무조건 적으로 동기화 하는 것보다 다음과 같이 이중검사를 사용하는 것이 좋다.


이중검사란 무엇인가? 락을 먼저 걸고 확인하면 무조건 락을 걸어야해서 비용이 증가하지만, 먼저 락없이 한번 확인하고 진행하기 때문에 부담이 줄어든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
String getField() {
        String result = field;
        // 첫 번재 검사 (락 없이 진행)
        if (null == result) {
            // 두 번재 검사  (락을 사용하여 진행)
            synchronized(this) {
                result = field;
                if (result == null) {
                    field = result = getData();
                }
            }
        }
        return result;
    }
    
    String getData() {
        return "dbs";
    }
cs


위 코드에서 result를 사용하여 field값을 대입한 이유는 field 값을 한번만 읽게 하였기에 동기화 사용시에도 비용이 감소된다. 저수준 병렬 프로그래밍에서 25%가량 향상된것을 볼 수있다.


만약 여러번 초기화 되어도 크게 무리가 없는 필드에 경우에는 락을 거는 로직을 빼고 단일 검사만 진행해도 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
String getField() {
    String result = field;
    // 첫 번재 검사 (락 없이 진행)
    if (null == result) {
        field = result = getData();
    }
    return result;
}
    
String getData() {
    return "dbs";
}
cs



결론은 필요없는 필드는 초기화 지연을 사용하지 말자. 비용이 커진다. 만약 비용이 큰 초기화 방법을 적절하게 변경하고 싶은경우 위에 소개한 방식대로 초기화를 진행하라.

JAVA/Effective Java

규칙 70 - 스레드 안전성에 대해문서로 남겨라.

클래스를 사용할 때 클래스의 객체와 정적 메서드가 병렬적으로 이용되었을 때, 어떠한 부작용이 있을 수 있는지 안전한지에 대한 정보가 없으면 추후에 큰 문제를 야기할 수있다.


JavaDoc에서 synchronized 키워드를 통해 병렬설 지원 여부를 확인할 수있다고 알고 있으나 실상 그렇지 않다.


왜냐하면 Javadoc이 만드는 문서에는 Javadoc이 들어가지 않는다. 왜냐하면 synchronized 키워드는 메서드의 구현 상세에 해당하는 정보이며, 공개 API의 일부가 아니기 때문이다.


그렇기 때문에 synchronized 키워드를 통해 판단해서는 안되고 병렬적으로 사용해도 되는지의 여부는 문서에 남겨져 있어야 한다.


출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 

푸터바

알림

이 블로그는 구글에서 제공한 크롬에 최적화 되어있고, 네이버에서 제공한 나눔글꼴이 적용되어 있습니다.

카운터

  • Today : 36
  • Yesterday : 651
  • Total : 55,511