본문 바로가기
IT 공부/객체지향 설계 공부

훌륭한 인터페이스의 특징 (오브젝트 6장)

by exdus3156 2023. 12. 8.

1. 디미터 법칙 (데메테르 법칙)

디미터의 법칙은 연속적인 메소드 호출을 하지 말라는 조언으로 설명된다.

하지만 이 법칙을 교조적으로 수용해 연속적인 메소드 호출을 무조건 금지해서는 안 된다. 디미터 법칙은 캡슐화 원칙과 관련이 있고, 따라서 특별한 경우에는 연속적인 메소드 호출이 더 나은 디자인일 수도 있다.

캡슐화란 인터페이스 뒤로 구현의 상세 사항을 전부 숨기는 것을 말한다. 적절하게 인터페이스가 설계되었다면, 내부 구현 정보를 최대한 숨겨 독립적이고 자율적인 객체를 만들 수 있다.

만약 인터페이스를 적절하게 설계하지 않고 내부의 구현 정보를 드러내면 어떻게 될까? 아래와 같은 코드를 생각해보자.

public class Screening {
    private Movie movie;
    
    public Movie getMovie() {
        return this.movie;
    }
}
public class Movie {
    private List<DiscountCondition> conditions;
    
    public List<DiscountCondition> getDiscountConditions() {
        return this.conditions;
    }
}

상영(Screening) 클래스는 영화(Movie) 클래스를 포함하고 있다. 이제 영화를 예매하는 코드를 생각해보자. 영화를 예매하기 위해서는 할인 요금을 적절하게 계산할 수 있어야 한다.

public class ReservationAgency {

    public Reservation reserve(Screening screening) {
        List<DiscountCondition> conditions = screening.getMovie().getDiscountConditions();
        
        switch (DiscountCondition each : conditions) {
            case DiscountConditionType.SEQUENCE: //...
            case DiscountConditionType.PERIOD: //...
        }
        
        //...
    }
}

위 코드의 문제는 screening.getMovie().getDiscountConditions(); 코드다. ReservationAgency 객체는 예매(reserve) 메소드를 수행하기 위해 상영(Screening) 객체가 품고 있는 영화(Movie) 객체를 끄집어내고, 다시 거기서 할인 조건(DiscountCondition) 객체 리스트를 끄집어내고 있다.

즉, 상영(Screening) 객체 내부가 Movie 객체로 구성되어 있다는 정보, 그리고 Movie 객체 내부가 DiscountCondition 리스트로 구성되어 있다는 정보가 줄줄 새고 있다.

디미터 법칙이 말하는 잘못된 메소드 연속 사용은 바로 위와 같은 것이다. 내부 구현 정보를 연속적으로 끄집어내어 사용하면 끄집어내는 클라이언트 객체는 너무 많은 내부 구현 정보에 의존하게 된다.

그러나 디미터 법칙을 그저 "도트 연속 사용 금지"로 익힌다면 아래와 같은 코드도 나쁘다고 잘못 판단할 수 있다.

const xLabel = xAxisGroup.append("g")
  .attr("class", "x label")
    .append("text")
    .attr("x", GRAPH_WIDTH / 2)
    .attr("y", 50)
    .attr("font-size", 20)
    .attr("text-anchor", "middle");

위 코드는 예전에 내가 D3.js 라이브러리를 익혔을 때 사용했던 코드다. D3.js 라이브러리는 웹으로 데이터 시각화를 제공하는 유명한 라이브러리다. 

변수 이름에서도 알 수 있듯이, 위 코드는 막대 그래프의 X축에 붙는 라벨(x축의 제목)을 구성하는 코드다. 그런데 7~8개나 되는 연속된 도트(.)를 사용했다. 디미터 법칙을 어긴 것일까? 

디미터 법칙은 캡슐화와 관련이 있다. 위 코드는 내부 구현 정보를 드러내지 않는다. 빌더 패턴과 비슷하다. xAxisGroup.append("g") 부분 뒤로 나오는 모든 메소드들은 자기 자신을 반환한다. attr 메소드는 이름 그대로 속성(attribute)을 설정하는 메소드다. 설정한 후에 객체가 자기 자신을 반환한다. 즉, 캡슐화를 어기는 패턴이 아니다.

디미터 법칙이 적용되어야 하는 예시는 내부 구현 데이터를 연속적으로 깊게 끄집어내는 행위다. 객체가 의존해도 되는 객체는 자기 자신이 상태로서 품고 있는 객체, 혹은 인자로 전달된 객체, 그리고 생성해야 하는 객체다. 이런 객체들의 공통점은 인터페이스와 상태에 객체들이 명시적으로 드러나있다는 점이다.

 

2. 의도를 명확하게 드러내는 인터페이스

