객체지향

황희윤·2023년 8월 18일

객체

자신의 데이터를 사용해 하나의 역할(기능)을 수행하는 독립된 단위

객체지향 4가지 특징

1. 추상화 (Abstraction)

  • 불필요한 부분을 제거함으로써 핵심만 나타낸 것
  • 목적 : 단순화로 복잡성을 낮춤
  • interfaceabstract를 사용
public interface Vehicle {
	public abstract void start(); // public abstract 키워드 생략 가능
    void moveForward();
    void moveBackward();
}

public class Car implements Vehicle {
	@Override
    void moveForward(){
    	System.out.println("자동차가 앞으로 갑니다.");
    }
    
    @Override
    void moveBackward(){
    	System.out.println("자동차가 뒤로 갑니다.");
    }
}
  • 인터페이스(interface)에는 추상적인 개념만을 규정하고, 실제적인 구현은 해당 인터페이스를 구현하는 클래스가 한다.

  • 추상화로 인해 보다 유연하고 변경에 열려있는 프로그램을 만들 수 있다.

implements VS extends

implements

  • 부모 클래스에서는 선언만 하며 자식 클래스에서 부모 객체의 구체적인 내용오버라이딩(재정의)해서 사용한다.

  • 부모의 메서드를 반드시 오버라이딩(재정의)해야 한다.

  • 하위 클래스에게 구체적인 구현을 하도록 강제한다.

  • 다중 상속을 지원한다.

  • 인터페이스(interface) 상속에 사용된다.

public class Son implements Father, Mother{...}

extends

  • 부모 클래스에서 선언 / 정의(내용)을 모두 할 수 있고, 자식은 부모의 속성과 메서드를 그대로 사용 가능

  • 오버라이딩(재정의)은 선택적이다.

  • 다중 상속을 지원하지 않는다.

  • 일반 클래스와 abstract 클래스 상속에 사용된다.

2. 상속 (inheritence)

  • 부모로부터 물려받는 것
  • 기존의 클래스를 재활용하여 새로운 클래스를 만듦
  • 추상화의 연장선에서, 여러 개의 클래스 중에서 공통적인 개념들을 상위 클래스추상화를 시키고, 나머지 하위 클래스들이 모두 상위 클래스의 메서드나 속성을 사용할 수 있다.
  • 코드의 재활용성을 높이고, 불필요한 코드를 줄인다.
public class Car extends Vehicle {
	boolean isConvertible;
    
    void openWindow(){
    	System.out.println("창문을 엽니다.");
    }
    
	@Override
    void moveForward(){
    	System.out.println("자동차가 앞으로 갑니다.");
    }
    
    @Override
    void moveBackward(){
    	System.out.println("자동차가 뒤로 갑니다.");
    }
}

3. 다형성 (Polymorphism 多形性)

  • 다양한 형태를 가지는 것
  • 어떤 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질
  • ex) 메서드 오버라이딩, 메서드 오버로딩(overloading)
  • 같은 이름의 메서드가 상황에 따라 다른 역할을 수행
	@Override
    void moveForward(){
    	System.out.println("자동차가 앞으로 갑니다.");
    }
  • 하나의 타입으로 여러 타입의 객체를 참조
  • 좀 더 구체적으로, 상위 클래스 타입의 참조변수하위 클래스의 객체를 참조할 수 있다.
public class Main{
	public static void main(String[] args){
    	// 원래 사용했던 객체 생성 방식
    	Car car = new Car();
        MotorBike motorBike = new MotorBike();
        
        // 다형성을 활용한 객체 생성 방식
        Vehicle car2 = new Car();
        Vehicle motorBike2 = new MotorBike();
    }
}
  • 여러 타입의 객체를 배열로 다룰 수 있다.
  • 다형성을 활용하면 하나의 타입만으로 여러 가지 타입의 객체를 참조할 수 있다.
