우테코

[Lv1] 레벨1 학습 내용 정리

mint* 2025. 4. 14. 00:00
728x90

 

객체지향

객체의 책임

객체란?

    • 상태행동을 함께 지닌 실체
      • 스스로 자기 자신을 책임지는 자율적인 객체
    • 상태 : 데이터(가변 필드, 불변 필드)
      • 과거에 얽매이지 않고, 현재를 기반으로 객체의 행동 방식을 이해할 수 있다.
상태를 가지지 않은 객체는 객체지향적인가? 
답변 ) 캡슐화, 추상화를 통해 다형성은 가능하지만 객체지향의 장점을 제대로 활용하지 못한 것이다.
객체지향은 상태와 행동을 함께 캡슐화하여 변화에 유연하게 대응할 수 있어야 하기 때문이다.
    • 행동 : 메서드
      • 객체가 취하는 행동은 상태를 변화시킨다.
    • 협력 : 객체는 시스템의 행위를 구현하기 위해 다른 객체와 협력한다.
      • 각 객체는 협력 내에서 정해진 역할(책임의 집합)을 수행한다.
      • 객체는 다른 객체와 협력하기 위해 메세지를 전송하고, 메세지를 수신한 객체는 메세지를 처리하는데 적합한 메서드를 자율적으로 선택한다.
협력하지 않은 객체는 객체지향적이지 않은가? yes

 

객체지향이란?

    • 시스템을 상호작용하는 자율적인 객체들의 공동체로 바라보고, 객체를 이용해 시스템을 분할하는 방법
상태를 캡슐화한 자율적인 객체가 서로 협력하며 시스템을 이룬다.

 

객체지향의 목적

소프트웨어를 좀 더 이해하기 쉽고, 유지보수하기 쉽고 확장하기 쉽게 만들기 위해 사용한다.

  • 유지보수 용이성 : 코드의 변경이 필요한 경우, 영향을 최소화하여 수정할 수 있다. (높은 응집도, 낮은 결합도)
    • 높은 응집도 : 하나의 모듈(클래스, 메서드)가 하나의 명확한 역할을 수행하는 정도
    • 낮은 결합도 : 하나의 모듈이 다른 모듈과 얼마나 의존적인지(변화시 영향을 미치는지) 정도
  • 현실 세계 모델링
    • 객체라는 개념을 사용하여 현실 세계의 개념을 그대로 코드로 표현할 수 있다.
  • 확장성
    • 기존 시스템을 크게 변경하지 않고 새로운 기능을 쉽게 추가할 수 있다. (다형성)

 

도메인의 책임

  • 도메인의 책임은 여러 객체에게 적절하게 분배된다.

 

도메인이란?

  • 해결하고자 하는 문제의 영역
은행이라면 계좌 관리, 이체 처리 등등

 

역할과 책임

  • 역할 : 관련성 높은 책임의 집합
    • 협력하는 과정 속에서 특정한 역할을 수여받는다. 특정한 역할은 특정한 책임들을 암시한다.
    • 역할은 책임의 집합이다.
  • 책임 : 객체가 수행해야하는 행동, 객체가 알고 있는 정보(상태)

ex) 계좌 엔티티라면 잔액 정보 조회 같은 것

 

책임 분리 기준 -> 객체지향적으로 잘 설계하는 방법

데이터(상태)보다 협력과 행동에 초점을 맞춰 설계하기

  • 상태에 맞춰 설계한다면?
    • 상태에 초점을 맞출 경우, 협력을 고려하지 않고 설계하여 명확환 책임 없이 상태에 관련된 메서드가 혼재하여 좋지 않은 설계가 나온다.
    • 상태에 필요한 행동을 결정하면, 객체 내부 상태가 public api에 노출된다. (객체의 내부 구현이 객체의 퍼블릭 인터페이스에 노출되도록 만든다.)
  • 협력에 초점을 맞춰 애플리케이션을 설계한다.
    • 객체가 협력에서 어떠한 역할을 수행하는지에 따라 책임이 결정된다.
      • 즉, 행동에 초점을 맞춘다.
    • 어떤 협력에 참여하는지가 객체에 필요한 행동을 결정하고, 필요한 행동이 객체의 상태를 결정한다.
    • 객체의 행동을 기준으로 정의하고(즉 각 메서드가 명확해지고), 인터페이스와 구현이 분리되어 유연하다.

