객체지향방식을 사용한다면 반드시 알아야할 원칙 : 객체 지향 생활 체조 원칙

조시현·2023년 8월 4일
0

클린코드

목록 보기
4/4
post-thumbnail

개요

최근 클린코드에 관심이 늘면서 클린코드와 관련된 공부를 진행하던 도중 객체지향방식에서 클린 코드를 적용하기에 가장 유명하면서 가장 효울적인 방식이 객체 지향 생활 체조 원칙이라고 생각하였다.

그러하여 객체 지향 생활 체조 원칙에 대해서 클린코드와 객체지향에 익숙하지 않은 사람도 쉽게 볼 수 있도록 블로그를 작성해보고 싶어 해당 블로그를 작성합니다

그럼 객체 지향 생활 체조 원칙에 대해서 알아보겠습니다.

장점

  • 코드의 일관성 유지
    코드베이스 전체에서 일관된 패턴과 스타일을 유지하면, 개발팀 내에서 코드의 이해와 협업이 용이해집니다.
  • 버그 감소
    작은 메서드, 클래스, 모듈로 나누어 작업하면, 각 구성 요소가 독립적이고 명확한 역할을 가지게 되어, 버그가 발생할 확률이 줄어듭니다.
  • 테스트 용이성
    객체 지향 원칙을 따르면, 각 클래스와 메서드가 독립적으로 테스트 가능하여 단위 테스트 작성이 수월해지고, 테스트 커버리지를 높일 수 있습니다.
  • 변경 용이성
    객체 지향 설계 원칙을 따르면, 특정 기능의 변경이 다른 부분에 미치는 영향을 최소화할 수 있어, 유지보수가 쉬워집니다.
  • 재사용성 향상
    잘 설계된 객체 지향 코드는 재사용 가능성이 높아, 코드의 중복을 줄이고, 생산성을 높일 수 있습니다.

9가지 방법

1. 한 메서드에 오직 한 단계의 들여쓰기(iddent)만 한다.

  • 메서드는 하나의 역할만 담당해야한다.
  • 코드라인 수가 줄어드는 것은 아니지만 가독성이 향상된다(Extract Method패턴)
public void setting(){
	private String name;
    
    if (name != null) {
    	if (name == "123"){ // 이 방식 X 
        	throw new RuntimeException();
        }
    }
    
    if (name != null){ // 이 방식 O
    	nameIs123(name);
    }
}
	

2. else 예약어를 쓰지 않는다.

  • 조기 반환을 사용하자
  • if-else에 해당하는 모든 로직을 수정하는 것보다, 조건을 추가하는 것이 리팩토링시 더 쉽다.(Null 객체 패턴, 상태 패턴, 전략 패턴)

3. 모든 원시값과 문자열을 포장한다.

  • 원시값 : string, number, bigint, boolean, undefined, symbol, 그리고 null
  • ex) int값을 LottoNumber 값 객체로 포장.
  • DDD. 원시값 변수에 동작(유효성 검사)이 있다면, 클래스로 포장하여 의도를 나타낼 수 있다.
  • 안티패턴인 Primitive Obsession을 피할 수 있다.
  • 타입안정성과 검증 로직의 캡슐화 및 관심사 분리가 가능하다.
    • 필요한 시점에 값 객체로 포장한다.
    • getter는 값 전체를 반환한다. 값 객체 내부 값을 직접 반환하지 않는다.
    • 값 객체 간의 비교는 원시 값이 아닌 메시지를 이용한다.
  • boolean은 가장 원시적인 값으로 일반적인 경우에서는 굳이 Wrapping 하지 않아도 된다고 한다.
  • 도메인을 담는 객체를 만들고, 스스로를 검증하는 자율적인 객체를 만들고 보는 것에 익숙해져야 한다.

이러한 리팩토링은 상위 클래스에 비대한 요구사항과 책임이 담기는 것을 막게 하고, 추가적인 요구사항에 대응하기가 편리해진다.

4. 한 줄에 점을 하나만 찍는다.

  • 메서드 체이닝을 제거하여 가독성 있는 코드를 만들 수 있다.
  • 그러나 Fluent Interfaces 와 Method Chaining Pattern 구현에는 적용되지 않는다.
  • 점은 멤버 변수에 접근하기 위한 점을 의미한다. 아래의 예시를 살펴보자.
if(user.getMoney().getValue() > 100_000L) {
    throw new IllegalArgumentException("소지금은 100_000원을 초과할 수 없습니다.");
}

