[엘레강트 오브젝트 2장] 학교 생활

유소정·2025년 3월 21일
0


엘레강트 오브젝트 책은 우테코 FE & AN 스터디를 진행하며 읽었습니다.

이 글은 2장을 읽으며 자문자답한 글입니다.

객체는 왜 작아야 할까?

'작을 수록 고치기 쉽다' 당연한 말이다. 객체도 작을 때 고치기 쉽다. 작아야 가변 부분이 작아진다. 유지보수하기 좋다.

이 책에서는 상태 개수를 4개로 제한하고 있다.

상태 개수를 어떻게 제한할 수 있을까?
점심 뭐 먹지로 예를 들면, 음식점이 가질 수 있는 상태는 '카테고리, 이름, 거리, 설명, 링크'가 있다. 4개가 넘는다.

상태 개수를 제한해보자.
필수로 받아야 하는 값과 아닌 값을 기준으로 상태를 나눠본다. 그리고 그에 따라 객체를 만든다. 그럼 각 객체의 필드는 4개 이하가 된다.

필수가 아닌 값이 필수로 요구사항이 바뀌면? 당연히 요구사항에 따라서 상태를 해당하는 객체로 이동시켜줘야 한다.

하지만 너무 작으면 오히려 복잡할 수도 있다. 어느 정도 선을 지키는 것도 중요하다.

식별자가 같다고 보장해야 하는 이유는?

현실에서도 "누군가"를 변하지 않는 무언가로 식별한다. 예) 주민등록번호가 같은 사람은, 이름이나 나이가 바뀌어도 "같은 사람"이라고 인식한다. 객체도 마찬가지. 속성이 바뀌어도 식별자가 같다면 같은 실체로 보는 거다. 객체를 새로 바뀌면 참조값이 무조건 바뀌어서 다른 객체 같지만, 객체의 불변 여부는 식별자로 판단한다.

따라서 id가 같으면 새로운 객체가 아니다. "과거의 이 객체"와 "지금 이 객체"가 같음을 알 수 있다. 반대로 식별자가 다르면 완전히 다른 객체니까, 변화가 아니라 새로 생성된 것으로 봐야 한다.

const user1 = { id: 1, name: "Hailey", age: 23 };
const user2 = { id: 1, name: "Hailey", age: 24 }; // 변화
const user3 = { id: 2, name: "Hailey", age: 23 }; // 전혀 다른 객체

이점: 중복 데이터를 방지할 수 있다.

const userList = [
  { id: 1, name: "Hailey" },
  { id: 1, name: "Hailey", age: 23 }, // 이미 있는 사용자인데도 또 들어감
];

가변객체가 등장하는 시점을 최대한 미뤄라

상태가 바뀌는 객체는 복잡하다. 최대한 그 복잡함을 늦게 끌어들여야 한다.
처음엔 불변 객체로 최대한 설계해서 예측 가능한 흐름을 만들고,
정말 필요할 때만 가변 객체를 명확한 책임을 가진 객체로 도입해야 한다.

근데 불변 객체를 만들라면서, 조정자(상태 변경 메서드)는 왜 필요한 걸까?

답변: 불변을 지킬 수 없을 때가 온다. 어쩔 수 없이 가변 객체가 등장해야 하는데, 이때 객체를 막 바뀌게 하지 말고 '조정자'를 이용해서 통제된 방식으로 바꾸라는 뜻이다.

  • 불변 객체 우선: 가능한 한 상태를 바꾸지 않도록 설계해라.
  • 가변 객체는 명확히: 어쩔 수 없이 바뀌어야 한다면, 의미 있는 조정자 메서드로만 바꿔라
  • 가변 객체는 늦게: 불변 객체로 만들다가 정 안되면 가변 객체로 해라.

강한 결합, 느슨한 결합

객체는 혼자 일하지 않고, 다른 객체화 협력해서 동작한다.
이때 다른 객체에 의존하게 되는데, 이런 의존 관계가 얼마나 강하게 연결돼 있는지를 '결합도'라고 한다.

