[Java/Design pattern] 우아한 객체지향

rul9office·2022년 1월 15일
0

우아한 테크 세미나 - 우아한 객체지향을 듣고 정리한 내용이다.

우아한 객체지향

의존성을 이용해 설계 진화시키기

  • 설계의 핵심은 의존성
    • 설계란? 코드를 어떻게 배치할 것인지에 대한 의사결정
    • 어디에 어떤 코드를 넣어야할까? 핵심은 변경에 초점을 맞추는 것. 변경의 핵심은 의존성을 어떻게 관리하는지

의존성 (Dependency)

A - - - - - - - - - → B

B가 변경될 때 A도 함께 변경
B클래스 내부가 변경되더라도 A 클래스가 변경되지 않을 수도 있다.
Dependency란 변경에 의해 영향을 받을 수 있는 가능성

Dependency의 구분

  • 클래스 의존성
  • 패키지 의존성

클래스 의존성의 종류

연관관계 (Association)

  • A 클래스에서 B 로 갈 수 있는 경로를 가지고 있다.
  • 객체 참조가 있다.
  • A에서 B로 영구적으로 갈 수 있는 경로가 있다.
class A {
	private B b;
}

의존관계

  • 파라미터에 그 타입이 나오거나 리턴타입에 그 타입이 나오거나 메소드 안에서 그 타입의 인스턴스를 생성하는 경우
  • 협력을 하는 그 시점에 일시적으로 관계를 맺은 후 헤어지는 관계
class A {
	public B method(B b) {
		return new B();
	}
}

상속관계

  • B라는 클래스의 구현을 A가 상속받는 것
  • B가 바뀔 때 A도 같이 변경
class A extends B {
}

실체화 관계

  • 인터페이스를 implements
  • 상속관계는 구현이 바뀌면 영향을 받고 실체화 관계는 인터페이스의 operation (연산), signature가 바뀌었을 때만 영향을 받음
class A implements B {
}

패키지 의존성

패키지에 포함된 클래스 사이의 의존성

클래스에서 import에 다른 패키지가 나오면 그 패키지끼리 dependency가 있다고 보면 됨

좋은 의존성을 관리하는 규칙

양방향 의존성을 피하라

  • B가 바뀔 때 A도 바뀌고, A가 바뀔 때 B 도 바뀌게 된다는 건 하나의 클래스로 볼 수 있는 걸 억지로 찢어놓은 것
  • A와 B의 상태를 동기화 시켜줘야하는 문제점이 발생할 수 있다. (성능 이슈, 싱크를 맞추는 과정에서 버그 발생)
  • 가급적 양방향 연관관계를 피하고 단방향 연관관계로 바꾸어야 함
/**
 * 양방향
 */
class A {
	private B b;

  // B클래스의 setter 메소드를 콜하게 됨. A-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;
	}
}
/**
 * 단방향
 */
class A {
	private B b;

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

class B {
}

다중성이 적은 방향을 선택하라

  • A에서 B의 Collection을 인스턴스 변수로 잡거나 Collection에 대해 Dependency를 가지게 하는 것보단 반대 방향의 Dependency를 가지도록 하는 것이 좋다.
  • B가 A의 단방향 참조를 가지는 것이 좋음
/**
 * One-To-Many
 */
class A {
	private Collection<B> b;
}

class B {}
/**
 * Many-To-One
 */
class A {}

class B {
	private A a;
}

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

정말 불필요하다면 제거하는 것이 좋다.

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

패키지 사이의 양방향 의존은 반드시 피해야 한다.

설계의 원칙

무조건 변경을 중점으로 하여 코드를 배치하는 것이 원칙이다.

관계의 방향
= 협력의 방향
= 의존성의 방향

관계의 종류 결정 순서

연관관계

  • 협력을 위해 필요한 영구적인 탐색 구조
  • 탐색 가능성
    • 어떤 객체가 있는데 그 객체를 알면 내가 원하는 다른 객체를 찾아갈 수 있다.
  • 두 객체 사이에 협력이 필요하고 두 객체의 관계가 영구적이라면 연관관계를 이용해 탐색 경로 구현
@Entity
@Table(name = "ORDERS")
public class Order {
	@ManyToOne
	@JoinColumn(name="SHOP_ID")
	private Shop shop;

}

@Entity
@Table(name = "SHOPS")
public class Shop {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name="SHOP_ID")
	private Long id;
}
  • Order에서 Shop으로 탐색가능
  • 객체 참조를 통한 탐색 (강한 결합도)
@Entity
@Table(name = "ORDERS")
public class Order {

	@Column(name="SHOP_ID")
	private Long shopId;
}

Shop shop = shopRepository.findById(order.getShopId());

@Entity
@Table(name = "SHOPS")
public class Shop {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name="SHOP_ID")
	private Long id;
}
  • Order에서 Shop으로 탐색 가능
  • Repository를 통한 탐색 (약한 결합도)

의존관계

  • 협력을 위해 일시적으로 필요한 의존성 (파라미터, 리턴타입, 지역변수)