개별적인 객체의 행동이나 상태가 아니라, 객체들 간의 협력에 집중하라.
어떤 데이터가 필요하고, 어떤 클래스로 구현해야할지 먼저 생각하지 마라.

  • 우선 요청과 응답의 흐름을 결정하라. -> 객체의 책임이 된다.
  • 책임 주도 설계 : 협력에 필요한 책임들을 식별하고 적합한 객체에게 책임을 할당하는 방식으로 설계하기
  • 예시
    • 어떤 객체가 어떤 책임을 갖고, 다른 객체와 어떻게 메시지를 주고받을 것인가 를 먼저 결정하기
    • 데이터 구조를 먼저 결정하지 않고, 객체들이 수행해야 할 역할과 상호작용에 집중하기

 

단일 책임 원칙 (SRP)

  • 하나의 객체는 하나의 책임을 갖도록 설계

 

응집도

  • 관련된 책임을 함께 묶는다.

 

정보를 가진 객체가 그 정보를 가진 기능도 수행하게 한다.

 

검증 책임

  • 도메인 내부 검증
    • 도메인의 비즈니스 로직
  • 도메인 외부 검증
    • 여러 도메인에 공통적으로 적용되는 검증 규칙
  • 도메인 로직은 도메인 안에서 검증하고, 객체들간 공통적인 검증은 도메인 밖에서 수행한다.
    • 객체의 자율성 : 자신의 상태를 관리하고 검증하는 것은 객체의 책임이다.
    • 비즈니스 로직을 도메인 외부에 두면, 여러 곳에서 같은 도메인 검증 로직을 중복 구현할 수 있다.

 

Enum의 책임

  • Enum은 상수의 집합이다.
  • Enum에 비즈니스 로직을 두어도 괜찮을까?
    • enum은 상태(필드값)와 함께 객체의 비즈니스 로직을 담을 수 있는 효율적인 타입이다.
    • 연관된 상수와 그 상수에 적용되는 로직을 한 곳에 모을 수 있다.

 

생성 책임

  • 객체가 생성 책임이 복잡하다면 따로 팩토리(또는 생성기)로 분리해도 될까?
    • 분리해도 좋다. 객체는 객체를 생성하기까지의 복잡성을 신경 쓰지 않고, 객체의 행위만 집중할 수 있다.

 

초기화 책임

    • 초기화하는 객체를 따로 두는 것이 좋을까?
      • 좋다. 초기화하는 로직이 복잡하거나 재사용될 경우, 별도의 초기화 객체를 두는 것이 좋다.
      • 객체의 행위만(비즈니스 로직)에만 집중할 수 있다.
객체와 관련된 모든 생성, 초기화 책임을 한 곳에 두지 않아도 된다!
  •  

원시값 포장

  • 원시값을 굳이 포장해야할까?
    • 당장 검증이나 비즈니스 로직 등이 필요하지 않은 원시값이라면 포장하지 않는 것이 좋다.
    • 모든 원시값에 대해 포장한다면, 불필요한 클래스가 많아져 오히려 관리에 어려움을 겪게 된다.
    • 요구사항이 생기면 그 때 클래스를 분리한다.

 

Controller의 책임

    • MVC 패턴
      • Model : 데이터와 관련된 부분
      • View : 사용자에게 보여지는 부분
      • Controller : ModelView를 이어주는 부분
장점 : 모델과 뷰를 분리하여 서로의 의존성을 낮추고, 독립적인 개발이 가능하다.
    •  
    • 콘솔 애플리케이션에서 MVC 패턴
      • 웹 애플리케이션과 다르게, Controller가 직접 View와 상호작용하면서 도메인 로직을 수행한다.
      • 일반적인 Controller에 비해 많은 책임을 가지므로, 적합하지 않다.
