우아한객체지향에 대해서 by 조영호님

Dev_ch·2023년 7월 7일
1

우아한테크 도전기

목록 보기
13/51
post-thumbnail

우아한테크 채널 - 우아한 객체지향 by 우아한형제들 개발실장 조영호님

우아한테크 채널에 있는 세미나와 영상들은 우아한테크를 도전하고 있는 입장에서나, 자바를 공부하고 백엔드 개발자로 나아가면서 많은 도움이 되기에 한편씩 정독하고 이해하고 회고를 작성해보면서 내가 알고있었던 또는 모르던 지식들을 쌓아가보려한다.

최근, 알고리즘 공부를 집중하다보니 프로젝트나 자바에 대한 객체지향적인 사고가 약간 떨어진 것 같아 다시 복습을 하다가 과연 좋은 설계가 무엇인지 의존성은 어떻게 관리하는 것인지 더욱 배우고 싶어 우아한테크 채널의 객체지향편을 보게되었다.


🤔 의존성이란?

의존성은 B가 변경되면 A도 함께 변경되는 개념인데 물론 의존성이 있다고 무조건 변경되는 것이 아니기 때문에 좋은 설계를 가져가야 한다. 객체지향에서 의존성은 클래스간의 의존성 / 패키지간의 의존성으로 나눌 수 있다.

  1. 클래스간의 의존성
// A -> B의 연관관계
class A {
	private B b;
}
// A -> B의 의존관계
class A {
	public B method(B b) {
		return new B();
	}		
}
// A -> B의 상속관계
class A extends B{
}
// A -> B의 실체화관계
class A implements B{
}
  1. 패키지간의 의존성
    패키지 B에 있는 B클래스가 변경될 때 패키지 A에 있는 A클래스가 영향을 받는 경우

그렇다면 좋은 설계는 무엇인가요?

좋은 설계를 가져가기 위해 여러가지 방법이 있다. 해당 세미나에서 강조했던 것은

1. 양방향 의존성을 피하라

class A {
	private B b;
    
    public void setA(B b) {
    	this.b = b;
        this.b.setA(this);
    }
}

class B {
	private A a;
    
    public void setA(A a) {
    	this.a = a;
    }
}

A클래스와 B클래스가 서로 양방향으로 의존을 하고 있는 경우이다.

2. 다중성이 적은 방향을 선택하기

2-1. One - To - Many
class A {
	private Collection<B> bs;
}

class B {
}
2-2. Many - To - One
class A {
}

class B {
	private A a;
}

JPA와도 관련이 있는 부분으로, 다중성이 적은 방향을 선택하는 것이 좋은 설계의 방법 중 하나라고 한다. 물론, 항상 그렇다는 것은 아니지만 지향하자의 의미를 담고있다. 위의 예시에 적어놓았듯이, oneToMany가 아닌 ManyToOne의 방향이 좋다는 것이다.

3. 의존성이 필요없다면 제거하라

class A {
	private B b;
}

class B {
}

단방향 관계에서, 의존성이 필요없으면 과감하게 제거하는 것이 좋다.

4. 패키지 사이의 의존성 사이클을 제거하라

예를 들어, A클래스와 B클래스간의 관계가 A패키지에서 B패키지 사이의 사이클이 돌아간다면 이를 제거한다. 제거하는 방법으로는 아예 새로운 객체를 생성하는 것도 한가지 방법이다.


📚 주문 플로우를 통한 의존성 이해

사용자가 주문하기 까지의 과정을 통해 객체간의 협력과 의존성에 대해 이해를 하는 파트였으며 가장 핵심적인 부분이다. 주문 플로우는 아래와 같다.

  1. 가게 선택
  2. 메뉴 선택
  3. 장바구니 담기
  4. 주문 완료

해당 파트에서 놀라웠던 부분은, 배달의 민족은 주문하는 메뉴를 장바구니에 담으면 해당 데이터를 핸드폰 로컬에 저장시킨다는 것 이였다. 항상 서버에 모든걸 저장한다는 편협한 사고방식을 완전히 깨주었다. 생각해보면 로컬에 무리가 되지 않는 데이터를 저장 시켜도 얻어갈 수 있는 것이 정말 많다고 생각했다.

