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

JAVA/Effective Java|2018. 6. 23. 21:10

클래스 선언부에 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("./test.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("./test.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)

댓글()

규칙 64 - 실패 원자성 달성을 위해 노력하라.

JAVA/Effective Java|2018. 6. 5. 23:22

예외가 발생한 다음에도 기존에 사용하던 객체의 상태가 그대로 유지되는 것이 좋다.

쉽게 이야기하면, 메서드 호출이 정상적으로 처리되지 못한 객체의 상태는 메서드 호출 전 상태와 동일해야한다. 이런 속성을 만족하는 메서드는 실패 원자성(failure atomicity)을 갖추었다고 한다.


이런 실패원자성을 해소하기 위한 방법을 알아보자.


먼저 간단하게 변경 불가능 객체로 설계하는 것이다.

-> 왜냐하면 원자성이 있는 객체는 생성된 이후에는 변경되지 않기 때문에 오류가 발생한다고 해도 원자성이 깨지지 않는다.


그럼 변경 가능한 객체의 경우 어떻게 해야할까?

-> 이는 실제 연산을 수행하기전에 인자 유효성을 미리 검사하는 방법이다. (객체가 변경되기 전에 예외를 발생시켜 발생하는 것을 막는것이다.)


아래 예를 살펴보자. 아래 예의 경우 실제 index 또는 elementCount를 사용하기 전에 먼저 예외를 설정함으로써 객체가 변하는것을 막을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public synchronized void removeElementAt(int index) {
    modCount++;
    if (index >= elementCount) {
      throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                     elementCount);
    }
    else if (index < 0) {
      throw new ArrayIndexOutOfBoundsException(index);
    }
    int j = elementCount - index - 1;
    if (j > 0) {
      System.arraycopy(elementData, index + 1, elementData, index, j);
    }
      elementCount--;
      elementData[elementCount] = null/* to let gc do its work */
}


cs


마지막 접근법은 연산 수행 도중에 발생하는 오류를 가로채는 복구 코드(recovery code)를 작성하는 것이다.


이런 접근법을 통해서 오류가 발생했을 경우에 변경된 객체를 연산이 시작되기 전으로 복구하는 과정을 거쳐야 한다.

그리고 기타 방법으로 임시로 복사를 진행하여 나중에 상태를 바꾸는 방법이 있다.


결론 

모든 방법을 통해 원자성을 유지할 수는 없다. 예를들면, 여러 스레드가 적절한 동기화 없이 변경할 경우 문제가 발생하는 경우에 일관성이 깨지기 때문이다.

그러므로 예외가 아닌 단순 error인 경우에는 실패원자성을 복구하려고 애쓸필요는 없다. 그러므로 비용과 복잡성등을 따져서 원자성을 지키는 것이 맞는지 확인해보고 진행하는 것이 좋다.


만약 이전상태로 돌릴수 없는 경우에는 API문서에 문제 발생에 관한 내용을 자세하게 기재해주어야 한다. 


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

댓글()

규칙 63 - 어떤 오류인지를 드러내는 정보를 상세한 메시지에 담으라.

JAVA/Effective Java|2018. 6. 1. 23:28

개발을 진행하다보면 예기치 못한 상황에서 에러가 자주 발생한다.



에러가 발생하는 것을 다 알고 차단할수있다면 정말 바람직한 프로그램이라고 할 수있을 것이다.

하지만 그럴수가 없기때문에 에러를 관리하고 효율적으로 에러정보를 전달하는것이 중요하다.



정확한 에러정보를 전달하는것이 빠르게 문제를 해결하는 실마리가 될것이다.

그래서 에러가 발생되었을 때 오류의 상세 메시지에 예외에 관련된 모든 인자와 필드값을 포함시켜야 한다.


예를 들어, IndexOutOfBounds Exception의 경우 해당  범위를 벗어난 인자값과 하한과 상한값도 포함되어있어야 한다.


그러면 정확히 어떻게 오류가 발생된 것인지 알기가 쉬워진다.



하지만 관련된 데이터를 담는 것이 중요하지만 잘못사용하면 별로 도움이 되지 않을 수 있다.

그리고 이런 에러메시지는 프로그래머나 서비스 담당자가 오류 원인을 분석하기 위한 것이기 때문에 가독성이 중요하다.




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

댓글()

규칙 62 - 메서드에서 던져지는 모든 예외에 대해 문서를 남겨라.

JAVA/Effective Java|2018. 6. 1. 23:17

메서드를 올바르게 사용하려면 메서드에서 던져지는 예외에 대한 설명이 문서에 있어야 한다.


그리고 


메서드가 던질수있는 모든 무점검 예외까지 선언할 필요는 없지만 점검지점 예외들과 마찬가지로주의해서 문서로 남겨놓으면 좋다.


특히 Javadoc @throws 태그를 사용해서 메서드에서 발생 가능한 모든 무점검 예외에 대한 문서를 남겨야 한다. 하지만 메서드 선언부의 throws 뒤에 무점검 예외를 나열하지는 말아야 한다.





요약하자면 메서드가 던질 가능성이 있는 모든 예외를 문서로 남겨라. 

점검지점 예외, 무점검 예외도 남겨라.



이를 지키지 않으면 해당 API를 사용하는 다른사람들이 효과적으로 사용하는게 어려워진다.




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

댓글()