웹 애플리케이션에서는 서비스에서 비즈니스 로직을 수행한다.

 

View의 책임

    • 단순히 데이터를 출력하는 책임을 가지므로 도메인 로직을 수행해서는 안된다.
    • 도메인에서 처리 후 결과만 view로 전달하자. (관심사의 분리)
관심사의 분리를 통해 각 계층을 독립적으로 개발하고 수정할 수 있다.

 

 

원칙

SOLID

SRP (단일 책임 원칙)

    • 하나의 클래스는 하나의 책임만 가져야 한다.
어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다. - 로버트 C.마틴

 

  • 설계: 클래스를 각각의 역할(객체가 수행하는 전반적인 기능)과 책임(객체가 해야하는 구체적인 작업)에 따라 분리한다.
  • 검증
    • 하나의 클래스가 너무 많은 역할과 책임을 가지고 있는가?
    • if문이 반복적으로 사용되는가?
    • 객체의 멤버(공개 메서드, 필드, 상수)가 단일 책임에 의해서만 변경되는가?

 

OCP (개방 폐쇄 원칙)

  • 확장에는 열려있고, 수정에는 닫혀 있어야 한다.
    • 기존 코드의 변경 없이, 시스템의 기능을 확장해야 한다.
  • 설계 : 추상화된 인터페이스를 의존하고, 구체 구현체만 변경한다.

 

LSP (리스코프 치환 원칙)

  • 하위 타입은 언제나 상위 타입으로 교체할 수 있어야한다.
    • 자식 클래스는 부모 클래스의 행동을 완전히 대체할 수 있어야 한다.
  • 설계
    • 하위 클래스가 부모 클래스의 행위를 변경하지 않으며 상속한다.
  • 검증
    • 하위 타입의 객체를 상위 타입의 객체로 변경할 수 있는가?
  • 존재 목적
    • 상위 타입을 사용하는 코드가 하위 타입으로 대체되어도 문제 없이 동작해야하기 때문이다.

 

ISP(인터페이스 분리 원칙)

  • 인터페이스를 작게 분리하라
    • 클라이언트는 자신의 사용하지 않는 인터페이스에 의존하면 안된다.
  • 설계
    • 인터페이스를 잘게 쪼갠다.
  • 존재 이유
    • 사용하지 않는 메서드를 구현하지 않아 불필요한 의존성을 제거하고, 재사용을 늘리기 위해서이다.

 

DIP (의존 역전 원칙)

  • 구체적인 것이 추상화된 것에 의존해야 한다.
  • 설계
    • 구체 클래스가 아닌 인터페이스, 추상 클래스에 의존하게 함으로써 구현체가 변경되어도 사용 코드에 영향을 주지 않는다.
  • 검증
    • 자주 변경되는 구체 클래스에 의존하는가?

 

ocp, dip의 차이점

    • ocp : 기존 코드를 변경하지 않고도 시스템의 기능을 확장할 수 있다.
      • 다형성, 추상화
      • 기능 확장에 중점
사용 코드에 영향을 주지 않는다
  • dip : 고수준 모듈, 저수준 모듈 둘 다 추상화에 의존해야 한다.
    • 인터페이스나 추상 클래스를 통한 의존성 주입(DI)
    • 의존성 방향에 중점

 

객체 지향 생활 체조

  • 객체지향 프로그래밍을 수행하기 위한 구체적인 가이드

1. 한 메서드에 오직 하나의 들여쓰기만 한다.

메서드가 한가지 일만 하도록 한다

2. else 예약어를 쓰지 않는다.

early return을 하게 해서, 코드를 단순하게 한다.

3. 모든 원시값과 문자열을 포장한다.

도메인 개념, 비즈니스 로직을 객체에 캡슐화

4. 한 줄에 하나의 점만 찍기

디미터 법칙 - 객체의 데이터를 가져오기 보다 물어보아서 자율성을 높이는 것

5. 줄여쓰지 않는다

이름을 명확하게 쓰기

6. 모든 엔티티를 작게 유지한다

