Skip to content
hello tis
Go back

유지보수하기 쉬운 객체지향 코드 만들기

개요

유지보수하기 쉬운 객체지향 코드 만들기 : 책임 주도 설계부터 아키텍처까지 강의를 듣고 정리했다.

유지보수하기 쉬운 객체지향 코드 만들기 강의 화면

1. 책임 주도 설계

책임 주도 설계는 객체 관점에서 런타임의 동작을 중심으로 설계하는 방식이다. 객체 관점은 이해하기 쉬워야 하고, 클래스 관점은 변경하기 쉬워야 한다.

설계는 정답이 있는 것이 아니라, 유용성과 확장성을 고려해 최적의 방법을 찾아가는 과정이다. 실제 설계 과정은 반복적인 재설계 과정을 거친다. 따라서 일단 기능을 구현한 후 설계를 개선하는 것이 중요하다. TDD 방식처럼 작은 관점에서 객체의 동작을 해결하고 고정시킨 후 설계를 개선하는 방법도 효과적이다.

설계시 유의사항은 다음과 같다.

실무와 이론 사이의 균형 잡힌 접근법은 다음과 같다.

2. 무엇을 먼저, 어떻게는 나중에

메시지는 ‘무엇을’ 요청하는 것이고, 메소드는 ‘어떻게’ 수행하는지에 대한 구체적 방법을 나타낸다. 객체는 혼자 일하지 않고 다른 객체와 협력하며 메시지를 주고받는다. 이때 내부 구현 사항인 메소드는 공유하지 않는다.

따라서 행위(책임)를 먼저 결정하고 데이터는 나중에 결정해야 한다. 객체 간 결합도를 낮추기 위해 구체적인 구현보다는 목적만 전달하는 것이 중요하다. 이러한 메시지 기반 설계는 유연성을 높이고 변경에 강한 코드를 만든다.

결국 객체지향 설계에서는 각 객체에게 적절한 책임을 할당하는 것이 핵심이다. 책임은 객체가 수행해야 할 행동으로, 누군가의 요청에 의해 수행된다.

메시지 기반 설계 프로세스는 다음과 같다.

  1. 시스템의 기능을 먼저 파악하고, 그에 필요한 책임과 객체를 결정한다.
  2. 도메인 개념을 활용하여 객체를 찾고, 그 객체에 적합한 책임을 할당한다.
  3. 객체 간 협력 관계를 설계하고, 메시지를 주고받는 방식으로 상호작용 구현한다.
  4. 대체 가능성을 고려하여 유연한 구조를 만든다.

3. 객체 간 협력을 통해 시스템 기능 구현해야 한다.

객체지향 설계는 책임을 기반으로 시스템을 구성하는 방식이다. 시스템의 기능을 먼저 정의하고, 그 기능을 수행할 적합한 객체를 찾아야 한다. 객체는 상태(데이터)와 행위(메소드)를 함께 가져야 한다.

다만 데이터를 가진 객체가 반드시 책임을 가져야 하는 것은 아니다. 책임을 수행하기에 필요한 정보를 가진 객체, 즉 정보 전문가(Information Expert)에게 책임을 할당해야 한다.

내부 구현은 캡슐화하여 외부에 노출되지 않도록 설계하며, 객체들 간의 결합도를 낮추는 것이 중요하다. 이때 데이터 처리와 비즈니스 로직은 구분되어야 한다. 데이터 변경이 있더라도 클라이언트가 영향을 받지 않도록 설계할 필요가 있다.

강연 중 실무와 이론의 차이를 다룬 질의응답도 정리했다.

객체지향 설계 기준에 관한 질문 내용