해당 코드에서는 User, Money 두 가지 의존성을 가지게 된다.

디미터 법칙에서는 객체 그래프를 따라 멀리 떨어진 객체에게 메시지를 보내는 설계를 피해라고 한다. 이런 설계는 거의 모든 객체간 결합도가 생기게 되고 캡슐화가 깨지게 된다.

if(user.hasMoney(100_000L)) {
    throw new IllegalArgumentException("소지금은 100_000원을 초과할 수 없습니다.");
}

5. 줄여쓰지 않는다(축약금지)

  • 같은 이름이 반복되어서? -? 메서드가 여러번 재사용될 수 있따. 중복 코드 의심
  • 이름이 길어서? -> 너무 많은 일을 하고 있을 수 있다.
  • 적당한 이름을 찾기힘든 경우, 문제가 없나 확인해보자.

6. 모든 엔티티를 작게 유지한다.

  • 클래스는 50줄을, 패키지는 파일을 10개를 넘기지 말아야한다.(기준은 바뀔 수 있다)
  • 중요한 점은, 긴 파일은 읽고, 이해하고, 재사용하기 매우 힘들다는 것이다.

7. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.

  • 인스턴스 변수: 클래스(예: 멤버 변수)에 정의된 변수
  • 가장 지키기 힘든 원칙이지만, 더 높은 응짚성을 갖고 더 나은 캡슐화가 가능하다.
  • 여기서 "두 개"는, 클래스를 더 많이 분리하도록 강요하기 위한 임의의 숫자이다.
  • 이렇게 분리한 클래스들을 사용하는 클래스의 경우 Facade / Mediator / Aggregator 등을 활용할 수 있다.

8. 일급 컬렉션을 쓴다.

  • 일급 컬렉션: 컬렉션 외에 다른 멤버 변수를 포함하지 않은 클래스.
  • 중요한 데이터 Set을 하나의 클래스에서 관리가능 + 캡슐화

아래 일급 컬렉션에 대한 정리를 해놓은 링크가 있으니 관심 있으면 꼭 보기를 추천한다!
일급 컬렉션 정리 링크

9. 게터/세터/프로퍼티를 쓰지 않는다

  • (Tell, don't ask) 원칙에 따르면 묻지 말고 객체에게 행위를 시켜라고 한다.
  • 객체의 상태에 따라 결정이 된다면, 그것은 객체 바깥이 아닌, 객체 내부에서 해야한다. (개방 폐쇠의 원칙)
  • 다시 말해, 객체의 상태를 결정하지 않는다면, getter는 외부에서 사용될 수 있다. setter은 안된다. (객체 내부에서 private를 이용한 setter는 가능)
    간단한 예시 코드
public Order {
    private ShippingInfo shippingInfo;

    public void changeshippingInfo(ShippingInfo newShippingInfo) {
        verifyNotYetShipped();
        setShippingInfo(newShippingInfo);
    }

    private void setShippingInfo(ShippingInfo newShippingInfo) {
        this.shippingInfo = newShippingInfo;
    }
}

왜 setter를 쓰지 말아야하는가??

도메인 모델의 엔티티나 벨류에 공개 set 메서드만 넣지 않아도 일관성이 깨질 가능성이 줄어든다.
공개 set 메서드를 사용하지 않으면 의미가 드러나는 메서드를 사용해서 구현할 가능성이 높아진다.
예를 들어 set 형식의 이름을 갖는 공개 메서드를 사용하지 않으면
자연스럽게 cancel이나 changePassword처럼 의미가 잘 드러나는 이름을 사용하는 빈도가 높아진다.

  • 도메인 로직의 분산 (도메인 로직이 한곳에 응집되지 않으므로 유지보수가 불편하다)
  • 잘못 정의한 메시지 (메서드들을 실행했을때, 문제가 생길 수도 있음에도 실행권한을 주어서 문제 발생)

왜 getter를 쓰지 말아야하는가??

  • 객체의 내부 구조를 외부에서 직접 조작하게 되면 캡슐화,모듈화가 깨지면서 코드의 안정성이 무너진다.
  • 필드의 접근자를 private로 하고 getter 형태의 메서드를 사용하더라도, 필드를 public으로 공개하는 것과 다를바 없는 구조라면, 똑같은 문제가 일어난다.
  • getter()형태의 메서드가 문제인게 아니라, 객체의 구성요소를 외부로 배내서 외부에서 조작하게 만드는 설계구조가 문제를 일으키는 것이다.

그럼 어떻게 해야하는가?

1. 다른 형태의 자료구조를 통해서 변환하여서 반환해보자.

Q. 나는 그냥 단순 조회가 목적인데 그럼 어떻게 해야하는가??
A. 다른 형태의 자료구조를 통해서 변환하여서 반환해보자.

ex) list형태의 자료구조가 있다.
반드시 list형태로 반환해줘야하는가??
-> 아니다.

