7.DTO, 제네릭

Alex·2024년 7월 22일
0

Seoul_Nolgoat

목록 보기
7/11

DTO를 사용하다보면

여러가지 고민이 들 때가 있다.

1)재사용

DTO를 여러 곳에서 재사용해도 괜찮을까?

상황에 따라 다를 것이라고 생각한다.
그렇게 하면 안되는 경우가 있고, 그래도 되는 경우가 있다.

우선, 목적에 따라 여러 DTO를 만들어주는 것이 좋은 거 같다.
다만, 이렇게 되면 클래스가 너무 많아지는데 이때는 이너 클래스로 DTO를 관리해도 괜찮다고 한다.

다만 이런 글도 있었다.

스프링 VO 재사용 하면 안되나요??

아마도 여러 목적에서 사용하게 되면
한가지 유스 케이스에서 DTO를 변경하게 될 때 다른 유스케이스까지 모두 영향을 받게 된다는 점이 문제일 수 있을 것 같다.

2)제네릭 사용?

List 내부에 타입이 다 다르지만, 전체 구조는 동일하다면 이걸 각각 DTO로 분리해줘야 하나? 라는 고민이 있었다.

이런 경우 그냥 간단하게 제네릭을 쓰면 된다는 것을 이번에 새롭게 배웠다.

@Getter
@RequiredArgsConstructor
public class SortConditionDto<T> {
    
    private final CoordinateDto startLocation;
    private final String condition;
    private final List<T> firstStores;
    private final List<T> secondStores;
    private final List<T> thirdStores;
}

이렇게 해놓고 저 T에다가 특정 DTO를 갈아 끼워주면 되는 방식이다.
다만, 이게 일반적으로 쓰이는 방식인가? 하는 궁금증이 있었다.

잘못된 방식은 아닌가보다.

DTO 필드값에 null이 들어가도 되나?

DTO를 무리하게 재사용하다보면 생기는 문제다.


public class DTO {

private String a;
private String b;
private String c;
}

1유스 케이스에서는 a와 b만 필요해서 c를 null로 보내고, 2 유스케이스에서는 모두 값을 채워서 보내는 경우를 생각해보자.
우선, null값을 보낸다는 게 불안감을 키운다. null값 때문에 nullpointer예외가 터질 수도 있기 때문에 이 DTO를 쓰는 곳마다 null 체크를 해줘야 한다.

영한샘은 유지보수의 관점에서 이야기를 해주셨다.

제네릭이란?

사실, 그전에는 제네릭을 써본 적이 없었다. 어떤 상황에서 필요한지 감이 잘 안 잡혔던 탓이다.

Java Generic 을 파헤쳐보자 - 개념편

"제네릭"은 "구체적인 타입에 대한 정보를 타입 정의 시점이 아닌 타입의 인스턴스화 시점에 전달함으로써 하나의 타입으로 여러 가지 타입을 표현하는 프로그래밍 기법"을 뜻한다.

제네릭이 없을 때...
"다양한 타입의 객체를 다루는 메서드나 클래스"

public class Tv {
    private String title;
 
    public Tv(String title) {
        this.title = title;
    }
 
    public String getTitle() {
        return title;
    }
}
 
public class Radio {
    private String name;
 
    public Radio(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
}

public class RemoteController {
    private Object connectedDevice;
 
    public RemoteController(Object connectedDevice) {
        this.connectedDevice = connectedDevice;
    }
 
    public Object getConnectedDevice() {
        return connectedDevice;
    }
}

위 공장은

Tv tv1 = new Tv("티비1");
Radio radio1 = new Radio("라디오1");
RemoteController tvRemoteController1 = new RemoteController(tv1);
RemoteController radioRemoteController1 = new RemoteController(radio1);

Object connectedDevice1 = tvRemoteController1.getConnectedDevice();
Tv connectedTv = (Tv)connectedDevice1;
System.out.println(connectedTv.getTitle());
 
Object connectedDevice2 = radioRemoteController1.getConnectedDevice();
Radio connectedRadio = (Radio)connectedDevice2;
System.out.println(connectedRadio.getName());

이를 통해서 다룰 수 있다.

위 방식의 문제는 타입이 나와있지 않아서 코드를 정확히 이해하고, 흐름을 기억하기 어렵다는 것이다.


public class RemoteController {
    private Tv connectedDevice;
 
