어댑터 패턴 (Adapter pattern)
- 현실 세계에서도 찾아보기 쉬운 패턴이다.
- 해외에서 한국의 전자제품을 사용하려면 110v '어댑터'를 가져가야 한다.
- 호환되지 않는 인터페이스를 가진 객체들이 협업할 수 있도록 해주는 구조적 디자인 패턴이다.
- 클라이언트가 사용하는 인터페이스를 따르지 않는 레거시 코드를 재사용할 수 있게 해준다.
다이어그램으로 어댑터 패턴 관계 살펴보기
- 각 클래스에 대한 설명은 아래와 같다.
Target
은 변화에 대한 요구사항이다.Adaptee
는 기존의 코드이다.Adapter
는 변화에 대한 요구사항을 구현한 새로운 코드이다.
Adaptee
가 가지고 있는 기능을Adapter
가 주입받아operation()
을 구현한다.Client
는 인터페이스인Target
을 통해 이를 사용할 수 있게 된다.
어댑터 패턴이 해결하는 문제
- 외부 라이브러리 클래스를 사용하고 싶은데, 클래스의 인터페이스가 다른 코드와 호환되지 않을 때 이를 해결해줄 수 있다.
- 여러 자식 클래스가 있는데, 부모 클래스를 수정하기엔 호환성이 문제가 될 때 이를 해결해줄 수 있다.
문제 해결 예제 1
- 외부 라이브러리에 전세계 날씨와 관련된 정보를 XML 형태로 반환하는 클래스가 있다.
- 국가, 날짜, 시간 등을 입력하면 날씨를 반환해준다.
- 회사의 프론트엔드 표준은 JSON 데이터를 기준으로 하고 있다.
- 이 때, 외부 라이브러리 데이터를 JSON 형태로 변환해주는 어댑터를 생성하여 이용할 수 있다.
문제 해결 예제 2
- 고객사 서비스에서는 통합회원관리 API 서비스를 제공한다.
- 회원관리 API 에 회원 정보를 넣어 가입, 로그인, 탈퇴 등을 할 수 있다.
- 애플리케이션을 만들 때, 회원 관련 API는 따로 만들기 번거로워서 고객사 서비스를 이용하기로 한다.
- 그런데 자사 서비스는 익명 서비스라 회원정보 조회 API 를 호출했을 경우에도 회원 이름, ID 등은 철저하게 비밀리에 관리해야 한다는 요구사항이 있다.
- 고객사 API 를 쓰되, 회원 정보 조회 API 를 사용하는 암호화 회원 정보 조회 어댑터를 만들어 자사 서비스에 맞게 해당 API 를 변환할 수 있을 것이다.
현실 요구사항 살펴보기
- 소셜 로그인 시스템을 만드려고 한다.
- 카카오 아이디 혹은 인프런 아이디가 있으면, 로그인할 수 있어야 한다.
- 카카오와 인프런은 계정 객체의 필드가 판이하게 다르다.
카카오 계정 정보 살펴보기
@AllArgsConstructor
@Builder
@Getter
public class KakaoAccount {
private final String id;
private final String password;
private final String name;
private final String email;
public static final String KAKAO_SECRET = "KA_SECRET";
public String getAuthToken() {
// 인증 절차 생략
System.out.println("카카오 로그인 성공");
return id + KAKAO_SECRET + password;
}
}
- 위와 같은 필드와 메서드가 있다고 가정한다.
인프런 계정 정보 살펴보기
@AllArgsConstructor
@Builder
@Getter
public class InflearnAccount {
private final String email;
private final String password;
private final String username;
public static final String INFLEARN_SECRET = "INF_SECRET";
public String login() {
// 인증 절차 생략
System.out.println("인프런 로그인 성공");
return email + INFLEARN_SECRET + password;
}
}
- 위와 같은 필드와 메서드가 있다고 가정한다.
소셜 로긴을 위한 Target
인터페이스 예제
public interface SocialNetworkAuthTarget {
String getServiceName();
String getUserName();
String getSecret();
String getToken();
}
- 소셜 로긴 인증정보를 구성하려면 위의 메서드 구현이 필요하다고 가정한다.
인프런 어댑터 구현 InflearnSocialNetworkAuthAdapter
@AllArgsConstructor
public class InflearnSocialNetworkAuthAdapter implements SocialNetworkAuthTarget {
private final InflearnAccount inflearnAccount;
@Override
public String getServiceName() {
return "INFLEARN";
}
@Override
public String getUserName() {
return inflearnAccount.getUsername();
}
@Override
public String getSecret() {
return InflearnAccount.INFLEARN_SECRET;
}
@Override
public String getToken() {
return inflearnAccount.login();
}
}
InflearnAccount
라는Adaptee
개념의 객체를 주입받아 인증정보를 구성한다.
카카오 어댑터 구현 KakaoSocialNetworkAuthAdapter
@AllArgsConstructor
public class KakaoSocialNetworkAuthAdapter implements SocialNetworkAuthTarget {
private final KakaoAccount kakaoAccount;
@Override
public String getServiceName() {
return "KAKAO";
}
@Override
public String getUserName() {
return kakaoAccount.getName();
}
@Override
public String getSecret() {
return KakaoAccount.KAKAO_SECRET;
}
@Override
public String getToken() {
return kakaoAccount.getAuthToken();
}
}
KakaoAccount
라는Adaptee
개념의 객체를 주입받아 인증정보를 구성한다.
소셜 로긴 메서드 생성
public interface SocialNetworkAuthService {
static void socialLogin(SocialNetworkAuthTarget socialNetworkAuthTarget) {
System.out.println("소셜 로그인을 시작합니다.");
System.out.println("이용하는 서비스: " + socialNetworkAuthTarget.getServiceName());
System.out.println("이름: " + socialNetworkAuthTarget.getUserName());
System.out.println("토큰: " + socialNetworkAuthTarget.getToken());
}
}
socialLogin()
메서드는SocialNetworkAuthService
라는 인터페이스에서 정적 메서드로 지원한다.SocialNetworkAuthTarget
을 인자로 받아 로그인을 수행한다.
Client
코드 구성하기
public class Client {
public static SocialNetworkAuthTarget getKakaoTarget() {
KakaoAccount kakaoAccount = KakaoAccount
.builder()
.id("kakaoman")
.password("kakaopassword")
.email("kakaoman@kakao.com")
.name("카카오제이크서")
.build();
return new KakaoSocialNetworkAuthAdapter(kakaoAccount);
}
public static SocialNetworkAuthTarget getInflearnTarget() {
InflearnAccount inflearnAccount = InflearnAccount
.builder()
.email("me@naver.com")
.password("mypassword")
.username("인프런제이크서")
.build();
return new InflearnSocialNetworkAuthAdapter(inflearnAccount);
}
public static void main(String[] args) {
SocialNetworkAuthService.socialLogin(getKakaoTarget());
SocialNetworkAuthService.socialLogin(getInflearnTarget());
}
}
Client
는Adapter
를 통해 생성된SocialNetworkAuthTarget
을 통해 편리하게socialLogin()
메서드를 이용할 수 있게 되었다.- 위 코드를 실행하면 결과로 아래와 같은 메세지를 볼 수 있게 된다.
소셜 로그인을 시작합니다.
이용하는 서비스: KAKAO
이름: 카카오제이크서
카카오 로그인 성공
토큰: kakaomanKA_SECRETkakaopassword
소셜 로그인 완료!
소셜 로그인을 시작합니다.
이용하는 서비스: INFLEARN
이름: 인프런제이크서
인프런 로그인 성공
토큰: me@naver.comINF_SECRETmypassword
소셜 로그인 완료!
다이어그램으로 살펴보기
InflearnAccount
와KakaoAccount
가Adaptee
에 해당하는 역할을 한다.- 다이어그램엔 나타나있지 않지만, 각
Adapter
가 사용하는 관계이다.
- 다이어그램엔 나타나있지 않지만, 각
Client
는 직접적으로Adapter
를 사용하지 않고Target
인터페이스를 사용한다.
어댑터 패턴은 가끔 너무 장황할 수 있다.
- 반드시
Adapter
를 도입해야 하는 걸까?- 기존의 클래스를 수정할 수 없는 경우엔 그렇다.
- 외부 라이브러리 등에서 클래스를 가져왔다면, 내가 직접 수정할 수 없다.
- 기존의 클래스를 수정할 수 없는 경우엔 그렇다.
- 수정 가능한 소스코드의 경우 의외로
Adapter
를 도입하지 않는 것이 더 간단하다.KakaoAccount
와InflearnAccount
는 접근 및 수정이 가능한 소스코드다.
- 위 다이어그램처럼 구현해도 충분히
SocialNetworkAuthService
를 이용 가능하다.- 별도의 클래스를 생성하지 않아도 된다는 장점이 있다.
- 기존 코드를 수정해야 한다는 단점이 있다.
- 객체지향 원칙만 보자면 클래스를 나누는게 객체지향 원칙에 가깝다.
- 그러나 이 방법은 실용적이다.
- 언제 어댑터 패턴을 적용하는게 이득일지 잘 따져봐야 한다.
장점
- 기존 코드를 변경하지 않고 원하는 인터페이스 구현체를 만들어 재사용할 수 있다.
- 기존 코드를 손상시키지 않는 것은 객체지향 원칙 중
개방/폐쇄 원칙
에 해당한다.
- 기존 코드를 손상시키지 않는 것은 객체지향 원칙 중
- 기존 코드가 하던 일과 특정 인터페이스 구현체로 변환하는 작업을 각기 다른 클래스로 분리하여 관리할 수 있다.
- 역할에 맞게 코드를 분리하는 것은 객체지향 원칙 중
단일 책임 원칙
에 해당한다.
- 역할에 맞게 코드를 분리하는 것은 객체지향 원칙 중
단점
- 다수의 새로운 인터페이스와 클래스를 도입해야 하므로 구조가 복잡해진다.
- 때로는 서비스 클래스를 변경하는 것이 더 간단할 때도 있다.
자바와 스프링에서는 어댑터 패턴을 어떻게 활용하고 있을까?
Collections
예제
List<String> strings = Arrays.asList("a", "b", "c");
Enumeration<String> enumeration = Collections.enumeration(strings);
ArrayList<String> list = Collections.list(enumeration);
- 클라이언트가 간단한 문자열만 인자로 넘겨도
Collections
클래스를 생성할 수 있도록 도와준다. - 둘째 줄의 경우,
strings
가Adaptee
의 역할이며,Collections
가Adapter
에 해당하는 역할이고,Enumeration
이Target
에 해당하는 역할이다.
java.io
패키지 예제
try(InputStream is = new FileInputStream("input.txt");
InputStreamReader isr = new InputStreamReader(is);
BufferedReader reader = new BufferedReader(isr)) {
while(reader.ready()) {
System.out.println(reader.readLine());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
txt
파일을 읽어 (File
)InputStream
으로 만든 후InputStreamReader
로 만들고BufferedReader
로 만든 후 코드에서 활용하고 있다.File
->InputStream
->InputStreamReader
->BufferedReader
- 어댑터를 통해 무려 3단 변신을 한다.
스프링의 HandlerAdapter
- 우리가 여태까지 봤던 형태와 다르게 인터페이스 형태로
Adapter
를 제공해주고 있다.- 디자인 패턴은 딱 정해진 것이 아니라 보는 시각에 따라 다른 패턴으로 보일 수 있다.
HandlerAdapter
인터페이스를 구현하면,DispatcherServlet
필드에HandlerAdapter
로서 등록되어 이용될 수 있게 해준다.- 스프링의 요청 처리 동작을 편의성 좋은 객체들을 통해 내 마음대로 확장할 수 있도록 도와준다.
HandlerAdapter
의 깊은 내부동작이 궁금하다면 이 포스팅 을 참고하면 좋다.레퍼런스
https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4/dashboard
반응형
'Java > 자바 디자인 패턴' 카테고리의 다른 글
브릿지 패턴 (Bridge Pattern) 이란? (0) | 2023.02.19 |
---|---|
자바 디자인 패턴, 객체 생성 관련 패턴 (Object Creational Patterns) 이란? (0) | 2023.02.17 |
프로토타입 패턴 (Prototype Pattern) 이란? (2) | 2023.01.28 |
빌더 패턴 (Builder Pattern) 이란? (0) | 2023.01.26 |
추상 팩토리 패턴 (Abstract Factory Pattern) 이란? (0) | 2023.01.24 |