카카오 사내 '객체지향 설계' 강의 리뷰

Jeongmin Yeo (Ethan)·2022년 3월 26일
9
post-thumbnail

이 글은 '카카오 사내 기술 교육 강의 채널 Rainbow' 에서 본 조영호님의 객체지향 설계 강의를 보고 난 후 리뷰한 글입니다.

객체지향 설계란

객체지향 설계는 우리가 만들어야 하는 시스템을 객체의 집합으로 보겠다는 뜻이다.

객체지향은 굉장히 작은 객체들이 모여서 시스템을 만든다. 그리고 하나의 객체는 무조건 다른 객체와 연결된다.

연결되는 이유는 하나다. 다른 객체에게 메시지를 전송하기 위해서.

객체지향 설계는 객체들이 메시지를 주고 받는 상호작용을 통해서 이뤄진다.

(메시지에 대해서 오해를 하는 경우가 있는데 메시지는 데이터가 아니다. 잘못된 메시지 전달은 해당 클래스의 데이터를 요청하는 경우다.)

객체지향 설계의 플로우는 요구사항을 정확히 이해한 후 기능의 맥락이 정해지면서 "누가 이 부분을 담당할래?" 를 묻는 과정에서 시작한다.

책임이 먼저 정해지면 이 책임을 수행할 객체가 정해진다.

(책임을 정하는 과정에서 답은 없다. 도메인마다 팀마다 다르다. 직관적인 방식으로 하면 된다.)

  • 직관적인 방식은 누가 이 책임을 수행하는데 필요한 정보들을 가지고 있는가? 를 물으면 된다.

해당 책임을 수행하는 과정속에서 다른 객체의 도움이 필요한 경우에 메시지가 필요하게 되고 이를 통해 객체간에 협력이 정해진다.

이렇게 객체간에 메시지를 주고 받는 협력이 정해지면 그 다음에서야 런타임 때 다양한 객체와 협력할 수 있게 클래스를 작성하는 것이다.

객체지향 설계는 절대 클래스를 먼저 설계하지 않는다.

만약 설계의 첫 과정이 ERD (entity relationship diagram) 를 먼저 그리고 그것에 맞는 클래스를 작성하고 있다면 그건 데이터 위주의 설계이지 객체지향 설계가 아니다.

절차적 설계란

다음과 같이 하나의 트랜잭션 내에서 처리하고 있다면 절차적인 설계다.

@Transactioal 
public Reservation reserveScreening(int customerId, int screeningId, int audienceCount) {
	(1) 데이터베이스로부터 Movie, Screening, DiscountCondition 조회
    
	(2) Screening에 적용할 수 있는 DiscountCondition이 존재하는지 판단

	(3) if (DiscountCondition 이 존재하면) { DiscountPolicy를 얻어 할인된 요금 계산
	} else { Movie의 정가를 이용해 요금 계산 }
	
    (4) Reservation 생성 후 데이터베이스 저장
}

절차적 설계는 하나의 스크립트 내에서 비즈니스 로직을 모두 처리한다.

이러한 방식은 객체지향 설계보다 읽기 쉽다.

객체지향 설계는 메시지를 주고 받는 협력을 이해해야 하므로 절차적인 방식보단 이해하기 어렵다.

대신에 한번 이 구조를 이해하면 변경 포인트를 찾기가 쉽다는 장점이 있다.

절차적 설계의 문제점은 낮은 응집도다. 이 코드 내에서 변경될 이유는 너무나 많다.

그리고 변경 포인트를 찾기 위해서는 알고리즘을 모두 이해해야만 찾을 수 있다.

즉 히스토리를 모두 아는 사람만 코드를 수정하기 쉽다는 특징이 있다.

절차적 설계가 나쁜 건 절대 아니다. 각각의 장단점이 있는 것이다.

다만 객체지향의 설계가 강할 때는 다음과 같다.

객체지향 설계를 통해 복잡성을 알고리즘에서 분리하고 객체 간의 관계로 만들 수 있다. 유효성 검사, 계산, 파생 등이 포함된 복잡하고 끊임없이 변하는 비즈니스 규칙을 구현해야 한다면 객체 모델을 사용해 비즈니스 규칙을 처리하는 것이 현명하다. - Martin Fowler

좋은 설계란 뭘까?

좋은 설계의 정의는 다음과 같다.

변경하기 쉽게 코드를 배치하는 것.

프로그램을 작성할 땐 두 가지 요구사항을 늘 생각하자.

1) 기능을 구현하는 코드를 작성하는 것

2) 내일 이 코드를 변경하기 쉽게 하는 것.

좋은 설계를 하기 위해서는 변경하기 쉽게 만들어야 한다. 즉 우리의 코드가 앞으로 어떻게 변경될 수 있는지 알아야 한다.

그걸 모른다면 좋은 설계인지 알 수 없다.

즉 요구사항의 변경에 따라 우리의 코드가 어디가 변경될 지를 예상하고 그 부분을 변경하기 좋게 만드는게 좋은 설계다.

어떻게 변경될 지 안다는 건 그만큼 도메인에 대해서 잘 알고 있다는 뜻이기도 하다.

이 말을 따르면 나쁜 설계에 대해서도 알 수 있다.

변경될 이유가 없는데 변경에 열려있게 코드를 짠 것.

변경에 강한 설계를 하려면 어떻게 해야할까?

Principle 1) 높은 응집도를 유지하자.