객체지향 설계 기준에 관한 강연자의 답변

  1. 책임 소재 결정에 대하여

    • 질문: GRASP 원칙을 적용할 때 여러 기준이 동시에 적용 가능하여 의견이 나뉘는 상황에서, 최종 결정을 내리는 기준이나 접근 방식이 궁금합니다.
    • 답변: GRASP 원칙은 객체에게 책임을 할당하는 기준이며, High Cohesion과 Low Coupling처럼 설계를 평가하는 기준도 함께 포함되어 있습니다. 책임을 할당하는 위치가 충돌할 경우에는 너무 많은 시간을 소비하지 말고 다음 방법으로 최대한 빨리 결정하는 것이 좋습니다.
      • 최대한 빠르게 코드를 작성해 본다. 구체적인 코드로 변환하다 보면 어떤 책임이 적합한지 피드백을 받을 수 있다.
      • 단위 테스트를 작성해 본다. 단위 테스트를 작성하기 편한 방식의 책임 할당이 더 좋다.
      • 여러 사람의 의견을 들어본다. 책임을 할당한 이유를 설명했을 때 많은 사람들이 동의하는 책임 할당이 더 이해하기 쉬운 안이다.
  2. 이론과 실제의 괴리에 대하여

    • 질문: GRASP 원칙에 따라 책임을 할당했을 때와 실제 도메인 관점에서 직관적으로 이해되는 구조가 다를 경우, 어떤 방식으로 우선순위를 정하는지 궁금합니다.
    • 답변:
      • 객체지향은 실세계를 그대로 모방하는 방식이 아니라, 오히려 현실과 다른 방식으로 책임을 할당합니다. 예를 들어 현실에서 실제로 주문을 취소하는 행위자는 고객이지만, 코드에서는 Order에 주문을 취소하는 책임을 할당합니다. 현실에서는 배달원이 음식을 배달하지만, 객체지향에서는 음식이 스스로 이동합니다. 객체지향에서 책임 할당의 목적은 변경하기 쉬운 코드를 작성하는 것이기 때문에 현실과는 다른 방식으로 사고할 필요가 있습니다. 실세계에서는 행동할 수 없는 물리적인 사물들이 코드에서는 직접 행동할 수 있어야 하므로, 두 가지는 서로 다른 것이 일반적입니다.
  3. 객체지향 설계의 변화 관리에 대하여

    • 질문: 개발 속도나 데이터 처리 효율성을 위해 설계를 변경하게 되어 원래의 객체지향 원칙이 훼손되는 경우를 경험하신 적이 있는지, 그리고 이에 대한 대처 방법이 궁금합니다.
    • 답변:
      • 빠르게 개발해야 하거나 코드가 빈번하게 변경될 가능성이 적다면 객체지향 원칙을 위반해서 코드를 작성해도 됩니다. 변경이 발생하지 않거나 변경이 어떤 방식으로 될지 알 수 없다면, 도메인 구조에 기반해서 코드를 분해하는 선에서만 마무리하고 완벽하게 객체지향 원칙을 적용하지 않는 것이 경제적입니다. 개발 속도가 중요하고 객체지향 설계 원칙을 적용하는 데 너무 많은 시간이 걸린다면, 일정을 기준으로 원칙을 약화시키는 것도 유용합니다. 데이터 처리 효율성이 중요한 영역은 원칙적으로 객체지향 설계가 적합하지 않을 수 있으며, 이 경우에는 대량의 데이터를 처리하는 로직을 절차적으로 구현하는 것이 더 좋습니다. 이후에 변경이 중요해진다면 언제라도 리팩터링을 할 수 있기 때문에 처음부터 너무 과하게 고민할 필요가 없습니다. 이 과정을 원활하게 하기 위해서는 경험과 학습, 반복적인 연습이 필요하며, 실제 업무 코드를 가지고 리팩터링하면서 기준을 잡는 것을 추천합니다.

4. 설계에 금칠하지 말자.

바뀌지 않는 기능에 확장성 있게 설계하는 것을 속된 말로 ‘금칠한다’고 표현한다. 단편적인 예시로 모든 클래스에 인터페이스를 추가하는 상황이 이에 해당한다.

자주 바뀌는 부분과 안 바뀌는 부분을 분리하고, 그 경계에 추상화를 도입해야 한다. 즉, 변경이 예상되는 부분을 추상화하여 안정적인 구조를 만들어야 한다. 추상화는 자주 바뀌지 않을 것으로 예상되는 부분에 적용돼야 한다.

다만 변경 가능성에 대한 가설이 설계에 영향을 줄 수 있으니 주의해야 한다. 객체지향의 최종 목표는 코드를 빨리 찾고, 사이드 이펙트를 조절하는 것이다.

5. 절차적 설계를 알아야 객체 지향 설계가 보인다.

절차적 설계는 데이터와 프로세스를 분리하는 접근 방식이다. 데이터 구조를 먼저 결정하고 이후에 프로세스를 구현하며, 프로세스가 데이터를 어떻게 사용할지 고려하지 않고 데이터 구조를 먼저 정의한다.

절차적 설계 장단점은 다음과 같다.

절차적 설계 문제점은 다음과 같다.

절차적 설계는 데이터가 자주 변경되지 않는 경우, 임시적으로 빠르게 구현해야 하는 기능, 특정 데이터를 한 곳에서만 사용하는 경우에 적합하다. 대안적 접근으로는 변경의 이유에 따라 코드를 구조화하는 방식이 필요하다. 중요하고 자주 변경되는 비즈니스 로직은 더 나은 설계 방식을 고려해야 하며, 변경 단위로 코드 분리하는 것이 중요하다.