코드로 구현할 때

  • 메소드를 만들고 메시지를 결정하는 것이 아닌 메시지를 만들고 메시지를 바탕으로 메소드를 만들어야 함
  • 설계를 개선할 때는 코드 작성 후 의존성 관점에서 설계 검토
  • 양방향 연관관계가 있다면 의심해봐야 한다.
    • 중간 객체를 이용하여 의존성 사이클을 끊어야한다.
    • 의존역전원칙(DIP)을 이용하여 구체적인 것에 의존하지 않고 추상적인 것에 의존하도록 함

연관관계 다시 보기

연관관계의 코드 구현 중 하나인 객체참조에는 문제점이 있다.

  • 두 객체 사이의 결합도 높음
  • 성능 문제 - 어디까지 조회할 것인가? (Lazy Loading 이슈)
  • 수정할 때 도메인 규칙을 함께 적용해야 하는 객체의 범위가 모호하고 트랜잭션의 경계도 모호해짐 (DB, DataMapping에서 이슈 발생)
  • 이렇게 되면 트랜잭션 경합이 일어나서 성능이 떨어지는 이슈 발생

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

객체 참조의 문제점

모든 것이 다 연결되어있는게 문제..
어떤 객체라도 다 접근 가능하고, 어떤 객체라도 함께 수정 가능하기 때문이다.
객체 참조는 결합도가 가장 높은 의존성, 필요한 경우 객체 참조를 모두 끊어야함

어떤 객체들을 묶고 어떤 객체들을 분리할 것인가?

간단한 규칙

  • 함께 생성되고 함께 삭제되는 객체들을 함께 묶어라
  • 도메인 제약사항을 공유하는 객체들을 함께 묶어라
  • 가능하면 분리하라

이를 결정하는 것이 도메인 룰 (도메인 관점에서 어떤 데이터를 같이 처리해야할지 결정 필요)
경계 안의 객체는 같이 묶어주는 편이 좋다.
경계 밖의 객체는 ID를 이용해 접근할 수 있도록 한다. (Repository를 통한 탐색)

왜 책이나 강의에서는 객체 참조로 구현되어 있는지?

객체 참조로 설명하는 편이 객체간의 메시지를 통한 협력을 표현하기 좋기 때문인데, 실무에서는 성능 이슈 등 현실적인 제약으로 분리를 하는 것이 더 좋다.

객체를 직접 참조하는 로직을 다른 객체로 옮겼을 때의 장점

  • 여러 객체를 오가면서 로직을 파악하지 않아도 된다.
  • 낮은 응집도의 객체를 높은 응집도의 객체로 변경할 수 있다.

때로는 절차지향이 객체 지향보다 좋을 때가 있다..

  • 비즈니스 플로우를 한 눈에 볼 수 있는 장점이 있다.
  • validation 로직을 그 객체가 가지고 있어야 할 필요는 없다. 결합도는 높이지만 응집도를 낮추는 코딩이 될 수 있다.
  • 객체 지향이 모두 정답이 아니다. 이에 대한 trade-off를 해야한다.

도메인 로직의 순차적 실행

해결 방법

  • 절차지향 로직 사용
    • 비즈니스 플로우가 한 눈에 보인다.
    • 객체 간의 결합도는 낮추고, 로직의 결합도를 높이고 싶다.
  • 도메인 이벤트 퍼블리싱

패키지 의존성 사이클을 제거하는 방법

  • 중간 객체 만들기 (새로운 객체로 변환)
  • 의존성을 인터페이스나 추상클래스를 통해 역전
  • 새로운 패키지 추가

위의 세 가지 중 어떤 것을 고를지는 판단에 따라 다르다. (trade-off 필요)

의존성과 시스템 분리

도메인 이벤트를 사용하여 도메인 단위를 분리하게 된다면 의존성 사이클이 제거됨
도메인 단위로 분리가 된다면 도메인 이벤트를 통해 협력을 하게 되고 시스템을 분리하기가 쉬워진다.
시스템을 쪼갤 때 의존성을 확인한 다음에 의존성 사이클을 분리하는 기법을 통해 분리하고 쪼개야 한다.


느낀 점

디자인 패턴 스터디를 하면서 연관관계 의존성에 대한 내용을 이해하기 어려워 검색하던 중 우아한 테크 세미나를 듣게 되었다.
책을 보거나 강의를 듣다보면 자주 '이렇게 구현했을 때 오히려 더 복잡하지 않나..?', '실무에서 진짜 이렇게 쓰는건가' 라고 의문이 들었던 적이 많았는데, 실무에서 사용하는 예제를 가지고 (비록 엄청나게 단순화했지만) 객체 지향이 모두 정답이 아니다. 실무에서는 trade-off가 필요하다. 라는 내용을 들으니 뭔가 정리되는 느낌을 받았다. 객체 지향의 사실과 오해 라는 책도 꼭 읽어봐야겠다.

참고

profile
Brings a positive attitude, loves challenges, and enjoys sharing knowledge with others.

0개의 댓글