본문 바로가기
스프링/스프링 웹 개발 활용

[Spring] 스프링 컨버전 서비스 - ConversionService, 그리고 인터페이스 분리 원칙(ISP)

by drCode 2023. 12. 28.
728x90
반응형

 

ConversionService  by logicbig

https://www.logicbig.com/tutorials/spring-framework/spring-core/conversion-service.html

 

Spring - Conversion Service

Spring - Conversion Service [Last Updated: Dec 22, 2023]

www.logicbig.com

 

https://drcode-devblog.tistory.com/571

 

[Spring] 스프링 타입 컨버터 - 타입 컨버터

스프링 타입 컨버터 스프링 강의는 스프링부트 2버전이었지만 필자는 더 이상 스프링 공식 사이트에서 2버전에 대한 지원을 종료하여 자바 17버전으로 업그레이드, 스프링부트 3버전을 사용합니

drcode-devblog.tistory.com

위 게시글의 타입 컨버터처럼 타입 컨버터를 하나 하나 직접 찾아서 타입 변환에 사용하는 것은 매우 불편하다.

 

그래서 스프링은 개별 컨버터를 모아두고 그것들을 묶어서 편리하게 사용할 수 있는 기능을 제공하는데,

이것이 바로 컨버전 서비스(ConversionService)이다.

 

다음은 ConversionService 인터페이스 내용이다.

package org.springframework.core.convert;

import org.springframework.lang.Nullable;

public interface ConversionService {
    boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
    boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
    
    <T> T convert(@Nullable Object source, Class<T> targetType);
    Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
}

 

컨버전 서비스 인터페이스는 확인하는 기능과 컨버팅 기능을 제공한다.

 

ConversionServiceTest - 컨버전 서비스 테스트 코드

package hello.typeconverter.converter;

import hello.typeconverter.type.IpPort;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.ObjectAssert;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.support.DefaultConversionService;

import static org.assertj.core.api.Assertions.*;

public class ConversionServiceTest {

    @Test
    void conversionService() {
        // 등록
        DefaultConversionService conversionService = new DefaultConversionService();
        conversionService.addConverter(new StringToIntegerConverter());
        conversionService.addConverter(new IntegerToStringConverter());
        conversionService.addConverter(new StringToIpPortConverter());
        conversionService.addConverter(new IpPortToStringConverter());

        // 사용
        assertThat(conversionService.convert("10", Integer.class)).isEqualTo(10);
        assertThat(conversionService.convert(10, String.class)).isEqualTo("10");

        IpPort ipPort = conversionService.convert("127.0.0.1:8080", IpPort.class);
        assertThat(ipPort).isEqualTo(new IpPort("127.0.0.1", 8080));

        String strIpPort = conversionService.convert(new IpPort("127.0.0.1", 8080), String.class);
        assertThat(strIpPort).isEqualTo("127.0.0.1:8080");

    }
}

 

DefaultConversionServiceConversionService 인터페이스를 구현했는데,

위 테스트 코드처럼 추가로 컨버터를 등록하는 기능도 제공한다.

 

등록과 사용 분리?

컨버터를 등록할 때는 StringToIntegerConverter 같은 타입 컨버터를 명확하게 알아야 한다.

반면에 컨버터를 사용하는 입장에서는 타입 컨버터를 전혀 몰라도 된다.

타입 컨버터들은 모두 컨버전 서비스 내부에 숨어서 제공된다.

따라서 타입 변환을 원하는 사용자는 컨버전 서비스 인터페이스에만 의존하면 된다.

물론 컨버전 서비스를 등록하는 부분과 사용하는 부분을 분리하고 의존관계 주입을 사용해야 한다.

 

컨버전 서비스 사용 방법

Integer value = conversionService.convert("10", Integer.class)

 

인터페이스 분리 원칙 - ISP(Interface Segregation Principle) 

인터페이스 분리 원칙은 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.

 

DefaultConversionService는 다음 두 인터페이스를 구현했다.

 - ConversionService : 컨버터 사용에 초점

 - ConverterRegistry : 컨버터 등록에 초점

 

이렇게 인터페이스를 분리하면,

컨버터를 사용하는 클라이언트와 컨버터를 등록하고 관리하는 클라이언트의 관심사를 명확하게 분리할 수 있다.

특히 컨버터를 사용하는 클라이언트는 ConversionService 만 의존하면 되므로,

컨버터 를 어떻게 등록하고 관리하는지는 전혀 몰라도 된다.

 

결과적으로 컨버터를 사용하는 클라이언트는 꼭 필요한 메서드만 알게된다.

이렇게 인터페이스를 분리하는 것을  ISP 라 한다.

 

ISP란?

https://ko.wikipedia.org/wiki/%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4_%EB%B6%84%EB%A6%AC_%EC%9B%90%EC%B9%99

 

인터페이스 분리 원칙 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 인터페이스 분리 원칙은 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙이다.[1] 인터페이스 분리 원칙은 큰 덩어리의 인터페이스

ko.wikipedia.org

 

스프링은 내부에서  ConversionService  를 사용해서 타입을 변환한다.

예를 들어서 앞서 살펴본 @RequestParam 같은 곳에서 이 기능을 사용해서 타입을 변환한다.

 

 

※ ISP 설명

인터페이스 분리 원칙(Interface Segregation Principle, ISP)은 소프트웨어 디자인 원칙 중 하나로,

클라이언트는 자신이 사용하지 않는 메서드에 의존해서는 안 된다는 원칙이다.

이 원칙을 Java에서 어떻게 구현할 수 있는지에 대한 예제를 알아보자.

// 인터페이스 분리 원칙을 지키지 않은 예제
interface Worker {
    void work();
    void eat();
}

class SuperWorker implements Worker {
    @Override
    public void work() {
        // 일하는 로직
    }

    @Override
    public void eat() {
        // 식사하는 로직
    }
}

// 클라이언트 코드
class Manager {
    private Worker worker;

    public Manager(Worker worker) {
        this.worker = worker;
    }

    public void manage() {
        worker.work();
        worker.eat();
    }
}

 

위의 예제에서 Worker 인터페이스는 work와 eat 두 가지 메서드를 가지고 있다. 

그리고 SuperWorker 클래스는 이 인터페이스를 구현하고 있다.

그러나 Manager 클래스에서는 Worker 인터페이스를 사용하면서 모든 메서드를 호출하고 있다.

 

이 경우, Manager 클래스는 eat 메서드를 사용하지 않지만 인터페이스에 포함되어 있어야 한다.

이는 인터페이스 분리 원칙을 위반한 예제이다.

 

이제 인터페이스를 분리한 예제를 살펴보자.

// 인터페이스 분리 원칙을 지키도록 수정한 예제
interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class SuperWorker implements Workable, Eatable {
    @Override
    public void work() {
        // 일하는 로직
    }

    @Override
    public void eat() {
        // 식사하는 로직
    }
}

// 클라이언트 코드
class Manager {
    private Workable worker;

    public Manager(Workable worker) {
        this.worker = worker;
    }

    public void manage() {
        worker.work();
    }
}

 

이제 Worker 인터페이스를 Workable Eatable 나누어서 각각의 인터페이스를 구현하도록 수정했다.

이렇게 하면 Manager 클래스에서는 필요한 메서드만 사용할 수 있어 인터페이스 분리 원칙을 지킬 수 있다.

728x90
반응형

댓글