높은 응집도

7. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다

클래스가 단일 책임

8. 일급 컬렉션을 쓴다

컬렉션과 관련된 동작을 캡슐화, 비즈니스 로직을 한 곳에 두려고

9. gettter/setter/property를 쓰지 않는다

객체의 내부 상태를 직접 노출하지 않도록 하기

 

주요 개념

캡슐화

  • 객체가 상태를 캡슐 안에 감춰둔 채, 외부로 노출하지 않은 것 => 객체의 자율성을 보존하기 위해서이다.
    • 객체는 행동만 외부로 노출한다.
  • 외부에서는 메세지만 던지고, 해당 메세지를 해석하는 것은 객체 자신의 판단에 따른다.
  • 자율적인 객체는 스스로 판단하고 스스로 결정하기 때문에, 협력이 유연하고 간결해진다.

 

추상화

    • 관심있는 것을 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 생략하거나 감춤으로써 복잡도를 극복하는 방법
중요한 것만 남기는 것

 

다형성

  • 서로 다른 유형의 객체동일한 메세지에 대해 서로 다르게 반응하는 것
    • 메세지를 송신하는 객체는 다형성을 가진 수신자를 구별할 필요가 없다.
  • 동일한 역할을 수행할 수 있는 객체들 사이는 대체 가능하다.
    • 대체 가능성을 이용해 설계를 유연하고 재사용가능하다.

 

상속

  • 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있다.
    • 단순히 메서드나 인스턴스 변수를 재사용하는 것이 아니다.
  • 외부 객체는 자식 클래스를 부모 클래스와 같은 타입으로 간주할 수 있다.
  • 상속의 단점
      1. 하위 클래스가 부모 클래스의 내부 구조를 잘 알고 있어야 한다. (캡슐화 위반)
      1. 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정하기 때문에 실행 시점에 객체의 종류를 변경하기 어렵다. (설계가 유연하지 않다.)

 

합성

  • 클래스가 다른 클래스의 인스턴스를 멤버 변수로 포함하여 그 기능을 활용하는 방식
  • 위임 : 포함된 객체의 메서드를 호출하여 기능을 수행한다.

 

합성과 상속

  • 공통점 : 모두 코드를 재사용할 수 있다.
    • 상속은 부모 클래스와 자식 클래스의 코드를 컴파일 시점에 강하게 결합한다.
    • 합성인터페이스를 통해 약하게 결합되어, 인터페이스에 정의된 메서드로만 코드를 재사용한다.
  • 관계
    • 상속 : is-a
      • 부모 클래스의 코드를 재사용한다.
    • 합성 : has-a
      • 전체를 포함하는 객체가 부분을 표현하는 객체를 포함해서 부분 객체의 코드를 재사용한다.
  • 합성은 실행시점에 동적으로 관계를 수정할 수 있다. (유연한 설게에 가능하다.)

 

테스트

JUnit과 AssertJ

  • 모두 테스트 프레임워크이다.

JUnit

  • java에서 사용하는 가장 대표적인 단위 테스트 프레임워크

AssertJ

  • JUnitAssertions를 보완하고 확장한 라이브러리
  • JUnit으로 검증 메서드를 작성하는 것보다 좀 더 가독성이 좋고, 메서드 체이닝을 지원하며 실패 원인을 파악하기 좋다.
    • 가독성
      • AssertJ : assertThat(actual).isEqualTo(expected) : 주어-동사-목적어 구조를 따라 문장처럼 읽힌다.
      • JUnit : assertEquals(expected, actual) : 목적어-주어 순으로 직관적이지 않다.
    • 메서드 체이닝 : 여러 검증들을 연결한다
    • 원인 파악 : 실패시에 더 자세한 에러 메세지를 전달한다.

 

테스트의 범위

내부 구현 테스트 (ex) 랜덤 테스트)

  • 로또를 생성한다고 가정했을 때, 원하는 로또를 생성하고 싶다면 인터페이스로 랜덤 기능을 추상화한다.
  • 실제 환경에서는 랜덤 구현체 사용하지만, 테스트 환경에서는 고정된 값을 반환하는 테스트용 구현체를 사용한다.

 

