[인프런 워밍업 0기 Day5] 한 걸음 더! 객체 지향으로 클린코딩!

[인프런 워밍업 0기 Day5] 한 걸음 더! 객체 지향으로 클린코딩!

!! 해당 글은 독자가 인프런 워밍업 0기를 수강하고 있다는 전제 하에 작성되었습니다 !!

과제 수행에 있어 스프링부트 3.2.2 버전을 사용하고 있다는 점을 미리 알려드립니다!

안녕하세요🙌! 인프런 워밍업 5일차 과제입니다!

이번에는 클린코드의 중요성에 대하여 학습하고 클린코드를 작성하는 방법에 대해 배웠습니다!😎

저는 이번 과제를 그동안 책으로만 공부했던 객체 지향을 적용하여 해결해보고자 했습니다.

이론으로만 공부했기 때문에 많이 서툴 수 있다는 점! 그렇기에, 저의 말이 정답이 아니라는 점을 미리 말씀 드리며!

지금부터 객체 지향을 향한 저의 여정을 소개하겠습니다! 🤸‍♂

💡과제 살펴보기

아래는 과제로 주어진 지저분한 코드입니다! 바라보기만 해도 머리가 어지러운데요.. 😥

public class Main {
    
    public static void main(String[] args) throws Exception {
        System.out.print("숫자를 입력하세요 : ");
        Scanner  scanner = new Scanner(System.in);
        int a = scanner.nextInt();
        
        int r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0, r6 = 0;
        
        for (int i= 0; i < a; i++) {
            double b = Math.random() * 6;
            if (b >= 0 && b < 1) {
                r1++;
            } else if (b >= 1 && b < 2) {
                r2++;
            } else if (b >= 2 && b < 3) {
                r3++;
            } else if (b >= 3 && b < 4) {
                r4++;
            } else if (b >= 4 && b < 5) {
                r5++;
            } else if (b >= 5 && b < 6) {
                r6++;
            } 
        }
        
        System.out.printf("1은 d%번 나왔습니다.\n", r1);
        System.out.printf("2는 d%번 나왔습니다.\n", r2);
        System.out.printf("3은 d%번 나왔습니다.\n", r3);
        System.out.printf("4는 d%번 나왔습니다.\n", r4);
        System.out.printf("5는 d%번 나왔습니다.\n", r5);
        System.out.printf("6은 d%번 나왔습니다.\n", r6);
    }
}

위 코드는 결국 다음과 같은 동작을 수행합니다!

  • 주어지는 숫자를 하나 받는다.

  • 해당 숫자만큼 주사위를 던져, 각 숫자가 몇 번 나왔는지 알려준다.

위 코드처럼 로직을 열거하여 프로그래밍 하는 방식을 절차 지향 프로그래밍이라 말하며, 저는 이 코드를 클린코드로 수정해야 합니다!
위의 코드는 클린 코딩의 중요성을 위한 극단적인 예시일 뿐, 절차 지향이라 지저분한 것이 아닙니다!
지나친 추상화는 오히려 코드의 가독성을 떨어트릴 수 있으니 무조건 '객체 지향이 좋다!'의 글이 아니라는 점 알아주세요!

저는 위의 코드를 깔끔하게 바꾸기 위해서 아래와 같이 객체 지향의 형태로 수정하여 과제를 완수했습니다!

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        Dice luckyDice = UnknownDice.decideFaces(6);
        Note memoPad = new MemoPad();
        
        Dealer dealer = Dealer.playWith(luckyDice, memoPad);

        System.out.print("숫자를 입력하세요 : ");
        dealer.rollDiceMultipleTimes(scanner.nextInt());
        
        dealer.tellTheResult();
    }
}

어떤가요? 내부 로직이 어떻게 되는지는 모르겠지만, 메소드명만을 보고도 원래의 코드와 똑같은 동작을 수행할 것이 기대할 수 있습니다!

그렇다면, 내부는 어떻게 구현됐을까요? 내부 구현을 함께 살펴보기 전에 객체 지향이 무엇인지 짧게 설명 드리고 가겠습니다!

 

💡객체 지향 프로그래밍이란?

