Super type token

오늘은 Super type token에 대해서 알아보자.

Super type token을 알기전에 일단 type token을 알아야 되는데 type token 이란 간단히 말해서 타입을 나타내는 토큰(?)이다. 예를들어 String.class는 클래스 리터럴이라하며 Class<String>가 타입토큰이라 말할 수 있다. 실제 Class은 String.class를 이용해서 메서드를 호출 할 수 있다.

Super type token 경우에는 제네릭과 관련이 많다. 일단 제네릭을 잘 모른다면 여기를 한번 보고 이글을 봐야 할 듯 하다.

Type token

타입 토큰의 간단한 예제를 만들어서 살펴보자.

public static <T> T typeToken(T value, Class<T> clazz) {
  return clazz.cast(value);
}

위의 코드는 의미가 없지만 예제이니.. 위는 T 타입의 value를 받아서 해당하는 타입토큰으로 형변환하는 그런 코드이다. 한번 사용해보자.

public static void main(String[] args) {
  System.out.println(typeToken(10, Integer.class));
  System.out.println(typeToken("string", String.class));
}

위와 같이 int일 경우에는 Integer.class 파라미터로 넘기고 String일 경우에는 String.class 클래스 리터럴을 파라미터로 넘기면 된다. 만약 형을 맞게 넘기지 않았을 경우에는 컴파일 에러가 발생한다.

System.out.println(typeToken(10, String.class)); //컴파일 에러

이와 같이 좀 더 안전하게 타입을 지정해서 사용할 수 있는 큰 장점이 있다.

Gson Example

위와 같은 예제말고 좀 더 실용적인 사용법이 있다. Gson과 jackson 등 json, xml을 Object으로 변경할 때 사용이 많이 된다.
필자는 jackson을 더 좋아하지만 여기서는 Gson을 사용했다. (그냥)

public class Account {
    private String username;
    private String password;

    public String getUsername() {
      return username;
    }

    public void setUsername(String username) {
      this.username = username;
    }

    public String getPassword() {
      return password;
    }

    public void setPassword(String password) {
      this.password = password;
    }
    @Override
    public String toString() {
      return "Account{" +
          "username='" + username + '\'' +
          ", password='" + password + '\'' +
          '}';
    }
  }
}

public static void main(String[] args) {
  String json = "{\"username\" : \"wonwoo\", \"password\" : \"test\"}";
  Gson gson = new Gson();
  Account account = gson.fromJson(json, Account.class);
  System.out.println(account);
}

우리가 흔히 API 통신을 하거나 특정한 데이터를 가공하기 위해 Object로 변환하기 위해 위와 같은 코드를 자주 이용한다. jackson도 마찬가지다. 아까 위에서 설명했듯이 Account.class 라는 클래스 리터럴을 이용해서 Account라는 타입을 파라미터로 넘겼다.

public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
  Object object = fromJson(json, (Type) classOfT);
  return Primitives.wrap(classOfT).cast(object);
}

위의 코드는 Gson의 fromJson 메서드이다. 아까 예제와 많이 비슷하다. Class<T>라는 타입을 파라미터로 받고 그 해당하는 T 타입을 리턴해 준다.

List

위의 경우에는 특별하게 주의 할 것 없지만 한개 주의 할 것이 있다. 만약 List 로된 json을 하고 싶다면 어떻게 할까? 아까 위에서 링크를 남겼던 곳에 가면 우리는 아래와 같은 코드를 작성할 수 없다고 했다.

List<Account>.class

그럼 그냥 List로 타입을 넘기면 될까? 한번 해보자.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
List<Account> accounts = gson.fromJson(jsons, List.class);
System.out.println(accounts);

위와 같이 List.class만 사용해서 코드를 작성하였다. 잘 된다. 딱히 문제는 없다. 출력도 원하는 값으로 된 듯 싶다. 하지만 안타깝게 이 코드에서 특정한 인덱스의 값을 가져올 때 에러가 발생한다.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
List<Account> accounts = gson.fromJson(jsons, List.class);
System.out.println(accounts);
System.out.println(accounts.get(0).getPassword());

위의 코드를 동작시켜 보자. 그럼 아래와 같은 에러를 발생시킨다.

com.google.gson.internal.LinkedTreeMap cannot be cast to ... Account..

