Skip to content

멀티 벤더 IoT 연동 설계하기 (1편) - Adapter 패턴으로 통합하기

멀티 벤더 IoT 연동 설계하기 (1편) - Adapter 패턴으로 통합하기




TL;DR

  • 문제: 제조사마다 IoT API가 완전히 달라서 분기 처리하면 유지보수 지옥
  • 원인: A사는 /device/control, B사는 /smart-home/command 등 엔드포인트, 인증, 응답 포맷이 전부 제각각
  • 해결: Adapter 패턴으로 공통 인터페이스 정의, 제조사별 Adapter가 변환 담당
  • 효과: 제조사 추가 시간 2주 → 2일로 단축, 비즈니스 로직은 제조사 몰라도 됨
  • 한계: 초기 개발 시간 증가 (1주 추가), 공통 인터페이스 한계, 팀 학습 곡선, 오버헤드 1-2ms




글 머리말

“각 제조사 API 연동은 언제쯤 끝날까요?”

기획자의 질문에 저는 이렇게 답했습니다.

“첫 번째 제조사는 2주 정도, 두 번째는 아마 일주일? 세 번째부터는 2-3일이면 될 겁니다.”

“왜 점점 빨라지는 거죠?”

사실 처음부터 이렇게 확신한 건 아니었습니다. 솔직히 첫 번째 제조사 연동할 때는 “이거 제조사마다 다 다시 짜야 하는 거 아닌가?” 하는 생각도 들었거든요. 하지만 추상화 레이어를 설계해두니까 신기하게도 갈수록 빨라지더군요.

저희 서비스는 IoT 기기 제어 기능을 제공합니다. 같은 “조명 켜기”인데 제조사마다 API가 완전히 다릅니다. A사는 /device/control, B사는 /v2/smart-home/command, C사는 /api/devices/action… 엔드포인트부터 요청 바디, 인증 방식, 응답 포맷까지 전부 제각각이었습니다.

이번 글에서는 여러 제조사 API를 하나의 인터페이스로 통합한 경험을 공유합니다.





문제 상황 - API가 제각각

처음 요구사항을 받았을 때의 상황입니다.


제조사별 API 차이

세 제조사의 “조명 켜기” API를 비교해보면 이런 식입니다.

A사 API

POST /devices/light/control
Authorization: Bearer {token}
{
  "deviceId": "ABC123",
  "command": "ON",
  "brightness": 80
}

Response:
{
  "status": "success",
  "deviceId": "ABC123",
  "currentState": "ON"
}

B사 API

POST /v2/smart-home/command
Authorization: API-Key {key}
{
  "device_uuid": "ABC123",
  "action": {
    "type": "POWER_ON",
    "params": {
      "brightness_level": 80
    }
  }
}

Response:
{
  "result": {
    "code": 200,
    "message": "OK"
  },
  "data": {
    "device_uuid": "ABC123",
    "state": "on"
  }
}

C사 API

POST /api/devices/action
X-API-Token: {token}
{
  "id": "ABC123",
  "control": {
    "power": "on",
    "level": 80
  }
}

Response:
{
  "success": true,
  "device": {
    "id": "ABC123",
    "status": {
      "power": "on",
      "brightness": 80
    }
  }
}

같은 “조명 켜기”인데, 엔드포인트도 다르고, 요청 바디 구조도 다르고, 인증 헤더도 다릅니다.

더 큰 문제는 앞으로 계속 늘어날 예정이라는 점이었습니다. 기획 문서에는 “향후 5-10개 제조사 연동 예정”이라고 적혀 있더군요.


나쁜 해결책 - 분기 처리

처음엔 이렇게 접근할 뻔했습니다.

@Service
public class IoTService {