public class Main{
	public static void main(String[] args){
    	Vehicle[] vehicle = new Vehicle[2];
        vehicle[0] = new Car();
        vehicle[1] = new MotorBike();
        
        for(Vehicle vehicle : vehicles) {
        	System.out.println(vehicle.getClass()); // 각각의 클래스 호출
            
            // 호출값
            // class Car
            // class MotorBike
        }
    }
}
  • 하나의 객체가 다른 객체의 속성과 기능에 접근하여 어떤 기능을 사용할 때, A클래스는 B클래스에 의존한다 라고 표현한다.
  • ex) 매개 변수 사용 : driver.drive(car);
  • Driver 클래스는 Car 클래스에 의존한다.
  • Driver 클래스와 Car 클래스는 서로 직접적인 관계를 가지고 있기 때문에, 객체들 간의 결합도가 높다라고 말할 수 있다.
// 다형성을 적용하지 않았을 때
public class Driver {
	void drive(Car car){
    	car.moveForward();
        car.moveBackward();
    }
    
    void drive(MotorBike motorBike){
    	motorBike.moveForward();
        motorBike.moveBackward();
    }
}

// 다형성을 적용했을 때
public class Driver{
	void drive(Vehicle vehicle){ // 매개 변수로 인터페이스 타입의 참조변수를 전달
    	vehicle.moveForward();
        vehicle.moveBackward();
    }
}
  • 여전히 높은 결합도를 낮추기 위해서 Dependency Injection(의존성 주입)을 사용한다.
  • new 연산자를 이용해서 객체를 생성하는 것이 아닌, 생성자(constructor)에 매개 변수(parameter)를 넘겨서 생성한다.
// DI를 적용하지 않았을 때
public static void main(String[] args){
	// new Car와 new MotorBike처럼 객체에 직접적으로 의존하고 있어서,
    // 해당 객체를 다른 객체로 변경할 시 코드의 변경이 불가피하다.
    Vehicle car = new Car(); // 높은 결합도
    Vehicle motorBike = new MotorBike(); // 높은 결합도
    Driver driver = new Driver();
    
    driver.drive(car);
    driver.drive(motorBike);
}

// DI를 적용했을 때
public class Driver {
	private final Vehicle vehicle;
    
    public Driver(Vehicle vehicle) {
    	this.vehicle = vehicle;
    }
    
    void drive() {
    	vehicle.moveForward();
        vehicle.moveBackward();
    }
}

public static void main(String[] args){
    Driver driver = new Driver(new Car());
    driver.drive();

    driver = new Driver(new MotorBike());
    driver.drive();
}
  • 이렇게 되면 Bus나 Boat 객체도 사용 가능해서 Driver 클래스의 재활용성이 높아진다.

  • 테스트 할 때 FakeCar() 혹은 Food를 사용할 수도 있다.

4. 캡슐화 (Encapsulation)

서로 연관있는 속성과 기능들을 하나의 캡슐(capsule) 안에 담아서 데이터를 외부로부터 보호

  • 객체 내부의 세부사항을 외부로부터 감추는 것(정보 은닉)
  • 객체 간에는 데이터를 주고 받지 않는다.
  • 목적 : 인터페이스만 공개해서 변경하기 쉬운 코드를 만드는 것

캡슐화 방법 2가지

1. 접근 제어자(Access Modifiers)

  • public : 접근 제한 없음
  • protected : 동일 패키지 + 다른 패키지의 하위 클래스 접근 가능
  • default : 동일 패키지에서만 접근 가능
  • private : 동일 클래스에서만 접근 가능

2. getter / setter

  • 클래스 내의 모든 속성값들은 private로 제한 되어 있지만, getter와 setter 메서드만 public으로 열려서 외부에서 접근 할 수 있다.
// getter / setter 적용 전 
public class Car {
	private String name;
    priate String color;
    
    public Car(String name, String color) {
    	this.model = model;
        this.car = car;
    }
    
    public String getName() {
    	return name;
    }
    
    public void setName(String name) {
    	this.name = name;
    }
    
    public String getColor() {
    	return color;
    }
    
    public void setColor(String color) {
    	this.color = color;
    }
    
    public void moveForward() {
    	System.out.println("자동차가 앞으로 갑니다.");
    }
    
    public void moveBackward(){
    	System.out.println("자동차가 뒤로 갑니다.");
    }
}


public class Driver {
	private Car car;
    
    public Driver(Car car) {
    	this.car = car;
    }
    
