Readable Code: 읽기 좋은 코드를 작성하는 사고법 - Section 3 : 객체 지향 패러다임

모깅·2024년 8월 2일
post-thumbnail

추상의 관점으로 바라보는 객체 지향

우리가 흔히 생각하는 프로그래밍 패러다임의 3가지

  1. 절차지향
    -> 정해진 순서, 차례대로 컴퓨터의 처리 구조와 유사하게 처리하는 프로그래밍
  2. 객체 지향
    -> 객체들을 만들어서 객체들 간의 협력을 통해 프로그래밍이 이루어지는 프로그래밍
  3. 함수형
    -> 함수형 프로그래밍은 순수 함수라는 것을 정의하는데 순수 함수는 말 그래도 Side Effect가 없는 함수를 의미한다. 그래서 a라는 값을 넣을면 항상 그 a에 대한 정해진 결과가 나오는 것을 순수 함수라고 한다. 외부의 어떤 요인 없이 항상 같은 값을 집어 넣으면 같은 리턴값을 내보는 것이 순수함수인데 이런 순수함수들의 조합으로 우리가 가변 데이터를 멀리하고 사이드 이펙트가 없는 형태로 순수 함수 기반의 프로그래밍을 의미한다.

객체 자체도 어떤 목적을 가지고 추상화가 된 것이 객체고 사실상 데이터와 코드의 조합이다.
데이터라 함은 객체가 가진 필드들, 실제 데이터들이 필드에 들어가는 것이고 그 데이터들을 어떻게 조작할 것인지에 대한 코드가 하나로 묶인다. 그래서 그 데이터와 데이터를 조작하는 혹은 가공하는 그런 코드가 같이 묶인 것이 객체이다.

객체라는 개념이 도출되면서 객체가 여러개 생기다 보니 객체들 간의 협력, 그리고 각 객체가 가지고 있는 책임이 대두가 된다. 그래서 이 객체 간의 협력과 객체가 담당하는 책임의 관점으로 우리가 추상화를 접목시켜서 이해하는 것이 중요하다.

캡슐화 : 객체라는 것의 특성상 객체가 가지고 있는 데이터를 숨기고 그리고 그 데이터를 가공하는 로직도 숨기고 외부에 일부만 드러내는 것

관심사에 따라 우리는 기능과 어떤 책임을 나누고 그 나눈 것들의 조합으로 프로그램이 돌아가도록 하는 것이 관심사의 분리다.

관심사를 모으면 유지보수가 원활해진다.
어떤 A라는 관심사를 가진 것들을 모아서 관리하면 A에 대한 기능은 그 항목에 가서 유지보수를 하면 되기 때문이다.

만약 A라는 관심사가 중구난방 퍼져 있따면 각각 다 찾아가서 확인해야 할 것이다.

그래서 관심사를 분리하다는 것은 일련의 작업들 혹은 개념들을 묶어서 이름을 짓고 역할을 부여하는 것인데 결국 이것도 추상화 과정의 한 일면인 것이다.

관심사를 모았을 때 우리가 특정한 관심사로 응집도가 높도록 설계해야 되고 각 관심사끼리는 결합도가 낮아야 된다.

결합도라는 것은 하나가 바뀌었을 때 다른 하나가 영향을 받는 정도를 의미한다.

A라는 객체가 있고 B라는 객체가 있다고 했을 때 A가 어떤 유지보수 할 때 기능을 변화시켰는데 B까지 같이 영향을 받아버리면 이 둘은 결합도가 엄청 높은 상태인 것이다.

그렇다면 관심사가 제대로 분리가 되었는가를 의심해 볼 수 가 있게 된다. 만약 관심사를 적절히 잘 분리해 놓고 적당한 협력 관계를 유지해서 프로그래밍을 하도록 작성을 했다면 A가 크게 변경되어도 B는 최소한의 변경 혹은 변경이 아예 없는 형태로 유지가 될 것이다.

객체 설계하기(1)

섹션 1에서 메서드를 추상화로 추출한다는 것은 외부 세계와 내부 세계를 나눈다는 것을 의미했다.

객체도 마찬가지로 객체를 만드는 순간 어떤 경계가 생기고 객체 내부에 있는 데이터나 로직들이 이제 내부 세계가 되는 것이고 외부 세계랑 소통을 하기 위해서는 어떤 공개 메서드를 통해서 외부 세계랑 소통해야 한다.