    public void turnOnLight(String vendor, String deviceId, int brightness) {
        if ("VendorA".equals(vendor)) {
            // A사 API 호출
            restTemplate.postForObject(
                "https://vendor-a.com/devices/light/control",
                Map.of(
                    "deviceId", deviceId,
                    "command", "ON",
                    "brightness", brightness
                ),
                VendorAResponse.class
            );
        } else if ("VendorB".equals(vendor)) {
            // B사 API 호출
            restTemplate.postForObject(
                "https://vendor-b.com/v2/smart-home/command",
                Map.of(
                    "device_uuid", deviceId,
                    "action", Map.of(
                        "type", "POWER_ON",
                        "params", Map.of("brightness_level", brightness)
                    )
                ),
                VendorBResponse.class
            );
        } else if ("VendorC".equals(vendor)) {
            // C사 API 호출 ...
        }
        // ... D사, E사, F사도 추가될 예정
    }
}

이렇게 하면 어떻게 될까요?

  1. 새 제조사 추가마다 모든 메서드 수정 (turnOnLight, turnOffLight, setBrightness…)
  2. 테스트 코드 폭발 (각 분기마다 테스트 케이스 추가)
  3. 코드 리뷰 지옥 (변경 범위가 너무 넓음)
  4. 버그 위험 증가 (한 곳 수정이 다른 곳에 영향)

다행히 코드 리뷰에서 팀원이 지적해줬습니다.

“이거 Strategy 패턴으로 분리하는 게 어떨까요?”

그때부터 제대로 된 설계가 시작되었습니다.





설계 원칙 - 추상화 레이어

핵심은 “제조사가 누구든 상관없는 인터페이스”를 만드는 것입니다.


전체 구조

먼저 전체 구조를 다이어그램으로 살펴보겠습니다.

graph TB
    subgraph 비즈니스 레이어
        SH[SmartHomeService]
    end

    subgraph 추상화 레이어
        AF[AdapterFactory]
        IA[IoTVendorAdapter<br/>인터페이스]
    end

    subgraph Adapter 구현체
        VA[VendorA<br/>Adapter]
        VB[VendorB<br/>Adapter]
        VC[VendorC<br/>Adapter]
    end

    subgraph 외부 API
        API_A[A사 API]
        API_B[B사 API]
        API_C[C사 API]
    end

    SH --> AF
    AF --> IA
    IA --> VA
    IA --> VB
    IA --> VC
    VA --> API_A
    VB --> API_B
    VC --> API_C

    style SH fill:#e8f5e9
    style AF fill:#fff3e0
    style IA fill:#fff3e0
    style VA fill:#e3f2fd
    style VB fill:#e3f2fd
    style VC fill:#e3f2fd

비즈니스 레이어는 AdapterFactory를 통해 적절한 Adapter만 가져오면 됩니다. 뒤에서 어떤 API를 호출하든, 어떤 형식으로 변환하든 상관없습니다.


설계 목표

  1. 제조사 추가 시 기존 코드 수정 최소화
  2. 각 제조사 구현체를 독립적으로 개발/테스트 가능
  3. 비즈니스 로직에서는 제조사를 몰라도 됨
  4. 새 제조사 추가는 새 클래스 하나만 추가

도메인 모델 설계

먼저 제조사에 독립적인 도메인 모델을 정의했습니다.

// 공통 기기 인터페이스
public interface IoTDevice {
    String getDeviceId();
    DeviceType getType();
    DeviceStatus getStatus();
}

// 기기 타입
public enum DeviceType {
    LIGHT,
    THERMOSTAT,
    DOOR_LOCK,
    AIR_CONDITIONER
}

// 기기 상태 - 제조사 무관하게 동일한 구조
public class DeviceStatus {
    private boolean power;
    private Map<String, Object> attributes;
    // 조명: brightness, color
    // 온도조절기: targetTemp, currentTemp
    // 도어락: locked
}

// 제어 명령 - 우리만의 표준
public class DeviceCommand {
    private CommandType type;
    private Map<String, Object> parameters;
}

public enum CommandType {
    TURN_ON,
    TURN_OFF,
    SET_VALUE
}

중요한 건 제조사 특정 필드가 하나도 없다는 점입니다.

brightness는 있지만 VendorA_brightness는 없습니다. 모든 제조사가 이 모델로 표현되어야 합니다.


Adapter 인터페이스 정의

각 제조사 API를 우리의 도메인 모델로 변환하는 Adapter를 정의합니다.

