본문 바로가기
Study/Java

우리 당당하게 getter 씁시다!

by dev_kong 2023. 2. 21.
728x90
728x90

학습을 위해 작성된 포스팅 입니다.
잘못된 내용이 있으면, 지적해주세요!

객체지향을 공부하다 보면, 한번쯤은 맞닥뜨리는 문장이 있다.


getter/setter/property를 쓰지 않는다.
객체지향 생활체조 9번에 해당하는 내용이다.

 

이제껏 위의 문장을 성서에 나온 문구처럼 가슴속에 깊이 새기고,
에 대한 의문은 품지 않았다.
(무교임)

 

최근에 반란군크루에 가입하게 되었는데,
반란군답게 "왜 안되는데?" 마인드로 한번 다가가보자.

 

setter에 관한건 이미 너무 멋진 글이 있으니 참고하면 좋을 듯 하다.

getter 왜 안쓰냐?

getter를 지양하자. 가 중론이기 때문에 당당하게 getter씁시다! 를 외치기 위해선
왜 getter를 쓰면 안되는지 를 먼저 샅샅이 파악하고, 그 빈틈을 노려야 한다.
언더독 싸움이 원래 그렇다.

 

캡슐화

캡슐화는 객체지향 프로그래밍의 핵심 원칙으로,
객체의 내부 상태에 객체 외부에서 직접 접근해서는 안 된다고 명시한다.

이를 위해, 접근제어자를 private 으로 두고, getter/setter를 사용하라고 배웠다.

public class Number {  
    private int number;  

    public Number(int number) {  
        this.number = number;  
    }  

    public int getNumber() {  
        return number;  
    }  

    public void setNumber(int number) {  
        this.number = number;  
    }
}

어. 난 분명 이렇게 배웠다.

 

근데 getter/setter를 쓰면 캡슐화가 깨진단다.

캡슐화의 실제정의 자체가

캡슐화한다는 것은 외부에서 객체 내부에 어떤 속성이 있는지 완벽하게 알지 못해야 한다는 것 인데

getter는 특정 필드가 있다는 사실을 외부에 노골적으로 공개한다.

 

즉, getter를 사용함으로 인해 객체의 캡슐화를 일정부분 깨게 된다.

근데 getter 없이 코딩 어케하누..?

 

객체지향의 본질

최근에 객체지향의 사실과 오해라는 멋진 책을 읽고있는데,
협력 이란 키워드가 자주 등장한다.

 

객체와 객체가 서로 요청과 응답을 통해 서로 협력을 하는것이 객체지향의 본질이라는 내용이다.


요청과 응답을 통한 협력이란 말이 듣기엔 멋있어 보이는데,
요청과 응답을 통해 협력하는 코드를 작성해라 라고 하면..?

tell, don't ask

당연하게도 이 멋진 책은 올바른 요청과 응답이 어떤 것인지 알려준다.

 

바로 tell, don't ask. 너무 함축적이라 조금 열받는데,
풀어써보면 정보를 묻지말고, 하고싶은걸 말해라! 정도가 되지 않을까 싶다.

 

코드를 보면 조금 더 이해가 쉽다.

public class Number {  
    private int number;  

    public Number(int number) {  
        this.number = number;  
    }  

    public int getNumber() {  
        return number;  
    }
}

 

public class Main {  
    public static void main(String[] args) {  
        Number number = new Number(10);  

        int numberValue = number.getNumber();  
        int divide = numberValue / 2; // 5  
    }  
}

 

위의 코드는 getter를 이용해서 number의 정보를 물은 뒤 로직을 수행하고 있다.

 

tell, don't ask. 라는 원칙을 적용해서 수정하면 아래와 같이 변경할 수 있다.

public class Number {  
    private int number;  

    public Number(int number) {  
        this.number = number;  
    }

    public int divide(int value) {  
        return number / value;  
    }
}

 