6. 객체 지향 설계의 핵심 원칙

코드의 품질은 응집도(높을수록 좋음), 결합도(낮을수록 좋음), 캡슐화로 평가할 수 있다.

응집도는 클래스 내 요소들이 얼마나 밀접하게 연관되어 있는지를 나타낸다. 높은 응집도는 같이 바뀌는 것들이 한 곳에 모여 있는 것이고, 낮은 응집도는 서로 다른 이유로 바뀌는 요소들이 한 클래스에 있는 것이다. 응집도는 절대적 개념이 아니라 비교를 통해 상대적으로 평가한다.

결합도는 한 모듈이 다른 모듈에 의존하는 정도이며, 캡슐화는 내부 데이터를 감추고 인터페이스를 통해 접근하게 하는 것이다. 변하는 것과 변하지 않는 것을 분리하고, 변하는 부분을 캡슐화하는 것이 중요하다.

코드 품질은 실제로 무엇이 자주 바뀌고 있는지에 따라 판단해야 한다. 요구사항 변경이 발생하는 순간부터 코드 품질이 중요해지며, 처음에는 잘 동작하던 코드도 변경 요구사항이 달라지면 나쁜 코드가 될 수 있다. 시스템에서 빠르게 바뀌는 부분과 천천히 바뀌는 부분을 식별하는 것이 중요하다.

7. 객체 지향으로 리팩토링

큰 메소드를 우선 작은 메소드로 분리하는 것부터 시작한다. 작게 분리된 메소드들을 관련성에 따라 클래스로 묶어 이동하고, 변경되는 부분과 변경되지 않는 부분을 분리한다. 메소드를 분리할 때는 추상화 수준을 일관되게 유지하며, 메소드 이름은 호출하는 쪽의 의도를 반영하도록 명명한다.

대규모 리팩토링보다 작은 단위로 점진적으로 변경하고 빠르게 배포하는 것이 좋다. 단위 테스트가 있으면 리팩토링이 안전하지만, 레거시 코드는 테스트 작성이 어렵다. 테스트 없이 안전하게 리팩토링하기 위해 최대한 IDE의 자동화 도구를 활용하며, 한 번에 큰 변경보다 작은 변경을 여러 번 하는 것이 안전하다.

메소드 크기보다 메소드의 이름과 내용 간의 의미적 차이가 중요하다. 의도를 드러내는 이름(Intention Revealing Name)이 중요하며, 메소드는 ‘what’을 설명하고 ‘how’는 감춰야 한다. 단일 추상화 수준 원칙(SLAP)에 따라 메소드 내 모든 코드가 동일한 추상화 수준에 위치해야 하며, 코드 이해를 돕기 위해 메소드를 작게 분리하는 것이 효과적이다.

절차적 코드를 객체지향적으로 만들기 위한 목표와 방향성이 필요하다. 책임을 어떻게 분배할지 이해해야 리팩토링이 가능하며, 도메인의 안정적인 구조를 기반으로 설계를 진행한다. 변경이 빈번한 영역은 작은 클래스로 분리하는 것이 좋다.

회고

이번 학습을 통해 객체지향 설계의 핵심은 변경 가능성에 있다는 점을 이해했다. 이론과 원칙을 그대로 적용하기보다 빠르게 바뀌는 부분을 먼저 파악하고, 그 경계에 필요한 만큼만 구조를 세우는 것이 중요했다.

객체지향이 현실 세계를 그대로 모방하는 것이 아니라는 점이 새로웠다. 현실에서는 고객이 주문을 취소하지만, 코드에서는 주문 객체가 그 책임을 갖는다는 예시가 이를 잘 보여준다. 결국 목표는 ‘변경하기 쉬운 코드’를 만드는 것이며, 이를 위해 현실과 다르게 사고해야 한다.

실무적인 관점에서도 많은 것을 배웠다. 상황에 따라 객체지향 원칙을 유연하게 적용해도 된다는 점이 현실적인 조언이었고, 완벽한 설계보다 적합한 설계를 선택해 빠르게 구현한 후 리팩토링하는 방식이 효과적이라는 점을 알게 됐다.

코드를 작성할 때 ‘이 부분이 자주 바뀔 것인가’를 먼저 생각하고, 변경이 예상되는 부분에만 추상화를 도입하는 습관을 길러보려고 한다.


Share this post:

Previous Post
온라인 추첨 서비스 사이드 프로젝트 회고
Next Post
켄트 벡의 Tidy First?로 배운 점진적인 코드 정리