우선 절차적 프로그래밍과 객체지향 프로그래밍은 서로 대조되는 개념이 아니다. 서로 상반되는 개념이 아니며 절차적 프로그래밍의 한계점들과 문제를 해결하기 위해 객체지향 프로그래밍 방식이 개발되었다.
절차적 프로그래밍은 순차적인 처리를 바탕으로 프로그램 전체가 유기적으로 실행되도록 한 프로그래밍 방식이다. 절차적 프로그래밍은 procedure를 이용하여 작성되는데, procedure에는 루틴, 서브루틴, 함수 등이 존재한다.
루틴
프로그램에서 반복적이거나 독립적으로 실행할 수 있는 코드 블럭을 뜻하며 main문 외에도 서브루틴이나 함수 역시 루틴으로 간주됨
서브루틴
main문 밖에서 정의된 코드 블럭이며 반환값이 없는 작업을 수행함
void printMessage() {...}
함수
main문 밖에서 정의된 코드 블럭이며 특정 작업을 수행한 뒤 반환값을 제공하는 작업을 수행함
int add(int a, int b) { return a + b; }
절차형 프로그래밍은 모듈 구성이 용이하며 구조적인 프로그래밍이 가능하다. 또한 컴퓨터의 처리구조와 유사해 실행 속도가 빠르다는 장점이 존재한다.
그런데 절차형 프로그래밍에서는 전역 변수를 통해 프로그램의 상태를 관리하는데, 이로 인해 여러 가지 한계점이 발생한다.
전역 변수를 통한 프로그램 상태 관리로 인해 추상적인 모델링이 어렵다. 예를 들어 복소수와 같은 새로운 자료형을 직접 도입할 수 없어 복잡한 자료구조나 데이터 관리에 제약이 생긴다.
모든 함수와 변수가 전역에서 공유되기 때문에 새로운 자료구조나 기능을 추가할 때 이름 충돌이 발생하기 쉽다. 이로 인해 코드의 재사용성이 낮아지며, 유지보수가 어려워진다.
전역 변수를 통해 여러 함수가 상태를 공유하다보니, 하나의 함수에서 발생한 오류가 전체 프로그램으로 확산될 위험이 있고, 오류 발생 시 전 프로그램 영역이 디버깅 대상이 되어 디버깅이 복잡하고 어려워진다.
이러한 한계점들과 문제를 해결하기 위해 객체지향 프로그래밍 방식이 개발되었다.
객체지향 프로그래밍은 프로그래밍에 필요한 속성(attribute)과 메서드(method)를 가진 클래스(class)를 정의하고, 정의된 클래스를 통해서 각각의 객체(object)를 생성하여, 객체들 간의 유기적인 상호작용을 통해 로직을 구성하는 프로그래밍 방법을 말한다.
간단하게 예를 들면 붕어빵을
const 팥붕어빵 = {
"flavor": "팥",
"per": 3,
"price": 1000
};
const 슈크림붕어빵 = {
"flavor": "슈크림",
"per": 2,
"price": 1000
};
const 피자붕어빵 = {
"flavor": "피자",
"per": 1,
"price": 1000
};
다음과 같이 일일히 생성하는 것이 아닌
class 붕어빵 {
constructor(flavor, per, price) {
this.flavor = flavor;
this.per = per;
this.price = price;
}
}
const 팥붕어빵 = new 붕어빵("팥", 3, 1000);
const 슈크림붕어빵 = new 붕어빵("슈크림", 2, 1000);
const 피자붕어빵 = new 붕어빵("피자", 1, 1000);
틀에 찍어내듯이 클래스를 정의하여 사용하는 방식이다.
객체지향 프로그래밍에는 중요한 4가지 개념인 캡슐화, 상속, 추상화, 다형성이 존재한다.
캡슐화는 데이터와 해당 데이터를 사용하는 함수를 같은 캡슐 혹은 컨테이너에 넣는 것을 의미한다. 이 경우 캡슐(컨테이너)는 class를 의미한다.
예를 들어
const 팥붕어빵 = {
"flavor": "팥",
"per": 3,
"price": 1000
};
function calculateOne붕어빵(per, price) {
return price / per;
}
calculateOne붕어빵(팥붕어빵.per, 팥붕어빵.price);
다음과 같이 붕어빵과 붕어빵 하나의 가격을 계산하는 함수가 있을 때 데이터와 함수가 개념적으로 연결되어 있기 때문에 캡슐화를 사용해서 다음과 같이 코드를 개선할 수 있다.
class 붕어빵 {
constructor(
private flavor: string,
private per: int,
private price: int
) {}
public calculateOne붕어빵() {
return this.price / this.per;
}
}
const 팥붕어빵 = new 붕어빵("팥", 3, 1000);
팥붕어빵.calculateOne붕어빵();
이제 코드가 좀 더 구조화되었고, 함수가 this 키워드를 사용하여 데이터에 직접 접근할 수 있기 때문에 인수를 전달할 필요가 없어졌다.
이때 캡슐화를 통해 공개하거나 숨길 속성을 정할 수 있는데 위의 경우 모든 속성이 private이기 때문에 다음과 같은 코드는 작동하기 않는다.
팥붕어빵.flavor = "와사비맛";
팥붕어빵.per = 999;
public, private와 같은 키워드를 접근자라 하는데, java에는 다음과 같은 접근자들이 존재한다.
즉, 캡슐화는 클래스의 정보에 접근하거나 수정하는 방법을 결정할 수 있다. 예를 들어 붕어빵 클래스의 flavor
필드를 public으로 설정하면 해당 필드를 공개하는 것이 가능해진다.
정리하자면, 캡슐화는 데이터를 포함하고 관련된 함수를 정리하며, 공개 범위에 대한 설정을 통해 외부에서의 접근을 제어하는 방법론이다.
클래스의 상속을 통해 코드를 더 작은 단위로 쪼개고 재사용할 수 있다.
예를 들어 붕어빵 외에 오뎅이라는 클래스가 존재하고 per, price, shape 라는 필드가 존재한다. 이때, 중복된 필드들에 대해 별도 클래스를 만드는 대신 상속을 이용해 다음과 같이 정의가 가능하다.
class 음식 {
constructor(
private per: int,
private price: int
) {}
}
class 붕어빵 extends 음식 {
constructor(
per: int,
price: int,
private flavor: string
) {
super(per, price);
}
}
class 오뎅 extends 음식 {
constructor(
per: int,
price: int,
private shape: string
) {
super(per, price);
}
}
즉, 음식 클래스의 per과 price 속성을 붕어빵과 오뎅 클래스로 상속할 수 있다.
상속은 코드를 작성하는데 있어 분할정복을 가능하게 해주는데, 클래스들을 작게 쪼갠 뒤 마치 레고처럼 클래스들을 조립할 수 있게 해준다.
추상화는 구현 세부 정보를 숨기는 일반 인터페이스를 지정하는 행위라 볼 수 있다.
예를 들어 운전자는 자동차 회사로부터 제공된 악셀, 브레이크 등의 인터페이스를 통해 자동차의 세부 구현 정보를 모르고도 운전이 가능하다.
코드에서의 경우 DB 구현시
public class MemberService {
private MemberRepository memberRepository = new MemoryMemberRepository();
}
MemoryMemberRepository를 사용하던
public class MemberService {
private MemberRepository memberRepository = new JdbcMemberRepository();
}
JdbcMemberRepository를 사용하던 간에 어떤 DB를 사용하는지에 상관없이 유저는 어플리케이션의 사용이 가능하다. 즉, 사용자는 각 메서드의 구현 정보를 알 필요 없이 그리고 DB의 변경이 발생할지라도 뭔가를 바꿀 필요 없이 그대로 쓸 수 있다. 왜냐하면 인터페이스는 그대로 유지되었기 때문이다.
다형성은 다양한 형태라는 뜻으로 메서드 오버라이딩과 관련이 있다.
오버라이딩
오버라이딩은 상속 또는 구현 관계에 있는 상위 객체의 메소드를 하위 객체 또는 구현 객체가 재정의하여 사용하는 메소드 정의 기법이다.
interface Animal { public String sound (); } public 고라니 implements Animal { @Override public String sound() { return "야!!!!!!!!!!!!!!!!!"; } }
부모 클래스(인터페이스)로 부터 상속(extend, implements)을 받고 메서드를 오버라이딩 하는데 이때 메서드가 어떻게 작동해야 하는지 규칙이 정해졌다. 즉, 클래스의 핵심은 그대로 있고 구현방식의 모양과 모습은 달라지게 되었다.
이 4가지 개념이 객체지향 프로그래밍의 핵심 개념들이고 좋은 객체 지향 설계를 위한 5가지 원칙인 SOLID가 존재하는데 그건 다음에 보기로 하자.