어댑터 패턴
서로 다른 두 인터페이스간 통신을 가능하게 하는 디자인 패턴이다.
SOLID 원칙 중 개방 폐쇄 원칙을 활용한 설계패턴인데, 이 어댑터 패턴은 사실 Java가 동작하는 근간에 있었다.
Java의 동작에 어댑터 패턴이 어디서 어떻게 사용되었는지 보기 전에 먼저
c++과 Java의 컴파일 과정을 살펴보자
c++과 Java의 컴파일과정
객체 프로그래밍을 지원하는 c++에서는 똑같은 로직이 구현된 프로그램을 실행하려면
OS별로 컴파일러가 다르고, 소스코드를 각 컴파일러에 맞게 수정해줘야해서, 소스 코드 내에
32비트의 os로 빌드할때, 64비트의 os로 빌드할때의 두가지의 상황을 하나의 소스코드로 빌드할 수 있도록 개발해야 하다보니
os별로 구현이나 자료형이 달라지는 경우 중복으로 코딩하거나 오버로딩을 활용하여 서비스할 모든 OS를 고려해서 가능한 경우를 모두 구현한 후 빌드해야 문제없이 컴파일 되었었다.
c++ 의 컴파일 과정
아래 사진의 컴파일러가 os 별로 다르고, 그에 따라 전처리가 완료된 소스코드를 어셈블리 코드로 변환하는 방식 또한 다르다.
정리하면 c++은 객체 지향 언어라고 하지만, java와 비교하면, 아직 절차 지향 프로그래밍의 유산이 개발자가 생각해야 할 것이 많고, 할 일 또한 더 많은 것이다.
java는 어떻길래?
자바 프로그램의 구동은 먼저 어떤 os에서든 똑같은 환경에서 실행될 수 있도록, 어떤 os(물론 지원하는 한)에서든 실행이 되는 JVM(Java Virtual Machine)을 개발하여 하나의 소스코드가 마치 여러os에 적응하여/적용되어 컴파일되고 실행되는 것 처럼 느끼게 해준다.
아래는 자바의 컴파일과정을 간략하게 나타낸 사진이다.
요즘 인기인 도커 컨테이너에서 서버를 띄우는 개념과 비슷하다는 생각이 든다.
또한 java에서 기본적으로 제공되는 정적 클래스 System 등의 java 프로그램 개발에 필요한 도구들은 JDK(Java Developement Kit) 에 저장되어있으며, JVM에서 내가 만든 프로그램을 실행하기 전에,
내 소스코드를 JDK가 java 목적 파일로 변환해준다.
이렇게 변환된 java 목적 파일(class 확장자)은 자바 실행 환경인 JRE에서 자바 실행기에 의해 실행되고, 실제로 실행되는 환경은 JVM인것!
mySource.java → JDK → 번역 → mySource.class → JRE → JVM 실행 → JVM에서 프로그램 구동
의 과정을 거쳐 c++과 달리 java는 하나의 소스코드로 여러 os에서 실행할 수 있다.
여기서 어댑터 패턴이 어디에 사용되었을까? 바로 JRE이다.
JRE는 사용자 or 프로그래머가 “어떤 os에서 이 프로그램이 실행될까?” 라는 고민을 덜기 위해 실행되는 os 환경을 감지해 그에 맞게 동작한다.
JRE 말고도 JDBC도 어댑터 패턴이 적용된 예시이다. mysql, oracle 등의 다양한 데이터베이스가 있는데, 해당 데이터베이스에 종속된 소스코드가 아닌, 단일 인터페이스로 해당 db들을 조작할 수 있게 해준다.
예제로 알아보는 어댑터 패턴
상황 가정
기존 상황 : 눈코입이 붙어있지 않은 곰인형만 들어와 눈코입을 붙여주는 공정이 있다.
문제 상황 : 직원들은 철저히 곰인형의 작업에 익숙해져있는 상황인데 “Javalin” 이라는 이름의 괴물의 얼굴 작업을 추가로 진행해야한다.
직원들을 어떻게 교육하면 좋을까?
기존 상황
얼굴 작업 전 곰인형 인터페이스
package adapter;
public interface EmptyBear{
public void eyesTask();
public void noseTask();
public void mouthTask();
}
얼굴 작업 전 테디베어 클래스
package adapter;
public class EmptyTeddyBear implements EmptyBear{
@Override
public void eyesTask() {
System.out.println("눈 두개 붙이기");
}
@Override
public void noseTask() {
System.out.println("코 하나 붙이기");
}
@Override
public void mouthTask(){
System.out.println("입 하나 붙이기");
}
}
얼굴 작업 과정
package adapter;
public class WithoutAdapter {
public static void main(String[] args) {
// 테디 베어 얼굴 작업
EmptyBear emptyTeddyBear = new EmptyTeddyBear();
emptyTeddyBear.eyesTask();
emptyTeddyBear.noseTask();
emptyTeddyBear.mouthTask();
}
}
문제 상황
얼굴 작업 전 괴물 인터페이스
package adapter;
public interface EmptyMonster {
public void attachThreeEyes();
public void hollowOutNose();
public void drawMouth();
}
얼굴 작업 전 Javalin 클래스
package adapter;
public class EmptyJavalin implements EmptyMonster{
@Override
public void attachThreeEyes() {
System.out.println("눈 세개를 붙이기");
}
@Override
public void hollowOutNose() {
System.out.println("코 모양대로 파내기");
}
@Override
public void drawMouth() {
System.out.println("입을 무섭게 그리기");
}
}
곰인형과 Javalin 작업 과정 비교
package adapter;
public class WithoutAdapter {
public static void main(String[] args) {
// 테디 베어 얼굴 작업
EmptyBear emptyTeddyBear = new EmptyTeddyBear();
emptyTeddyBear.eyesTask();
emptyTeddyBear.noseTask();
emptyTeddyBear.mouthTask();
// 괴물 javalin 얼굴 작업
EmptyMonster emptyJavalin = new EmptyJavalin();
emptyJavalin.attachThreeEyes();
emptyJavalin.hollowOutNose();
emptyJavalin.drawMouth();
}
}
곰인형과 Javalin의 작업과정은 호환성이 없다.
직원들이 곰인형의 얼굴 작업과 Javalin의 얼굴 작업을 완전히 별개의 작업으로 인식하게 되어, 많은 걸 기억하고 싶지 않은 직원들의 불만이 폭주한 상황이다!
사실은 그냥 어떤 인형의 얼굴을 작업 할 때
눈 작업 → 코 작업 → 입 작업 의 순서로 생각하고 하면 쉬울 텐데…
직원들이 괴물 Javalin의 얼굴 작업 과정을 곰인형의 얼굴 작업과 비슷하게 생각하도록 해서 직원들이 괴물 Javalin 작업을 어려워하지 않고 불만을 잠재울 수 없을까?
어댑터 패턴 활용
괴물 어댑터 클래스
package adapter;
public class MonsterAdapter implements EmptyBear{
EmptyMonster monster;
public MonsterAdapter(EmptyMonster monster){
this.monster = monster;
}
@Override
public void eyesTask() {
monster.attachThreeEyes();
}
@Override
public void noseTask() {
monster.hollowOutNose();
}
@Override
public void mouthTask() {
monster.drawMouth();
}
}
- Adapter : 클라이언트와 Adaptee 사이를 중개한다. 불일치된 인터페이스를 맞추기 위해 사용
- Target : 클라이언트가 사용하려는 인터페이스를 정의함, 궁극적으로 클라이언트는 Target의 인터페이스를 사용하여 Adaptee에서 적절한 처리가 이루어지길 원함
- Adaptee : 인터페이스 변환 대상이 되는 클래스, 클라이언트는 Adaptee의 내부 동작 방식에 대해 몰라도 된다.
MonsterAdapter 클래스는 Target인 EmptyBear 인터페이스를 구현하며, Adaptee인 EmptyMonster에 적용되는, Adaptee가 이해할 수 있는 방법으로 구현된 함수를 통해 전달하여, 실제 처리는 Adaptee에서 이루어진다.
어댑터를 활용한 곰인형과 Javalin 얼굴 작업 비교
package adapter;
public class WithAdapter {
public static void main(String[] args) {
// 테디 베어 얼굴 작업
EmptyBear emptyTeddyBear = new EmptyTeddyBear();
emptyTeddyBear.eyesTask();
emptyTeddyBear.noseTask();
emptyTeddyBear.mouthTask();
// 괴물 javalin 얼굴 작업
EmptyMonster emptyJavalin = new EmptyJavalin();
MonsterAdapter monsterAdapter = new MonsterAdapter(emptyJavalin);
monsterAdapter.eyesTask();
monsterAdapter.noseTask();
monsterAdapter.mouthTask();
}
}
이제는 직원들이 어떤 인형이든 눈 작업 → 코 작업 → 입 작업의 플로우로 생각하여 편하게 일을 할 수 있고, 많은 것을 기억하지 않아도 된다!