그래서 외부 세계에 내가 어떤 데이터를 받을 것인지 어떤 기능을 제공해 줄 수 있을 것이지를 외부 세계에 알려주는 용도로 공개 메서드를 사용한다고 했다.

어떤 전체 로직이 있을 때 공통된 관심사들이 퍼져있을 것이다. 안쪽에 있는 사각형들을 공통된 관심사, 어떤 하나의 관심사라고 봤을 때 이것들을 추출해서 관심사를 분리시켜 오브젝트라는 객체를 만들었다.

이렇게 오브젝트를 만들게 되면 분리한 바깥쪽은 외부 세계가 될 것이고 오브젝트는 이 바깥 세계에다가 어떤 기능을 제공해주고 공개 메서드를 통해서 기능을 제공해주고 책임을 가지게 된다.

객체 내부에는 비공개 필드와 로직이 존재할 것이고 공개 메서드 선어부만 외부에 노출함으로 써 외부 세계에서 이 객체를 사용할 때 혹은 협력할 때 이 메서드 선언부를 통해서 이 객체가 어떤 기능을 제공해주는구나를 알게 된다. 그래서 객체의 책임을 이 공개 메서드를 통해서 드러낼 수 있다.

이러한 객체들이 모여서 서로 상호간의 협력을 하게 된다.

이전에는 0보다 작으면 예외 상황이 발생한다는 검증 로직이 어딘가에 떠돌아 다니고 있었을 것이다.

이것을 객체 안으로 갖고와서 어떻게 보면 응집도가 높아진것이다. 유효성 검증 로직도 어떤 하나의 관심사, 같은 관심사로 본 것이다.

객체 설계하기 (2)


  • Cell 기반 Board로 리팩토링 하자.


  • get으로 꺼내와서 비교하는게 아니라 객체한테 물어봐야한다!
    -> Minesweeper 객체는 Cell의 내부를 모른다. 물어보는게 최선이다.
    -> get은 알고있듯이 행동하는 것이고 무례한 것이다.


  • Cell에다가 보드를 그려달라고 하는게 더 이상하다.
    -> 그리는 쪽에서 Cell에 sign을 달라고 해서 board를 그리는게 더 자연스럽다.


  • Cell에다가 물어보도록 하자. (미리 만둘어둔 메서드가 있다.)
  • 부정연산자가 있기 때문에 부정연산자를 없애고 메서드에 부정의 의미를 담자.

  • BOARD를 지우고 BOARD2를 BOARD로 변경!

  • Cell 이 생기면서 sign이 Cell 안으로 들어갔는데 SIGN 상수들은 아직 Minesweeper에 존재한다.
    -> Cell로 옮겨주자.

-> 외부에서 사용하지 않기 때문에 private으로 변경


  • 상수들이 Cell 내부로 들어갔기 때문에 새로운 정적 팩토리 메서드를 만들자.


  • CLOSED_CELL_SIGN이 내부로 들어갔기 때문에 물어보기만 하면 된다.

  • NPE를 방지하기 위해서 상수로부터 equals를 호출한다.





  • nearbyLandMineCount랑 land_mines도 Cell의 속성으로 볼 수 있다.
    -> Cell로 옮기자.


  • ofFlag는 nearbyLandMineCount를 몇으로 해야하나..?, isLandmine은 맞을 수도 있고 아닐 수도 있고,, 뭔가 이상하다.

  • 깃발은 아직 열지 않았는데 사용자가 지뢰가 있을것 같다고 판단하여 깃발을 꽂았다.
    -> 닫힌 동시에 사용자가 확인한 셀이 깃발이 꽂힌 셀이다.

  • 현재 게임 종료조건은 모든 셀이 열렸을 때이다.
    -> 그러나 깃발이 꽂힌것과 열리다/닫히다는 별개의 개념이다.
    -> 따라서 종료 조건은 어떤 셀이 열려 있거나 혹은 닫혀 있지만 깃발로 확인했을 때이다.

  • 다시 리팩토링 진행해보자

  • CLOSE된 곳에 Flag가 꽂힌다.
    -> 뭔가 도메인적으로 겹친다.

  • sign 파라미터는 곧 없어지는 잠시 냅두자.


  • create() 할 때 landmineCount를 0으로 해놨기 때문에 지워도 된다.


  • 깃발 꽂았다는 상태값이 필요하다.


  • 열렸다/닫혔다가 아니라 체크/언체크로 바꿔야 한다.

  • 부정의 부정이다.
    -> 긍정으로 바꾸자.

  • isChecked는 열렸거나 깃발이 꽂힌 상태를 의미한다.

  • 다음과 같이 메서드 명도 바뀌어야 한다.



  • 이미 turnOnLandMine 했기 때문에 필요없다.
    -> 대신 열어줘야 한다.


  • 닫혀있지 않으면 -> 열려있으면


  • getter 사용하지 말고 객체한테 물어보자!


  • Cell로 변경하는 부분에서는 숫자는 이미 Cell에 있기 때문에 열어주기만 하면된다.
    -> 이전 버전에서 숫자를 할당하는 것이 열어주는 기능과 같기 때문이다.


  • Cell로 모두 모였으니 제거!

  • 모두 제거!

  • getSign()을 구현해야 하는데 현재 Cell의 상태에 따라 구현 해줘야 한다.

  • Sign 변수를 삭제하자.
    -> 필요없어짐.