    // 만약 Car 클래스의 두 가지 메서드에 변경이 있으면,
    // Driver 클래스의 drive 메서드도 수정해야 한다.
    // 높은 결합도
    void drive() {
    	car.moveForward();
        car.moveBackward();
    }
}



// getter / setter 적용 후
public class Car {
	private String name;
    priate String color;
    
    public Car(String name, String color) {
    	this.model = model;
        this.car = car;
    }
    
    public String getName() {
    	return name;
    }
    
    public void setName(String name) {
    	this.name = name;
    }
    
    public String getColor() {
    	return color;
    }
    
    public void setColor(String color) {
    	this.color = color;
    }
    
    // public -> private
    private void moveForward() {
    	System.out.println("자동차가 앞으로 갑니다.");
    }
    
    public void moveBackward(){
    	System.out.println("자동차가 뒤로 갑니다.");
    }
    
    public void operate() {
    	moveForward();
        moveBackward();
    }
}

public class Driver {
	private Car car;
    
    public Driver(Car car) {
    	this.car = car;
    }
    
    void drive() {
    	car.operate(); // Car 클래스에 있는 메서드를 간단하게 호출
    }
}

Driver 클래스 입장에서는 Car 클래스의 내부 로직을 알 수도, 알 필요도 없졌다.


객체지향의 5가지 설계 원칙 (SOLID)

1. SRP : Single Responsibility Principle (단일 책임)

  • 각 객체는 하나의 책임에만 집중한다.

2. OCP : Open/Closed Principle (개방 폐쇄)

  • 기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있어야 한다.
  • 객체 내의 데이터와 기능은 연관성이 높아야 한다.
  • 확장에는 열려 있고, 변경이나 수정에는 닫혀 있다.

3. LSP : Liskov's Substitution Principle (리스코프 치환의 법칙)

  • 상위 타입의 객체를 하위 타입의 객체로 치환해도, 동작에 전혀 문제가 없어야 한다.

4. ISP : Interface Segregation Principle (인터페이스 분리의 법칙)

  • 많은 기능을 가진 인터페이스를 작은 단위로 분리시킴으로써 클라이언트에게 필요한 인터페이스들만 구현

5. DIP : Dependency Inversion Principle (의존성 역전의 원칙)

  • 의존관계를 맺을 때 변경이 쉽게 일어나는 쪽보다 변경이 거의 일어나지 않는 쪽에 의존하라
  • 변하기 쉬운 것에 의존하게 되면, 변화에 영향을 많이 받는다.

객체지향 개념정리

객체지향이란

적절한 객체에게 적절한 책임을 할당하여 서로 메세지를 주고 받으며 협력하도록 하는것

객체지향이 나오게 된 배경

점점 증가하는 소프트웨어의 복잡성을 낮추기 위해

객체지향에서 중요한 두 가지 포인트

  • 클래스가 아닌 객체에 초점을 맞추는 것
  • 객체들에게 얼마나 적절한 역할과 책임을 할당하는지

높은 응집도와 낮은 결합도 (High Cohesion, Loose Coupling)

  • 코드를 수정할 때 특정 한 부분만 수정해도 된다면 높은 응집도
  • 코드를 수정할 때 여러 객체들이 아닌 특정 객체만 영향을 받으면 낮은 결합도

절차지향 vs 객체지향

절차지향

  • 책임이 한 곳에 집중되어 있는 방식 (getter)
  • 데이터중심으로 구현하기 때문에 데이터의 변경은 곧 데이터를 사용하는 코드의 변경이 따른다.
  • 코드가 데이터에 의존적이기 때문에 유지 보수 비용이 높아진다.
  • ex) 달려라(철수);

객체지향

  • 책임이 여러 객체로 적절히 분산되어 있는 방식
  • 객체 : 하나 이상의 데이터와 기능의 묶음
  • 객체는 저마다의 데이터를 가지고 있다.
  • 특정 객체의 데이터에 변경이 생기면 해당 객체만 변경하면 된다.
  • ex) 철수.달려라();
profile
HeeYun's programming study

1개의 댓글

comment-user-thumbnail
2023년 8월 18일

유익한 자료 감사합니다.

답글 달기