목표
객체 지향 프로그래밍을 사용하는 이유로서 초보적인 절차적 프로그래밍의 문제점을 살펴본다.
객체 지향 프로그래밍의 개념과 특징에 대해 이해한다.
class와 인스턴스에 대해 이해한다.
객체 지향 언어로서 java를 이해한다.
&전문적인 깊이가 부족한 필자의 지식 수준에서 비교한 것이니 더 좋은 글을 찾아가는 걸 추천한다.
초보적인 절차적 프로그래밍의 문제점 이해하기
로또 뽑기 프로그램을 만든다고 생각해보자.
유저는 프로그램에 로또 번호 7개를 입력하면 프로그램은 7개의 난수를 뽑고 당첨 규칙에 따라 순위를 매겨 상금을 부과한다. 유저는 로또를 프리미엄, 비즈니스, 이코노미의 등급 중에 선택할 수 있고, 한번에 몇 장을 살지도 선택할 수 있다.
위 내용을 구현한다고 생각해보자.
초보적 절차적 프로그래밍
내가 javascript 초급 개발자라 상상해보자.
아마, 각 과정을 쪼갤 수 있을 것이다.
유저의 선택 단계 = 로또 종류, 장 수 선택, 번호 입력
7개의 난수를 뽑는 단계
- 난수와 유저의 로또 번호를 비교하는 단계
- 일치 갯수, 자리 수, 등급을 고려하여 배당금을 산출하는 단계
- 유저에게 배당금을 지급하는 단계
- 유저에게 재시작 여부를 물어보는 단계
이렇게 나눈 각 단계를 web api나 기능을 이용해서 구현할 수 있을 것이다.
- 유저의 선택은 window.prompt(message, default)로 메시지를 받을 수 있다.
- 로또 종류와 장 수, 번호를 입력받으면 전역 변수로 저장한다.
- Math.floor(Math.random())를 반복하여 7개의 난수를 뽑는다. 그리고 배열로 저장한다.
- 추첨된 번호는 유저가 작성한 로또 번호와 비교하며 일치 갯수, 자리 수, 등급을 각각 변수로 저장한다.
- 이제 당첨규칙을 프리미엄, 비즈니스, 이코노미로 구분하고 유저에게 줄 배당금 변수를 만들어 축적한다.
- 유저의 지갑을 변수로 미리 선언해두고 배당금액을 전달한다.
- 유저에게 재시작 여부를 프롬프트로 물어본다.
이처럼 추상적 목적을 구체적 절차로 구분했으니 절차적 코딩이라 부르자.
절차 중심적 코딩의 가장 큰 장점은 쉽고 빠르다는 점이다.
문제점
그런데, 문제가 있다.
만약 여기에서 새로운 기능을 추가한다면 어떨까.
이벤트 기능을 넣어서 10번째 시도 때는 이전까지 유저가 사용한 배당금의 20%를 공짜 이코노미 로또로 지급한다면?
그러면 유저의 이전 배당금을 저장하기 위한 변수를 만들고 로직을 추가할 것이다.
만약 기능이 바뀐다면 어떨까.
7개 전부가 난수인 것이 아니라 6개만 난수이고 나머지 하나는 이벤트 넘버로 유저가 직접 뽑는 방식을 선택할 수도 있다.
이처럼 새로운 기능을 위해 새로운 절차를 추가하고 그것을 위한 변수를 추가하면 문제가 생긴다.
1. 절차 간에 종속성
절차는 이전 절차와 다음 절차에 종속적이다. 따라서, 하나의 절차를 추가하거나 수정하면 있따른 절차에 예기치 못한 오류가 발생할 수 있다. 흔히 사이드 이펙트라 부르는 것도 이것의 일환으로서 절차가 외부 변수를 건드리며 발생한다.
2. 재사용의 어려움
절차나 변수는 때때로 재사용가능함에도 다른 절차와의 의존성의 문제로, 혹은 인자로 처리하기 어려운 외부 변수 때문에 재사용하지 못한다.
3. 외부변수 의존성
절차에는 다양한 외부 변수들이 사용된다. 절차가 복잡해지면 각 변수들이 절차에 어떻게 이용되는지 파악하기가 어려워져서 새로운 기능을 추가하거나 수정하기 어려워진다.
대안으로서 객체지향 프로그래밍
이 때 우리는 '객체'라는 개념으로 이 문제를 풀어갈 수 있다.
객체란, 관련 있는 변수나 기능을 하나로 그룹화하는 것이다.
위 예시를 객체 지향적 프로그래밍으로 생각해보자.
유저 객체
- 유저의 시드머니, 유저가 가지고 있는 복권, 매 시도간 사용한 복권 금액을 저장함.
- 메소드로 복권 구매 프롬프트 입력, 배당금 수령 후 시드머니로 저장, 매 시도의 복권 금액 기록이 있을 수 있다.
복권 추출기
- 복권의 추첨번호를 저장한다.
- 메소드로 복권 추첨번호를 뽑는 난수 기능, 혹은 유저의 이벤트 복권 넘버 선택 요청 기능이 있다.
복권 구매 및 배당금 계산기
- 유저가 구매한 복권 종류, 갯수, 배당금 계산 규칙을 저장한다
- 메소드로 유저가 구매할 복권의 종류, 갯수를 요청하고 이에 따라 돈을 계산한다.
- 메소드로 배당금 계산 규칙과 유저의 당첨에 따라 배당금을 계산한다.
- 메소드로 유저에게 배당금을 지급한다.
간단하게 정리하자면
객체지향 프로그래밍이란, 추상적인 목적을 구체적인 목표사항과 절차로 환원할 때 유사한 사항들을 객체로 그룹화하고
각 객체에게 해당 절차의 책임을 위임하는 것이다.
이런 구분을 통해서 각 절차는 서로 간에 의존성을 줄일 수 있어야 한다.
장점, 특징
추상화
객체는 여러 절차를 묶어 더 큰 단위로 추상화되어 있다. 따라서 기능을 짐작하기 용이하다.
절차 지향적 방식은 구체적인 절차 하나하나를 봐야 하지만, 객체지향은 유저, 복권, 계산기라는 추상화된 객체를 보고
전체적인 과정을 파악하기가 더 용이하다.
또한, 기능을 추가할 때도 객체의 용도에 따라 어느 곳에 추가할지 명확해진다.
캡슐화
객체는 자신의 복잡한 내부구조를 외부로 드러내지 않으며 오로지 외부 인터페이스를 통해서만 통신한다.
외부에서는 이 객체의 프로퍼티에 직접 접근하거나 수정하는 것이 일부 제한된다.
이처럼 의존성을 최소화하는 느슨한 결합 방식은 객체 간에 분리와 재사용이 용이해진다.
개발자 입장에서는 내부 로직의 전체를 알지 못하더라도 외부 인터페이스만 신경쓰면 되니 편리하다.
상속
만약, 프리미엄 복권의 배당금 산출 규칙과 비즈니스 복권, 이코노미 복권 간에 배당금 규칙에 유사성이 존재한다면
아예 공통 배당금 규칙을 별도로 만들고 각 복권 규칙이 이를 상속하게 할 수도 있다.
이처럼 모객체와 자객체 간에 상속관계를 만드는 걸 상속이라 부른다.
상속은 적은 코드로 파생객체를 만들어 생산성을 올려준다. 또한 객체 간에 일관성을 만들어주므로 개발자 입장에서는 모객체 파악으로 파생 객체의 파악이 용이해진다.
다형성
만약, 모객체의 상속을 받은 자객체가 모객체의 모든 부분을 상속받길 원하지 않거나 일부 기능을 변경하고 싶다면
오버라이드(overwrite)도 가능하다. 이는 객체 간에 일관성이 주는 경직 문제를 해결한다. 이를 다형성이라 부른다.
중간 정리
절차지향적 프로그래밍의 문제
이는 주어진 문제를 구체적 절차로 환원해서 연결하는 방식으로 작성 단계에서는 직관적이고 편리하다.
기능을 추가하거나 수정하며 프로그램이 고도화되면 문제가 생긴다.
기능 간에 의존성이 높아 중간에 무언가를 추가했을 때 예상치 못한 오류가 발생하기 쉽다.
또한, 외부 변수를 참조하고 조작하는 사이드 이펙트로 흐름을 파악하기 어렵다.
유사한 절차를 재사용하기 어려워 다시 작성하는 번거로움이 발생한다.
객체 지향 프로그래밍의 방식
유사한 역할을 하는 절차와 변수를 객체로 그룹화한다.
외부에서 객체의 내부를 참조하거나 추가하지 못하도록 제한한다. 대신 인터페이스로 조작한다.
장점
추상성 | 개발자는 복잡한 절차 간에 연결을 분석하거나 유지보수할 때 객체를 단위로 추상적인 파악이 가능하다.
캡슐화 | 객체는 캡슐화되어 있으므로 외부 인터페이스를 제외하고는 신경쓸 필요가 없다.
상속 | 유사한 역할을 하는 객체들은 모객체를 만들어 상속할 수 있다. 이를 통해 자객체는 동일한 기능을 재작성을 필요가 없어 생산성이 증가한다.
다형성 | 모객체의 변수나 메소드를 오버라이드 함으로써 상속 관계의 경직성을 어느정도 해결가능하다.
재사용성 | 객체는 다른 객체와 인터페이스로만 통신할 뿐 독립적이므로, 재사용에 용이하다.
단점
간단한 기능조차 객체를 만들고 내부 로직과 인터페이스를 만들며 인스턴스를 생성해야 하니 코드양이 많아진다.
Class와 인스턴스
클래스는 객체를 만드는 틀이다. 아래는 java에서 클래스를 생성하는 예시이다.
import java.util.ArrayList;
import java.util.HashMap;
// 클래스 선언
public class LottoGame {
// 멤버 변수
private int reward;
private ArrayList<LottoType> lottos;
// 생성자 함수
public LottoGame() {
this.reward= 0;
this.lottos = new ArrayList<LottoType>();
}
// 메서드
public int getReward() {
return reward;
}
public void setReward(int reward) {
this.reward = reward;
}
public ArrayList<LottoType> getLottos() {
return lottos;
}
public void setLottos(ArrayList<LottoType> lottos) {
this.lottos = lottos;
}
// 정적 메서드
public static int randomLotto() {
return (int)(Math.random() *45);
}
}
인스턴스 생성
new와 함께 클래스를 호출하여 인스턴스를 생성한다.
인스턴스란, 클래스를 틀 삼아 생성된 객체를 의미한다.
LottoGame lottoGame =new LottoGame();
클래스의 구성
- 멤버 변수(속성)
- 생성자 함수
- 메서드
- 정적 메서드
생성자함수
new로 클래스를 호출할때 제일 먼저 호출되는 함수다. 호출 시, 새로운 인스턴스가 생성되고 함수 내용이 실행된다.
생성자는 반환 타입이 없고 클래스와 동일한 이름이어야 한다.
만약 생성자를 작성하지 않는다면 컴파일 시점에서 기본 생성자를 만든다.
public LottoGame(String username) {
this.reward= 0;
this.lottos = new ArrayList<LottoType>();
this.username =username;
}
this는 생성자의 내부를 가리킨다. 생략가능하지만, 외부에서 주입받는 변수와 생성자 내부의 변수가 동일할 경우의 구분을 위해서 this를 사용하면 편리하다.
복수 생성자
클래스는 여러 개의 생성자 함수를 가질 수 있다.
이런 경우 클래스의 호출 시점에서 주어지는 매개 변수에 따라 자동으로 알맞는 생성자가 호출된다.
// 여러 개의 생성자를 가진 경우
public LottoGame(String username) {
this.reward= 0;
this.lottos = new ArrayList<LottoType>();
this.username =username;
}
public LottoGame(String username, int id) {
this.reward= 0;
this.lottos = new ArrayList<LottoType>();
this.username =username;
this.id =id;
}
// 호출 시
LottoGame lottoGame1 = new LottoGame("charchar", 1);
// 주입되는 생성자에 알맞는 2번째 생성자가 호출됨
복수 생성자는 class를 생성할 때 유연성을 줄 수 있다.
메서드
인스턴스에 할당된 함수이다.
네임 컨벤션으로 동사+ 목적어 형태에 카멜케이스가 일반적이다.
중복 메서드(overloading)
동일한 이름의 메소드를 선언하는 것.
호출부의 매개 변수의 차이(타입이나 갯수)에 따라 거기에 부합하는 것이 호출.
중복 메서드 간에 변수 차이가 없으면 컴파일 오류가 발생한다.
static
멤버 변수나 메서드에 static을 붙이면 인스턴스가 아니라 클래스 자체에 할당된다.
static은 객체 간에 속성이나 기능을 공유할 때 유용.
주의사항
static 메소드는 this로 인스턴스 속성을 참조할 수 없다.
정적 메소드 호출은 인스턴스에서도 가능하다. 단, 가독성과 성능 측면에서 클래스로 호출을 추천한다.
'코드 디자인 패턴 > java' 카테고리의 다른 글
[java & 디자인 패턴] Observer 패턴 학습 정리 (0) | 2024.07.14 |
---|---|
[java & 디자인 패턴] Builder 패턴 학습 정리 (0) | 2024.07.14 |
[java & 디자인 패턴] flyweight 패턴 학습 내용 정리 (0) | 2024.07.14 |
[java & 디자인 패턴] singleton 패턴 학습 내용 정리 (0) | 2024.07.07 |
[java/객체 지향] 상속과 클래스, 인터페이스, 추상 클래스 (0) | 2024.04.21 |