LinkedTreeMap 을 Account 캐스팅을 할 수 없다는 것이다. 당연히 LinkedTreeMap은 Account로 형변환을 할 수 없다. 근데 왜 이런 에러가 발생할까? 그 이유는 List.class 클래스 리터럴에는 제네릭 정보가 없기 때문이다. 그렇기 때문에 gson은 List 제네릭 정보의 Account 라는 클래스 자체를 모른다. List의 어떤 값이 들어가야 될 지 모르니 그냥 Map으로 파싱하는 것이다. jackson의 경우에는 자바의 LinkedHashMap 으로 파싱한다.
그렇다면 어떻게 이 문제를 해결할까?

Super type token

이런 제네릭 정보가 지워지는 문제 때문에 Super type token 기법이 생겨났다. Super type token은 수퍼타입을 토큰으로 사용하겠다는 의미이다. 이건 또 무슨말인가? 제네릭 정보가 컴파일시 런타임시 다 지워지지만 제네릭 정보를 런타임시 가져올 방법이 존재한다. 제네릭 클래스를 정의한 후에 그 제네릭 클래스를 상속받으면 런타임시에는 제네릭 정보를 가져올 수 있다.

public class SuperTypeToken<T> {

}
public class TypeToken extends SuperTypeToken<String> {

}

System.out.println(TypeToken.class.getGenericSuperclass()); //SuperTypeToken<java.lang.String>

위와 같이 SuperTypeToken 을 제네릭으로 만든 후에 TypeToken 클래스에 SuperTypeToken 을 상속받으면 된다. 그럼 위와 같이 SuperTypeToken<java.lang.String> 정보를 가져올 수 있다.

이걸 이용해서 우리는 아까 Gson에서 하지 못했던 (gson에서 하지 못한건 아니지..) List 를 형태로 파싱 할 수 있다. 아래의 TypeToken은 Gson에 있는 클래스이다. 필자가 만든 클래스와는 다르다.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
class AccountTypeToken extends TypeToken<List<Account>> {

}
List<Account> accounts = gson.fromJson(jsons, new AccountTypeToken().getType());
System.out.println(accounts);

메서드의 내부 클래스를 만들어서 손쉽게 List 형태로 만들 수 있다. 위에서 본 Map과 다르게 특정한 정보도 가져올 수 있다.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
class AccountTypeToken extends TypeToken<List<Account>> {

}
List<Account> accounts = gson.fromJson(jsons, new AccountTypeToken().getType());
System.out.println(accounts);
System.out.println(accounts.get(0).getPassword()); //test

하지만 코드가 좀 지저분하다. 메서드 안에 내부 클래스를 만들고 나니 가독성도 그닥 좋지 않는 듯 하다. 좀 더 줄여 보자.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
TypeToken<List<Account>> typeToken = new TypeToken<List<Account>>() {};
List<Account> accounts = gson.fromJson(jsons, typeToken.getType());

TypeToken 을 익명 클래스로 작성하였다. 이렇게 한다면 상속한 클래스를 만들지 않았지만 실제로 내부적으로 임의의 클래스를 만든다. 그래서 그 클래스의 인스턴스만 한개 돌려주는 것 뿐이다. Gson, 혹은 기타 다른 TypeToken 클래스들은 {} 가 없다면 컴파일 에러를 발생시킨다. gson의 TypeToken 클래스 생성자는 protected 접근제한을 두고 있기 때문이다. 그래서 {}를 꼭 사용해야 한다. 필자도 처음에는 저걸 왜 사용할까 생각했는데 제네릭을 알고 나니 이해가 되었다. 물론 Jackson도 gson의 TypeToken 과 동일한 역할을 하는 TypeReference가 존재한다.

좀 더 간결하게 할 수 도 있다. 아래와 같이 말이다.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
List<Account> accounts = gson.fromJson(jsons, new TypeToken<List<Account>>() {}.getType());
System.out.println(accounts);

그냥 바로 메서드를 호출할 때 생성해서 사용해도 된다. 위의 코드가 제일 깔끔한 듯 싶다. 근데 왜 Gson은 TypeToken을 받는 fromJson메서드를 만들지 않았을까? 굳이 사용자가 getType() 메서드도 호출해야 한다니.. Jackson 경우에는 TypeReference를 받는 메서드가 존재하는데..
이래서 Jackson을 더..

Spring RestTemplate