    public RemoteController(Tv connectedDevice) {
        this.connectedDevice = connectedDevice;
    }
 
    public Tv getConnectedDevice() {
        return connectedDevice;
    }
}
Radio connectedRadio = tvRemoteController1.getConnectedDevice();
System.out.println(connectedRadio.getName());
 
Tv connectedTv = radioRemoteController1.getConnectedDevice();
System.out.println(connectedTv.getTitle());

타입을 잘못 지정하면 컴파일 에러가 뜬다.

Java Generic 을 파헤쳐보자 - 심화편

제네릭이 없을 때와 유사하게 캐스팅 변환이 일어난다.

이렇듯 제네릭과 관련된 소스 코드 상의 정보들은 컴파일러에 의해 제거되는데 이러한 특징을 Type Erasure 라고 한다.

제네릭이 이러한 방식으로 동작하는 가장 큰 이유는 제네릭의 없던 시절에 작성된 코드, 그러니까 JDK5 이전의 코드와의 호환성 이슈 때문이다.

결국 제네릭은 런타임 실행 코드에는 영향을 주지 않으면서 컴파일 타임에 개발자에게 Type Safety 를 포함한 편의 기능을 제공하는 방식으로 동작하는데 이러한 특징 때문에 Java Generic을 syntactic sugar라고 한다.

제네릭의 불공변 때문에 RemoteController 와 RemoteController 는 전혀 상관없는 타입으로 인정된다.

불공변성 때문에 사용하는 입장에서 유연성이 너무 떨어지는 단점도 있다.

이러한 문제점을 해결하기 위해 Java Generic 에서 추가적으로 지원하는 기능이 와일드카드(?) 라는 개념이 있다.

https://dev.gmarket.com/50

나중에 이것도 공부할것!

코드에 구현

근데 이걸 코드에 구현해보니 문제가 생겼다.

public <T> List<CombinationDto> sortByGrade(SortConditionDto<T> sortConditionDto) {

      List<Store> sortedFirstStoresByKakaoGrade = sortByGrade(sortConditionDto.getFirstStores());
      List<Store> sortedSecondStoresByKakaoGrade = sortByGrade(sortConditionDto.getSecondStores());
      List<Store> sortedThirdStoresByKakaoGrade = sortByGrade(sortConditionDto.getThirdStores());

      List<CombinationDto> combinations = generateTopGradeCombinations(
              sortedFirstStoresByKakaoGrade,
              sortedSecondStoresByKakaoGrade,
              sortedThirdStoresByKakaoGrade);

      return Collections.unmodifiableList(sortCombinationsByGrade(combinations, KAKAO)
              .subList(FIRST_RANK, TENTH_RANK));
  }

이렇게 kakaograde와 nolgoatgrade 두개로 나뉘었던 api를 하나로 묶을 수 있게 됐다.

다만, 문제는 제네릭 특성상 코드를 가져올 수가 없다는 것이었다...

private <T> List<Store> sortByGrade(List<T> stores) {

      stores.sort(Comparator.comparingDouble(Store::getKakaoAverageGrade).reversed());

      return stores;
  }

여기서는 컴파일 에러가 뜬다. 왜?
제네릭은 이 단계에서 아직 어떤 타입인지 구체화가 돼있지 않다.
즉, 어떤 타입인지 알수가 없어서 런타임에 제네릭을 특정 타입으로 구체화하기 전까지는
특정 타입의 메서드나 필드를 호출할 수가 없는 것이다.

이 부분을 어떻게 고쳐야할지 고민이 필요하다.

profile
답을 찾기 위해서 노력하는 사람

0개의 댓글