인터페이스의 메서드가 각기 다른 리턴 타입과 파라미터를 필요로 한다면?

kindof

·

2023. 5. 8. 23:19

1. 문제

인터페이스를 정의하여 구체 클래스의 세부 구현을 숨기는 것은 객체 지향에서 중요한 캡슐화의 한 방법입니다.

 

그런데 구체 클래스가 인터페이스를 구현하는 시점에는 인터페이스의 모든 메서드(Private, Default, Static 제외)를 반드시 오버라이딩해야 합니다. 

 

그런데 회사에서 코드를 짜다보니 하다보니 아래와 같은 상황을 마주했습니다.

public interface Reward {
    void giveReward();
}
public class CashReward implements Reward{
}
public class CouponReward implements Reward {
}

 

Reward는 '보상'과 관련된 인터페이스이며 보상을 지급하는 giveReward() 메서드가 있습니다.

세부 구현체에는 CashReward, CouponReward가 존재합니다.

 

그리고 아래와 같은 요구사항이 있다고 해보겠습니다.

  • CashReward 구현체는 전체 보상에 대해 '유저의 나이에 따라' 다른 '금액'을 지급해야 합니다.
  • CouponReward 구현체는 전체 보상에 대해 '유저의 성별에 따라' 다른 '쿠폰'을 지급해야 합니다.
  • 인터페이스에는 메서드 하나만 정의하고 구체 클래스들은 이 메서드만을 오버라이딩해야 합니다.

 

현금과 쿠폰 클래스는 아래와 같습니다.

// 현금(Cash)
public class Cash {
    private final Integer total;

    public Cash(Integer total) {
        this.total = total;
    }

    public Integer getTotal() {
        return this.total;
    }
}

// 쿠폰(Coupon)
public class Coupon {
    private final String name;
    private final Long expiredAt;

    public Coupon(String name, Long expiredAt) {
        this.name = name;
        this.expiredAt = expiredAt;
    }

    public String getName() {
        return this.name;
    }

    public Long getExpiredAt() {
        return this.expiredAt;
    }
}

 

인터페이스에 각각의 메서드를 따로 정의하는 방법은 구체 클래스에서 각 메서드들을 모두 오버라이딩 해야되기 때문에 좋지 않은 방법이며, 인터페이스 자체를 분리하는 것도 의미가 퇴색됩니다.

 

그럼 이 문제를 어떻게 해결할 수 있을까요?

 

2. 해결책

2-1. 제네릭 사용하기

먼저 giveReward()의 리턴 타입이 CashReward는 Cash, CouponReward는 Coupon 타입이라고 해보겠습니다.

 

그러면 아래와 같이 제네릭을 사용해서 코드를 수정할 수 있습니다.

public interface Reward<T> {
    T giveReward();
}


public class CashReward implements Reward<Cash> {
    ...
    
    @Override
    public Cash giveReward() {
        return new Cash(10000);
    }
}

public class CouponReward implements Reward<Coupon> {
    ...
    
    @Override
    public Coupon giveReward() {
        return new Coupon("NICE", 1683552394L);
    }
}

 

그리고 각각의 Reward를 선언할 때는 Reward 인터페이스로 선언하되, 제네릭에 필요한 타입을 넣어서 사용할 수 있습니다.

public class Main {
    public static void main(String[] args) {
        Reward<Coupon> couponReward = new CouponReward(); // 쿠폰 타입의 Reward
        Coupon coupon = couponReward.giveReward();

        Reward<Cash> cashReward = new CashReward(); // 현금 타입의 Reward
        Cash cash = cashReward.giveReward();

        System.out.println("couponName = " + coupon.getName() + ", " + "expiredAt = " + coupon.getExpiredAt());
        System.out.println("cash Total = " + cash.getTotal());
    }
}

정상적으로 동작한다.

 

2-2. 제네릭과 파라미터 상속 구조 사용하기

위의 요구사항에서 CashReward는 '유저의 나이에 따라', CouponReward는 '유저의 성별에 따라' 다른 비즈니스 로직이 필요합니다.

 

설명을 위해 고객 나이와 성별은 각각 Integer, String 파라미터가 필요하다고 가정하겠습니다. 

 

먼저 아래와 같이 Reward 인터페이스 내 giveReward() 메서드의 파라미터를 RewardParams라는 클래스를 상속하는 제네릭 K로 설정합니다.

public interface Reward<T, K extends RewardParams> {
    T giveReward(K rewardParams);
}

 

이제 CashReward, CouponReward의 파라미터는 각각 RewardParams를 상속하는 클래스로 정의할 것입니다.

public abstract class RewardParams {

    // 공통 파라미터 정의
    // ...

    public static class CashRewardParam extends RewardParams {
        private final Integer userAge;

        public CashRewardParam(Integer userAge) {
            this.userAge = userAge;
        }

        public Integer getUserAge() {
            return userAge;
        }
    }

    public static class CouponRewardParam extends RewardParams {
        private final String userGender;

        public CouponRewardParam(String userGender) {
            this.userGender = userGender;
        }

        public String getUserGender() {
            return userGender;
        }
    }
}

위와 같이 정의를 하면 Reward에 공통으로 사용하는 파라미터도 정의할 수 있고, CashRewardParam, CouponRewardParam에 각각 필요한 파라미터들을 정의할 수 있습니다.

 

public class CashReward implements Reward<Cash, RewardParams.CashRewardParam>{

    @Override
    public Cash giveReward(RewardParams.CashRewardParam params) {
        Integer userAge = params.getUserAge();

        if (userAge <= 20) {
            return new Cash(100000);
        }
        return new Cash(2000000);
    }
}

public class CouponReward implements Reward<Coupon, RewardParams.CouponRewardParam> {

    @Override
    public Coupon giveReward(RewardParams.CouponRewardParam param) {
        String userGender = param.getUserGender();

        if (userGender.equals("MALE")) {
            return new Coupon("A COUPON", 1683552394L);
        }
        return new Coupon("B COUPON", 1683552394L);
    }
}

이제 각 구체 클래스에서는 필요한 파라미터 형태를 정의해서 사용할 수 있습니다.

 

지금까지의 코드를 바탕으로 테스트를 해보면 원하는대로 동작하는 것을 알 수 있습니다.

public class Main {
    public static void main(String[] args) {
        Reward<Coupon, RewardParams.CouponRewardParam> couponReward = new CouponReward();
        Coupon coupon = couponReward.giveReward(new RewardParams.CouponRewardParam("MALE"));

        Reward<Cash, RewardParams.CashRewardParam> cashReward = new CashReward();
        Cash cashAdult = cashReward.giveReward(new RewardParams.CashRewardParam(30));
        Cash cashChild = cashReward.giveReward(new RewardParams.CashRewardParam(10));

        System.out.println("couponName = " + coupon.getName() + ", " + "expiredAt = " + coupon.getExpiredAt());
        System.out.println("cashAdult Total = " + cashAdult.getTotal());
        System.out.println("cashChild Total = " + cashChild.getTotal());
    }
}

정상 동작

 

3. 해결책

이번 글에서는 인터페이스의 메서드가 서로 다른 리턴 타입과 파라미터들을 필요로 할 때 어떻게 할 수 있을까에 대한 고민을 정리해봤습니다.

 

예시로 든 상황이 내용을 설명하는 데 100% 적절한 것 같지는 않으나, 제네릭과 상속을 이용하면 인터페이스의 메서드도 유연하게 설계할 수 있다는 점을 설명하고 싶었습니다.

 

감사합니다.