1. 객체 지향 프로그래밍
1-1. 정의와 장점
객체 지향 프로그래밍(OOP)은 순차적 프로세스에서 벗어나서 프로그램을 여러 독립적인 객체들의 유기적인 결합으로 바라보는 패러다임이다.
객체 지향 프로그래밍은 아래와 같은 장점이 있다.
- 객체라는 부품을 갈아끼우는 개념으로 유연하고 변경이 용이한 프로그램을 만들 수 있음
- 코드 재사용을 통해 반복을 줄이고 코드를 간결하게 표현할 수 있음
1-2. OOP의 객체
위에서 설명했듯이 OOP의 기본 단위는 객체이다.
OOP에서 프로그램은 객체의 상호작용을 통해서 동작하기 때문이다.
OOP에서 객체를 추상화하여 속성(변수)와 기능(함수)로 분류할 수 있다.
2. 객체 지향 프로그래밍의 특징
2-1. 추상화
추상화는 객체의 역할과 구현을 독립적으로 정의하는 것이다.
어떤 객체를 추상화하기 위해서 인터페이스나 추상 클래스를 사용할 수 있다.
- 인터페이스 : 어떤 객체에 필요한 핵심적인 역할을 규정하고, 실제 구현은 구현 클래스에 위임
- 추상 클래스 : 공통된 변수와 메서드를 정의하고, 세부적인 구현은 자식 클래스에 위임
이것만 봐서는 정확하게 이해되지 않는다. 아래 예시를 통해 알아보자.
public interface FileManager{
String tag = "oop";
// 파일 저장 및 경로 반환
String saveFile(String fileName, byte[] file);
// 파일 조회 및 바이트 배열로 반환
byte[] readFile(String fileName);
}
위 코드는 파일을 저장 및 불러오는 FileManager라는 인터페이스이다.
다른 FileManager 구현체에서 공유하는 tag라는 변수를 FileManager에 정의했다.
또한, 파일 저장과 불러오기 함수 시그니처를 정의하여 코드 변경 발생 시 수정 사항을 최소화한다.
이러한 인터페이스는 아래와 같이 사용된다.
@Component
public class LocalFileManager implements FileManager{
@Override
String saveFile(String fileName, byte[] file){
// todo : 호스트 환경에 파일 저장
return "/tmp/oop/" + fileName + FileManager.tag;
}
@Override
byte[] readFile(String fileName){
Path path = Paths.get("/tmp/oop/" + fileName);
return Files.readAllBytes(path);
}
}
@Component
public class ExternalFileManager implements FileManager{
// 외부 클라우드 저장소에서 파일 관리
@Autowired
private AwsS3FileManager s3FileManager;
@Override
String saveFile(String fileName, byte[] file){
return s3FileManager.upload(fileName+FileManager.tag);
}
@Override
byte[] readFile(String fileName){
return s3FileManager.download(fileName);
}
}
위 코드에서 인터페이스는 구현체 간 일종의 약속으로 작용했다. 좀 더 자세히 살펴보자
- tag 변수를 인터페이스에 정의했기에 코드 중복이 줄어 간결해졌다.
- 메서드 시그니처가 인터페이스에 고정되어 있으므로, 구현이 변경되더라도 이를 사용하는 다른 객체의 수정사항을 최소화한다.
- FileManager에 필요한 역할을 미리 판단 및 정의하므로 불필요한 기능들을 추가로 구현하게되는 문제를 최소화한다.
정리하자면, 추상화는 아래와 같은 장점을 갖는다.
- 유연한 설계 : 유지보수 시 코드의 수정으로 인한 수정사항을 최소화할 수 있음
- 복잡도 감소 : 객체의 역할에 대한 핵심적인 부분만을 추출하고, 나머지는 제거하여 복잡도 관리가 가능함
- 간결한 코드 : 여러번 반복 정의되는 변수에 대한 중복 제거가 가능함
2-2. 상속
상속은 클래스를 재사용하여 새로운 클래스를 작성하는 자바 문법이다.
즉, 부모 클래스의 기능과 속성을 자식 클래스가 재사용할 수 있기에 코드 반복을 줄이고 간결한 코드를 작성하기 좋다.
아래 예시를 통해서 보다 자세히 알아보자.
public class ProcessManager{
private final Map<String, ProcessHolder> processes = new HashMap<>();
public void stopProcess(String key){
// todo : stop process
}
public String startProcess(Process process){
// todo : start process
}
public Process getProcess(String key){
// todo : retrieve process from processes
}
}
public class RecordingProcessManager extends ProcessManager{
private Process loadRecordingProcess(){
// todo : load process
}
@Override
public void stopProcess(String key){
// todo : 필요한 코드
super.stopProcess(key);
}
@Override
public String startProcess(){
Process process = loadRecordingProcess();
return super.saveProcess(process);
}
}
위 코드는 애플리케이션 내 Process 객체의 생명주기를 관리하는 두 클래스가 정의되어 있다.
- ProcessManager : Process 객체 저장, 조회 및 제거 담당
- RecordingProcessManager : 녹화 작업의 Process 객체에 대한 생명주기 담당
위 코드는 Process 관리 코드를 부모 클래스에 위임하고, 자식 클래스는 해당 코드를 재사용하는 방색을 채택한다.
이는 코드 중복과 실수를 줄이고, Process 관리 코드 수정 시 하위 클래스의 수정사항도 줄일 수 있다.
[ 상속 vs 추상화 ]
얼핏 보면 상속과 추상화는 비슷해보이지만, 매우 큰 차이가 있다.
- 상속은 코드의 재사용에 초점을 두었기에 역할을 정의하는 추상화와는 차이가 있음
- 인터페이스 기반 추상화는 인터페이스 내용을 모두 정의해야하지만, 상속은 오로지 부모 클래스의 기능이나 속성을 재사용하면 된다.
즉 추상화는 역할과 구현을 분리하는 것이고, 상속은 기능을 재사용하는데 초점이 있다고 볼 수 있다.
따라서 두 기술의 이점을 고려하여 적절한 상황에서 구분 및 응용하여 사용하는 것이 중요하다.
2-3. 다형성
드디어 객체 지향 프로그래밍의 꽃, 다형성이다.
다형성은 객체의 속성이나 기능이 상황에 따라서 여러가지 형태를 가질 수 있는 성질이다.
더 나아가서 OOP의 다형성은, 한 타입의 참조변수가 여러 타입의 객체를 참조할 수 있도록 하는 것이다.
마찬가지로 기존에 사용했던 FileManager 예시를 통해 다형성에 대해서 알아보자.
public interface FileManager{
String tag = "oop";
// 파일 저장 및 경로 반환
String saveFile(String fileName, byte[] file);
// 파일 조회 및 바이트 배열로 반환
byte[] readFile(String fileName);
}
@Component
public class LocalFileManager implements FileManager{
@Override
String saveFile(String fileName, byte[] file){
// todo : 호스트 환경에 파일 저장
return "/tmp/oop/" + fileName + FileManager.tag;
}
@Override
byte[] readFile(String fileName){
Path path = Paths.get("/tmp/oop/" + fileName);
return Files.readAllBytes(path);
}
}
@Component
public class ExternalFileManager implements FileManager{
// 외부 클라우드 저장소에서 파일 관리
@Autowired
private AwsS3FileManager s3FileManager;
@Override
String saveFile(String fileName, byte[] file){
return s3FileManager.upload(fileName+FileManager.tag);
}
@Override
byte[] readFile(String fileName){
return s3FileManager.download(fileName);
}
}
일반적으로 OOP에서는 인터페이스와 그 구현체 사이에는 is-a 관계가 형성된다고 말한다.
- LocalFileManager is-a FileManager
- ExternalFileManager is-a FileManager
그리고 Java는 인터페이스 타입의 참조변수로 인터페이스 구현체 객체를 참조할 수 있다.
public class ImageService{
private FileManager fileManager;
public ImageService(FileManager fileManager){
this.fileManager = fileManager;
}
public void save(byte[] file){
// todo : save file with fileManager
// todo : save file on DB
}
public FileDto load(String fileName){
// todo : get file with fileManager
// todo : get file info from DB and return
}
}
public class Main{
public static void main(String[] args){
ImageService is1 = new ImageService(new LocalFileManager());
ImageService is2 = new ImageService(new ExternalFileManager());
}
}
이제 위 코드에 대해서 살펴보자.
- ImageService는 다형성을 통해 FileManager 인터페이스를 통해서 구현체를 참조하는 구조를 갖는다
- 다형성 극대화를 위해 구현체를 의존관계 주입을 통해서 받고 있고, 이를 통해 ImageService와 구현체 간 결합을 느슨하게 하고 있다.
다형성의 핵심은 상황에 따라서 적절한 전략을 선택할 수 있고, 객체 간 결합을 느슨하게 하는 것이라고 생각한다.
이를 통해서 상황에 따른 적절한 전략을 선택하는데 있어 수정사항을 최소화할 수 있다는 장점이 있다.
2-4. 캡슐화
마지막으로 OOP의 마지막 특징인 캡슐화에 대해서 알아본다.
캡슐화는 클래스 내 속성이나 기능을 선택적으로 외부에 노출하는 것이다.
캡슐화의 장점은 다음과 같다.
- 데이터 보호 : 외부로부터 클래스 속성과 기능을 보호
- 데이터 은닉 : 내부 동작을 감싸고, 필요한 부분만을 노출
[ 접근제어자를 통한 캡슐화 ]
접근제어자는 클래스 내 속성이나 기능의 노출 수준을 설정하는 키워드이다.
Java에서는 private, public, default, protected 키워드를 제공한다.
| 키워드 | 접근 수준 |
| private | 동일 클래스 내에서만 접근 가능 |
| protected | 동일 패키지와 다른 패키지의 하위 클래스에서 접근 가능 |
| public | 접근 제한 없음 |
| default | 동일 패키지 내에서만 접근 가능 |
다음으로 간단한 예시를 통해서 살펴보자. 기존에 사용한 ProcessManager 예제를 사용한다.
public class ProcessManager{
private final Map<String, ProcessHolder> processes = new HashMap<>();
public void stopProcess(String key){
// todo : stop process
}
public String startProcess(Process process){
// todo : start process
}
public Process getProcess(String key){
// todo : retrieve process from processes
}
}
public class RecordingProcessManager extends ProcessManager{
private Process loadRecordingProcess(){
// todo : load process
}
@Override
public void stopProcess(String key){
// todo : 필요한 코드
super.stopProcess(key);
}
@Override
public String startProcess(){
Process process = loadRecordingProcess();
return super.saveProcess(process);
}
}
이 코드에서는 작업의 시작 및 종료 기능은 외부로 노출하고 있지만, 그 외 기능은 private을 통해서 접근을 제한한다.
- processes 등 민감한 기능에 접근을 제어하여 데이터를 보호한다.
- loadRecordingProcess와 같이 불필요한 기능을 은닉하여 사용자에게 필요한 기능만을 노출한다.
[ Getter / Setter를 통한 캡슐화 ]
이는 클래스 내부 필드에 대한 접근을 제어하기 위해서 별도의 메서드를 생성하는 방식이다.
public class PageObject{
private int pageSize = 0;
private String password = "secret-password";
public PageObject(int pageSize, String password){
this.pageSize = pageSize;
this.password = password;
}
public void setPageSize(int pageSize){
if(pageSize<0) throw new IllegalArgumentException("");
this.pageSize = pageSize;
}
public int getPageSize(){
return this.pageSize;
}
}
위 코드에서는 다음과 같이 캡슐화가 적용되었다.
- password와 같은 민감한 필드에 대한 접근 은닉
- setter를 통한 내부 필드 상태 보호
3. 마무리
최근 대학 생활을 하면서 생각보다 내가 OOP에 대해서 잘 알지 못한다는 느낌을 받아서 이에 대해서 다시 공부하게 되었다.
지금까지는 인터페이스를 왜 쓰고, 어떤 것이 다형성인지 등을 경험적으로 알고 있었지만 설명하지는 못하는 수준이었다고 생각한다.
하지만 이번에 학습하면서 어떻게 객체 지향 프로그래밍 패러다임에 맞게 프로그램을 설계해야하는지 어느정도 갈피를 잡은 것 같다.
앞으로도 이러한 기초적이지만 어려운 개념들에 대해서 계속해서 학습해볼 예정이다.
'CS 및 기본 개념' 카테고리의 다른 글
| [ cs ] Dependency Management (0) | 2025.09.15 |
|---|---|
| [ cs ] Object Oriented Method (1) | 2025.09.10 |
| [ Java 공식문서 ] Lock과 Executor (1) | 2025.08.20 |
| [Java 공식문서] 불변 객체 (4) | 2025.08.15 |
| [cs] Cache Control 헤더 (4) | 2025.08.13 |