public interface IoTVendorAdapter {

    /**
     * 어댑터가 지원하는 제조사 식별자
     */
    String getVendorId();

    /**
     * 기기 상태 조회
     */
    DeviceStatus getDeviceStatus(String deviceId);

    /**
     * 기기 제어
     */
    void executeCommand(String deviceId, DeviceCommand command);

    /**
     * 기기 목록 조회
     */
    List<IoTDevice> listDevices(String userId);

    /**
     * 연결 상태 확인
     */
    boolean isConnected();
}

이 인터페이스가 추상화 레이어의 핵심입니다.

비즈니스 로직은 이 인터페이스만 알면 됩니다. 뒤에서 API 형식이 어떻든, 인증 방식이 어떻든 상관없습니다.





구현 - Adapter Pattern

각 제조사별로 Adapter를 구현합니다.


요청 흐름

실제 요청이 어떻게 처리되는지 흐름을 살펴보면:

sequenceDiagram
    participant Client as 클라이언트
    participant Service as SmartHomeService
    participant Factory as AdapterFactory
    participant Adapter as VendorAdapter
    participant API as 외부 API

    Client->>Service: turnOnLight(deviceId, brightness)
    Service->>Factory: getAdapter(vendorId)
    Factory-->>Service: VendorAAdapter
    Service->>Adapter: executeCommand(deviceId, command)
    Note over Adapter: 도메인 모델 → API 형식 변환
    Adapter->>API: POST /devices/control
    API-->>Adapter: 응답
    Note over Adapter: API 응답 → 도메인 모델 변환
    Adapter-->>Service: void
    Service-->>Client: 완료

핵심은 변환 로직이 Adapter 안에 캡슐화된다는 점입니다.


Adapter 구현 예시 (REST API 제조사)

@Component
@RequiredArgsConstructor
public class VendorAAdapter implements IoTVendorAdapter {

    private final RestTemplate restTemplate;
    private final VendorAProperties properties;

    @Override
    public String getVendorId() {
        return "VENDOR_A";
    }

    @Override
    public DeviceStatus getDeviceStatus(String deviceId) {
        // A사 API 호출
        VendorADeviceResponse response = restTemplate.getForObject(
            properties.getApiUrl() + "/devices/" + deviceId,
            VendorADeviceResponse.class
        );

        // A사 응답 → 우리 도메인 모델로 변환
        return convertToDomainModel(response);
    }

    @Override
    public void executeCommand(String deviceId, DeviceCommand command) {
        // 우리 도메인 명령 → A사 API 형식으로 변환
        VendorARequest request = convertToVendorRequest(deviceId, command);

        // A사 API 호출
        restTemplate.postForObject(
            properties.getApiUrl() + "/devices/control",
            request,
            VendorAResponse.class
        );
    }

    private DeviceStatus convertToDomainModel(VendorADeviceResponse response) {
        DeviceStatus status = new DeviceStatus();
        status.setPower("ON".equals(response.getCommand()));

        Map<String, Object> attributes = new HashMap<>();
        if (response.getBrightness() != null) {
            attributes.put("brightness", response.getBrightness());
        }
        status.setAttributes(attributes);

        return status;
    }

    private VendorARequest convertToVendorRequest(String deviceId, DeviceCommand command) {
        VendorARequest request = new VendorARequest();
        request.setDeviceId(deviceId);

        switch (command.getType()) {
            case TURN_ON:
                request.setCommand("ON");
                break;
            case TURN_OFF:
                request.setCommand("OFF");
                break;
            case SET_VALUE:
                request.setCommand("SET");
                request.setBrightness(
                    (Integer) command.getParameters().get("brightness")
                );
                break;
        }

        return request;
    }