여기서 의문이 생긴다. 만약 로컬에 장바구니를 저장한다면 그 저장하는 사이에 가게의 사장님이 메뉴와 옵션을 변경해버린다면 장바구니에 담긴 메뉴와 현재 변경된 메뉴가 일치하지 않는 데이터의 불일치가 발생하게 된다. 그래서 해당 주문 플로우는 주문을 했을때 메뉴와 현재 등록되어있는 메뉴의 데이터가 일치하는지 검증하는 과정이 필요하다.

🪄 주문 플로우 Validation

사용자가 주문을 하게 되면 Validation은 해당 과정을 거치게 된다.

🪄 사용자가 주문을 한다.
1-1. 영업여부확인 -> 가게는 OPEN 상태여야 함.
1-2. 최소주문금액 이상인지 확인 -> 주문한 메뉴의 합이 최소 OO원 이상이여야 함.
1-3. 메뉴 이름과 주문 항목의 이름 비교 -> 데이터가 불일치하는 것을 방지하기 위함
1-4. 메뉴의 옵션 그룹 이름과 주문 옵션 그룹 이름 비교 -> 데이터가 불일치하는 것을 방지하기 위함
1-5. 메뉴 옵션의 이름과 주문 옵션의 이름 비교 -> 데이터가 불일치하는 것을 방지하기 위함
1-6. 메뉴 옵션의 가격과 주문 옵션의 가격 비교 -> 데이터가 불일치하는 것을 방지하기 위함

해당 과정을 거치게 되면 사용자는 성공적으로 주문 완료가 된다.

👩‍💼 클래스 다이어그램 설계

🫂 연관관계?

일단 연관관계를 설정할때 중요한 조건은 해당 객체들 사이에 빈번하게 협력을 하는지를 생각해봐야 한다. 만약 Order와 OrderLineItem 이라는 객체간의 관계가 있을때 두 관계는 잠깐만 생각해봐도 단순히 한번, 두번 협력하는게 아닌 수없이 협력하게 되는데 간단하게 보면 애초에 Order가 생성되면 OrderLineItem도 생성되는 강결합 연관관계이다.

이런 경우 그 관계까 인스턴스 변수로 잡아두는게 좋다고 판단이 되면 연관관계로 만들어줘야 하는데 경로를 여영구적으로 만들어준다고 생각하면 된다. (객체참조)

👥 의존관계는?

의존관계는 협력을 위해 일시적으로 필요한 의존성이라고 생각하면된다. 예를 들자면 파라미터, 리턴타입, 지역변수와 같은 것 이다.

🙋‍♂️ 결론적으로는

결국은 뭔가를 참조한다면 항상 이유가 필수로 있어야 한다. 만약 객체참조를 하는데 이유가 굳이? 라는 생각이 든다면 그것은 의존성이 필요없는 경우이고 제거해도 된다는 것 이다. 연관관계는 탐색의 가능성 이라고 말할 수 있다고 하는데, 되게 좋은 설명이였던 것 같다.

보통 연관관계라 하면 약간 복잡해이고 구체적인 개념이 쉽게 떠오르기 어려운데 탐색의 가능성이라고 하면 이해가 쉽다.

한가지 예시에서, Order와 OrderLineItem이라는 객체가 존재하는 경우 Order가 뭔지 알면 Order를 통해 OrderLineItem을 찾을 수 있다. 이렇듯 두 객체 사이에 협력이 필요하고 두 객체의 관계가 영구적이라면 연관관계를 이용해 탐색의 경로를 구현해 메세지를 받고 메서드를 만든다.

ex) 주문은 항상 주문항목이 있네? -> 주문과 주문항목은 강력한 연관관계를 가지고 있기에 물리적인 통로가 필요 -> 객체참조 사용


💪 설계 개선하기

