제네릭(Generic)으로 해결한 공통 추상 클래스의 반환 타입 문제

밀초·2025년 4월 17일
0

spring

목록 보기
26/34

오늘은 추상 클래스 리턴 타입 구조에 의해 겪었던 문제를 Java 제네릭(Generic)을 통해 해결한 구조 개선 이야기를 공유해보려 합니다.

아래 문제 상황을 예시로 들며 설명하겠습니다.


문제 상황: 공통 Executor 클래스, 반환 타입이 String으로 고정

프로젝트를 진행하던 중 Payment 클래스의 usePay 메서드에서 반환 타입을 String이 아닌 DTO로 변경하려고 했을 때 문제가 발생했습니다.

// Executor 추상 클래스
abstract public class Executor {

    public String usePay(int count) {
        // ...
        return "결과 문자열"; // 항상 String으로 반환
    }
}

Payment는 이 Executor를 상속받고 있었습니다.

// Payment 클래스
public class Payment extends Executor {
    @Override
    protected String usePay(int count) {
        // ...
        return "결과 문자열"; // 항상 String으로 반환
    }
}

이 구조에서는 결과가 문자열이든, DTO든, JSON이든 반드시 String 형태로 가공해서 반환해야 했습니다.

즉, 실제로는 PayResDto 같은 구조화된 데이터가 필요한 상황에서도 무조건 문자열로 변환한 뒤 다시 DTO로 매핑하거나, 별도로 전달해야 했기 때문에 유지보수가 불편하고, 불필요한 변환 작업이 발생했습니다.


저는 결제 처리 결과를 다음처럼 구조화해서 반환하고 싶었습니다

// 반환하려는 DTO
class PayResDto {
    String id;
    String name;
    int count;
    boolean success;
    String message;
}

하지만 기존 구조에서는 결과를 String으로만 반환해야 했기 때문에, 결과 값을 다시 파싱하거나, 별도의 메서드에서 DTO를 새로 생성해야 했습니다.

이로 인해 로직이 중복되고, 유지보수가 점점 어려워지는 문제가 생겼습니다.

물론 Executor 클래스의 반환 타입을 단순히 String에서 PayResDto로 바꾸면 Payment의 요구사항은 해결할 수 있었을지도 모릅니다.
하지만 문제는, Executor를 상속받는 다른 클래스들도 많았기 때문에, 이 변경은 연쇄적으로 여러 클래스의 코드 수정도 함께 요구하였습니다.

또한 추상 클래스는 인터페이스와는 달리, 상속 시 모든 메서드를 직접 구현하거나 오버라이딩해야 하며, 기존 추상 메서드의 시그니처를 바꾸는 것도 어렵습니다.
따라서 Executor 안에 새로운 반환 타입을 갖는 usePay 메서드를 추가하는 것도 현실적으로 불가능했습니다.


해결 방법: Executor 제네릭 적용

그래서 Executor를 제네릭 추상 클래스로 변경했습니다.

abstract public class Executor<T> {
    public T usePay(int count, HashMap prop) {
        // ... 공통 로직 ...
        return usePay(count);
    }

    abstract protected T usePay(int count);
}

그리고 구현체에서 필요한 타입을 명시하여 사용하였습니다.

public class Payment extends Executor<PayResDto> {
    @Override
    protected PayResDto usePay(int count) {
        // ... Payment 관련 로직 ...
        PayResDto result = new PayResDto();
        result.id = "결제 ID";
        result.name = "결제 이름";
        result.count = count;
        result.success = true;
        result.message = "결제 성공";
        return result;
    }
}

이 구성을 통해서 아래의 결과를 얻을 수 있었습니다.

Before (String usePay())After (T usePay())
반환 타입모든 결과를 문자열로 반환원하는 타입(DTO 등)으로 직접 반환
추가 작업파싱/변환 코드 필요반환 즉시 사용 가능

제네릭(Generic)을 적절히 활용하면, 공통 추상 클래스의 유연성과 타입 안정성을 동시에 확보할 수 있습니다. 특히 Executor, Service, ResponseBuilder와 같은 공통 구조를 기반으로 한 클래스 설계에서 매우 효과적이라고 생각합니다.

이번 프로젝트를 통해, 구조 리팩토링이 필요한 순간마다 "이 클래스의 반환 타입은 고정이어도 괜찮은가?" 라는 질문을 던져보는 것이 얼마나 중요한지 체감하게 되었습니다.

저에게도 작지만 확실한 성장의 계기가 되었던 경험이었습니다.

감사합니다.

profile
안녕하세요 :) 성장하는 개발자 밀초입니다 !

0개의 댓글