    @Override
    public boolean isConnected() {
        try {
            restTemplate.getForObject(
                properties.getApiUrl() + "/health",
                String.class
            );
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

핵심은 두 변환 메서드입니다:

  • convertToDomainModel: A사 응답 → 우리 도메인 모델
  • convertToVendorRequest: 우리 명령 → A사 API 형식

이 변환 로직이 Adapter의 전부입니다.


다른 형식의 API 제조사 (B사)

B사는 더 복잡한 중첩 구조의 API를 사용합니다. 하지만 인터페이스는 동일합니다.

@Component
@RequiredArgsConstructor
public class VendorBAdapter implements IoTVendorAdapter {

    private final RestTemplate restTemplate;
    private final VendorBProperties properties;

    @Override
    public String getVendorId() {
        return "VENDOR_B";
    }

    @Override
    public void executeCommand(String deviceId, DeviceCommand command) {
        // 우리 도메인 명령 → B사 API 형식으로 변환 (중첩 구조)
        Map<String, Object> request = convertToVendorBRequest(deviceId, command);

        // B사 API 호출 (인증 헤더 추가)
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "API-Key " + properties.getApiKey());

        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);

        restTemplate.postForObject(
            properties.getApiUrl() + "/v2/smart-home/command",
            entity,
            VendorBResponse.class
        );
    }

    private Map<String, Object> convertToVendorBRequest(
        String deviceId,
        DeviceCommand command
    ) {
        Map<String, Object> action = new HashMap<>();
        Map<String, Object> params = new HashMap<>();

        switch (command.getType()) {
            case TURN_ON:
                action.put("type", "POWER_ON");
                if (command.getParameters().containsKey("brightness")) {
                    params.put("brightness_level",
                        command.getParameters().get("brightness"));
                }
                break;
            case TURN_OFF:
                action.put("type", "POWER_OFF");
                break;
            case SET_VALUE:
                action.put("type", "SET_VALUE");
                params.put("brightness_level",
                    command.getParameters().get("brightness"));
                break;
        }

        action.put("params", params);

        return Map.of(
            "device_uuid", deviceId,
            "action", action
        );
    }

    // ... 나머지 구현 생략
}

API 형식이 다르더라도, 인터페이스는 동일합니다. 각 Adapter는 자신의 API 형식으로만 신경 쓰면 됩니다.


Adapter Factory - 자동 선택

여러 Adapter 중 적절한 것을 선택하는 Factory를 만듭니다.

@Component
public class IoTAdapterFactory {

    private final Map<String, IoTVendorAdapter> adapters;

    // Spring이 모든 IoTVendorAdapter 구현체를 주입
    public IoTAdapterFactory(List<IoTVendorAdapter> adapterList) {
        this.adapters = adapterList.stream()
            .collect(Collectors.toMap(
                IoTVendorAdapter::getVendorId,
                adapter -> adapter
            ));
    }

    public IoTVendorAdapter getAdapter(String vendorId) {
        IoTVendorAdapter adapter = adapters.get(vendorId);
        if (adapter == null) {
            throw new UnsupportedVendorException(
                "Vendor not supported: " + vendorId
            );
        }
        return adapter;
    }

    public List<IoTVendorAdapter> getAllAdapters() {
        return new ArrayList<>(adapters.values());
    }
}

Spring의 의존성 주입으로 모든 Adapter가 자동으로 등록됩니다.

새 Adapter를 추가하면? @Component 붙이기만 하면 끝입니다.


비즈니스 로직 - 깔끔해진 서비스

이제 비즈니스 로직은 이렇게 간단해집니다.

@Service
@RequiredArgsConstructor
public class SmartHomeService {

    private final IoTAdapterFactory adapterFactory;
    private final DeviceRepository deviceRepository;

    public void turnOnLight(Long deviceId, int brightness) {
        // 1. 기기 정보 조회
        Device device = deviceRepository.findById(deviceId)
            .orElseThrow();

        // 2. 해당 제조사 Adapter 선택 (Factory가 알아서 찾아줌)
        IoTVendorAdapter adapter = adapterFactory.getAdapter(
            device.getVendorId()
        );

        // 3. 명령 생성 (우리 표준 모델)
        DeviceCommand command = DeviceCommand.builder()
            .type(CommandType.TURN_ON)
            .parameter("brightness", brightness)
            .build();

        // 4. 실행 (Adapter가 알아서 변환)
        adapter.executeCommand(device.getExternalId(), command);

        // 5. 상태 업데이트
        device.updateStatus(true, brightness);
        deviceRepository.save(device);
    }