마지막으로 Spring의 Super Type Token도 살펴보자. Spring에서 자주 사용될 Super Type Token은 바로 RestTemplate 클래스이다. 이 클래스 용도는 클래스 이름과 동일하게 Rest API를 호출할 때 사용되는 클래스이다. 통신을 할때 Json이나 Xml로 받을 메세지를 Object로 변환 할 수 있는데 이때에도 List 같은 클래스 리터럴을 사용하고 싶다면 Spring의 ParameterizedTypeReference 클래스를 이용하면 된다.

RestTemplate restTemplate = new RestTemplate();
restTemplate.exchange("http://localhost:8080",  HttpMethod.GET,
    null, new ParameterizedTypeReference<List<Account>>() {});

아주 간편하게 사용가능 하다. 필자가 말한 세개의 Super Type Token 클래스 구현은 거의 동일하다. Gson의 TypeToken 와, Jackson의 TypeReference, Spring의 ParameterizedTypeReference 모두 구현은 비슷하게 되어 있다.

우리는 이렇게 Super Type Token에 대해서 살펴봤다. Gson과 Jackson을 사용하다보면 new TypeToken<List<String>>() {} 이런 익명클래스를 사용하곤 했는데 왜 저렇게 사용할까 생각은 했지만 무심코 넘어갔다. 이제는 왜 저렇게 사용하는지 알게 되었으니 필요하다면 자주 이용하자.

리스코프 치환 원칙

오늘도 어김없이 객체지향의 5대 원칙 중에 리스코프 치환의 원칙을 살펴보도록 하자. 이것만 하면 이제 단일책임원칙만 마무리하면 된다. 리스코프 치환의 원칙이라.. 말은 어렵지만 내용은 이해하는 자체는 어려운 편은 아니다. 5대원칙들 중에 나머지들은 이름만 봐도 대충 감을 잡을 수 있었지만 이 리스코프 치환 원칙은 이름만 보고는 전혀 감이 오지 않는다. 이름만 봐도 거부감이 들어서 맨 나중에 살펴본 내용이다. (그럼 단일책임 부터 했어야 했나..)

그럼 일단 리스코프 치환의 원칙의 정의를 보자.

상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.

라고 하지만 정의를 봐도 전혀 이해가 가지 않는다. 그러니 어서 코드를 보자. 리스코프 치환원칙에서 가장 유명한 직사각형 정삼각형 예제가 있으니 일단 그걸 살펴보도록 하자.

public class Rectangle {
    protected double width;
    protected double height;

    public void setWidth(double width) {
        this.width = width;
    }
    public double getWidth() {
        return this.width;
    }
    public void setHeight(double height) {
        this.height = height;
    }
    public double getHeight() {
        return this.height;
    }
    public double getArea() {
        return this.getWidth() * this.getHeight();
    }
}

간단한 예제이다. 직사각형의 면적 구하는 공식은 가로 * 세로이다. 특별한 내용 없이 아주 완벽한(?) 코드이다.
우리는 다음과 같이 사용할 수 있다.

public class DoWork {

  public void work() {
    Rectangle rectangle = new Square();
    rectangle.setHeight(5);
    rectangle.setWidth(4);
    System.out.println(rectangle.getArea())
  }
}

public class Main {
  public static void main(String[] args) {
    Main main = new Main();
    main.work();
  }
}

문제 없이 우리는 20의 값을 출력할 수 있다. 만약 면적이 20이 아니면 예외를 던지라고 요구사항을 받았다 가정해보자. 그래서 우리는 다음과 같이 코드를 변경하였다.

public class DoWork {
  public void work() {
    Rectangle rectangle = new Rectangle();
    rectangle.setHeight(5);
    rectangle.setWidth(4);
    if(!isCheck(rectangle)) {
      throw new RuntimeException();
    }
  }

  public boolean isCheck(Rectangle rectangle) {
    return rectangle.getArea() == 20;
  }
}

public class Main {
  public static void main(String[] args) {
    Main main = new Main();
    main.work();
  }
}

여기 까지도 문제 없이 우리는 20이 아닐 경우 예외를 던진다. 꽤 시간이 흘러 또 다시 요구사항이 추가되었다. 정사각형을 추가로 만들어 달란 요구사항이다. 그래서 우리는 이렇게 생각 할 수 있다. 직사각형을 상속받아 정사각형을 구현할 수 있지 않을까 하고 구현해보도록 하자.