public class Main {  
    public static void main(String[] args) {  
        Number number = new Number(10);  

        int divide = number.divide(2); // 5  
    }  
}

위의 코드와 아래의 코드는 동일한 결과를 나타낸다.

하지만 결과를 만들어 내는 방식은 완전히 다르다.


위의 코드는 '너가 갖고있는 값을 알려줘! 내가 그걸로 필요한 결과를 만들어낼게!' 라면,
아래의 코드은 '결과를 위한 재료는 내가 줄게, 내가 원하는 결과를 만들어줘!' 이다.

 

 

getter 왜 쓰냐?

이제 까지 getter를 안쓰는 이유와, getter를 쓰지 않고 객체에 메세지를 전달하여 객체에게 직접 일을 시키는 방법을 알아봤다.

 

여기까지 봤을 때는 getter를 사용하지 않아야 할 이유가 너무나 충분해서,
"getter 쓰지 맙시다" 의 의견에 설득당해 버릴 것 같다.

 

그럼 이제부터 getter 를 써야 할 이유에 대해 알아보자.

요구사항 추가

요구사항을 하나 추가해보자.

 

number 의 숫자만큼 '-'를 출력하는 요구사항이 추가되었다.

만약, number가 5라면, '-----'가 출력되어야 한다.

 

이제껏, getter를 쓰지 않아야 되는 이유를 알아봤으니,
그에 맞게 Number 인스턴스에 메세지를 전달하여 원하는 결과를 얻어보자.

 

public class Main {  
    public static void main(String[] args) {  
        Number number = new Number(5);  

        String parsedNumber = number.parseToString();  
        OutputView.printParsedNumber(parsedNumber);  
//      "-----" 출력  
    }  
}

 

public class Number {  
    public static final String format = "-";  
    private int number;  

    public Number(int number) {  
        this.number = number;  
    }  

    public String parseToString() {  
        StringBuilder stringBuilder = new StringBuilder();  
        stringBuilder.append(format.repeat(number));  
        return stringBuilder.toString();  
    }
}

 

public class OutputView {  
    public static void printParsedNumber(String parsedNumber) {  
        System.out.println(parsedNumber);  
    }
}

위와 같이 코드를 작성했을 때,
Number 인스턴스에 출력 형식에 맞게 변환해! 라는 메세지를 보내서,
원하는 값을 받아온 뒤, OutputView를 통해 출력한다.

 

getter 없이 요구사항을 충족하여 구현하였다.

 

요구사항 변경

getter 없이 구현을 잘마쳐서 기분이 좋은데,
뜬금없이 요구사항이 변경되었다.

출력할 때 "-" 말고 "=" 으로 변경해주세요

그닥 어려운 내용이 아니라 Number 클래스 파일을 열어서 요구사항을 다음과 같이 수정하였다.

public class Number {  
    public static final String format = "=";  // 여기가 변경됨
    private int number;  

    public Number(int number) {  
        this.number = number;  
    }  

    public String parseToString() {  
        StringBuilder stringBuilder = new StringBuilder();  
        stringBuilder.append(format.repeat(number));  
        return stringBuilder.toString();  
    }
}

기분 좋게 commit 을 하려는데,
마음 한 구석 어딘가에서 찜찜함을 느낀다.

 

'출력에 대한 요구사항이 변경되었는데, 왜 나는 domain 을 수정하고 있는거지..?'

 

비록, Number 클래스 내부에서, OutputView의 코드가 존재하지는 않지만,
대상을 의식하고 도와주려 하는 것 만으로도 의존한다고 볼 수 있다.


객체지향에서 의존성은 유지보수 시점에 같이 변경되냐, 아니냐를 말한다.

의존성을 없애기 위해, 코드를 변경하면 다음과 같다.

public class Main {  
    public static void main(String[] args) {  
        Number number = new Number(5);  

        int value = number.getNumber();  
        OutputView.printParsedNumber(value);  
//      "-----" 출력  
    }  
}

 

