토비의 봄 (재사용성과 다이나믹 디스패치)

오늘은 토비님이 강의 하신 내용을 정리해보려고 한다.
이야기할게 더 있긴 하지만 오늘은 재사용성과 다이나믹 디스패치에 대해서만 살펴보자.

dependency

dependency 란 무엇인가? dependency를 네이버 사전에서 찾아보면 의존, 종속 이라는 단어가 제일 먼저 나온다. 그럼 의존이란 무엇인가? 다시 네이버 사전을보자. 그럼 다른 것에 의지하여 존재함. 이라고 설명한다. 말그대로 어떤것에 의지하는 것을 말한다.
객체지향 프로그램에서 의존이라하면 A(클래스)가 B(클래스)를 의존한다고 말한다. 즉 아래와 같은 상황이다.

A(클래스) – – – > B(클래스)

다이어그램으로 그리기는 귀찮아서 그냥 위와 같은 표현을 했다.
저 말은 즉 A가 B에 의존하고 있다라고 설명 할 수 있다. 그럼 의존 관계를 어떻게 발생하나.
1. B가 A의 필드로 사용 될 경우
2. B가 A의 메서드 파라미터로 사용 될 경우
3. B가 A의 로컬 변수로 사용 될 경우
4. B로 메시지를 보냄

위와 같이 네가지로 인해 의존 관계가 발생 할 수 있다.

다시 다이어그램(?)을 보자

A(클래스) – – – > B(클래스)

A라는 클래스는 B라는 클래스를 의존하고 있다. 클래스로 의존 하기 때문에 A라는 클래스는 재사용이 어렵다. 아래의 코드를 살펴보자.

class A {
  private B b;

  A(B b) {
    this.b = b;
  }

  void print() {
    b.print();
  }
}

class B {
  public void print() {
    System.out.println("B");
  }
}

위의 코드는 아까 다이어그램과 같이 A는 B를 의존하고 있다. 물론 지금은 생성자로 의존하지만 다른 방법일 수도 있다.
그리고 재사용이 어렵거나 더 좋은 방법이 있다는 이야기지 재사용을 아예 하지 못한다는 것은 아니다.

런타임시 결정되는 의존 관계

다시 위의 코드를 살펴보자. 위의 코드에서 A클래스의 print() 메스드안에 B클래스 print()를 호출 하는 부분이 있다. 만약 위의 코드를 작성하고 컴파일 했을 때 b.print()는 정말로 B클래스의 print()가 실행 될지를 알고 있을까? 맞다. 정답은 알고 있다. 나도 알고 이걸 보는 개발자도 알고 컴파일러도 알고 있다.
우리는 이것을 정적 디스패치라 한다. 굳이 실행 시점이 아니더라도 컴파일 시점에 어느 메서드로 호출이 일어날 것을 결정 되는 것을 말한다.

그럼 다시 아래의 코드를 살펴보자.

class A {
  private BB bb;

  A(BB bb) {
    this.bb = bb;
  }

  void print() {
    bb.print();
  }
}

class B implements BB {
  public void print() {
    System.out.println("B");
  }
}
class B1 implements BB {
  public void print() {
    System.out.println("B1");
  }
}

interface BB {
  void print();
}

위의 코드를 다이어그램으로 그리면 다음과 같다.

A(클래스) – – – > BB (인터페이스)
BB의 구현체들은 B와 B1이 존재 한다.

A의 클래스는 BB라는 인터페이스를 의존하고 있다. 이렇게 된다면 굳이 B가 아니더라도 B1 혹은 BB를 구현한 구현체들이 올 수 있다. 이걸로 좀 더 A라는 클래스는 BB를 구현한 구현체에 변화에 영향을 받지 않고 지속적으로 재사용이 가능하다.

다시 A 클래스의 print() 메서드를 살펴보자. 거기에는 아까와 동일하게 bb.print()를 호출 하는 부분이 있다. 그렇다면 과연 bb.print()를 호출 할때 컴파일러가 B를 호출 할지 B1을 호출할지 결정이 되어 있을까? 그랬으면 좋겠지만 안타깝게 그렇지 않다. 안타까운건가? 물론 우리는 알고 있지만 컴파일러는 결정하지 못한다. 컴파일 시점에는 모르지만 런타임 시점에 BB에 할당된 object가 무엇인가를 보고 결정하게 되는 것이다.
우리는 이것을 다이나믹 디스패치라 부른다.