SOLID

SRP : Single Responsibility Principle

두가지 책임을 갖고 있다면 나누자는 원칙이다.

상수도 주의해야 하는데 A와 B가 동시에 사용한다고 가정하자. 상수는 어디에 있어야 하는가? A? B? 혹은 제 3자인 C 이런것들을 고민해 볼 필요가 있다.

SRP를 지키면 클래스 내의 요소들이 긴밀하게 연관되기 때문에 응집도가 높아진다.

또한 객체가 변경 되었을 때 협력하고 있는 다른 객체에 영향을 줄일 수 있다.
-> 낮은 결합도를 의미하며 의존성을 최소화 한다.


  • GameApplication에서는 Minesweeper를 실행시키는 역할만 하게 하자.
    -> Minesweeper 이외에 게임도 시작할 수 있도록 하는 관점도 갖자.
    -> 나머지 로직은 모두 Minesweeper 클래스로!
    -> Minesweeper에서의 모든 메서드에서 static을 지우자.
    -> 메인함수에서 불리는게 아니기 때문이다.

  • 입출력에 대한 로직을 분리해보자.

  • 클래스 생성


  • board에서 ROW, COL 모두 알 수 있으니 Board만 인자로 받으면 된다.




  • BOARD도 이곳저곳에서 사용하고 있다.
    -> 하나의 객체로 뽑을 수 있지 않을까?
    -> GameBoard 클래스를 만들자.

  • 상수 BOARD와 관련된 모드 로직들을 GameBoard로 가져오자.
    -> initialize 먼저!

  • Minesweeper에서 상수인 LAND_MINE_COUNT를 필드로 가져오자.

  • isLandMineCell() 외부에서 가져오자.

  • rowSize, colSize로 바꿔주자.

  • 객체한테 반드시 요청하자.

  • Cell을 뽑아내는 코드가 많다.
    -> 메서드 추출


  • 화살표로 변경



  • open() 메서드는 GameBoard가 담당하는게 자연스럽다.
    -> GameBoard 클래스로 옮기자.

  • open 이름이 겹친다.
    -> openSurroundedCells로 변경!

  • findCell().isOpened() 맘에 안드니 변경해주자.


  • isAllCellChecked() 도 BOARD를 사용하므로 GameBoard로 이관하자.

  • 모두 이관했으니 BOARD 없애자.

OCP : Open-Closed Principle


  • 길이가 고정되어 있기 때문에 OCP원칙을 잘 지키지 못한다.

  • 요구사항 중 가로길이와 세로길이를 늘려달라고 한다.
    -> 다음과 같이 이상하게 보드가 나오는 것을 알 수 있다.


  • 두자리 수 대응 성공

  • row 인덱스와 col인덱스로 변환하는 과정 자체가 또 하나의 책임으로 분리 될 수 있지 않을까? 라는 생각이 든다.
    -> SRP에 따라 다른 객체로 이관시켜 보자.

  • BoardIndexConverter로 이관

  • 예외를 통일시켜주고 싶다.


  • col이 colSize에 따라 나오지 않는 것을 수정해보자.


  • 게임 난이도를 인터페이스로 만들고 구현체로 변경된 난이도에 대한 클래스를 넣어주자.


