[ cs ] 객체 지향 프로그래밍( OOP ) 4가지 특징

2025. 9. 4. 21:44·CS 및 기본 개념

 

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
'CS 및 기본 개념' 카테고리의 다른 글
  • [ cs ] Dependency Management
  • [ cs ] Object Oriented Method
  • [ Java 공식문서 ] Lock과 Executor
  • [Java 공식문서] 불변 객체
코드래곤
코드래곤
코드래곤 님의 블로그 입니다.
  • 코드래곤
    코드래곤 님의 블로그
    코드래곤
  • 전체
    오늘
    어제
    • 분류 전체보기 (61)
      • 알고리즘 (3)
        • 그리디 (1)
        • 그래프 (2)
      • 시스템 설계 (6)
      • CS 및 기본 개념 (17)
      • Docker (5)
      • Spring (23)
        • 백준 서비스 구현하기 (1)
        • 기초 개념 (14)
        • MSA (2)
        • JPA (1)
      • Dart (3)
      • Flutter (1)
      • Kubernetes (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
코드래곤
[ cs ] 객체 지향 프로그래밍( OOP ) 4가지 특징
상단으로

티스토리툴바