결합도에 따라서 강한 결합, 느슨한 결합이 존재한다.

  • 강한 결합
    • 한 객체가 다른 객체의 내부 구현까지 너무 많이 알고 있을 때
    • 예) 상속
    • 상위 클래스의 변경이 하위 클래스에 큰 영향을 주기 때문에 강한 결합이다.
    • 문제점: A가 망가지면 B도 망가져서 유지보수가 어렵다.
  • 느슨한 결합
    • 서로 역할(인터페이스)만 알고, 구현에는 신경쓰지 않을 때
    • 예시) 의존성 주입
    • 장점: A가 B의 변경에 영향을 덜 받는다. 재사용성과 확장성이 높아진다.

그래서 객체 서로가 잘 협력하면서도 느슨한 결합을 하려면 인터페이스가 필요하다.
인터페이스는 "너 이런 기능만 제공해줘"라고 말하는 계약서 느낌이다.

// 계약서
interface PaymentGateway {
  pay(amount: number): boolean;
}

class KakaoPay implements PaymentGateway {
  pay(amount: number) {
    console.log(`${amount}원 결제`);
    return true;
  }
}

class OrderService {
  // 의존성 주입
  constructor(private payment: PaymentGateway) {}

  checkout(amount: number) {
    return this.payment.pay(amount);
  }
}

→ OrderService는 KakaoPay의 구현 방식에 의존하지 않음, 오직 pay라는 역할(인터페이스)에만 의존함.
→ 그래서 OrderService는 TossPay, NaverPay로 쉽게 바꿔도 코드 안 깨진다.

하지만 그렇다고 너무 느슨한 결합도 오히려 코드를 복잡하게 만들 수 있으니까 주의하자.
조합보다 상속이 필요할 때가 있는 것처럼 때론 강한 결합도 필요하다.

상수를 객체로 묶으면 강한 결합?

<문제>

이런 식으로 상수를 객체로 묶으면 사용은 편하지만 강하게 결합돼버린다.
어디서든 USER_ROLES.ADMIN를 가져다 쓰면, 의존성 분산, 의미 중복.
결국 모든 코드가 저 상수 객체에 직접 의존하게 된다 👉 변경에 약해짐.

export const USER_ROLES = {
  ADMIN: 'admin',
  GUEST: 'guest',
  USER: 'user',
};

<해결책>

"클래스로 만들어라!"
캡슐화와 의존성 주입으로 강결합을 해소할 수 있다.

class Role {
  static readonly ADMIN = new Role('admin');
  static readonly GUEST = new Role('guest');

  private constructor(private readonly value: string) {}

  toString() {
    return this.value;
  }
}
  • 외부에서는 Role.ADMIN.toString()으로 사용
  • 내부 구조가 바뀌어도 사용자는 몰라도 됨 → 캡슐화
  • 테스트할 때도 대체 가능 → 유연성 증가

다시 얘기하는, 메서드 네이밍

  • CQS 원칙과 연결됨: 메서드는 두 가지 중 하나만 해야 한다. (Command-Query Separation)
    • Command (명령) = 조정자: 객체의 상태를 변경한다, 값을 반환하지 않는다
    • Query (조회) = 빌더: 객체의 상태를 변경하지 않는다, 값만 반환한다

소괄호 여부로 의도 전달할 수도 있다.

  • 소괄호가 있다 → Command (조정자)
    • 예) updateEmail("abc@naver.com")
  • 소괄호가 없다 → Query (조회자)
    • 단순 값을 읽는 느낌이라서, 바로 사용할 수 있는 "값처럼 읽힘"
    • 예) user.name, user.totalPrice, user.fullName

테스트 코드는 작동 확인이자 클라이언트를 위한 설명서다

TDD(Test-Driven Development)는 테스트를 먼저 작성하면서 개발을 이끄는 방식이다.
이 개념은 XP(익스트림 프로그래밍)에서 나왔고,
문서와 설계를 미리 거창하게 하지 않고, 변화에 유연하게 대응하기 위한 방식이다.

테스트 코드는 다음과 같은 역할을 한다.

  • 작동 보증, 문서, 리펙터링 지원
profile
기술을 위한 기술이 되지 않도록!

0개의 댓글