우리가 개발해준 DuckSimulator 가 엄청난 성공을 거둔 덕분에, 회사는 공격적으로 사업을 확장하면서 M&A를 진행했습니다.
그 결과, 유서 깊은 프로그램인 TurkeySimulator 를 제공하는 회사와 합병해서 해당 기능을 DuckSimulator 에 병합시키려고 합니다.
public class MallardDuck implements Duck{
@Override
public void quack() {
System.out.println("꽥");
}
@Override
public void fly() {
System.out.println("날고 있어요!");
}
}
우리가 이미 작성했었던 덕 클래스입니다.
꽥! 하는 소리를 내는 것과 날 수 있는 기능을 각각 클래스마다 구현하거나, 전략 패턴을 통해 인터페이스를 활용 할 수 있었죠.
public class WildTurkey implements Turkey{
@Override
public void gobble() {
System.out.println("골골");
}
@Override
public void fly() {
System.out.println("짧게밖에 못납니다요");
}
}
그리고 이번에 합병된 터키시뮬레이터의 터키 구상클래스입니다.
gobble() 이라는 다른 매서드가 보이네요!
public class TurkeyAdapter implements Duck {
// Duck 으로 변형시켜주는 어댑터입니다. 따라서 "원하는" 타입을 상속받아줍니다.
// 이 어댑터를 사용하면 Turkey 는 Duck 의 타입을 상속하게 되는것이죠.
Turkey turkey;
public TurkeyAdapter(Turkey turkey) { // 생성자에서 기존 Turkey 객체의 레퍼런스를 가져옵니다.
this.turkey = turkey;
}
@Override
public void quack() {
turkey.gobble();
}
/**
* 공통 인터페이스 부분입니다. 칠면조와 오리가 공유하지만 다른 인터페이스(행동)입니다.
* 터키의 fly 는 짧으므로 좀 더 많이 호출해서 오리와 비슷한 거리를 날 수 있도록 조정했습니다.
*/
@Override
public void fly() {
for (int i = 0; i < 5; i++) {
turkey.fly();
}
}
}
어댑터 역할을 하는 클래스를 하나 작성해봅시다
상속받고자 하는 타입을 구현한 클래스입니다.
그리고, 변환이 필요한 ( 이 경우에는 터키 -> 오리 가 되어야 하므로 터키 ) 객체를 필드 변수로 가집니다.
생성자 매서드에서 패러미터로 변환이 필요한 객체(터키)를 가져온 후, 우리가 구현하고자 하는 변환할 클래스(오리) 의 매서드들로 재정의해주는 과정을 거칩니다.
public class AdapterTest {
public static void main(String[] args) {
Duck duck = new MallardDuck();
Turkey turkey = new WildTurkey();
Duck turkeyAdapter = new TurkeyAdapter(turkey);
// 터키 객체를 어댑터로 감싸서 Duck 객체처럼 보이게 만듭니다.
System.out.println("=====칠면조입니다=====");
turkey.gobble();
turkey.fly();
System.out.println("==================");
System.out.println("=====오리입니다=====");
duck.quack();
duck.fly();
System.out.println("==================");
System.out.println("=====칠면조 어댑터입니다=====");
turkeyAdapter.quack();
turkeyAdapter.fly();
System.out.println("==================");
}
}
조금 더 활용할법한 예를 들어볼까요?
자바의 초기 컬렉션들은 (Vector, Stack, HashTable) 은 Enumeration 을 리턴하는 elements() 매서드가 구현되어 있습니다.
그리고, 최근에는 Enumeraion 과 마찬가지로 컬렉션에 있는 일련의 항목에 접근하고, 그 항목을 제거할 수 있게 해주는 Iterator 인터페이스를 사용합니다.
두 녀석들은 remove() 매서드 외에는 별 차이가 없네요. 이거 어댑터로 변환하기 딱 좋아보입니다.
public class EnumerationIterator implements Iterator<Object> {
Enumeration<?> enumeration;
public EnumerationIterator(Enumeration<?> enumeration) {
this.enumeration = enumeration;
}
@Override
public boolean hasNext() {
return enumeration.hasMoreElements();
}
@Override
public Object next() {
return enumeration.nextElement();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
// Enumerations 는 remove 를 지원하지 않으므로 예외를 던짐
}
}
어댑터 패턴은?
호환되지 않는 인터페이스들을 연결해서 사용할 수 있게 한다!
Duck 과 Turkey 는 서로 다른 인터페이스를 가지지만, 어댑터를 통해서 이 두 인터페이스를 적절히 호환되게 변경 가능하다.
파사드(프랑스어: Façade)는 건물의 출입구로 이용되는 정면 외벽 부분을 가리키는 말이다. 한글화하여 순화하려면 '정면'(正面)이 무난할 것으로 여겨진다. 건축에서 파사드의 궁극적 목적은 '소통'이다. 건물의 입면이 다양해지면서 파사드는 건물 외피 전체를 의미하기도 한다.
출처 : 위키백과
홈시어터로 영화 한편 볼까요?
public class TheaterTest {
public static void main(String[] args) {
Popper popper = new Popper(); // 팝콘기계
Projector projector = new Projector();
Movie movie = new Movie("아저씨");
Screen screen = new Screen();
Lights lights = new Lights();
System.out.println("========영화 한편 볼까?========");
popper.on();
popper.pop();
lights.dim(10);
projector.on();
screen.down();
screen.movieOn(movie);
}
}
영화 한편 보기 너무 힘드네요
이렇게 매번 하나하나 할 필요 없이, 이렇게 해봅시다
public class HomeTheaterFacade {
Lights lights;
Popper popper;
Projector projector;
Screen screen;
public HomeTheaterFacade(Lights lights, Popper popper, Projector projector, Screen screen) {
this.lights = lights;
this.popper = popper;
this.projector = projector;
this.screen = screen;
}
public void watchMovie(Movie movie){
System.out.println("홈씨어터 영화보기 시퀀스 시작!");
popper.on();
popper.pop();
lights.dim(10);
projector.on();
screen.down();
screen.movieOn(movie);
}
public void endMovie(){
System.out.println("홈시어터 끄는 중");
popper.off();
// ...
// 전부 .off() 매서드로 꺼버리기
}
}
클라이언트 입장에서는 훨씬 더 편하게 바뀌었습니다!
그리고 추가로, 각각 객체들( 프로젝터, 팝콘기계 ... ) 들도 여전히 개별적으로 사용 가능합니다.
@Service
public class UserChannelServiceImpl implements UserChannelService {
private final UserChannelRepository userChannelRepository;
private final RedisDao redisDao;
private final ChannelService channelService;
private final UserService userService;
...
@Override
@CacheEvict(cacheNames = CacheNames.CHANNELS, key = "#user.id")
@Transactional
public void inviteUser(User user, Long channelId, String email) {
if (!isUserJoinedByChannel(user, channelId)) {
throw new CustomException(USER_CHANNEL_FORBIDDEN);
}
Channel channel = channelService.getChannelById(channelId);
User invitedUser = userService.getUserByEmail(email);
if (isUserJoinedByChannel(invitedUser, channelId)) {
throw new CustomException(USER_CHANNEL_DUPLICATED);
}
UserChannel userChannel = UserChannel.builder().user(invitedUser).channel(channel).build();
userChannelRepository.save(userChannel);
//invitedUser 쪽의 CHANNELS cache data 삭제
redisDao.deleteValues("CACHE_CHANNELS::" + invitedUser.getId());
}
다대다 (@ManyToMany) 연관관계를 중간 테이블(FK를 들고있는)을 사용해서 풀어낸 부분입니다.
이 매서드에서는 userService, ChannelService 를 둘 다 의존주입 받습니다.
그리고 각각의 매서드를 적절히 조합해서 하나의 기능으로 리턴합니다.
이를 통해서 컨트롤러 측에서는 인증, 인가를 처리하고, 서비스 쪽의 복잡한 부분은 신경쓰지 않은 채로 위의 매서드를 호출하면 됩니다.
API의 스펙의 변경 없이(URI를 바꾸거나, 여러번의 호출이 있어야 하거나) 처리할 수 있게 됐습니다.
어댑터 패턴?
: 특정 클래스 인터페이스를 클라이언트에서 요구하는 다르 인터페이스로 변환합니다. 인터페이스가 호환되지 않아 같이 쓸 수 없었던 클래스를 사용할 수 있게 도와줍니다.
퍼사드 패턴?
: 서브시스템에 있는 일련이 인터페이스를 통합 인터페이스로 묶어줍니다. 또한 고수준 인터페이스도 정의하므로(ex : 홈싸어터) 서브시스템을 더 편하게 사용할 수 있습니다.