LSP: Liskov Substitution Principle

자식에 기능이 더 많다.
-> 부모에서 기능을 구현하고 중복을 없애기 위해 상속을 사용한다.


  • Cell을 복사해서 Cell2를 만들자.
    -> 추상클래스로 만들자.

  • 추상클래스는 인스턴스를 생성 할 수 없기 때문에 컴파일 에러가 생긴다.
    -> 삭제하자.

  • 공통 기능 이외에 추상 메서드로 만들자.

  • LandMineCell 클래스 생성

  • turnOnLandMine()은 기존 Cell2에서 isLandMine을 true로 바꿔줬으니 필드로 가져와서 똑같이 해주면 된다.
    -> 이미 LandMineCell 인데 isLandMine을 true로 바꿔준다? 뭔가 이상하지만 일단 넘어가자.

  • 숫자 셀에서만 사용하므로 예외를 던져주자.

  • isOpened인지 보려고 했는데 부모에서 private으로 막고 있다.
    -> protected로 변경하자.

  • 부모의 FLAG_SIGN, UNCHEKED_SIGN은 공통으로 사용할 것이니 protected로 열어주자.

  • NumberCell 클래스 생성

  • NumberCell에서 turnOnLandMine()은 지원하지 않으므로 예외를 던지자.

  • nearbyLandMineCount를 부모에서 가져와서 할당해주자.



  • findCell을 Cell2를 리턴하도록 하자.
  • 모두 Cell2로 변경해주도록 하자.


  • 0이라는 지금까지 본 적 없던 값이 나왔다.

  • count가 0일때 할당 안되도록 continue 시켜주자.

  • 이런식으로 타입체크를 한 후 메서드를 호출한다는 것이 LSP 원칙을 위반한다는 것이다.

  • 지뢰 셀인데 지뢰를 켜는 메서드는 필요없다.
    -> 제거

  • 업데이트 하는 메서드때문에 지뢰셀이나 빈셀에서 해당 메서드를 구현해야하는 상황이다.
    -> 없애고 생성자에서 넣어주자.

  • 업데이트 제거

  • Cell2에 해당 기능 제거하자.
    -> 하위 클래스에서도 삭제하자.

  • Cell을 이제 사용하지 않으니 삭제하자.
    -> Cell2를 Cell로 변경하자.

ISP: Interface Segregation Principle



  • 또 다른 게임이 있다고 가정해보자.

  • 이 게임은 초기화가 필요없는 상황이다.
    -> ISP 위반사항이 된 것이다.
    -> initialize 라는 기능이 만약에 메서드 시그니처가 변경되거나 했을 때 AnotherGame은 initialize에 대한 구현을 안하고 있다가 기능적으로 필요 없는데 메서드 시그니처가 변경됨으로써 AnotherGame 크래래스도 같이 영향을 받게 된다.
    -> 인터페이스를 쪼개자.

DIP: Dependency Inversion Principle

의존성 : 하나의 모듈이 다른 하나의 모듈을 알고 있거나 직접적으로 생성하거나 사용하는 모든 것들을 의미한다.
-> 한 모듈이 다른 모듈을 참조하는 것을 의존성이 있다고 표현한다.

저수준 모듈은 자주 바뀔 수가 있다.
-> 구체쪽에 가깝기 때문에 어떤 기능을 구현하는데 있어서 방법들이 자주 바뀔 수 있다.


  • Minesweeper 입장에서는 consoleInputHandler와 consoleOutputHandler 모두 저수준 모듈에 의존하고 있다.
    -> Mineseeper 게임이 웹사이트에 만들어졌다고 할 때 웹 사이트에 알맞은 인풋과 아웃풋을 내놓을 것이다.
    -> 그러나 현재 consoleInputHandler와 consoleOutputHandler이 코드에 박혀있기 때문에 변경이 불가피해진다.

  • 각 클래스에서 모두 복사해와서 바디를 없애주자.


  • outputHandler 입장에서는 print보다 show가 더 알맞은 단어이다.
    -> print는 console에 적합하며 구체적이다.
    -> 반면 show는 추상적이다.
profile
멈추지 않기

0개의 댓글