    public DeviceStatus getDeviceStatus(Long deviceId) {
        Device device = deviceRepository.findById(deviceId)
            .orElseThrow();

        IoTVendorAdapter adapter = adapterFactory.getAdapter(
            device.getVendorId()
        );

        return adapter.getDeviceStatus(device.getExternalId());
    }
}

제조사가 누군지 신경 쓰지 않습니다. 그냥 “해당 제조사 Adapter”를 가져와서 쓸 뿐입니다.

새 제조사 추가? 이 코드는 한 글자도 바꾸지 않습니다.





실전 - 새 제조사 추가하기

3개월 후, 정말로 새 제조사 연동 요청이 들어왔습니다.


Before (분기 처리 방식이었다면)

  1. IoTService의 모든 메서드 수정 (10개+)
  2. 각 메서드마다 else if 추가
  3. 전체 서비스 테스트 다시 돌리기
  4. 영향받는 코드 리뷰 (수백 줄)
  5. 예상 시간: 1-2주

After (Adapter 패턴)

  1. VendorDAdapter 클래스 하나 생성
  2. IoTVendorAdapter 인터페이스 구현
  3. 해당 Adapter만 단위 테스트
  4. 실제 소요 시간: 2-3일
@Component
@RequiredArgsConstructor
public class VendorDAdapter implements IoTVendorAdapter {

    private final VendorDApiClient client;

    @Override
    public String getVendorId() {
        return "VENDOR_D";
    }

    @Override
    public DeviceStatus getDeviceStatus(String deviceId) {
        // D사만의 구현
    }

    @Override
    public void executeCommand(String deviceId, DeviceCommand command) {
        // D사만의 구현
    }

    // ... 나머지 구현
}

끝입니다. 이 클래스만 추가하면 모든 비즈니스 로직에서 자동으로 D사 기기를 제어할 수 있습니다.


실제 숫자로 보는 효과

항목Before (분기)After (Adapter)
신규 제조사 추가 시간1-2주2-3일
변경 파일 수10+1
영향받는 테스트전체신규 Adapter만
코드 리뷰 범위수백 줄신규 클래스만
버그 위험도높음낮음

기획자의 질문에 자신있게 대답할 수 있게 된 이유입니다.





이 접근의 아쉬운 점

완벽한 설계는 없습니다. Adapter 패턴도 트레이드오프가 있습니다.


1. 공통 인터페이스의 한계

문제: 모든 제조사를 하나의 인터페이스로 표현하기 어렵습니다.

A사는 지원하는 기능인데, B사는 없는 경우가 있습니다.

예를 들어:

  • A사: RGB 색상 지원
  • B사: 밝기만 지원
  • C사: RGB + 색온도 지원

해결: Optional 기능을 Map<String, Object> 형태로 처리했습니다.

public class DeviceStatus {
    private boolean power;
    private Map<String, Object> attributes; // 선택적 속성

    public Optional<Integer> getBrightness() {
        return Optional.ofNullable((Integer) attributes.get("brightness"));
    }

    public Optional<RgbColor> getColor() {
        return Optional.ofNullable((RgbColor) attributes.get("color"));
    }
}

타입 안정성은 조금 떨어지지만, 유연성은 확보했습니다.

더 나은 방법이 있을까요? 아마 있을 겁니다. 하지만 당시 상황에서는 이게 최선이었습니다.


2. 성능 오버헤드

Adapter를 거치면서 객체 변환이 발생합니다.

요청 → 도메인 모델 → 제조사 API 형식 → 제조사 응답 → 도메인 모델 → 응답

측정한 결과:

  • Adapter 변환 시간: 평균 1-2ms
  • 네트워크 통신 시간: 평균 200-500ms

변환 오버헤드는 전체의 1% 미만입니다. 무시할 수 있는 수준이죠.

만약 초고성능이 필요한 시스템이었다면? 다른 접근이 필요했을 겁니다. 하지만 저희 서비스는 사용자 요청 기반이라 이 정도면 충분했습니다.


3. 초기 개발 시간

첫 Adapter 개발할 때는 오히려 시간이 더 걸렸습니다.

