변환(Conversion)
•
객체 간의 변환은 어떻게 표현하는 것이 좋을까?
•
다른 패턴들과 마찬가지로 변환 패턴의 목표는 프로그래머의 의도를 명확히 전달하는 것
•
변환을 효과적으로 표현하기 위해 고려해야하는 몇 가지 기술적 사항
◦
변환이 얼마나 다양한가
◦
변환을 위해 새로운 의존성이 추가되는가?
변환 메서드(Conversion Method)
•
변환에 대한 요구사항이 적다면, 아래와 같이 기존 객체에 메서드를 추가해서 변환을 나타낼 수 있음
class Polar {
Cartesian asCartesian() {
...
}
}
Java
복사
•
주목해야할 점은 변환 메서드의 타입이 대상 객체(destination object)라는 것
•
변환의 포인트는 다른 프로토콜(인터페이스?)을 가진 객체를 얻는 것
•
변환 메서드는 가독성이 좋다는 장점 때문에 널리 사용됨
•
그러나 변환 메서드를 만들기 위해서 원본 객체의 프로토콜을 변경해야함
•
의존성이 존재하지 않았는데 변환 메서드 때문에 의존성이 생기는 것은 바람직하지 않음
•
특정 클래스에 asXXX()와 같은 메서드가 다수 존재하는 경우, 코드를 관리하기 힘들어짐
◦
이런 경우 변환을 이용하는 대신, 클라이언트(대상 객체) 측에서 원본 객체를 다룰 수 있게 하는 것이 좋음
•
이러한 담점들로 인해 변환 메서드는 자주 사용되지 않으며,
•
대개의 경우 변환 메서드보다는 변환 생성자 사용을 선호함
변환 생성자(Conversion Constructor)
•
변환 생성자는 원본 객체를 파라미터로 취해서 대상 객체를 반환함
•
변환 생성자는 하나의 원본 객체를 여러 다른 대상 객체로 변환할 때 유용함
•
변환을 사용하더라도 원본 객체의 코드가 지저분해지지 않기 때문
String.asFile() -> X
File(String name) -> O
URL(String spec) -> O
StringReadStream(String contents) -> O
Java
복사
생성(Creation)
•
크기가 작은 프로그램은 큰 프로그램에 비해서 수정하기 쉬움
•
따라서 프로그램 수정을 쉽게 하기 위한 초기 전략은 커다란 프로그램을 수행하는 하나의 컴퓨터를 더 작은 프로그램을 수행하는 여러 개의 컴퓨터(객체)로 나누는 것이었음
•
객체 사용으로 인해 프로그램 수정 비용은 크게 낮아짐
•
객체 생성은 독자들에게 연산에 대해 관련된 세부 내용은 당장 알 필요 없다는 메시지를 전달해줌
•
의미 있는 객체를 생성하기 위해서는 명확하고 직접적인 표현과 유연성 사이에서 균형을 잡아야함
•
생성과 관련된 구현 패턴을 이용하면 "객체 만들기"를 더 잘 표현할 수 있음
완결 생성자(Complete Constructor) = 완전한 생성자
•
객체는 연산을 수행하기 위해 정보를 필요로함
•
생성자를 통해 사용자(=클라이언트 개발자)에게 전제 조건(필요한 정보)을 전달해야함
•
객체를 설정하는 방법이 여러 가지라면, 각 경우마다 제대로 된 객체를 반환하는 생성자를 제공해야함
•
때로는 인자를 사용하지 않는 생성자와 여러 개의 설정 메서드를 사용해서 객체를 생성하는 것이 좀더 유연성을 높이는 경우가 있음
•
하지만 이런 경우 독자 입장에서 온전한 객체를 생성하기 위해 어떤 파라미터들이 필요한지 알기 어려움
new Rectangle(0, 0, 50, 200);
Java
복사
Rectangle box = new Rectangle();
box.setLeft(0);
box.setWidth(50);
box.setHeight(200);
box.setTop(0);
Java
복사
공장 메서드(Factory Method)
Rectangle.create(0, 0, 50, 200);
Java
복사
•
객체 생성을 나타내는 다른 방법은 클래스의 정적 메서드를 사용하는 것임
•
정적 메서드는 생성자에 비해 몇 가지 장점을 가짐
공장 메서드 장점
•
정적 메서드는 추상 타입을 반환할 수 있음
◦
인터페이스 혹은 추상클래스에서는 생성자를 선언할 수 없음
•
의도가 담긴 별도의 이름을 사용할 수 있음
•
객체를 생성하는 것보다 복잡한 작업을 수행하는 경우 유용함
•
그러나 공장 메서드를 사용하면 복잡성이 증가하므로, 공장 메서드는 이득이 있을 경우에만 사용해야함
•
혹은 단순 객체 생성 이외에 다른 의도가 있을 경우에만 사용해야함
내부 공장(Internal Factory)
•
객체 생성을 위한 메서드가 private이지만, 복잡하거나 하위 클래스에 의해 변경되어야 한다면 어떻게 해야할까?
•
내부 공장 패턴은 게으른 초기화(lazy initialization)를 사용하는 경우 흔히 사용됨
getX() {
if (x == null)
x = ... ;
return x;
}
Java
복사
•
변수 x에 저장할 객체 생성 로직이 복잡한 경우에는 내부 공장을 사용하는 편이 좋음
getX() {
if (x == null)
x = computeX();
return x;
}
class Child extends Parent {
@Override
X computeX() {
...
return x;
}
}
Java
복사
•
내부 공장은 객체 생성 로직을 하위클래스에서 변경할 수 있다는 것을 의미함
컬렉션 접근자 메서드(Collection Accessor Method)
class Library {
List<Book> getBooks() {
return books;
}
}
Java
복사
•
컬렉션에 대한 접근을 위 코드 처럼 직접 반환하는 경우,
클라이언트 클래스가 컬렉션을 직접 조작하게 되므로 Library 객체의 내부 상태의 유효성을 잃을 수 있음
List<Book> getBooks() {
return Collections.unmodifiableList(books);
}
Java
복사
•
해결 방법 중 하나로 컬렉션을 반환하기 전에 불변 컬렉션으로 바꿔서 반환하는 것임
•
하지만 컬렉션을 수정하려고 하면 예외가 발생함
•
클라이언트 개발자는 해당 객체가 불변인지 아닌지 모를 수 있음
•
이런 방법 대신 좀 더 의미적인(meaningful) 접근을 제공하는 메서드를 사용하는 것이 좋음
void addBook(Book arrival) {
books.add(arrival);
}
void bookCount() {
return books.size();
}
Java
복사
•
하지만 만약 여러분이 대부분의 컬렉션 인터페이스를 중복해서 구현하고 있다면, 설계상에 문제가 있을 확률이 높음
•
객체에서 클라이언트가 필요로 하는 적합한 작업을 제공한다면, 내부 데이터 접근을 많이 허용해야 할 필요가 없음
Ex. borrowBook(), returnBook()
불린 설정 메서드(Boolean Setting Method)
•
불린 상태를 설정하는 간단한 해법은 설정 메서드(Setter)를 사용하는 것
void setValid(boolean newState) {
...
}
Java
복사
•
만약 설정 메서드의 인자가 언제나 상수값(참 혹은 거짓)이라면, 각 불린 상태마다 더 명확한 인터페이스를 제공할수도 있음
void valid()
void invalid()
Java
복사
•
위 처럼 상태에 따라 인터페이스를 만들어주는 것이 코드가 더 읽기 쉽고, 언제 어떤 상태로 변하는지 알기 쉬움
•
단, 다음과 같은 코드 형태라면 설정 메서드를 사용하는 편이 나음
Boolean valid = true;
if (valid == true)
cache.valid();
else
cache.invalid();
Java
복사
•
위 코드에 설정 메서드를 사용하면 아래와 같음
cache.setValid(valid);
Java
복사
질의 메서드(Query Method)
•
하지만 메서드를 통해 다른 객체의 상태를 알아야하는 경우에는 "be" 동사나 "have" 동사를 사용하면됨
class Client {
...
if (widget.isVisible())
widget.doSomething();
else
widget.doSomethingElse();
...
}
Java
복사
•
클라이언트 객체가 다른 객체의 상태에 의존적인 로직을 많이 가지고 있다면, 이는 로직의 위치에 문제가 있다는 신호임
•
위 코드의 경우 widget 객체로 로직의 위치를 옮기는 것이 나음
class Client {
...
widget.run();
...
}
class Widget {
void run() {
if (isVisible())
doSomething();
else
doSomethingElse();
}
}
Java
복사
취득 메서드(Getting Method)
일명 Getter
•
로직과 데이터를 함께 배치한다는 원칙에 입각하면, Public 혹은 Default 접근 제한자를 취하고 있는 취득 메서드를 사용하는 것은 객체 내부에 특정 로직이 있다는 힌트가됨
•
따라서 무작정 취득 메서드를 제공하는 것보다는 가급적 필요한 로직을 데이터가 있는 쪽으로 옮기는 것이 옳음
•
하지만 다른 도구(tool)에서 취득 메서드를 요구하는 상황처럼, 공용 취득 메서드를 사용해야하는 경우도 있음
설정 메서드(Setting Method)
일명 Setter
•
설정 메서드의 가시성을 높이는 것은 취득 메서드의 가시성을 높이는 것 보다 더 주의를 기울어야함
•
설정 메서드의 이름은 의도가 아닌 구현에 의해 정해짐
•
즉, 객체의 내부 구현을 직접적으로 노출 시킴
•
설정 메서드가 필요하다면, 클라이언트가 값 설정을 통해 어떤 문제를 해결할 수 있는지 파악하고, 그 문제를 직접해결할 수 있도록 하는 메서드를 그 객체에게 할당하라
•
취득 메서드와 마찬가지로, 어떤 도구(tool)에서 설정 메서드를 호출해야 한다면, "도구 전용"이라는 주석을 붙인 후 해당 메서드를 공용으로 만들라
안전한 복사
•
취득 메서드나 설정 메서드를 사용하는 경우 발생하는 문제를 안전한 복사를 통해 어느정도 해결할 수 있음
List<Book> getBooks() {
List<Book> result = new ArrayList<Book>();
result.addAll(books);
return result;
}
Java
복사
•
위의 경우 단순히 컬렉션 접근자 메서드를 제공하는 편이 나음
•
하지만 전체 데이터에 대한 접근을 제공하기 위해서라면 이 기법을 사용하는 것이 더 안전함
List<Book> setBooks(List<Book> newBooks) {
List<Book> result = new ArrayList<Book>();
result.addAll(books);
}
Java
복사
•
설정 메서드도 위 처럼 안전한 복사를 통해 구현할 수 있음
•
하지만 안전한 복사 또한 문제점을 여전히 가짐
•
처리하고자 하는 컬렉션의 크기가 크면 성능이 대폭 저하됨
•
안전한 복사는 외부 접근에서 코드를 보호하는 일시적인 해결책일 뿐임
•
근본적인 해결 방법은 프로토콜을 좀 더 의미 있게 수정하는 것