객체 지향은 '프로그램이'라는 거대한 로직을 '객체'라는 작은 역할로 나누고, 그 '객체'들끼리 상호 협력하여 데이터를 처리하는 방식을 말합니다!

어젯밤에 저는 오렌지를 하나 먹었는데요! 이 오렌지가 집에 오기 까지의 과정을 '프로그램'이라고 보겠습니다.

그리고, 지금부터 오렌지의 여정을 절차 지향적으로 설명해보겠습니다! 😎

먼저 오렌지 나무를 볕 좋은 곳을 찾아서 심고, 거름도 포대를 찢어서 뿌리 주변에 뿌려주고, 해충도 유기농을 위해 핀셋으로 잡아주고.. 설명이 끝도 없이 길어집니다!

그렇다면 이번엔 오렌지의 여정을 객체 지향적으로 설명해볼까요?

오렌지를 농부가 '재배'하고, 운송 회사가 '운송'하고, 마트에서 '판매'되어 저의 집까지 왔습니다!

어떤가요? 훨씬 설명이 쉽고 이해하기 좋지 않은가요? 오렌지가 어떻게 재배되었는지, 운송되었는지, 판매되었는지 우리는 구체적으로 알 필요가 없습니다! 궁금하지도 않고요!

이렇게 내부 구현을 숨기고, 역할 만을 외부에 공개하여 코드의 가독성을 올리고 협업을 용이하게 하는 것이 객체 지향의 장점입니다!

이는 개체 지향의 단편적인 장점입니다! 더 많은 장점이 있지만, 지금은 클린코딩과 관련하여 추상화와 캡슐화로 인한 장점 만을 이야기하고 넘어가겠습니다! 😭😭

 

💡도메인 선정하기!

  자! 이제 다시 과제로 넘어와 보겠습니다~ 위에서 객체 지향에서 중요한 것은 역할과 협력이라고 했습니다!

우리는 요구 사항을 잘 읽고 작은 역할을 찾고 그 역할을 수행할 주인공(도메인)을 선정해야 합니다. 😎

  • 사용자로부터 숫자를 하나 입력 받는다.

  • 해당 숫자만큼 주사위를 던져, 각 숫자가 몇 번 나왔는지 알려준다. -> 핵심 로직!!

저는 핵심 로직에서 두 가지 역할을 찾았습니다!
하나는 무작위의 수를 생성하는 것, 다른 하나는 생성된 수를 각각 세는 것입니다.

그리고, 발견한 역할을 바탕으로 이를 수행할 두 가지 도메인을 만들었습니다.

무작위 수를 생성하는 🎲주사위(Dice)와 이를 기록해주는 📃노트(Note)입니다!

그렇다면, 이 둘을 재빠르게 설계해 볼까요?

abstract public class Dice {
    private final int faces;

    protected Dice(int faces) {
        this.faces = faces;
    }

    protected int getFaces() {
        return this.faces;
    }

    abstract int throwDice();
}


public interface Note {
    void record(Integer number);
    void printTheResult();
}

다양한 경험을 위해, 저는 주사위는 추상 클래스로 노트는 인터페이스로 만들었습니다!

Dice의 throwDice()는 주사위를 굴리는 행위를 나타내며 무작위 수를 생성합니다!
Note의 record()는 입력되는 수를 기록하고 기록된 수는 printTheResult()를 통해 출력할 예정입니다!

이렇게, 추상 클래스와 인터페이스로 만드는 이유는 해당 동작을 수행할 수 있다면 그게 무엇이든 역할을 대체할 수 있게 하기 위함입니다! 숫자를 기록하고 출력할 수 있다면 메모지던, 스케치북이던, 스마트폰이던 상관이 없습니다!

이제 이 둘을 구현해보겠습니다!

public class UnknownDice extends Dice {

    private UnknownDice(int faces) {
        super(faces);
    }

    public static UnknownDice decideFaces(int faces) {
        return new UnknownDice(faces);
    }

    @Override
    public int throwDice() {
        return (int) (Math.random() * super.getFaces()) + 1;
    }
}


import java.util.HashMap;
import java.util.Map;

public class MemoPad implements Note {

    private final Map<Integer, Integer> page = new HashMap<>();