오늘은 간단하게 다이나믹 디스패치가 무엇인가를 살펴봤다. 다음시간에는 좀더 구체적인 예시를 들어 보며 더욱 확장성있게 만들 수 있는 더블디스페치에 대해서 정리해보자. 오늘은 짧게 이만!

자바의 마커 인터페이스

오랜만에 자바 포스팅을 해보자.
자바의 마커 인터페이스의 대해 살펴보려고 한다. 책도 조금 참고 하였다.
자바의 마커 인터페이스란 일반적인 인터페이스와 동일하지만 사실상 아무 메서드도 선언하지 않은 그런 인터페이스를 말한다. 예를들어 다음과 같다.

public interface SomeObject {
}

얼핏 보기엔 조금 난해한 코드이다. 인터페이스만 있고 메서드가 없으니 어디에다 쓸지도 난해하다. 자바로 코딩을 하다보면 저런 인터페이스가 종종 있긴하다. 자바의 대표적인 마커 인터페이스로는 우리가 흔히 아는 Serializable, Cloneable 와 흔히 알지는 못하지만 Spring에서 event리스너를 사용한다면 종종 보이는 EventListener 라는 인터페이스도 있다. 참고로 Spring의 ApplicationListener 인터페이스가 상속받고 있다. 아무튼 이런 인터페이스는 어떻게 무엇을 위해 만들어 졌나..

뭔가 대단한거 처럼 보일 수 도 있지만 실질적으로는 아주 간단하다. 대부분의 경우에는 단순한 타입체크라고 할 수 있다. (물론 아닌 인터페이스도 있을 수도 있다. 확실하지 않아서… 그래서 대부분이라고 표기를 했다. )

자바의 대표적인 마커 인터페이스인 Serializable 를 살펴볼까 한다. Serializable 인터페이스는 다음과 같다.

public interface Serializable {
}

아까 필자가 위에서 말했듯이 메서드가 한개도 선언되지 않았다. Serializable 인터페이스 같은 경우에는 직렬화를 할 수 있다는 뜻이다. 만약 이 인터페이스를 구현(?) 하지 않은 클래스 경우에는 직렬화를 하지 못한다.
아주 간단하게 예제를 만들어 보자.

@Test
public void serializableTest() throws IOException, ClassNotFoundException {
  File f= new File("a.txt");
  ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(f));
  objectOutputStream.writeObject(new SomeObject("wonwoo", "test@test.com"));
}

class SomeObject {
  private String name;
  private String email;
  //생성자 및 기타 메서드 생략
}

위의 코드를 실행해보면 당연히 에러가 발생한다.

java.io.NotSerializableException: me.wonwoo.SomeObject

왜냐하면 직렬화를 할 수 있다는 Serializable 를 구현하지 않았기 때문이다. 그럼 위의 코드를 초록불이 들어오게 하고 싶다면 Serializable만 구현해 주면 된다.

@AllArgsConstructor
class SomeObject implements Serializable {
  private String name;
  private String email;
}

간단하다 인터페이스의 메서드도 없으니 구현할 메서드도 필요 없다. 그냥 선언만 해주면된다. 그럼 위에서 사용했던 writeObject() 메서드 안을 들여다보자. writeObject() 메서드 안에는 writeObject0() 가 존재 한다. writeObject0() 메스드 맨 아래에 보면 다음과 같은 코드가 있다.

  //... 

  if (obj instanceof String) {
    writeString((String) obj, unshared);
  } else if (cl.isArray()) {
    writeArray(obj, desc, unshared);
  } else if (obj instanceof Enum) {
    writeEnum((Enum<?>) obj, desc, unshared);
  } else if (obj instanceof Serializable) {
    writeOrdinaryObject(obj, desc, unshared);
  } else {
    if (extendedDebugInfo) {
      throw new NotSerializableException(
        cl.getName() + "\n" + debugInfoStack.toString());
    } else {
      throw new NotSerializableException(cl.getName());
    }
  }
  //...