public class Square extends Rectangle {
    @Override
    public void setWidth(double width) {
        this.width = width;
        this.height = width;
    }
    @Override
    public void setHeight(double height) {
        this.height = height;
        this.width = height;
    }    
}

우리는 별다른 생각없이 setWidth 와 setHeight를 오버라이딩 받았다. 정사각형은 가로 세로가 같으니 동일하게 값을 넣어 주었다. 그리고 나서 다시 실행 코드를 변경해보다.

public class DoWork {
  public void work() {
    Rectangle rectangle = new Square();
    rectangle.setHeight(5);
    rectangle.setWidth(4);
    if(!isCheck(rectangle)) {
      throw new RuntimeException();
    }
  }

  public boolean isCheck(Rectangle rectangle) {
    return rectangle.getArea() == 20;
  }
}

public class Main {
  public static void main(String[] args) {
    Main main = new Main();
    main.work();
  }
}

그리고 나서 우리는 Rectangle 대신에 Square 클래스를 사용 하였다. 하지만 우리는 원하는 결과를 얻지 못하고 예외를 던져버린다. 이것이 바로 리스코프 치환의 원칙을 어겨서 일어난 일이다. 이제야 조금 이해 가지 않는가?

다시 정의를 살펴보면 조금 이해가 될 싶다. 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.

그럼 위의 문제를 어떻게 해결할까? 실제 직사각형과 정사각형 문제는 개념적으로 상속 관계에 있는 것처럼 보이지만 구현에서는 상속 관계가 아닐 수도 있다는 것을 보여 주고 있다. 개념상 정사각형은 높이와 폭이 같은 직사각형이므로 상속을 받는게 괜찮은 방법이라고 생각하지 모르겠지만 실제 프로그램에서의 이둘은 상속 관계로 묶을 수 없다는 것이다. 만약 isCheck() 메서드 같은 기능이 필요하다면 상속보다는 별개의 타입으로 구현해 주는 것이 맞다.

이 경우 말고도 상위 타입에서 지정한 리턴 코드(값) 등을 하위 타입이 상속받아 잘 못된 코드를 리턴한다면 이것 역시 리스코프치환 원칙을 어기는 일이 된다.

예를들자면 어떤 파일을 읽는데 파일의 데이터가 없다면 -1 리턴한다고 가정해보자. (실제로 InputStream 경우에도 -1을 리턴한다)

class FileCopy {
  public void copy(InputStream inputStream) {
    while (inputStream.read(data) != -1){
      // blabla
    }
  }
}

위와 같이 InputStream의 read() 메서드는 파일의 데이터가 없을 경우에 -1을 리턴한다. 하지만 만약에 InputStream의을 상속받아 구현한 하위 클래스가 파일의 데이터가 없을 경우 -1이 아닌 다른 값을 리턴하면 어떻게 될까?

class SomeInputStream extends InputStream {
  @Override
  public int read(byte[] data) throws IOException {
    //blabla
    return 0; //파일의 데이터가 없을 경우
  }
}

이러면 FileCopy의 copy 메서드는 결코 끝나지 않는 무한루프가 돈다. 이와 같은 문제가 발생하는 이유는 하위 클래스가 상위 클래스의 규칙을 지키지 않았기 때문이다. 이제는 조금 이해가 가지 않는가? 이게 바로 리스코프 치환의 원칙을 의미한다. 내용 자체는 그리 어려운 내용은 아닌 듯 싶다. 이름이 왜케 어려운건지..

리스코프 치환의 원칙은 기능의 명세(계약) 에 대한 내용이다. 기능 실행의 계약과 관련해서 흔히 발생하는 위반 사례로는 다음과 같다.

  1. 명시된 명세에서 벗어난 값을 리턴한다.
  2. 명시된 명세에서 벗어난 익셉션을 발생시킨다.
  3. 명시된 명세에서 벗어난 기능을 수행한다.

리턴 값은 0이나 또는 그 이상을 리턴하도록 정의되어 있는데 하위 타입에서 음수값을 리턴한다거나 IOException을 발생시킨다고 했는데 기타 다른 타 Exception을 발생시킨다던가 하는 위반사례들이 있으면 구현한 코드는 비정상적으로 동작할 수 있기에 하위 타입은 상위타입에서 정의한 명세에 벗어나지 않도록 주의해야 한다.