    @Override
    public void record(Integer number) {
        page.put(number, page.getOrDefault(number, 0) + 1);
    }

    @Override
    public void printTheResult() {
        for(Map.Entry<Integer, Integer> number : page.entrySet()){
            System.out.printf("%d은(는) %d번 나왔습니다.\n", number.getKey(), number.getValue());
        }
    }
}

이렇게, 구현이 끝이 났습니다! 그런데, 이럴 수가! 여전히 문제가 있습니다. 도메인을 구현한 것 만으로는 로직을 수행할 수가 없습니다..! 😥

바로, 주사위와 노트를 어떻게 사용할 것인지 맥락(컨텍스트)이 없기 때문입니다!

 

💡컨텍스트 만들기!

주사위는 무작위 수를 생성하고! 노트는 기록을 합니다! 제가 생성한 도메인은 자신의 역할을 잘 수행합니다!

그러나 노트는 숫자라면 무엇이든 잘 기록할 수 있습니다! 그게 꼭 주사위의 숫자가 아니어도 상관이 없습니다.

그렇기에, 우리는 맥락(컨텍스트)이 필요한 것입니다. 도메인을 연결하여 의미가 있는 역할을 수행하게 하는 것이죠!

그렇게 저는 딜러(Dealer)라는 새로운 객체를 만들었습니다! 요청을 받아 주사위를 굴리는 게 게임 같았거든요..!

public class Dealer {

    private final Dice dice;
    private final Note note;

    private Dealer(Dice dice, Note note) {
        this.dice = dice;
        this.note = note;
    }

    public static Dealer playWith(Dice dice, Note note) {
        return new Dealer(dice, note);
    }

    public void rollDiceMultipleTimes(int numberOfRoll) {
        for (int i=0; i<numberOfRoll; i++) {
            note.record(dice.throwDice());
        }
    }

    public void tellTheResult() {
        note.printTheResult();
    }
}

딜러의 역할은 게임의 진행입니다! 사용자에게 요청 받은 숫자만큼 주사위를 굴려주고! 노트에 결과를 전달하고 기록된 결과를 사용자에게 알려주는 역할을 수행합니다! 😃

주사위노트는 딜러의 게임 진행이라는 맥락(컨텍스트) 아래에서 자신들의 역할을 수행합니다! 어떤가요? 객체들이 서로 협업 하며 역할을 잘 수행하여 로직을 수행하고 있습니다!

또한, 흥미로운 점은 딜러는 주사위와 노트가 자신의 역할만 잘 수행할 수 있다면(인터페이스를 충실히 구현했다면) 얼마든지 다른 주사위나 노트로 바꿀 수 있습니다! 사기를 칠 지도 모르겠군요! 😜

 

💡정리하며...

자, 이제 클린코딩을 수행한 코드를 다시 보겠습니다! 현재의 내부 구현은 모두 알지만, 구현체인 주사위와 노트는 언제든지 바뀔 수 있습니다!

그러나, 우리는 주사위와 노트가 자신의 역할만 잘 수행할 수 있다면, 그것이 바뀌어도 상관이 없다는 사실도 알고 있습니다!

이것이 객체 지향이 주는 다형성이라는 장점입니다! 글이 너무 길어져서 짧게 설명하는 점 죄송합니다...😭

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        Dice luckyDice = UnknownDice.decideFaces(6);
        Note memoPad = new MemoPad();
        
        Dealer dealer = Dealer.playWith(luckyDice, memoPad);

        System.out.print("숫자를 입력하세요 : ");
        dealer.rollDiceMultipleTimes(scanner.nextInt());
        
        dealer.tellTheResult();
    }
}

객체 지향 정말 매력적이지 않은가요? 책을 통해 이론으로만 배운 객체 지향을 직접 설계부터 구현하며 쓴 저의 긴 기록을 지금까지 읽어주셔서 감사드리며, 객체 지향을 모르시던 분들께 조금이라도 도움이 되었으면 합니다!

사실.. SOILD부터 대뜸 외우라고 하면, 어렵습니다 객체 지향..

남은 스터디 기간도 다들 즐거운 코딩하시길 바라겠습니다! 🙇‍♂

댓글을 작성해보세요.