if문이 꽤 있다. 보면 String은 Serializable구현 했으니 되고 배열도 Serializable 할 수 있고 Enum도 Serializable를 구현 했으니 되고 다음으로는 Serializable가 되어있는지 체크하는 부분이다. 만약 Serializable가 없다면 에러로 처리 한다. 위에서 보았듯이 간단하게 Serializable가 선언 되었는지 안되어 있는지 체크정도만 한다. 실질적으로 뭘 하는 건 아니다. 그래서 마커 인터페이스로 부른다.
근데 조금 의아한게 왜 writeObject() 메서드의 파라미터를 Object으로 했을까? Serializable로 했다면 컴파일 타임에 에러를 발견하고 잡을 수 있을 텐데 말이다. 무슨 이유가 있나? 메서드 명이 Object이라?

마커 인터페이스는 어노테이션으로도 대체 가능하다. 만약 @SomeAnnotation 이라는 어노테이션이 있다면 아래와 같이 가져 와서 체크하면 된다.

final SomeAnnotation someAnnotation = someObject.getClass().getAnnotation(SomeAnnotation.class);

마커 인터페이스와 마커 어노테이션의 차이를 살펴보자.
마커 인터페이스 같은 경우에는 컴파일 시점에 발견할 수 있다는 큰 장점이 있다. 그리고 또한 적용범위를 좀 더 세밀하게 지정 할 수 있다.
만약 어노테이션 자료형을 선언할 때 target 에 ElementType.TYPE 이라고 지정해서 사용한다고 하면 ElementType.TYPE 은 클래스 뿐만 아니라 인터페이스에도 적용 가능하다. 그런데 특정한 인터페이스를 구현한 클래스에만 적용할 수 있어야 하는 마커가 필요하다가 해보자. 마커 인터페이스를 쓴다면 그 특정 인터페이스를 상속 하도록 선언만 하면 된다. 그럼 마커를 상속한 모든 장료형은 자동으로 그 특정 인터페이스의 하휘 자료형이 된다.

그렇다면 마커 어노테이션의 장점은 뭘까? 마커 아노테이션은 유연하게 확장이 가능하다. 어노테이션을 만들어 사용한 뒤에도 계속적으로 더 많은 정보를 추가 할 수 있는 것이 큰 장점이다. 예를들어 어떤 어노테이션을 만들고 배포를 한 뒤에 뭔가 더 정보를 추가 하고 싶다면 새로 추가 된 요소들에 대해 default 값을 갖게 하면 하위 호환성도 지킬 수 있으며 처음에는 마커 어노테이션으로 시작했다가 나중에는 기능이 많은 어노테이션으로 진화 가능하다.
하지만 인터페이스 경우에는 메서드를 만드는 순간 하위 호환성이 깨지므로 마커 어노테이션처럼 지속적인 진화는 불가능하다.

마커 어노테이션과 마커 인터페이스 중 둘 중 어느게 낫다고 할 수 없을 거 같다. 각각의 쓰임새가 다르기 때문이다. 위에서 말했듯이 새로운 메서드가 없는 자료형을 정의하고 싶다면 마커 인터페이스를 이용해야 하고 클래스나 인터페이스 이외의 마커를 달아야 하고 앞으로도 더 많은 추가 정보가 있다고 생각하면 마커 어노테이션을 사용하면 된다. 어떤 쓰임인지 잘 판단하고 만들어야 할 듯 싶다.

참고로 Spring의 대표적인 마커 인터페이스는 Aware Spring data에 대표적인 마커 인터페이스는 Repository 등이 있다. 좀 더 찾아 볼라고 했는데 귀찮.. 개발하다가 나오겠지 뭐..

이렇게 오늘은 마커 인터페이스를 살펴봤다!

java Immutable class

오늘은 Immutable class에 대해서 알아보자. Immutable 클래스란 불변의 클래스를 말한다. 불변이라는 말은 변할수 없는 것을 말한다. 불변인 클래스가 좋은점은 객체가 안전하기 때문이다.

java의 대표적인 불변 클래스

java의 대표적인 불변의 클래스는 String이다.

String hi = "hi";
String wonwoo = hi.concat(" wonwoo");
System.out.println(wonwoo);

