포스트

(Day15) - OOP, 객체지향 프로그래밍 상세 [1] class..?

이 글은 제가 교육을 수강하며 기록하고 추가한 내용입니다. 강사님과 무관하게 잘못된 내용이 있을 수 있습니다.


클라우드 기반 웹 데브옵스 프로젝트 개발자 교육 과정 (5기)

  • 비트캠프 엄진영 강사님 (https://github.com/eomjinyoung/)
  • 훈련기관 : 네이버클라우드주식회사
  • 기간: 2023-11-14 ~ 2024-5-22
  • 남은 일자 : 114 일 ( 15/129 )

본 내용은 제가 교육을 수강하며 기록한 내용으로, 빠른 템포를 따라가며 기록하기에 일부 틀린 내용이나 어색한 문장이 있을 수 있는 점 양해 부탁드립니다.

15일(2023-12-04, 월)

학습 내용

클래스의 용도

클래스의 용도는 딱 두 가지다.

  • 메서드를 분류할 때 쓴다.
  • 프로젝트에 적합한 새로운 데이터타입을 정의할 때 쓴다.

클래스를 배울 때 상속을 먼저 배우는 경우가 있는데, 이것은 잘못된 것이다. 상속은 클래스가 고안된 근본적인 원인이 아니다. 위 두 가지를 먼저 이해하고 상속을 배워야 한다.

클래스의 활용 1: 메서드의 분류

작업경로: package com.eomcs.oop.ex02;

예시
Expression의 연산자 우선 순위를 고려하지 않고 순서대로 계산하는 프로그램을 구현하려고 한다. 이 프로그램에 클래스를 활용하는 경우를 생각해보자.

1단계: 메서드만 분리한 경우

1단계
각 메서드를 1레벨에 다 동일하게 둔다. 클래스 추출은 하지 않는다. 각 메서드의 결과를 담는 int 타입의 result 변수가 있다.
  • App
    • main()
      • int result
    • plus()
    • minus()
    • multiple()
    • divide()
    • abs()

2단계: 클래스 추출

2단계
관련된 메서드를 한 클래스에 묶어서 클래스 추출한다. 이는 GRASP => High Cohesion 을 적용한 것이다. 마찬가지로 main()에는 계산 결과를 저장하는 int 타입의 result 변수가 있다. static method 문법이다.
  • App
    • main()
      • int result
  • Calculator
    • plus()
    • minus()
    • multiple()
    • divide()
    • abs()

main() 메서드의 바디가 어떻게 변하는지 가독성 위주로 보자.

1
2
3
4
5
6
7
8
9
// 계산 결과를 담을 변수를 준비한다.
int  result  =  0;
// 클래스 메서드를 호출하여 작업을 수행하고,
// 리턴 결과는 로컬 변수에 저장한다.
  result  =  Calculator.plus(2,  3);
  result  =  Calculator.minus(result,  1);
  result  =  Calculator.multiple(result,  7);
  result  =  Calculator.divide(result,  3);
  System.out.printf("result = %d\n",  result);

3단계: 클래스 변수 도입

3단계
클래스 변수가 등장한다. main에서 result 라는 변수는 변수를 사용하는 곳인 Calculator 클래스에서 관리하도록 한다. 이것이 static variable(Class variable) 을 사용하는 이유이다.
  • App
    • main()
  • Calculator
    • plus()
    • minus()
    • multiple()
    • divide()
    • abs()
    • int result

4단계: 클래스 변수는 다중사용 불가능

4단계 (3단계 단점 확인)
클래스 변수를 사용하는 경우에는 단 하나의 계산만 진행할 수 있다. 하나의 계산을 완료한 이후에는 main()에서 클래스 변수를 초기화한 다음에 사용해야 한다.
1
Calculator.result = 0;

아래의 클래스 변수의 라이프 사이클을 참조하자. 클래스 변수를 동시에 여러 개를 사용할 수 없는 것은, 클래스가 생성할 때 단 한번만 생성되는 라이프 사이클에서 수반되는 특성이다.

static 변수는 클래스를 사용할 때 Method Area에 배치된다. 정확히 말하면 어떤 *.class 파일을 사용하기 위해 메모리에 올릴 때, Method Area에 적재된다. 이 때 static 변수도 같이 Method Area에 저장된다.

5단계: 인스턴스 변수로 힙 영역 사용해 다중사용하기

5단계
클래스 변수에서 인스턴스 변수로 전환하자. 왜 그래야 하는가? 하나의 클래스로 여러 계산을 처리하기 위함이다.

static을 붙이지 않으면 non-static 변수가 된다. 이를 instance 변수라고 한다. 인스턴스 변수는 Heap 영역에 생성된다. 필요한 만큼 생성하는 것이다. 즉 이 행동은 Method Area에 생성하던 변수를 Heap 영역에 생성하는 행동이다.

static 키워드를 제거한다. new 명령으로 인스턴스 생성해주고, c1 레퍼런스로 그 주소를 받는다.

1
Calculator c1 = new Calculator();

이렇게 하고 인스턴스 변수로 변경한다. 메서드도 인스턴스의 주소를 받아야 한다. 이렇게 하면 result 변수는 인스턴스 변수이므로 각각의 인스턴스마다 별개의 변수가 된다. 즉 새로 초기화하지 않아도 된다.

클래스의 메서드를 사용하기 위해서 인스턴스의 주소를 알려주기 위해 레퍼런스를 함께 줘야 한다.

Calculator 클래스는 이렇게 변경이 된다.

1
2
3
Calculator(인스턴스 주소, value) {
	...
}

main() 바디에서 메서드 호출은 이렇게 변경된다.

1
2
3
Calculator c1 = new Calculator();
...
Calculator.plus(c1,value);

이로써 Calculator는 계산을 하나 다 끝내고 변수를 초기화할 필요 없어졌다.

6단계: 인스턴스 메서드 사용하기

6단계
메서드도 Method Area => Heap 영역으로 옮기기 인스턴스의 힙 영역에서 사용하기

5단계에서는 클래스.메서드(레퍼런스, 아규먼트) 구조로 사용했다. 매번 인스턴스 주소를 파라미터로 전달해야하니 불편했다. 그래서 더 간결한 구조를 자바에서 지원한다.

그것이 무엇인가? 레퍼런스.메서드(아규먼트) 구조를 사용할 것이다. 위 구조와 보면 클래스(정확히는 클래스 이름)를 알려주는 부분이 없어진 것을 볼 수 있다.

클래스 메서드는 아래와 같이 사용된다.

  • 형태
    • 클래스.메서드();
  • 사용하는 경우
    • 작업결과를 개별적으로 관리하지 않을 때
    • 파라미터 값만으로 작업을 수행할 때

인스턴스 메서드는 아래와 같이 사용된다.

  • 형태
    • 레퍼런스.메서드();
  • 사용하는 경우
    • 작업결과를 개별적으로 관리하는 경우
    • 파라미터를 사용해서 작업을 수행한 후 그 결과를 내부적으로 보관할 때 Sure, here’s the information presented in a table:
종류형태사용하는 경우
클래스 메서드클래스.메서드(레퍼런스, 파라미터);- 작업 결과를 개별적으로 관리하지 않을 때
- 파라미터 값만으로 작업을 수행할 때
인스턴스 메서드레퍼런스.메서드(파라미터);- 작업 결과를 개별적으로 관리하는 경우
- 파라미터를 사용해서 작업을 수행한 후 그 결과를 내부적으로 보관할 때

Instance Method를 모르는 경우 Class Method를 써서 클래스.메서드(레퍼런스, 파라미터...) 를 써서 구현해도 동일한 결과를 얻을 수 있다. 그럼에도 인스턴스 변수라는 문법이 생긴 이유는 편의성 때문이다. 레퍼런스.메서드(변수)가 코드가 더 간결해지고 가독성이 좋고 작성도 편해서 그렇다. 실제로 static method가 먼저 나오고 나중에 instance method가 나중에 나왔다.

클래스 작성시에도 레퍼런스.변수명 혹은 레퍼런스.메서드명 으로 매번 레퍼런스를 파라미터로 정의하여 받아온 다음 쓰는 것보다는 내장된 this.변수명, this.메서드명으로 작성하면 더 편하다.

7단계: 패키지 사용해서 클래스 분류하기

7단계
클래스를 분류하는 문법인 패키지를 사용하자. 왜? 유지보수하기 쉽도록 하려는 것이다.
패키지
클래스를 그 역할에 따라 분류해두는 것이 패키지다.

이는 리팩터링의 기법 중 하나인 Move Class 이다.

  • 패키지에 직접 소속된 클래스
    • 패키지 멤버 클래스
    • 패키지명.클래스명 으로 사용한다.
      • 이러면 귀찮으니까 import 패키지명.클래스명; 을 선언한 다음에 클래스명.멤버 로 사용한다.

결론

  1. 여러 개의 클래스를 쓸 때 별도로 생성할 변수는 static을 쓰지 말고, 인스턴스 변수로 Heap 영역에 따로따로 만들어서 사용해라.
  2. 공통으로 사용하는 경우 static을 써서 클래스 변수로 Method Area에 생성하도록 하자.
  3. 메서드의 경우는 레퍼런스 주소를 받도록 하자.
  4. JVM 스택에는 로컬 변수가 만들어지고 힙에는 static 키워드가 안붙은 인스턴스 변수가 생성된다. 메서드는 Method Area에 생성된다.
  5. 인스턴스 변수를 사용할 때, 인스턴스 메서드를 사용할 수 있으면 그것을 사용하자. 빌트인변수인 this 변수를 이용하면 코드의 유지보수에 유리하다.

  1. 메서드를 추출하자.
  2. 클래스를 추출하여 캘르스 문법을 적용하자.
  3. 클래스를 통해 메서드를 분류하고 스태틱 메서드를 사용하자.
  4. 스태택 변수(클래식 변수)를 사용한다.
  5. 인스턴스 변수를 사용한다. new 명령어를 사용해야 한다. 레퍼런스를 통하여 메서드 및 변수에 접근한다.
  6. 인스턴스 메서드를 사용한다. 내장변수
  7. 패키지로 클래스를 분류한다.

클래스의 활용 2: 사용자 정의 데이터 타입 생성

1단계: 낱개의 변수를 사용

각각의 데이터들을 각각의 변수에 저장한다.

1
2
3
4
5
6
String  name;
int  kor;
int  eng;
int  math;
int  sum;
float  aver;

2단계: 새 데이터타입을 정의

여러 사람의 데이터를 개별적으로 다루어야 하므로 클래스 변수가 아닌 인스턴스 변수로 선언해야 한다.

1
2
3
4
5
6
7
8
class Score{
	String  name;
	int  kor;
	int  eng;
	int  math;
	int  sum;
	float  aver;
}

new를 통해 인스턴스를 힙 영역에 생성한다. 레퍼런스를 통해 해당 인스턴스에 접근한다.

1
2
3
4
5
Score s = new Score(); 
s.name = "Lee";
s.kor = 90;
...
s.float = sum/3;

공통 토막지식

우클릭하여 뜨는 메뉴

정확히는 Context Menu 라고 해야 한다. 컨텍스트라는 단어의 의미처럼, 문맥에 따라서 메뉴가 동적으로 나오는 것이다. 잘 모르는 사람들을 위해서는 우클릭한다고 하면 된다. 팝업 메뉴라고도 하는데, 정확한 용어는 컨텍스트 메뉴다. 결론을 내리면, 표현할 때 ‘우클릭하여 컨텍스트 메뉴를 띄우세요’ 가 적절하다.

클래스와 메서드의 명명법

클래스는 명사구로 이름을 짓는다. 메서드는 동사구나 전치사구로 이름을 짓는다. 왜? 그게 자연스러우니까. 클래스는 대문자로 시작한다. 메서드는 소문자로 시작하고 단어가 바뀔 때마다 공백 없이 대문자로 적는다. _ 혹은 -를 사용하지는 않는다.

클래스 로딩(Class Loading)

CPU는 보조기억장치의 정보를 그대로 읽는 경우가 없다. RAM만 읽어올 수 있다. 로딩 없이는 처리할 수 없다. 폰 노이만 구조.

RAM과 HDD 속도 차이? 100_000 배 정도. RAM에서 1초라고 하면 하면 100_000초 = 27.777시간이다. 많은 상황에서 CPU의 연산보다는 항상 입출력 속도가 성능에 절대적인 영향을 미친다. 물론 RAM보다 CPU 내 캐시가 훨씬 빠르다.

이걸 줄여 말하면 클래스 로딩(Class Loading)한다고 한다.

클래스 변수(static variable)의 라이프사이클은?

생성
클래스 변수는 클래스가 최초로 생성될 때 Method Area 영역에 생성된다.
삭제
JVM이 종료될 때 삭제된다.

변수를 사용할 때는 라이프사이클을 파악하고 활용할 수 있어야 한다.

뭔가 배울 때는 왜 등장했고 무엇을 개선했는지 찾아보자.

역사를 통으로 배우는 게 깊은 이해를 하게 해준다.

SOLID 원칙

https://hckcksrl.medium.com/solid-%EC%9B%90%EC%B9%99-182f04d0d2b

1. 단일 책임 원칙 (Single Responsiblity Principle)

모든 클래스는 각각 하나의 책임만 가져야 한다. 클래스는 그 책임을 완전히 캡슐화해야 함을 말한다.

  • 사칙연산 함수를 가지고 있는 계산 클래스가 있다고 치자. 이 상태의 계산 클래스는 오직 사칙연산 기능만을 책임진다. 이 클래스를 수정한다고 한다면 그 이유는 사직연산 함수와 관련된 문제일 뿐이다.

2. 개방-폐쇄 원칙 (Open Closed Principle)

확장에는 열려있고 수정에는 닫혀있는. 기존의 코드를 변경하지 않으면서( Closed), 기능을 추가할 수 있도록(Open) 설계가 되어야 한다는 원칙을 말한다.

  • 캐릭터를 하나 생성한다고 할때, 각각의 캐릭터가 움직임이 다를 경우 움직임의 패턴 구현을 하위 클래스에 맡긴다면 캐릭터 클래스의 수정은 필요가없고(Closed) 움직임의 패턴만 재정의 하면 된다.(Open)

3. 리스코프 치환 원칙 (Liskov Substitution Principle)

자식 클래스는 언제나 자신의 부모 클래스를 대체할 수 있다는 원칙이다. 즉 부모 클래스가 들어갈 자리에 자식 클래스를 넣어도 계획대로 잘 작동해야 한다.

자식클래스는 부모 클래스의 책임을 무시하거나 재정의하지 않고 확장만 수행하도록 해야 LSP를 만족한다.

4. 인터페이스 분리 원칙 (Interface Segregation Principle)

한 클래스는 자신이 사용하지않는 인터페이스는 구현하지 말아야 한다. 하나의 일반적인 인터페이스보다 여러개의 구체적인 인터페이스가 낫다.

5. 의존 역전 원칙 (Dependency Inversion Principle)

의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라는 것이다. 한마디로 구체적인 클래스보다 인터페이스나 추상 클래스와 관계를 맺으라는 것이다.

What is GRASP?

https://velog.io/@lsb156/GRASP-object-oriented-design

GRASP는 객체 지향 디자인의 클래스 및 객체에 책임을 할당하기위한 지침으로 구성됩니다.
SOLID 설계 원칙과 관련이 없다고는 하지만 서로 엮여있는 부분이 조금씩은 있습니다.

SOLID 원칙을 따르는 디자인패턴 중 하나가 GRASP이며, GRASP보다 더 실용적인(구체적인) 설계기법은 GoF’s Design Patterns 이다.

  • General Responsibility Assignment Software Patterns
  • Object-Oriented 디자인의 핵심은 각 객체에 책임을 부여하는 것.
  • 책임을 부여하는 원칙들을 말하고 있는 패턴.
  • 구체적인 구조는 없지만, 철학을 배울 수 있다.
  • 총 9가지의 원칙을 가지고 있다.

“After identifying your requirements and creating a domain model, then add methods to the appropriate classes, and define the messaging between the objects to fulfill the requirements”
“요구 사항을 식별하고 도메인 모델을 작성한 후 적절한 클래스에 메소드를 추가하고 요구 사항을 충족시키기 위해 오브젝트 간 메시징을 정의하십시오.”

  • Craig Larman

Controller

컨트롤러 패턴은 시스템 이벤트를 처리하는 책임을 전체 시스템 또는 사용 사례 시나리오를 나타내는 non-UI 클래스에 할당합니다. 컨트롤러 개체는 시스템 이벤트를 받거나 처리하는 non-User Interface 개체입니다.

컨트롤러는 모든 시스템 이벤트를 처리하는 데 사용되어야 하며 둘 이상의 use case에서도 사용 가능합니다. 예를들어 사용자 생성 및 사용자 삭제의 경우 두 개의 개별 컨트롤러 대신 UserController라는 단일 클래스로 구성할 수 있습니다.

컨트롤러는 시스템 작동을 수신하고 제어하는 UI 계층을 이외의 첫 번째 개체로 정의됩니다. 컨트롤러는 수행해야 할 작업을 다른 개체에 위임해야 하며, 활동을 조정하거나 통제해야 하지만 컨트롤러 자체가 많은 일을 해서는 안 됩니다. GRASP 컨트롤러는 정보 시스템 논리 아키텍처에 공통 계층이 있는 객체 지향 시스템에서 애플리케이션/서비스 계층(애플리케이션/서비스 계층과 도메인 계층을 명시적으로 구별했다고 가정)의 일부라고 생각할 수 있습니다.

Creator

객체 생성은 객체 지향 시스템에서 가장 일반적인 활동 중 하나입니다. 객체 생성을 담당하는 클래스는 특정 클래스의 객체 간 관계에 대한 기본 속성입니다.

일반적으로, 클래스 B는 다음 중 하나 또는 그 이상이 적용되는 경우 클래스 A의 인스턴스를 생성할 책임이 있습니다.

  • B 객체가 A 객체를 포함하고 있다.
  • B 객체가 A 객체의 정보를 기록하고 있다.
  • A 객체가 B 객체의 일부이다.
  • B 객체가 A 객체를 긴밀하게 사용하고 있다.
  • B 객체가 A 객체의 생성에 필요한 정보를 가지고 있다.

Factory Pattern

Indirection

두 객체 사이의 중간 요소에 중재 책임을 할당하여 두 요소 간의 낮은 결합 및 재사용 가능성을 지원합니다. 이에 대한 예로는 Model-View Controll(MVC) 패턴에서 데이터(모델)와 표현(뷰) 사이의 중개를위한 컨트롤러 구성 요소를 도입합니다. 이것은 그들 사이의 낮은 결합 상태를 유지하도록 보장합니다.

Information expert

Information expert는 역할을 수행할 수 있는 정보, 계산 된 필드 등의 책임을 위임 할 위치를 결정하는데 사용되는 원칙입니다.

Information expert를 이용하여, 책임을 할당하는 일반적인 방법은 주어진 책임을 확인하고 그 책임을 이행하는 데 필요한 정보를 결정한 다음 해당 정보가 저장되는 위치를 결정하는 것입니다.
그리고 그것을 이행하는 데 필요한 가장 많은 정보를 가지고 있는 Class가 책임을지게됩니다.

객체는 데이터와 처리로직이 함께 묶여 있으며 자신의 데이터를 감추고자 하면 오직 자기 자신의 처리 로직에서만 데이터를 처리하고, 외부에는 그 기능(역할)만을 제공하게합니다.

High cohesion

높은 응집력은 객체를 적절하게 집중시키며 관리할 수 있고 이해가능하게 만드는 패턴입니다.
높은 응집력은 일반적으로 낮은 결합을 지원하는데 사용됩니다. 응집력이 높다는것은 주어진 요소의 책임이 밀접하게 관련되어 있고 집중되어 있음을 의미합니다. 프로그램을 클래스와 서브 시스템으로 나누는 것은 시스템의 응집력을 높이는 한 예입니다. 대한적으로, 낮은 응집력은 주어진 요소들이 관련없는 책임을 너무 많이 갖는 상황입니다. 응집력이 낮은 요소들은 종종 이해하기 어렵고 재사용이 어려우며 유지보수를 힘들게 만듭니다.

Low coupling

Object-Oriented 시스템은 각 객체들과 그들 간 상호작용을 통하여 요구사항을 충족시키는 것을 기본으로 합니다. 그러므로 각 객체들 사이에 커플링이 존재하지 않을 수 없습니다.
커플링이란 요소가 다른 요소에 얼마나 강력하게 연결되어있거나 다른 요소에 대해 알고있거나 의존하는지에 대한것을 나타내는 척도입니다. 낮은 결합도는 지원 책임을 할당하는 방법을 결정하는 평가 패턴입니다.

  • 클래스간의 종속성이 낮다
  • 한 클래스의 변화가 다른 클래스에 미치는 영향이 낮다.
  • 높은 재사용 가능성

Polymorphism

다형성 원리에 따르면, 유형에 따른 행동의 변경을 정의하는 책임은 이러한 변형이 발생하는 유형에 할당됩니다. 이것은 다형성 연산을 사용하여 달성됩니다. 객체의 Type으로 분기를 처리하는 영역에서는 명시적인 분기 대신 다형성 연산을 사용해야합니다. Object-Oriented 시스템의 특징이자 장점인 상속과 Polymorphism(다형성)을 아낌없이 사용하길 권장합니다.

Protected variations

Protected variations 패턴은 변경될 여지가 있는곳에 안정된 인터페이스를 정의하여 사용합니다. 인터페이스로 감싸고 다형성을 사용하여 이 인터페이스의 다양한 구현을 생성함으로써 다른 요소(객체, 시스템, 서브시스템)에 대한 변화로부터 요소를 보호합니다.

Pure fabrication

Pure fabrication은 문제 영역의 개념을 나타내지 않는 클래스로 특히 낮은 결합도, 높은 응집력 및 그 재사용 가능성을 달성하기 위해 구성됩니다. (Information expert 패턴 적용 시 Low Coupling과 High Cohesion의 원칙이 깨어진다면, 기능적인 역할을 별도로 한 곳으로 모으도록합니다.)
DB 정보, 로그 정보를 저장하는 기능을 예로들면 각 정보는 객체를 가지고있으며 이때 Information Expert 패턴을 적용하면 각 객체들이 정보를 저장하고 로그를 기록하는 역할을 담당해야 하지만, 실제로 그렇게 사용하는 예는 찾기 힘듭니다.
이것은 그 기능들이 시스템 전반적으로 사용되고 있기 때문에 각 객체에 그 기능을 부여하는 것은 각 객체들이 특정 데이터베이스의 종속을 가져오거나 로그를 기록하는 프로세스를 수정할 경우, 모든 객체를 수정해야 하는 결과를 가져오게됩니다.
이럴때는 공통적인 기능을 제공하는 역할을 한 곳으로 모아서 가상의 객체, 서브시스템을 만들어야합니다.
이러한 종류의 클래스는 도메인 중심 설계에서 (DDD) 서비스라고 불리웁니다.

출처
김대곤 님의 GRASP 패턴
위키피디아 https://en.wikipedia.org/wiki/GRASP_(object-oriented_design)

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.