ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] 다형성을 이용하여 null 처리하기
    프로그래밍/JAVA 2022. 8. 30. 00:05

    개요

    자바를 이용하여 개발하면 NPE(NullPointerException)을 방지하기 위해 null 체크를 하여 예외 로직을 작성하는 일이 잦다. 자바 8부터는 Optional API를 통해 좀 더 편리하고 깔끔하게 null 처리를 할 수 있지만, 현재 나의 팀에서는 자바 8을 사용하고 있지 않기 때문에 이를 활용할 수 없었다. 이러한 문제점을 해결하기 위해 다형성을 이용한 null 처리를 적용하였고, 다형성을 이용해서 어떻게 null 처리가 가능한 지 공유하려고 한다.

     

    일반적인 null 처리

    실습의 내용은 고객이 신한/하나/현대/삼성 카드 중 유효한 카드를 체크하여 결제를 진행한다고 가정한다. 

    public interface Card {
    
        void pay();
    }
    
    
    public class ShinhanCard implements Card {
    
        @Override
        public void pay() {
            System.out.println("신한카드로 결제합니다.");
        }
    }
    
    
    public class HanaCard implements Card {
    
        @Override
        public void pay() {
            System.out.println("하나카드로 결제합니다.");
        }
    }
    
    
    public class HyundaiCard implements Card {
    
        @Override
        public void pay() {
            System.out.println("현대카드로 결제합니다.");
        }
    }

    먼저 각 카드의 최상위 인터페이스에 pay 메서드를 정의하고, 각 카드사마다 클래스를 생성하여 pay 메서드를 재정의한다.

     

    public class Client {
    
        private List<Card> cardList;
    
        public Client(List<CardType> cardTypeList) {
            this.cardList = CardChecker.getValidatedCardList(cardTypeList);
        }
    
        public List<Card> getCardList() {
            return cardList;
        }
    }

    고객은 카드 리스트 중 유효한 카드를 cardList에 보관한다.

     

     

    public enum CardType {
    
        SHINHAN, HANA, HYUNDAI, SAMSUNG
    }
    
    
    public abstract class CardChecker {
    
        private CardChecker() {
        }
    
        protected static List<Card> getValidatedCardList(List<CardType> cardTypeList) {
            List<Card> result = new ArrayList<>();
            for (CardType company : cardTypeList) {
                switch (company) {
                    case SHINHAN:
                        result.add(new ShinhanCard());
                        break;
                    case HANA:
                        result.add(new HanaCard());
                        break;
                    case HYUNDAI:
                        result.add(new HyundaiCard());
                        break;
                    default:
                        result.add(null);
                }
            }
    
            return result;
        }
    }

    카드사를 enum 타입으로 정의하고, CardChecker에서 for문을 돌며 List에 담긴 카드가 유효한 카드인지 체크한 후, 이를 다시 List에 담아 리턴한다.

     

    이제 위에서 작성한 코드를 사용해보자. 

    class ClientTest {
    
        @Test
        void 일반_null_처리_테스트() {
            // 고객은 4개의 카드사 중 유효한 카드 리스트를 얻는다.
            List<CardType> cardTypeList = Arrays.asList(CardType.SHINHAN, CardType.HANA, CardType.HYUNDAI, CardType.SAMSUNG);
            Client client = new Client(cardTypeList);
    
            // 고객이 가지고 있는 카드사별 카드로 결제한다.
            List<Card> cardList = client.getCardList();
            for (Card card : cardList) {
                // 클라이언트 코드에서 null 체크가 필요하다.
                if (card == null) {
                    System.out.println("지원되지 않은 카드사의 카드를 가지고 있습니다.");
                } else {
                	// 결제 로직
                    card.pay();
                }
            }
        }
    }

    CardChecker 클래스에서 신한, 하나, 현대 카드는 유효한 카드로 검증하기 때문에 고객은 이에 해당하는 카드 객체를 가지고 있을 것이고, 삼성 카드는 유효하지 않으므로 null 값을 가지고 있을 것이다. 따라서 위의 코드처럼 null 체크를 통해 null일 경우의 분기 로직을 따로 정의해줘야 한다.

     

    현재 예제처럼 단순한 경우에는 null 체크를 하는 것이 나쁜 것은 아니다. 그러나 해당 비즈니스 로직이 다양한 클라이언트 코드에서 호출된다면, 매번 null 체크를 포함하게 될 것이다. 이런 번거롭고 반복되는 작업은 다형성을 이용하면 손쉽게 해결할 수 있다.

     

    다형성을 이용한 null 처리

    public class NullCard implements Card {
    
        @Override
        public void pay() {
            System.out.println("지원되지 않은 카드사의 카드를 가지고 있습니다.");
        }
    }

    Card를 구현하는 NullCard 클래스를 만들고 신한, 하나, 현대가 아닌 다른 카드사의 경우 실행될 비즈니스 로직을 재정의한다.

     

    public abstract class CardChecker {
    
        private CardChecker() {
        }
    
        protected static List<Card> getValidatedCardList(List<CardType> cardTypeList) {
            List<Card> result = new ArrayList<>();
            for (CardType company : cardTypeList) {
                switch (company) {
                    case SHINHAN:
                        result.add(new ShinhanCard());
                        break;
                    case HANA:
                        result.add(new HanaCard());
                        break;
                    case HYUNDAI:
                        result.add(new HyundaiCard());
                        break;
                    default:
                        result.add(new NullCard());
                }
            }
    
            return result;
        }
    }

    CardChecker 클래스에서 이전에 List에 담긴 카드가 유효하지 않을 경우 null을 리턴할 List에 담았다면, 이를 null을 담는 것이 아닌 새롭게 구현한 NullCard 객체를 담는다.

     

    class ClientTest {
    
        @Test
        void null_클래스_처리_테스트() {
            // 고객은 4개의 카드사 중 유효한 카드 리스트를 얻는다.
            List<CardType> companyList = Arrays.asList(CardType.SHINHAN, CardType.HANA, CardType.HYUNDAI, CardType.SAMSUNG);
            Client client = new Client(companyList);
    
            // 고객이 가지고 있는 카드사별 카드로 결제한다.
            List<Card> cardList = client.getCardList();
            for (Card card : cardList) {
                card.pay();
            }
            
            // null 체크가 필요 없어짐
        }
    }

    이런식으로 리팩토링을 진행하면 CardChecker 클래스에서 신한, 하나, 현대 카드는 유효한 카드로 검증하기 때문에 고객은 이에 해당하는 카드 객체를 가지고 있을 것이고, 삼성 카드는 유효하지 않으므로 NullCard 객체를 가지고 있을 것이다. 이는 null 값이 아니므로 null 체크 로직이 필요 없어지지만, 로직 실행 결과는 동일하게 유지된다.

     

    마무리

    간단한 예제를 통해 다형성을 이용하여 null 처리를 어떻게 할 수 있는지를 살펴보았다. null 체크는 나쁜 것이 아니라 꼭 필요한 부분이다. 그러나 null 체크와 연관된 특정 비즈니스 로직이 여러 클라이언트 코드에서 호출되면 유지보수에 어려움을 겪을 수 있다. 따라서 다형성을 이용해 null 처리를 함으로써 변경으로 인한 파급효과를 줄이는 데 큰 도움이 될 수 있다고 생각한다.

     

    댓글

Designed by Tistory.