높은 응집도는 변경될 이유가 적다. 궁극적으로 변경될 이유가 하나가 되도록 해야한다.

이 말은 SRP (single responsibility principle) 와도 관련이 있다.

낮은 응집도의 문제점은 다양한 일을 하기 때문에 테스트를 작성하기 어렵다.

이게 굉장한 문제를 일으키는데 테스트를 작성하지 않으면 내가 작성한 코드를 믿기 어렵다.

덜 테스트된 코드는 버그를 양산하고 비즈니스에 타격을 줄 수 있다.

이는 개발자에게 코드 작성의 무서움을 느끼게 한다.

Principle 2) 느슨한 결합을 유지하자.

결합도의 정의를 "한 모듈이 다른 모듈에 대해서 얼마나 많이 알고 있는가?" 이렇게 알고 있는 경우가 많다.

이 정의는 결합도를 측정할 수 없다.

관점을 바꿔서 이렇게 정의해보자.

"한 모듈 (클래스) 가 변경되면 다른 모듈이 함께 변경되는가?"

무조건 안바뀌게 작성하라는 뜻은 아니다.

인터페이스의 시그니처가 바뀐다면 그 인터페이스를 사용하고 있는 다른 모듈은 바뀔 수 밖에 없다.

다만 이 경우는 흔하지 않다. 이 정도의 결합이라면 느슨한 결합이라고 볼 수 있다.

결합도를 낮추기 위해서는 "구현""추상화" 를 분리해야한다.

구현은 자주 바뀌는 부분을 나타내는 반면에 추상화는 자주 바뀌지 않는 부분으로 핵심을 나타낸 부분을 말한다.

모듈이 다른 모듈의 세부사항에 대해서 알고 있다면 한 모듈이 변할 때 다른 모듈도 같이 변경해야한다.

즉 변경의 전파가 일어난다.

그러므로 모듈과 모듈이 결합할 땐 추상화 된 인터페이스를 통한 결합만으로 이뤄지도록 하자. 이게 느슨한 결합이다.

인터페이스에 의존한 결합은 세부적인 구현은 인터페이스 안에 숨겨지므로 구현이 바뀐다고 해서 다른 모듈이 변경되지 않는다.

Principle 3) 캡슐화를 하자.

일반적인 캡슐화의 정의는 다음과 같다.

"상태 (데이터) 와 행동 (메소드) 를 하나로 묶고 데이터를 감추고 메소드를 외부에 공개하는 방식이다."

그렇다면 만약에 데이터를 private 으로 선언하고 getter 를 난무하는 코드는 캡슐화가 잘 되어있는 것인가?

그렇지 않다.

캡슐화의 정의를 이렇게 가져가자.

"캡슐화는 변경 가능성이 높은 부분 즉 구현을 내부로 숨기는 추상화의 기법이다."

캡슐화의 한 종류로는 데이터 캡슐화 가 있다.

자주 변하는 데이터를 인터페이스 뒤에 숨기는 기법이다.

예를 들면 돈을 리턴하는 타입을 primitive 타입을 그대로 쓰는 것이 아니라 Money 라는 클래스를 만들어서 숨기는 기법을 말한다.

그리고 또 다른 캡슐화의 기법으로는 타입 캡슐화 가 있다.

타입 캡슐화는 메시지를 처리할 수 있는 객체가 여러개가 있을 때 이러한 객체의 타입을 인터페이스 뒤에 숨겨서 어떠한 객체와도 협력할 수 있도록 만드는 기법이다.

예를 들면 영화 할인 정책을 위해서 절대 금액을 할인해주는 AmountDiscountPolicy 와 비율로 할인해주는 PercentDiscountPolicy 가 있다면 이것들을 모두 DiscountPolicy 라는 인터페이스 뒤에 숨겨서 영화 입장에서는 어떠한 세부적인 할인 정책 객체가 있어도 협력할 수 있다.

이를 DIP (Dependency inversion principle) 라고 부르기도 한다.

Principle 4) 적재적소에 디자인 패턴을 쓰자.

디자인 패턴도 추상화 기법 중 하나다.

그러므로 이를 잘 쓰면 변경에 강한 설계를 만들 수 있다.

다만 디자인 패턴을 사용할 땐 사용해도 되는 경우에만 쓰자.

절대 남용 (abuse) 하지말자.

예로 영화 할인에서 중복 할인 정책이라는 요구사항을 전달받았다고 가정해보자.

이 경우에는 Composite 패턴을 통해서 DiscountPolicy 를 만들면 기존 코드 변경없이 깔끔하게 처리할 수 있다.

객체지향 설계는 처음부터 완벽하게 할 수 없다.

처음 설계부터 완벽하게 객체지향 설계를 하는 것은 너무나 어렵다.

그러므로 너무 압박감을 받지 않아도 된다.

객체지향 설계는 코드를 작성하면서 중복된 코드가 보여서 책임을 나누거나, 도메인을 더 잘 이해하게 되면서 새로운 책임을 만들어주면서 발전시켜 나가는 것이다.

그리고 객체지향 설계를 했더라도 요구사항이 변경되면서 현재 시스템과 맞지 않을 수도 있다.

그러므로 리팩터링을 항상 염두해두자. 리팩터링을 잘하도록 연습하자.

profile
좋은 습관을 가지고 싶은 평범한 개발자입니다.

2개의 댓글

comment-user-thumbnail
2022년 4월 3일

ㄷ ㄷ 잘읽었습니다

1개의 답글