※ 본문은 혼자 공부한 내용을 기록한 글입니다. 오개념이 있다면 댓글로 알려주세요!
[ 0 ] 절차 지향 프로그래밍
절차 지향 프로그래밍(Procedural Programming)이란, 물이 위에서 아래로 흐르는 것처럼 순차적인 처리가 중요시되며 프로그램 전체가 유기적으로 연결되는 프로그래밍 기법이다.
절차 지향 프로그래밍은 컴퓨터의 작업 처리 방식과 유사하기 때문에 객체 지향 프로그래밍에 비해 프로그램의 실행 속도가 빠르다는 장점이 있다. 하지만, 유지보수가 어렵고, 실행 순서가 정해져 있으므로 코드의 순서가 바뀌면 동일한 결과를 보장하기 어렵다는 단점이 있다.
대표적인 절차 지향 프로그래밍 언어로는 C언어가 있다.
[ 1 ] 객체 지향 프로그래밍
객체 지향 프로그래밍(Object-Oriented Programming, OOP)이란 절차 지향 프로그래밍에서 벗어나 컴퓨터 프로그램을 '객체'들의 유기적인 협력과 결합으로 파악하고자 하는 프로그래밍 기법이다.
하나의 자동차가 수 많은 부품들의 결합과 연결로 만들어지는 것처럼, OOP에서 하나의 프로그램은 부품에 해당하는 '객체'를 먼저 만들고, 그 객체들을 조립함으로써 만들어진다.
OOP의 장점은 다음과 같다.
- 프로그램을 유연하고 변경이 용이하도록 만들 수 있다.
- 객체 지향적 원리를 잘 적용하여 각 객체가 독립적인 역할을 갖게 한다면 코드의 변경을 최소화하고 유지보수를 쉽게 할 수 있다.
- 코드의 재사용을 통해 반복적인 코드를 최소화하고, 코드를 최대한 간결하게 표현할 수 있다.
- OOP는 실세계를 프로그램 설계에 반영할 수 있도록 발전했기에, 인간 친화적이고 직관적인 코드를 작성하는 데 용이하다.
이러한 객체 지향의 장점들을 잘 살릴 수 있는 OOP의 특징으로는 추상화, 상속, 다형성, 캡슐화가 있다.
[ 2 ] 추상화
'추상'이란 '사물이나 표상을 어떤 성질, 공통성, 본질에 착안하여 그것을 추출하여 파악하는 것'이다. 즉, 추상화의 핵심 개념은 '공통성과 본질을 모아 추출'하는 것이며, OOP에서의 추상화는 '객체의 공통적인 속성과 기능을 추출하여 정의하는 것'을 의미한다.
예를 들어, K3와 셀토스는 모두 자동차이며 모든 자동차는 시동을 걸고, 전진과 후진을 할 수 있다는 공통점을 가진다. 이를 자바 문법으로 나타내면, K3와 셀토스라는 하위 클래스들의 공통적인 기능(시동 걸기, 전진 및 후진)을 추출하여 '자동차'라는 상위 클래스로 정의할 수 있다. 이처럼 자바에서 추상화를 구현할 수 있는 문법 요소로는 추상 클래스와 인터페이스가 있는데, '자동차'를 인터페이스로 정의한다고 가정하자.
public interface Car {
public abstract void start();
void moveForward();
void moveBackward();
}
우선, K3와 셀토스의 공통 기능을 추출하여 Car 인터페이스에 정의한다. 즉, 인터페이스에는 어떤 객체가 수행해야 하는 핵심적인 역할만을 규정하는 것이다. 해당 인터페이스를 구현하는 각각의 클래스에 실제적인 구현을 한다.
public class K3 implements Car {
@Override
public void start() {
System.out.println("K3의 시동을 겁니다.");
}
@Override
public void moveForward() {
System.out.println("K3가 전진합니다.");
}
@Override
public void moveBackward() {
System.out.println("K3가 후진합니다.");
}
}
public class Seltos implements Car {
@Override
public void start() {
System.out.println("Seltos의 시동을 겁니다.");
}
@Override
public void moveForward() {
System.out.println("Seltos가 전진합니다.");
}
@Override
public void moveBackward() {
System.out.println("Seltos가 후진합니다.");
}
}
매우 간단한 예제이지만, K3와 Seltos 클래스는 앞서 Car 인터페이스에 정의한 역할을 각 클래스의 맥락에 맞게 구현하고 있다.
이것을 OOP에서는 역할과 구현의 분리라고 하며, OOP의 다른 특징인 '다형성'과 함께 유연하고 변경이 용이한 프로그램을 설계하는 데 핵심적인 내용이다.
[ 3 ] 상속
상속이란 기존의 클래스를 재활용하여 새로운 클래스를 작성하는 자바의 문법이다.
클래스 간 공유될 수 있는 속성과 기능들을 추출하여 상위 클래스(혹은 인터페이스)로 정의한 것이 '추상화'였다면, '상속'은 상위 클래스로부터 확장된 여러 개의 하위 클래스들이 상위 클래스의 모든 속성과 기능들을 그대로 물려받아 사용할 수 있는 것을 의미한다.
즉, 추상화를 통해 클래스 간 공유하는 속성과 기능을 딱 한 번만 정의하고, 상속을 통해 해당 속성과 기능을 간편하게 재사용하며 코드의 중복을 줄일 수 있는 것이다.
[ 4 ] 다형성
다형성의 개념에 대한 내용은 해당 게시글에서 확인하자.
위의 예제에서 Driver 클래스를 추가하고, Car 인터페이스가 없다고 가정하자.
만약 Driver가 K3와 셀토스를 운전한다면 다음과 같이 코드를 작성할 수 있을 것이다.
public class Driver {
private final K3 k3 = new K3();
private final Seltos seltos = new Seltos();
void driveK3() {
k3.start();
k3.moveForward();
k3.moveBackward();
}
void driveSeltos() {
seltos.start();
seltos.moveForward();
seltos.moveBackward();
}
}
하나의 객체(A클래스)가 다른 객체(B클래스)의 속성과 기능에 접근하여 어떤 기능을 사용하는 것을 "A클래스는 B클래스에 의존한다"라고 표현한다.
즉, 위의 예제에서 Driver 클래스는 K3 클래스와 Seltos 클래스에 의존하고 있다고 설명할 수 있다. 하지만, Driver 클래스와 나머지 두 개의 클래스는 서로 직접적인 관계를 맺고 있으며 이는 "객체들 간의 결합도가 높다"고 표현하는데, 결합도가 높은 상태는 객체 지향적 설계를 하는 데 불리하다.
Driver는 운전면허만 있으면 어떤 자동차든 운전할 수 있다. 그런데, 자동차의 종류는 수천여 개이며 Driver 클래스에 모든 자동차 class를 추가해야 한다면 어떨까? 심지어 각각의 클래스에서 내부 변경이 나타나 Driver 클래스를 수정해야 한다면? 코드의 중복이 발생하며 유지보수가 어려워 질 것이다.
이를 해결하는 데 다형성을 활용할 수 있다. 위에서 정의했던 Car 인터페이스를 사용하자.
public class Driver {
private final Car car = new K3();
void drive(Car car) {
car.start();
car.moveForward();
car.moveBackward();
}
}
Car 인터페이스를 활용하면 더 이상 모든 종류의 자동차 class를 추가할 필요가 없고, 현재 운전하고 있는 자동차 class의 인스턴스만 생성해 주면 된다. 또한, Driver 클래스는 각각의 클래스에서 내부 변경이 나타나더라도 신경 쓸 필요가 없기 때문에(인터페이스에 역할이 다 정의되어 있고 각 클래스는 그에 따라 구현되었으므로), 코드의 중복이 나타나지 않고 유지보수가 쉬워진다.
이런 맥락에서 OOP는 추상화, 상속, 그리고 다형성의 특성을 활용하여 프로그래밍을 설계할 때 역할과 구현을 구분하여 객체들 간의 직접적인 결합을 피하고, 느슨한 관계 설정을 통해 보다 유연하고 변경이 용이한 프로그램 설계를 가능하게 만든다. 이 부분이 OOP의 핵심이다.
하지만, 여전히 Driver 클래스 내부에서 객체를 생성할 때 new K3() 처럼 객체에 직접적으로 의존하고 있어서, 해당 객체를 다른 객체로 변경할 때 코드의 변경이 나타난다. 즉, 여전히 객체 간 높은 결합도를 보이고 있는 것이다.
스프링 프레임워크는 이를 해결하기 위해 'DI(Dependency Injection) - 의존관계 주입'라는 핵심 기술을 제공하고 있으며 이에 대해서는 다음에 포스팅할 계획이다.
[ 5 ] 캡슐화
캡슐화란 클래스 내부의 서로 연관있는 속성과 기능들을 하나의 캡슐로 만들어 데이터를 외부로부터 보호하는 것을 의미한다.
캡슐화를 하는 이유로는 크게 두 가지가 있다.
- 데이터 보호 : 외부로부터 클래스에 정의된 속성과 기능들을 보호하기 위해서
- 데이터 은닉 : 내부 로직을 감추고 외부에는 필요한 부분만 노출하기 위해서
또한, 데이터 보호, 데이터 은닉을 통해 하나의 객체가 해당 객체의 속성과 기능에 대해 독점적인 책임을 지도록 할 수 있고, 결과적으로 객체 간의 결합도를 낮출 수 있다. 어떻게 이것이 가능할까?
public class Car {
public void start() {
System.out.println("차의 시동을 겁니다.");
}
public void moveForward() {
System.out.println("차가 전진합니다.");
}
public void moveBackward() {
System.out.println("차가 후진합니다.");
}
}
public class Driver {
private final Car car = new Car();
public void drive() {
car.start();
car.moveForward();
car.moveBackward();
}
}
Driver 클래스의 drive() 메서드 내부에는 Car 클래스의 메서드들이 순차적으로 실행되고 있다. 이는 Driver 클래스가 Car 클래스의 내부 로직을 잘 알고 있기에 객체 간의 결합도가 높음을 의미한다.
즉, Car 클래스의 3개의 메서드에 변경이 생기면 Driver 클래스의 drive() 메서드의 변경도 불가피할 것이다. 이를 해결하기 위해 Car 클래스의 서로 연관있는 기능들을 하나의 캡슐로 만들어 보자.
※ 캡슐화 구현 방법에는 접근제어자, getter/setter가 있으며 여기에서는 접근제어자를 활용한다.
package encapsulation;
public class Car {
private void start() {
System.out.println("차의 시동을 겁니다.");
}
private void moveForward() {
System.out.println("차가 전진합니다.");
}
private void moveBackward() {
System.out.println("차가 후진합니다.");
}
public void operate() {
start();
moveForward();
moveBackward();
}
}
이전 코드와 return 값은 동일하지만 서로 연관있는 3개의 메서드를 operate() 메서드로 묶었고, 이제 Driver 클래스에서는 Car 클래스의 내부 로직을 전혀 알지 못해도 단순히 operate() 메서드를 호출하여 같은 return 값을 받을 수 있다.
또한, 더 이상 operate() 메서드 내부의 메서드들은 외부에서 호출되어 사용할 일이 없으므로 접근제어자를 private로 변경하였으며 이제 Car 클래스와 관련된 기능들은 온전히 Car 클래스에서만 관리되도록 구현하며 불필요한 내부 로직의 노출을 최소화하였다.
이렇게 캡슐화를 통해 Driver 클래스와 Car 클래스가 자신의 속성과 기능에만 책임을 지도록 할 수 있고, 두 객체 간의 결합도를 낮출 수 있다.
'CS > 기본 용어' 카테고리의 다른 글
[용어 사전] UI와 API (0) | 2022.08.09 |
---|---|
[용어 사전] 컴파일 언어 vs 인터프리터 언어 (0) | 2022.07.12 |