  • 인터페이스 설계 고민: 3일
  • 도메인 모델 설계: 2일
  • 첫 Adapter 구현: 1주

분기 처리로 했으면 3-4일이면 됐을 일입니다.

하지만 두 번째부터는?

  • 2번째 Adapter: 3-4일
  • 3번째 Adapter: 2-3일
  • 4번째 Adapter: 2일

누적 투자 대비 수익입니다. 제조사 2개만 넘어가도 본전이고, 3개부터는 이득입니다.


4. 팀 학습 곡선

팀원들이 처음엔 낯설어했습니다.

“그냥 if문으로 하면 되는데 왜 이렇게 복잡하게 하나요?”

실제로 첫 PR에서 이런 코멘트가 달렸습니다:

“이해는 되는데, 오버엔지니어링 아닌가요? 제조사 많아봐야 3-4개인데…”

6개월 후, 제조사가 5개가 되었을 때 그 팀원이 말했습니다:

“감사합니다. 만약 분기로 했으면 지금쯤 유지보수 지옥이었을 것 같아요.”

디자인 패턴은 미래를 위한 투자입니다. 당장은 과해 보일 수 있습니다. 하지만 확장성이 필요한 시스템이라면 초기 투자가 나중에 빛을 발합니다.





내 프로젝트에 바로 적용하기

체크리스트

Adapter 패턴을 도입하기 전에 확인하세요:

  • 외부 시스템이 2개 이상인가? (1개면 굳이 불필요)
  • 앞으로 더 늘어날 예정인가?
  • 각 시스템의 API 형식이 다른가?
  • 비즈니스 로직에서 외부 시스템 형식을 몰라도 되는가?

구현 순서

  1. 도메인 모델 먼저 설계 - 외부 시스템에 독립적인 모델
  2. 인터페이스 정의 - 필요한 기능만 추상화
  3. 첫 번째 Adapter 구현 - 가장 간단한 것부터
  4. Factory 구현 - Spring DI 활용
  5. 두 번째 Adapter 추가 - 인터페이스가 적절한지 검증

주의사항

절대 하지 말 것:

  • 모든 외부 시스템 기능을 인터페이스에 넣기 (공통 기능만)
  • 제조사 특정 필드를 도메인 모델에 넣기
  • Factory 없이 직접 Adapter 주입

추천 설정:

  • Adapter별 설정은 @ConfigurationProperties로 분리
  • 연결 테스트용 isConnected() 메서드 필수
  • 각 Adapter는 독립적으로 테스트 가능하도록

트러블슈팅

Q. “어떤 기능은 A사만 지원해요”

  • Optional<T> 반환 타입 사용
  • UnsupportedOperationException 던지기
  • 또는 Map<String, Object> 형태로 유연하게

Q. “인터페이스가 너무 커져요”

  • 기능별로 인터페이스 분리 (인터페이스 분리 원칙)
  • IoTControlAdapter, IoTStatusAdapter 등으로 분리




시스템 점검 체크리스트

저도 Adapter 패턴을 적용할 때 이 항목들을 확인합니다. 멀티 벤더 통합을 고려한다면 참고하시면 좋을 것 같습니다.

