본문 바로가기

Java/Spring

전략 패턴에서 Object vs 제네릭, 왜 나는 제네릭을 선택했을까?

최근 개인 프로젝트(실시간 채팅 시스템)를 진행하면서 전략 패턴을 적용하는 과정에서 재미있는 고민을 하게 되었다.

바로 공통 인터페이스의 파라미터를 Object로 받을지, 아니면 제네릭으로 추상화할지에 대한 문제였다.

 

처음에는 단순한 문제처럼 보였지만, 실제로 구조를 짜면서 생각보다 깊은 고민을 하게 되었고, 결국 제네릭 기반 설계를 선택했다. 이 글에서는 그 과정을 회고해보고자 한다.

문제 배경

내가 만든 프로젝트에는 여러 종류의 채팅방 생성 방식이 필요했다.

  • AuctionChatRoomCreator : Long productId 기반 경매 채팅방 생성
  • ProductChatRoomCreator : ProductChatRequest 기반 일반 상품 채팅방 생성
  • GroupChatRoomCreator : GroupChatDto 기반 그룹 채팅방 생성

즉, 각 전략마다 입력 파라미터 타입이 완전히 달랐다.

이를 전략 패턴으로 묶으려면 당연히 ChatRoomCreator라는 공통 인터페이스가 필요했다. 문제는 파라미터 타입을 어떻게 정의할 것인가였다.

 

첫 번째 선택지: Object

처음 떠올린 건 단순하게 Object로 두는 방식이었다.

public interface ChatRoomCreator {
    ChatRoom createRoom(Object param);
}

 

장점은 분명했다. 어떤 타입이든 받을 수 있으니 유연하고, 런타임 동적 바인딩에도 유리하다.

  • 매번 다운캐스팅을 해야 하고,
  • 잘못된 타입을 넘겨도 컴파일러는 알려주지 못하며,
  • 결국 런타임에서 ClassCastException 같은 오류가 터질 수 있었다.
ChatRoom room = auctionChatRoomCreator.createRoom("잘못된 타입");
// 런타임에서야 오류 발생

즉, 개발자가 실수하기 딱 좋은 구조였다.


두 번째 선택지: 제네릭

그래서 생각한 방법이 제네릭 기반 인터페이스였다.

public interface ChatRoomCreator<T> {
    ChatRoom createRoom(T param);
}

 

구현체마다 필요한 타입을 명시적으로 지정할 수 있다.

@RequiredArgsConstructor
public class AuctionChatRoomCreator implements ChatRoomCreator<Long> {
    public ChatRoom createRoom(Long productId) {
        // ...
    }
}

@RequiredArgsConstructor
public class ProductChatRoomCreator implements ChatRoomCreator<ProductChatRequest> {
    public ChatRoom createRoom(ProductChatRequest request) {
        // ...
    }
}

 

이 방식의 장점은 명확했다.

  • 타입 안정성 보장: 잘못된 타입을 넘기면 컴파일 시점에 바로 오류 발생
  • 가독성 향상: 전략이 어떤 타입을 다루는지 코드만 봐도 알 수 있음
  • 캐스팅 불필요: 내부에서 안전하게 그대로 활용 가능

물론 제네릭은 런타임에 타입 정보가 지워지는(Type Erasure) 한계가 있다.

하지만 이번 케이스에서는 런타임보다 컴파일 시점의 안정성이 훨씬 더 중요했다.


선택의 기준과 이유

최종적으로 나는 제네릭 기반 구조를 선택했다. 이유는 다음과 같다.

  1. 전략별 입력 타입이 명확히 다르다 → Long, DTO 등
  2. 잘못된 타입 입력 시 버그로 직결될 수 있다
  3. 안정성과 유지보수성이 유연성보다 중요하다

전략 패턴에서 제네릭이 항상 정답일까?

꼭 그렇지는 않다.

만약 입력 타입이 동적으로 변하거나, 런타임에서만 알 수 있다면 Object 기반이 더 나을 수도 있다.

혹은 Visitor 패턴이나 타입 변환기를 두는 것도 대안이 될 수 있다.

 

하지만 내 상황처럼 각 전략이 서로 다른 타입을 확실히 요구하고, 안전성이 중요한 경우라면 제네릭이 훨씬 유리하다.

 

마무리

이번 경험을 통해 배운 점은 단순하다.

 

“유연성이 항상 좋은 것은 아니다. 때로는 타입 안정성과 유지보수성을 위해 제네릭이 훨씬 더 적합한 선택이 된다.”

 

전략 패턴을 적용하면서 Object와 제네릭 사이에서 고민하는 분들이 있다면,

내 경험이 작은 참고가 되었으면 한다.

선택 기준 요약

항목 제네릭 (Generics) Object 기반
타입 안정성 ✔ 컴파일 타임 타입 체크 ✖ 런타임 캐스팅 오류 가능성 있음
가독성 ✔ 명확한 타입 지정 ✖ 다운캐스팅 많아 추적 어려움
유지보수 ✔ 타입 안전으로 유지보수 용이 ✖ 실수 발생 가능성 높음
유연성 △ 타입 제한 있으나 안전한 확장 가능 ✔ 모든 타입 수용 가능, 유연함
런타임 처리 △ 타입 소거(Type Erasure)로 제약 있음 ✔ 런타임 처리에 유리
  • ✔ : 장점 / ✖ : 단점 / △ : 중립