이렇게 오늘은 리스코프 치환의 원칙에 대해서 살펴봤다. 다음에는 마지막으로 단일책임원칙에 대해서 알아보도록 하자!

개방 폐쇄 원칙

오늘은 저번시간에 이어 개방 폐쇄 원칙(Open-clased principle) 대해서 알아보도록 하자. 예제를 고민하느라 책도 많이 참고 했고 인터넷 정보도 많이 활용했다. 계속 이 원칙들을 살펴봐야 되는데 중간중간에 다른 포스팅을 하느라 저번 포스팅이 어디있는지도 모르겠다. 오늘도 역시 단일책임원칙은 못한다.

개방 폐쇄 원칙 (OCP)

조금 모순이 있는 단어같다. 개방과 폐쇄가 공존한다. 하지만 여기에는 깊은 뜻이다. 일단 개방 폐쇄 원칙이 무엇인지 살펴보자. 개방 폐쇄 원칙이란 (OCP) 확장에는 열려 있어야 하고, 변경에는 닫혀있어야 한다. 라고 정의 되어있다. 흠.. 무슨 이런 어이 없는 말을 할까하는데 좀 더 풀어서 이야기 하면 다음과 같다.
기능을 변경 또는 확장할 수 있으면서 그 기능을 사용하는 코드는 수정하지 않아야 한다. 라고 하면 좀 더 와닿을까? 근데 그게 가능할까나 어떻게 변경하거나 확장하면서 그를 사용하는 코드는 수정하지 않아야 한다니 말이다. 먼저 글(의존 역전의 원칙) 을 읽었다면 가능해 보인다. 왜나면 의존역전의 원칙을 잘 지키면 개방 폐쇄 원칙도 알아서 잘 지켜지니 말이다.

일단 우리는 어떤 파일을 읽어와 엑셀로 다운로드 한다는 기능이 있다고 가정하자. 그걸 코드로 작성해보면 다음과 같다.

public class ExcelController {

    private final FileByteReader fileByteReader = new FileByteReader();

    public void download() {
        byte[] read = fileByteReader.read();

        //blabla
    }
}

public class FileByteReader {
    public byte[] read() {
        //파일을 읽는다.
        return new byte[0];
    }
}

아주 간단한 코드를 작성했다. ExcelController는 다운로드 하는 부분의 로직이 들어가면 되고 FileByteReader 클래스는 파일을 읽어와 byte 배열로 리턴해주는 코드가 들어가면 되겠다. ExcelController 클래스는 FileByteReader 클래스를 사용하고 있다. 그러다 어느날 Database에서 값을 꺼내 다운로드 하라는 요청이 들어왔다. 그럼 우리는 다음과 같이 변경해야 한다.

public class ExcelController {

  private final DataBaseByteReader dataBaseByteReader = new DataBaseByteReader();

  public void download() {
    byte[] read = dataBaseByteReader.read();

    //blabla
  }
}

public class DataBaseByteReader {
  public byte[] read() {
    //데이터 베이스에서 값을 가져온다.
    return new byte[0];
  }
}

우리는 DataBaseByteReader 클래스를 생성하고 다시 ExcelController에서 사용되었던 FileByteReaderDataBaseByteReader 클래스로 변경해야 한다. 요구사항이 변경될 때 마다 계속 ExcelController 클래스를 변경해야 한다. 이것은 개방 폐쇄 원칙을 지키지 않아 일어난 일이다. 눈치 빠른 사람들은 눈치 챗겟지만 인터페이스를 이용하면 좀 더 확장성 있게 되지 않을까 생각 한다. 그럼 인터페이스를 만들어보자.

public interface ByteReader {
  byte[] read();
}

public class DataBaseByteReader implements ByteReader {
  @Override
  public byte[] read() {
    //데이터 베이스에서 값을 가져온다
    return new byte[0];
  }
}

public class FileByteReader implements ByteReader {
  @Override
  public byte[] read() {
    //파일을 읽는다.
    return new byte[0];
  }
}

ByteReader 라는 인터페이스에 read() 라는 메서드를 만들고 구현은 해당 클래스의 맞게 개발하면 된다. 그럼 한번 사용해보자.

public class ExcelController {

  private final ByteReader byteReader = new DataBaseByteReader();

  public void download() {
    byte[] read = byteReader.read();

    //blabla
  }
}