list에 포함된 메서드들이 너무 투머치해서 문제를 일으킬 수 있다.
그럼 조회라는 목적에 알맞는 자료구조는 뭐가 있을까? ->
hasNext(),next() 딱 2개 뿐인 iterator를 통해서 반환해보자.

2.불변 컬렉션

이런 상황에 알맞은 자료구조가 자바에 있으니~

결국 객체의 내부가 조작되지 않도록 만드는 것이 중요한데,
이와 관련해서 자바에서는 불변 컬렉션을 제공한다!!

ImmutableCollections에서 제공하는
ImmutableList
ImmutableSet
ImmutableMap의 내부 구조를 보면 엉청난 특징이 숨겨져 있는데,

static abstract class AbstractImmutableList<E> extends AbstractImmutableCollection<E>
            implements List<E>, RandomAccess {

        // all mutating methods throw UnsupportedOperationException
        @Override public void    add(int index, E element) { throw uoe(); }
        @Override public boolean addAll(int index, Collection<? extends E> c) { throw uoe(); }
        @Override public E       remove(int index) { throw uoe(); }
        @Override public void    replaceAll(UnaryOperator<E> operator) { throw uoe(); }
        @Override public E       set(int index, E element) { throw uoe(); }
        @Override public void    sort(Comparator<? super E> c) { throw uoe(); 
        }
...

자료구조의 내부를 수정하는 어떤 메서드를 호출하면 수정작업을 하지 않고 무조건 예외가 발생하게 구현이 되어있다!!

Collections.unmodifiableList(리스트) 를 통해서 불변 자료 구조를 반환 할 수 있다는 것이다.

이렇게 자료구조를 복사해 왔다고 해도, 자료구조 내부의 값들이 원본의 메모리 주소를 향하고 있으면 복사한 값에 변경이 일어났을때 원본도 함께 변경된다는 것이다.

3.복사체(깊은 복사)

그러기 위해서는 원본과의 연결 끊어야한다.
원본인 값인 객체 하나하나 까지 새로운 객체로 만들어 준 뒤, 리스트에 담는다면
원본과는 상관없는 새로운 조회용 값을 새로 받을 수 있을 것이다.

 public List<Name> getStudentNames() {
       return studentNames.stream()
               .map(name -> new Name(name.getName()))
               .collect(Collectors.toUnmodifiableList());
   }

이렇게 제공 할 수가 있단 거다!
new Name()으로 새로 만든 객체이기 때문에 원본과 다른 메모리 값을 가지게 된다.

new생성자를 통해서 만드는 것이 귀찮다면,
복사하려는 객체가 Cloneable 인터페이스를 구현하게 만들어,
객체.clone() 메서드를 호출해 간단하게 깊은 복사를 하는 방법도 있습니다.

  • Clonable 인터페이스란?

추가

  • 객체의 원자성을 지키기 위해서 final 키워드를 자주 사용해라.
  • 캡슐화
    객체의 세부적인 구현 사항이 변하는 것을 사용자가 전혀 몰라도 되게 도와준다.
    (내부 구현을 완벽히 숨겨, 실제 구현한 코드와 외부의 사용자가 사용하는 코드를 깔끔하게 분리한다)
  • 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각하게 고민해야한다.

한 줄 정 리

객체한테 일을 시킨다는 마인드로 코드를 작성해야한다!

지금까지 정리한 객체지향생활체조 원칙을 코드에 적용한다면 기존 코드에 비해 생산성과 유지보수성, 보안성등을 모두 향상 시킬 수 있을 것이다.

전부를 한 번에 적용해 보는 것은 어려울 수도 있으니 한두가지씩 나눠서 적용해보는 것을 추천해봅니다!


참조 링크

참조1
참조2
참조3
참조4
감사합니다~

profile
소프트웨어 관련 고민을 좋아하고 상황에 맞는 답을 함께 찾아가는 과정을 좋아합니다. 😀

1개의 댓글

comment-user-thumbnail
2023년 8월 4일

정보에 감사드립니다.

답글 달기