  • 공통 인터페이스 설계: 모든 제조사가 지원하는 공통 기능만 인터페이스에 포함했는가?
  • 선택적 기능 처리: 일부 제조사만 지원하는 기능을 Optional이나 Map으로 처리했는가?
  • 에러 핸들링 통일: 모든 Adapter가 동일한 예외 타입을 던지도록 했는가?
  • 새 Adapter 추가 시간: 2~3일 이내에 추가 가능한 구조인가?
  • 테스트 독립성: 각 Adapter를 독립적으로 테스트할 수 있는가?




결론

두 가지를 배웠습니다.


첫째, “지금 당장 필요한 설계”와 “미래를 위한 설계”의 균형입니다.

처음엔 솔직히 오버엔지니어링 같았습니다. 제조사 2개인데 추상화 레이어를 만든다고요? if문 두 개면 끝나는데?

코드 리뷰에서 팀원도 똑같은 말을 했습니다. “이거 너무 복잡한 거 아닌가요?” 저도 순간 흔들렸습니다. 맞는 말 같았거든요. 근데 기획 문서를 다시 읽어보니 “향후 5-10개 제조사 연동 예정”이라고 써있더군요. 아, 이건 확장성이 필수구나.

3개월 뒤, 정말로 제조사가 5개가 됐습니다. 그때 그 팀원이 말했습니다. “그때 이렇게 설계해둬서 정말 다행이에요.”

기획 의도를 제대로 파악하는 게 좋은 설계의 시작입니다. 단순히 “지금 당장 뭐가 필요한가”가 아니라 “앞으로 어떻게 성장할 건가”까지 봐야 합니다.


둘째, 패턴은 도구일 뿐, 목적이 아니다라는 것입니다.

Adapter 패턴, Strategy 패턴, Factory 패턴… 이런 이름은 사실 중요하지 않습니다. 중요한 건 “새 제조사 추가 시 기존 코드 변경을 최소화한다”는 목적입니다.

실제로 저는 설계 리뷰 때 패턴 이름을 한 번도 언급하지 않았습니다. “이거 Adapter 패턴입니다”가 아니라 “새 제조사 추가할 때 기존 코드 안 건드려도 되게 하려고요”라고 설명했습니다.

그게 팀원들에게 훨씬 와닿았습니다. 패턴 이름을 모르는 주니어 개발자도 바로 이해했고, 나중에 자기도 똑같이 적용할 수 있었거든요.


아키텍처는 미래의 변경에 대비하는 것입니다.

지금 당장은 복잡해 보이지만, 변경이 잦은 부분에 추상화를 넣어두면 나중에 자신에게 감사하게 됩니다.

저도 6개월 뒤에 이 코드를 다시 봤을 때 “그때 내가 잘했네” 싶더군요. 신규 제조사 추가가 정말 쉬웠거든요.

그 “나중”이 언제일지는 모릅니다. 하지만 확실한 건, 확장이 필요한 시스템이라면 언젠가는 온다는 겁니다.


P.S. 만약 제조사가 1-2개로 고정이고 절대 안 늘어날 거라면? 그냥 if문 쓰세요. 정말입니다. 확장성이 필요 없는데 추상화 레이어 만드는 건 진짜 오버엔지니어링입니다. 상황에 맞는 설계가 최고의 설계입니다. 저희 프로젝트는 확장 계획이 명확했기 때문에 이 선택이 맞았던 거고요.





다음 편 예고

“그런데 이 설계도 한계가 있었습니다.”

기기 제어가 성공했을 때 알림을 보내야 한다면? 로그를 남겨야 한다면? 통계를 수집해야 한다면?

지금 구조에서는 SmartHomeService에 계속 로직을 추가해야 합니다. 그러면 다시 코드가 복잡해지기 시작합니다.

다음 편에서는 Event-Driven Architecture를 도입하여 이 문제를 해결한 경험을 공유합니다.

Spring의 @EventListener를 활용하면 핵심 로직과 부가 기능을 완전히 분리할 수 있습니다. 새로운 부가 기능이 추가되어도 기존 코드는 전혀 수정하지 않아도 됩니다.

IoT 기기 제어 → 알림 발송 → 로그 기록 → 통계 수집

이 모든 과정이 느슨하게 결합되면서도 완벽하게 동작하는 방법을 다음 편에서 만나보세요.





참고 :

https://refactoring.guru/design-patterns/adapter
> https://refactoring.guru/design-patterns/strategy
> https://martinfowler.com/articles/injection.html
Head First Design Patterns (에릭 프리먼, 한빛미디어)




읽어주셔서 감사합니다.🖐


Ramsbaby
Written byRamsbaby
이 블로그는 직접 개발/운영하는 블로그이므로 당신을 불쾌하게 만드는 불필요한 광고가 없습니다.

#My Github#소개 페이지#Blog OpenSource Github#Blog OpenSource Demo Site