의도를 명확하게 드러낸다는 것은 객체가 담당하는 책임을 적절한 추상 레벨에서 스스로 무엇을 하는지 설명할 수 있어야 한다는 원칙이다.

극단적인 예시를 들어보면, 어떤 객체 Movie.doSomething(); 이라는 메소드가 있다고 하자. 도대체 이 메소드는 무엇을 하겠다는 것일까? 극단적으로 추상화되어 도대체 Movie에게 무엇을 시키는지 알 수가 없다.

Movie.calculate(); 도 마찬가지다. 도대체 무엇을 계산한다는 말인가? 영화의 요금? 영화의 재생시간? 이 인터페이스만으로는 알 수 없다.

Movie.calculateFee();와 같이 요금을 계산한다고 명확하게 설명하는 인터페이스가 낫다.

 

3. 명령 쿼리 분리 원칙

명령(Command)과 쿼리(Query)란 각각 객체의 상태를 수정하는 부수효과를 가진 오퍼레이션을 명령이라고 하고, 부수효과 없이 값만 반환하는 오퍼레이션을 쿼리라고 부른다.

명령 쿼리 분리 원칙이란, 오퍼레이션은 반드시 명령이나 쿼리 둘 중 하나여야 한다는 원칙이다. 따라서 어떤 오퍼레이션이 명령을 수행하며 객체의 상태를 변경시키면서 동시에 객체의 정보를 반환해서는 안 된다.

이것은 객체지향 관점에서 설명되지만 이런 케이스는 다른 기술에서도 볼 수 있다. 대표적으로 웹 API 호출 시 REST api 를 구성하게 되는데, 이때 GET, POST의 분리가 명령 쿼리 분리 원칙과 닮아 있다.

GET으로 호출하는 API는 웹 콘텐츠(html, css, js)나 데이터(json, ..)를 요청하되 내부 데이터를 변경하지 않아야 한다.

만약 로그인, 비밀번호 변경 등 내부 데이터를 바꿔야 할 수도 있는 API는 POST로 요청한다. POST는 쿼리(query)가 아니라 명령(command)이므로 데이터를 반환하지 않는다. 그래서 POST 요청이 완료되면 웹 서버 내부에서 GET을 자동으로 호출해서 특정 페이지로 이동시킨다. (리다이렉션)

이러한 HTTP GET, POST 메소드 차이를 멱등성(Idempotent)이라고 부르는데, 여러 번 호출해도 같은 결과를 보장할 수 있는지 여부로 갈린다. GET은 쿼리 오퍼레이션이므로 내부 데이터를 변경하지 않고 적절한 계산 결과 데이터를 반환한다. 그러나 POST는 명령 오퍼레이션이므로 내부 데이터를 변경시킨다. 

 

3-1. 분리의 이점

명령과 쿼리를 분리해 설계하지 않으면 쿼리를 믿을 수 없게 된다. 쿼리 오퍼레이션은 여러 번 호출해도 그 의미가 동일해야 한다. 결과도 항상 같아야 한다. 그저 현재 객체의 상태를 기반으로 정보를 묻는 것일 뿐인데, 정보를 물었다는 이유로 내부 정보가 달라진다면 쿼리 오퍼레이션은 버그의 원인이 된다.

상식적으로 생각해도 이상하다. 궁금한 것이 있어 물었다. 그렇게 답을 얻었다. 그런데 똑같은 질문을 다시 했을 때 답이 달라진다면?

정보가 궁금해 물었다면 정보만 알려줘야 한다. 정보를 알려주면서 동시에 답을 바꿔버리면 클라이언트 코드는 그 객체에게 안전하게 쿼리를 날릴 수 없다. 즉, 예측 불가능하다.

객체 내부의 상태에 따라 내부 정보를 변경시키는 명령(command) 메소드를 실행시켜야 한다면 차라리 퍼블릭 인터페이스로 드러내어 클라이언트 객체가 스스로 판단하고 요청할 수 있게 만들어야지, 객체가 쿼리 오퍼레이션 내부에 제멋대로 정보를 변경시켜서는 안 된다.

 

3-2. 인터페이스의 원칙

명령 쿼리 분리 원칙은 구체적으로 인터페이스에서 그 차이가 드러나야 한다고 책에서는 권장하고 있다. 마치 웹 HTTP 통신에서 GET 메소드는 구체적인 웹 콘텐츠(html, css, js, json, ..)을 반환하고, POST에 대해서는 서버가 무언가를 반환하는 것이 아니라 리다이렉션해준다는 것과 거의 비슷하다.

객체 내부의 정보를 변경시키는 명령 오퍼레이션은 반환값이 없어야 한다. 그리고 객체 내부 정보를 변경시키지 않는 쿼리 오퍼레이션은 반환값이 있어야 한다.

이러한 원칙을 일관적으로 적용하면 클라이언트 코드는 실행 시점에 대해 적절하게 통제할 수 있다.