public class Number {  
    private int number;  

    public Number(int number) {  
        this.number = number;  
    }  

    public int getNumber() {  
        return number;  
    }
}

 

public class OutputView {  

    public static final String format = "=";  

    public static void printParsedNumber(int number) {  
        System.out.println(parseToString(number));  
    } 

    public static String parseToString(int number) {  
        return format.repeat(number);  
    }
}

위와 같이 작성하면,
이전과 같이 출력에 관한 요구사항이 변경되어도 도메인로직을 건들 필요 없이 View에 관한 코드만 수정하면 된다.
이를 통해, domain 이 view 에 의존적이던 것을 해결하였다.

getter를 통해 캡슐화가 깨졌다?

위에서 얘기했듯, getter를 사용함으로 인해 객체의 캡슐화가 일정부분 깨진다고 하였다.

허나, 위와같은 요구사항이 주어졌을 때,
getter로 인해 캡슐화가 깨어진 것이 맞는지 생각해볼 필요가 있다.

 

캡슐화의 의미는 객체 내부 속성의 은닉에 있다.
하지만 주어진 요구사항에서 이미 내부의 속성을 밖으로 드러내는 것을 요구하고 있다.

number 의 숫자만큼 '-'를 출력하라.

이와 같이, 객체의 값을 외부로 표현(Presentation) 해 주어야 하는 경우에는
getter를 사용하는 것을 피할 수 없다.

 

이미 요구사항이 정보를 은닉하지 말것을 요구하고 있기에,
getter의 사용으로 캡슐화가 깨어졌다라고 말하기는 힘들다.

 

 

그럼 getter 막써도 되는거?!

당연히 아니다;;

예를 들어, 한가지의 요구사항이 더 추가되었다고 해보자.

number의 값이 5이상 이면 "5이상입니다!"를, 5미만이면 "5미만입니다!" 출력해주세요.

 

이미 만들어둔 getter 가 있으니, 이를 이용해 신나게 코딩을 해보면

public class Main {  
    public static void main(String[] args) {  
        Number number = new Number(5);  

        int value = number.getNumber();  

        OutputView.printOverFive(value >= 5);  
//      "5 이상입니다!"  
        OutputView.printParsedNumber(value);  
//      "-----" 출력  
    }  
}

// #OutputView
public static void printOverFive(boolean isOverFive) {  
    if (isOverFive) {  
        System.out.println("5 이상입니다!");  
    }  
    if(!isOverFive) {  
        System.out.println("5 미만입니다!");  
    }
}

이런 코드가 생겨나는데..


표현을 목적으로 getter를 통해 객체내부의 값을 노출하는 경우에는
노출된 값을 이용한 비즈니스 로직이 도메인 외부에 생성되지 않도록 유의해야 한다.

 

이런 경우에는 굳이 객체 내부의 값을 노출 시키지 않고도 로직을 수행할 수 있다.

public class Main {  
    public static void main(String[] args) {  
        Number number = new Number(5);  

        int value = number.getNumber();  

        OutputView.printOverFive(number.isOverFive());  
//      "5 이상입니다!"  
        OutputView.printParsedNumber(value);  
//      "-----" 출력  
    }  
}
public class Number {  
    private int number;  

    public Number(int number) {  
        this.number = number;  
    }  
    public int getNumber() {  
        return number;  
    }  
    public boolean isOverFive() {  
        return number >= 5;  
    }}

누군가는 "기왕 만들어 둔 getter 그냥 쓰면 안되나?" 라고 생각할 수 있다.

 

getter는 만능이 아니다.
하지만 무작정 사용하지 않는다해서 능사는 아니다.

분명하게 getter 사용해야 되는 곳이 있고,
분명하게 사용해야 되는 곳이있다.

 

이를 잘 구별하고, 판단한다면 당당하게 getter를 쓸 수 있다.

728x90
728x90

댓글