설계를 개선하기 위해선 현재 의존성(dependency)이 어떻게 되어있는지 알아야한다. 종이에 의존성을 그려보면서 잘못되어있는 것 같은 부분이 있다면 그것은 진짜 잘못된거다 🥲 여기서 중요한 점은 의존성을 항상 직접 종이에 그려보는 것 이다. 직접 그리다 보면 설계를 개선할 수 있는 그림이 보여진다고 하는데, 이러한 습관을 가져야겠다 🧐

⚠️ 현재 문제점

해당 세미나에서 보여주신 예시들은 아래와 같은 문제가 있었다.

  1. 객체 참조로 인한 결합도 상승
  2. 패키지 의존성 사이클 존재
  3. 객체참조로 구현한 연관관계의 문제점 (특히, 컬렉션과 같은 one-to-many)

객체참조로 구현한 연관관계의 문제점들을 많은 고민을 불러오게 한다.

1. 성능 문제 : 어디까지 조회해야 하는가? 에 대한 고민이 생긴다. 메모리 에서는 큰 이슈가 되지 않지만 DB에 직접 매핑이 된다면 Lazy 로딩 이슈와 같다 (흔히, 쿼리가 수도없이 나가는 상황) 이러한 경우에는 어디까지 읽어야 된다는 가이드나 기준이 명확히 존재하지 않기 때문에 개발자는 고민에 빠질 수 밖에 없다.

2. 경계는 어디까지? : 도메인 규칙을 함께 적용할 경계는 어디까지 일지, 트랜잭션의 경계는 어디까지 일지 고민하게 된다. 특히 비즈니스 로직이 추가되면 될수록 하나의 트랜잭션은 점점 길어지는 상황이 발생한다.

3. 트랜잭션의 경합 : Shop, Order, Delivery의 상태에 관한 변경 주기는 전부 다르다. 결국 이는 트랜잭션의 경합으로 인한 성능 저하와 응답성이 떨어지게 된다.

결국 고민해야 할것은 이거다.

🤔 객체참조가 정말로 필요한가?

📝 문제를 개선해보기

🔎 패키지 의존성 사이클을 중간객체로 변환해보자

  • 중간객체를 이용하여 의존성 사이클을 끊는다
  • 추상화라고 하면, abstract 클래스를 말하는 것이 아니라 구체적인 것보다 조금 더 모호한 클래스를 직접 구현하여 추상화 할 수 있다.

🔎 객체참조의 결합도를 낮출려면 객체참조를 사용하지 마세오 (진지함)

그러면 어떻게 탐색하나요..? 👀

객체참조를 한다면 탐색을 할것이고 과정은 보통 이러한 방식을 따를 것 이다. 만약 Order를 알고있다면 -> Order를 통해 원하는 Shop을 찾아갈 수 있다.

이는 역시 두 객체간의 강결합이 되어있기 때문에 가능하지만, Order와 Shop의 관계가 강결합이 필요로 하지 않는 상태 (빈번히 일어나지 않는 탐색) 라면 과연 객체참조가 필요한가? 라는 질문을 던진다. 그리고 이를 해결 하기 위해선

  • 객체 참조를 사용하지 않는다.
  • Repository를 통해 탐색 한다.

이 두가지 방법을 이용해 해결한다. 객체참조를 이용한 탐색을 통해 Shop을 찾아가는 것이 아닌 Id값을 이용해 Shop을 조회하는 것 이다.

🔎 그렇다면 어떤 객체들까지 그룹화하고 어떤 객체들까지는 분리해야 하나요?

과연 어디까지 객체참조로 탐색을 해야할까 라는 의문은 들 수 밖에 없다. 대체 어느 객체까지 탐색할 수 있는 그룹을 만들고 어떤 객체들은 분리해야할까?

세미나에서 제시한 간단한 규칙은 아래와 같다. 그룹화 한다는 것은 결국 탐색의 범위에 포함시킨다는 것 이다.

i. 함께 생성되는 객체들은 그룹화한다. 
ii. 함께 삭제되는 객체들은 그룹화한다.
iii. 도메인 제약사항을 공유하는 객체들을 그룹화한다.
iv. 가능하면 분리해라