래핑 객체 테스트

  • Participants가 있고, 내부 필드에 Dealer, Players가 있다고 가정한다.
  • Participants는 단순히 DealerPlayers의 메서드를 호출하는 위임 역할만 수행한다고 가정할 때, Participants를 테스트해야할까?
    • 테스트해야한다. Participants도 객체의 관점에서는 다른 역할을 수행한다고 볼 수 있고, 위임한다는 사실은 객체 자신만 알고 있기 때문에 추후에 변경이 있을 때 실수가 발생할 수 있다.

 

equals & hashcode, getter

  • 자동 생성되는 코드, 단순 조회 코드는 추가 로직이 없기 때문에 테스트할 필요가 없다.
  • 만약 equals를 재정의한다면, 테스트를 통해 명확히 검증하는 것이 필요하다.

 

TDD

의미

  • 테스트를 구현보다 먼저 작성함으로써 테스트가 개발을 주도하도록 만드는 것
  • 테스트를 통과하는 만큼만 기능을 구현한다.
  • 구현하기 쉬운 테스트부터 작성하고, 예외 상황을 먼저 테스트하자.

red-green-refactor

  • red : 실패하는 테스트 만들기
  • green : 성공한 테스트 만들기
  • refactor : 리팩토링하기

 

장점

  • 빠른 피드백이 가능하다.
    • 새로운 코드를 추가하거나 기존 코드를 수정하면 테스트를 돌려서 코드가 올바른지 바로 확인할 수 있다.
  • 테스트 코드를 작성하는 과정에서 설계를 진행하게 된다.
    • 테스트를 작성하면서 클래스 이름, 메서드 이름, 메서드 파라미터, 실행 결과를 고려한다.
    • 필요할 것을 예측해서 미리 설계를 유연하게 만들지 않고, 필요한 만큼만 설계를 변경하므로 설계가 불필요하게 복잡해지는 것을 막는다.

단점

  • 짧은 시간 내에 구현해야할 경우 직접 코드를 작성하는 것보다 더 많은 시간이 소요된다.
  • 만약 짧은 시간 내에 개발 할 때 설계가 변경된다면, 이미 작성한 테스트코드까지 수정해야하므로 변경 비용이 커진다.

 

설계

확장성