위의 코드는 변경하는 것으로 보이지만 실제로는 새로운 String을 리턴한다. String의 속성중 value를 변경하지는 않는다. 아래 코드는 String의 concat 메서드이다. 새로운 객체들 다시 만든다.

public String concat(String str) {
  int otherLen = str.length();
  if (otherLen == 0) {
    return this;
  }
  int len = value.length;
  char buf[] = Arrays.copyOf(value, len + otherLen);
  str.getChars(buf, len);
  return new String(buf, true);
}

String의 어느 메서드를 봐도 리턴값은 모두 new String() 으로 객체를 다시 만든다. 몇가지 this를 리턴하는게 있는데 그중 하나가 replace() 메서드이다. 그 이유는 replace 할게 없다면 그냥 자기 자신을 리턴한다. 그래도 값은 변경되지 않았으니 불변의 클래스가 된다.

final이면 된다.

불변의 클래스를 만들기 위해서는 몇가지만 알면된다. 클래스의 필드가 final이고 setter가 없으면 된다. 물론 final이면 변경 자체가 안되니 setter가 있어도 소용없다. 아주 간단하게 불변의 클래스를 만들어보자.

public class ProductString {
  private final String name;

  ProductString(String name) {
    this.name = name;
  }

  @Override
  public String toString() {
    return this.name;
  }
}

위의 코드는 별로 하는 일은 없지만 불변의 클래스이다. setter도 없고 name이라는 필드에도 final 로 선언 되어있다. 만약 여기에 setter가 있어도 컴파일 에러가 나니 모든 변수에 final만 해줘도 불변의 클래스라고 할 수 있다.

String name = "mac book";
ProductString productString = new ProductString(name);
name.concat(" pro");
System.out.println(productString);

결과는 mac book이 출력된다. concat경우에는 새로운 객체를 리턴하니 productString을 출력 할때 기존 name에 있던 변수의 값이 출력 된다.
하지만 우리는 저렇게 단순한 코드를 만들지 않기에 세심하게 봐야 할 것 같다.

final 이어도 불변이 깨질 수 있다.

필드가 모두 final 이더라도 불변이 아닐 수도 있다. 아래의 코드를 유심히 보자.

public class ProductStringBuilder {
  private final StringBuilder name;

  ProductStringBuilder(StringBuilder name) {
    this.name = name;
  }

  @Override
  public String toString() {
    return this.name.toString();
  }
}

언뜻보면 setter도 없고 필드도 final이니 불변의 클래스 같다. 조금만더 생각해보면 불변이 아니라는 것을 알 수 있다. ProductStringBuilder 클래스의 안에 name이라는 필드는 StringBuilder로 선언 되어있다. StringBuilder는 불변의 클래스가 아니다. StringBuilder의 append 메서드는 문자열을 합치고 자기 자신을 리턴한다. 새로운 객체를 리턴하는 것이 아니고 변경된 값을 리턴한다.
어떻게 하면 불변의 클래스가 아닐 수 있을까?

StringBuilder stringBuilder = new StringBuilder("mac book");
ProductStringBuilder productStringBuilder = new ProductStringBuilder(stringBuilder);
stringBuilder.append(" pro");
System.out.println(productStringBuilder);

위의 String과 똑같은 테스트 코드이다. 하지만 결과는 다르다. 불변의 클래스를 기대하려면 mac book만 출력 되어야 하는데 mac book pro가 출력이 된다. 그럼 우리는 불변의 클래스를 만들 수 없을까? 위의 코드는 간단하게 불변의 클래스로 만들 수 있다.

public class ProductStringBuilder {
  private final StringBuilder name;

  ProductStringBuilder(StringBuilder name) {
    this.name = new StringBuilder(name);
  }

  @Override
  public String toString() {
    return this.name.toString();
  }
}

위와 같이 다시 새로운 객체를 만들면 된다. 그럼 우리가 원하던 mac book 만 콘솔창에 출력된다.
불변의 클래스를 만들 때에는 위와 같은 상황을 조심해서 만들어야 될 듯하다. 필드가 불변의 클래스가 아닌 경우를..

이상으로 Immutable class에 대해 알아 봤다.