끄적이기

[TIL - 7] 객체 지향 설계를 어떻게 해야하나

배씌 2025. 3. 6. 10:58

단순히 코드만 정상적으로 돌아간다고 해서 좋은 코드가 아님을 뼈저리게 깨닫은 날이다. 클래스를 나누고 메서드를 분리한다고만 해서 객체 지향이라고 할 수 없는 것을 점점 깨닫고 있다.

 

현직자 분께도 여쭤보니, 아무리 좋은 책, 좋은 강의를 보더라도 자신이 경험해보는 수 밖에 없다고 하신다. 누가 그랬는데, 개발자는 일할 때 타자를 치는 시간 보다 머리 싸매고 모니터만 뚫어지게 쳐다보는 시간이 더 많다고.. 슬슬 공감이 가기 시작한다.

 

과제나, 기본 단순 구현 문제도 객체 지향적으로 설계를 고민하다보니, 시간이 많이 걸리게 되었다. 이를 생각하면, 알고리즘 문제와는 완전히 딴판이다. 왜 사람들이 알고리즘이나 코테 준비는 생각보다 빡세게 안해도 된다고 하는지 알겠다. 앞으로는 단순한 구현이더라도 최대한 초반 설계에 많이 신경쓰고, 확장 가능성을 고려할 것이다.

 

아래는 내가 생각하지 않고 구현에만 집중했던 코드이다.

public boolean checkCollision(double canvasWidth, double canvasHeight, Paddle paddle) {
    // 좌우 경계 충돌
    if(getMinX() <= 0 || getMaxX() >= canvasWidth) {
        dx = -dx; // x축 속도 반전
    }
    // 상하 경계 충돌
    if(getMinY() <= 0 || getMaxY() >= canvasHeight) {
        dy = -dy; // y축 속도 반전
    }
    if(getMinY() >= canvasHeight) {
        return true;
    }
    
    if(getMaxY() >= paddle.getMinY() && getMaxX() <= paddle.getMaxX() && getMinX() >= paddle.getMinX()) {
        dy = -dy;
    }
    
    return false;
}

 

위 메서드는 아래 기능을 구현하기 위해 처음 작성했던 Ball 클래스의 메서드이다.

Ball 은 바닥에 닿으면 게임이 종료되고, Paddle 에 닿으면 다시 튕겨진다.

 

 

이를 위해, 나는 checkCollision 이라는 충돌 확인용 메서드를 만들고, 내부에 공이 canvas 의 가장자리에 닿거나, 패들이 닿으면 튕겨지도록, 그리고 바닥에 닿을 경우에는 true를 반환하여 게임 종료 여부를 판단하도록 작성하였다.

 

그러나, 이는 좋지 못한 코드이다. 애초에 boolean 타입의 이유가 이상 유무를 판단하는 것인데, false 가 반환되었는데도 다른 동작을 한다? 이는 생각해보면 말이 안되는 코드다. -> "하나의 메서드는 하나의 역할을 한다." 의 원칙에 위배된다.

 

두번째로, Ball 클래스는 Ball의 상태만을 나타나내는 것이지, Paddle 의 위치를 판단해서 스스로 튕긴다? 이것도 말이 조금 이상하다.

 

요즘 보고 있는 '오브젝트' 라는 책에서도 비슷한 말을 보았다.

 

아래의 코드를 예시로,

// 소극장 클래스
public class Theater {
    private TicketSeller ticketSeller;

    public Theater(TicketSeller ticketSeller) {
        this.ticketSeller = ticketSeller;
    }

    public void enter(Audience audience) {
        // 초대장이 있으면 입장권으로 교환
        if(audience.getBag().hasInvitation()) {
            Ticket ticket = ticketSeller.getTicketOffice().getTicket();
            audience.getBag().setTicket(ticket);
        }
        else { // 초대장이 없으면 돈 주고 입장권 구매
            Ticket ticket = ticketSeller.getTicketOffice().getTicket();
            audience.getBag().minusAmount(ticket.getFee());
            ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
            audience.getBag().setTicket(ticket);
        }
    }
}

 

Theater 라는 소극장 클래스이다. 

초대장이 있는 관람객은 티켓으로 바로 교환 후 입장, 초대장이 없는 관람객은 돈 주고 입장권 구매 후 입장

 

이를 구현하기 위해 위처럼 작성하면, 기능에는 문제가 없으나, 명백히 잘못된 코드이다. 조금 더 좋은 설계를 위해서는 우리가 쓰는 말이나 생각 그대로를 코드로 구현해야 한다고 했다.

 

하지만, 위 코드를 보면

 

"소극장이 티켓 판매소에 접근하여 티켓을 가지고, 관람객의 가방에 집어넣는다."

 

상식적으로 생각하면 당연히 말이 안되는 말이다. 어느 극장이 관람객의 가방에 티켓을 마음대로 집어넣는가.

 

그리고 초대장이 없는 경우에는 더 심하다.

 

"소극장이 티켓 판매소에 접근하여 티켓을 가지고, 관람객의 가방에서 현금을 가져온 뒤, 티켓 판매소에 현금을 넣어두고, 관람객의 가방에 티켓을 집어넣는다."

 

이렇게 해석하고 보니, 왜 잘못된 코드인지 한눈에 보였다. 수정하자면 아래처럼 고칠 수 있을 것이다.

public void sellTo(Audience audience) {
    // 초대장이 있으면 입장권으로 교환
    if(audience.getBag().hasInvitation()) {
        Ticket ticket = ticketOffice.getTicket();
        audience.getBag().setTicket(ticket);
    }
    else { // 초대장이 없으면 돈 주고 입장권 구매
        Ticket ticket = ticketOffice.getTicket();
        audience.getBag().minusAmount(ticket.getFee());
        ticketOffice.plusAmount(ticket.getFee());
        audience.getBag().setTicket(ticket);
    }
}

 

Theater 클래스의 enter 메소드에서 관리하는 것 보다는, 티켓 판매원(TicketSeller 클래스)과 관람객(Audience 클래스) 이 각자 상호작용 하는 것이 일반적이다. 위 코드도 더 수정을 고쳐야 하지만, 우선은 처음 코드보다는 납득이 간다.

(if 문의 조건 : 티켓 판매원이 관람객의 가방에 직접 접근하는 것이 아니라, 체크하는 메서드가 필요해보임)

 

조금씩 이해가 간다. 객체의 목적은 실생활에 존재하는 물체나 사물을 코드로 표현하기 위해 사용하는 것이다.

 

어렵다.. 하지만 이를 위해 더 많이 고민해보고 썼다 지웠다를 반복하며 성장해보겠다.