YAGNI (You Aren't Gonna need it)

  • 실제로 필요하다고 확실해질 때까지 기능을 추가하지 말아야 한다.
    • 미래에 필요할 것이라는 예측에 기반한 개발을 피하자.
    • 실제로 필요할 때 기능을 추가하자
  • YAGNI 를 통해 설계를 단순하게 유지하고, 실제 요구사항에 집중하도록 돕는다.

 

확장 기준

  • 경험적으로 이 기능이 확장될 여지가 많은가?
  • 확장성있게 설계하는데 훨씬 많은 리소스가 드는가?
  • 확장성있게 설계하지 않았는데, 추후 예상했던 대로 기능이 확장된다면 수정하는데 많은 노력이 들지는 않는가?
도메인 지식과 연관이 된다고 느껴졌다.

 

상속, 합성, 인터페이스

사용 기준

  • 상속 : 자식 is a 부모이다.
    • 부모 클래스의 모든 속성과 동작이 자식 클래스에 적합할 때 (복제)
  • 합성 : 자동차 has a 엔진
    • 행위를 재사용하되 그 클래스의 종류가 되고 싶지 않을 때
    • 구현을 런타임에 변경할 필요가 있을 때
    • 다중 상속과 유사한 효과가 필요할 때
  • 인터페이스 : can do (할 수 있다)
    • 서로 다른 클래스가 공통 동작을 구현해야할 때

 

객체의 깊이 vs 유지보수

    • 합성을 사용할 경우 객체의 깊이가 깊어진다.
      • 한 객체가 여러 객체를 포함하고 있다.
    • 만약 한 객체가 여러 책임을 가진다면, 한눈에 파악하기도 어렵고 테스트, 유지보수도 어렵다.
    • 단일 책임을 가진 작은 객체들로 나누어 갖게 하면, 객체의 깊이가 깊어지더라도 코드 재사용성, 테스트 용이성을 높이게 되므로 tradeoff한 것이다.
여러개의 단순한 부품으로 복잡한 시스템을 구성하는 것이, 하나의 복잡한 덩어리보다 낫다.

 

객체 그래프

  • 객체들과 그 객체들의 참조 관계 표현

참조 방향 : 도메인이 가장 내부에 존재해야하는 이유

  • 외부 계층이 내부 계층에 의존한다.
  • 도메인은 비즈니스 로직을 표현하므로, 가장 안정적이고 변경이 적다. 인프라나 외부 기술이 변경되더라도, 핵심 비즈니스 로직은 영향을 받지 않는다.

도메인이 DTO를 의존하면 안되는 이유

  • 도메인은 가장 안쪽(중심)에 존재하고, 의존성은 외부에서 내부로 향해야한다.
  • DTO는 외부 계층과 도메인 계층간의 데이터 교환을 위한 객체이므로, 도메인이 DTO에 의존하게 되면 의존 방향이 역전된다.
  • DTO는 외부 인터페이스 변화에 따라 자주 변경될 수 있는데, 도메인이 DTO에 의존하면 변경이 핵심 비즈니스 로직에까지 영향을 미치게 된다.

 

UML

클래스 다이어그램

  • 클래스의 속성과 메서드를 포함하는 사각형으로 표현
  • 상속, 관련성, 집합, 합성, 의존성

시퀀스 다이어그램

  • 시스템의 동적 행동과 객체 간 상호작용 시각화
  • 객체, 생명선, 메세지, 활성화 박스

 

  • 클래스 다이어그램 : 시스템의 구조적 측면 설계
  • 시퀀스 다이어그램 : 객체 간 상호작용과 행동 설계

 

UML 사용 이유

  • 복잡한 시스템 구조를 알기 쉽게 시각화
    • 협업할 때 개발자, 디자이너, 비즈니스 관계자와의 협력

 

디자인 패턴

커맨드 패턴

    • 명령(요청) 별로 객체로 캡슐화한다.
    • Command 인터페이스 -> execute() 구현
    • Invoker : 명령 실행 역할
    • Receiver : 실제 작업 수행
    • Client : Reciever 설정

 

상태 패턴

    • 객체의 내부 상태에 따라 객체의 행동이 변경되도록 하는 디자인 패턴
    • Context : 상태 전환
    • State 인터페이스 : 상태별 행동 정의
    • Concrete State : 각 상태별 행동 구현

 

전략 패턴

    • 각 전략을 구현하여 교체 가능하게 만드는 디자인 패턴
    • Strategy 인터페이스
    • ConcreteStrategy 클래스들
    • Context : 전략 객체 참조, 사용

 

클린 코드

이름 짓기

  • 네이밍 컨벤션, 단축어 사용 X, 직관적인 이름

 

호출 순서

  • public -> protected -> default -> private
    • 수직 거리 : 호출하는 메서드와 호출되는 메서드를 가깝게 두는 것이 좋다. 위-> 아래 순으로 코드를 읽고 파악하는데 편하다.

 

가독성

  • 읽고 이해하기 쉬운 코드를 작성하라
  • 클린 코드 책 : 코드를 처음 보는 사람도 동작을 직관적으로 파악할 수 있어야한다.

 

DRY(Don't Repeat Yourself) 원칙

  • 중복 코드를 만들지 마라
    • 중복 코드 : 코드를 수정하는데 드는 노력이 몇 배 늘어난다.
    • 중복 : 요구사항이 변경되었을 때 같이 변경되어야한다면 중복이다.
  • 상속을 이용해서 중복 코드를 제거하는 방법
    • 부모와 자식 클래스가 강하게 결합된다. 추천하지 않는다.
  • 합성을 이용하여 객체를 쪼개고, 한 클래스가 하나의 책임만 가지고 서로 협력하도록 리팩토링하자.

 

 

728x90