인터페이스를 이용하여 DataBaseByteReader 클래스를 생성하였다. 하지만 아직까지 FileByteReader 클래스로 변경해야 된다면 ExcelController의 클래스를 변경해야 한다. 아직까지 완벽한 개방폐쇄가 되지 않았다. 엇 이건 저번에 봤던 의존역전원칙과 비슷한 구석이 많다고 생각 들 수 있다. 맞다. 저번에도 말했지만 의존역전원칙을 잘 지키면 개방폐쇄원칙도 자연스레 지키게 된다.

다시 코드를 변경해 보자.

public class ExcelController {

  private final ByteReader byteReader;

  public ExcelController(ByteReader byteReader) {
    this.byteReader = byteReader;
  }

  public void download() {
    byte[] read = byteReader.read();

    //blabla
  }
}

ByteReader 인터페이스는 사용하는 클라이언트에서 결정하면 된다. 만약 또 다시 요구사항이 추가 되었다고 생각해보자. 이번에는 DataBase에서 값을 가져오는 것이 아니고 타 API를 이용해서 가져와야 한다고 가정해보자.

public class RequestByteReader implements ByteReader {

  @Override
  public byte[] read() {
    //api 호출
    return new byte[0];
  }
} 

API를 통해 값을 가져와 Byte 배열로 가져오는 RequestByteReader 클래스를 만들었다. 그리고 ExcelController를 변경하려고 보니까 그럴 필요가 없어졌다. 왜냐하면 ExcelController 클래스는 직접적으로 어떤 클래스를 사용한게 아니라 의존성 주입을 받고 있기에 ExcelController는 변경이 일어나지 않는다. 이로써 우리는 수정과 확장은 하였지만 코드는 변경하지 않은 개방폐쇄 원칙을 따르게 된 것이다.
개방폐쇄원칙은 꼭 인터페이스만 사용하라는 법은 없다. 상속을 통해서도 개방폐쇄원칙을 지킬 수 있다.

class HttpServlet {

  protected void doGet(Request request) {
    response.send(400);
  }
  protected void doPost(Request request) {
    response.send(400);
  }
  // etc..
  public void service(Request request) {
    if(method.equals("get")) {
      doGet(request);
    } else {
      doPost(request);
    }
  }
}

어떤 request는 받는 클래스가 있다고 생각해보자. service()라는 메서드는 요청을 받아서 각각의 상태에 맞게 doGet과 doPost로 전달하는 역할을 한다고 치자. 기본적으로 각각의 상태코드는 400을 리턴한다. 하지만 만약 계정 정보를 조회하는 요구사항이 들어왔다고 가정해보자. 그리고 계정정보의 조회의 상태코드는 get일 경우 에만 200의 상태코드를 리턴해야 한다고 하면 다음과 같이 할 수 있다.

class AccountServlet extends HttpServlet {

  @Override
  protected void doGet(Request request) {
    response.send(200);
  }
}

이렇게 HttpServlet 클래스는 변경하지 않고 확장해 나갔던 이유는 바로 개방폐쇄 원칙을 잘 지켜서 할 수 있는 일이 되었다. 가만 보면 위의 코드는 어느 디자인 패턴과 비슷해보인다. 맞다. 디자인 패턴 중 템플릿 메서드 패턴을 사용한 것이다. 템플릿 메서드 패턴은 상위 클래스에서 실행할 기본 코드를 만들고 하위 클래스에서 필요에 따라 확장해가는 패턴으로 아주 유용하게 쓰인다. 나중에 기회가 된다면 패턴들도 한번씩 살펴보기로 하자.
실제 위의 코드는 java servlet의 HttpServlet 클래스를 간단하게 구현해 본 것이다. HttpServlet 클래스는 템플릿 메서드 패턴으로 구현되어 있다.

이렇게 오늘은 객체지향의 5대 원칙 중 개방폐쇄의 원칙을 살펴봤다. 내용자체는 그리 어려운 말은 아니지만 실제로 구현하려면 많이 생각하고 구현해야 될 듯 싶다. 언제쯤 단일책임원칙의 예제가 생각날까.. 리스코프 치환원칙은 아주 유명한 예제가 있어서 그걸 기반으로 설명하면 될 듯 싶은데.. 단일 책임 원칙은.. 왜이렇게 생각이 나지 않을까

일단 아마도 다음시간에는 리스코프 치환원칙을 살펴볼 듯 싶다. 그럼 오늘은 이만.