객체참조는 연관관계를 설명하기 쉽고 이해에 도움되지만, 결국 실무 레벨에서는 객체참조를 어디까지하고 분리하는 것은 매우 중요하다.

  • 그룹 단위의 영속성 저장소가 변경이 가능해짐
  • 그룹은 트랜잭션 / 조회 / 비즈니스 제약의 단위 -> 하나의 단위로 저장이 가능해짐

🔎 설계를 변경했을 경우 기존 컴파일에러에 대한 대처

위 사항을 적용하면 객체를 직접 참조하던 로직들은 컴파일 에러가 발생할 것 이다 🥲

객체참조를 하던 것들을 Repository로 조회하거나 패키지 사이클을 중간 객체로 변경하게 되면 기존 객체참조 하던 것 들은 컴파일에러가 발생 할 것 이다. 이러한 로직을 어떻게 수정해야할까?

* 해결
컴파일에러가 나는 로직을 새로운 객체를 생성하여 전부 옮긴다.
-> 낮은 응집도의 객체가 생성

❓ 응집도 : 같이 변경되는 코드가 많으면 응집도가 높은 것, 반대로 변경되지 않는데 같이 있으면 응집도가 낮은 것

객체지향은 여러 객체를 오가며 로직을 파악하는데, 새로운 객체를 생성하여 전부 옮겨주면 하나의 객체 안에서 여러가지 로직을 한번에 확인이 가능해진다. 이는 절차지향적인 프로그래밍 방법이기도 하지만, 때로는 절차지향이 객체지향보다 좋을때가 있다. 하지만 역시 이는 단위테스트가 어려워질 수 있다.

이러한 경우들이 있기에 꼭 객체안에 Validate가 있어야할 필요는 없다.

🔎 인터페이스를 이용해서 의존성을 역전시키는 것도 좋다.

객체를 재설계하게 되었을때 중간 객체나, Id 참조를 하게 된 경우 인터페이스를 이용하여 의존성을 역전시켜야 하는 상황이 발생한다.

위와같이 기존 Delivery 패키지에서 Order 서비스의 인터페이스를 이용해 Delivery 패키지 내부에서 서비스를 구현해 의존성을 역전시키는 방법이다.


💻 정리

💡 의존성(dependency)을 따라 시스템을 진화시키자

  • 추상화 : 추상화를 통해 의존성을 역전 시킨다. 추상화가 존재하는 패키지의 위치를 의존성을 고려하여 선택하며 인터페이스와 구현체는 서로 다른 패키지에 존재할 수 있다.
  • 객체 그래프 분리 : 라이프 사이클로 경계를 지정하고, 경계를 벗어난 객체는 참조를 끊고 Id 참조로 대체한다. 객체를 끊어서 컴파일 에러가 나면 해당 로직을 별도의 객체로 분리하여 도메인 이벤트를 발행한다.
  • 패키지 분리 : 새로운 도메인을 찾아내고, 패키지를 분리하여 순환 의존성을 없앤다.

🤔 어렵지만.. 재밌었다

사실 객체지향에 대해 열심히 공부해보고 이해봤지만, 설계에 대한 자신감이 크게 없었는데 해당 강의를 들으면서 도메인을 어떻게 설계하는지, 의존성을 어떻게 관리해야하는지에 대해 고민할 수 있게 되었다.

그리고 그 고민을 어떻게 해결할 수 있을지에 대한 답도 어느정도 얻어갈 수 있었다 생각했다. 모든 서비스들이 그렇듯 결국 설계를 잘해야 더 좋은 기능을 만들고 오류를 줄일 수 있다. 이러한 세미나는 사실 한번이 아닌 계속 들어야 더욱 이해가 잘 되기에 아마 몇번 더 들을 것 같지만, 이렇게 배운 것을 프로젝트를 시작할때 사용해보고, 재설계 해보는 시간을 가져보고 싶다 🧐

아무튼 너무나도 도움되는 강의였고 더더욱 우아한테크 하고싶다 라는 열망이 커졌다.. 더 노력하자 💪

profile
내가 몰입하는 과정을 담은 곳

0개의 댓글