OOP

최창효·2022년 10월 17일
0

자바 이해하기

목록 보기
8/8
post-thumbnail

객체 지향 프로그래밍이란?

•  데이터(변수)와 행위(메서드)를 하나로 묶어 객체를 만들고, 객체들 간의 상호작용으로 로직을 구현하는 프로그래밍 방법론 입니다.

•  OOP는 객체를 구성할 때 하나의 기능(혹은 최소한의 기능)을 가지도록 정의하는 걸 지향합니다. 객체는 자신에게 없는 필요한 것을 다른 객체에게 요청하거나 위임하게 되며 이를 객체들 간의 상호작용이라고 합니다.

OOP is A.P.I.E

Abstraction, Polymorphism, Inheritance, Encapsulation은 객체 지향 프로그래밍의 특징입니다.

추상화

추상화란 공통된 속성이나 기능을 하나로 묶는 것을 말합니다.

30명의 친구들 중 A,B,C만 교과서가 있고, 이들을 가리켜 모범생그룹으로 부른다면 이는 추상화에 해당합니다. (공통된 속성을 묶고 이름을 부여)

추상화는 공통된 속성만 추려내기 때문에 그 과정에서 불필요한 특성이 제거되기도 합니다.

자바에서 클래스를 정의하고 사용하는 게 곧 추상화 개념을 활용하는 것과 동일합니다.


다형성

다형성은 ‘같은 모양의 코드가 다른 행위를 할 수 있다’는 의미로, 하나의 클래스나 메소드가 다양한 방식으로 동작하는 걸 말합니다.

다형성을 이용하면 상위 클래스가 동일한 메시지(객체가 다른 객체에게 어떤 행위를 하라고 명령하는 것)로 하위 클래스들을 서로 다르게 동작시킬 수있습니다.


// 상위 클래스
class Animal{
	void howling(){
		System.out.println("Grrr");
	}
}

// 상속받은 클래스1
class Dog extends Animal{
	@Override
	void howling(){
		System.out.println("멍멍");
	}	
}

// 상속받은 클래스2
class Cat extends Animal{
	@Override
	void howling(){
		System.out.println("야옹");
	}	
}

// 실행
public class Main {	
	public static void main(String[] args) {
		Animal choco = new Dog();
		Animal cheeze = new Cat();
		// GetHowlingSound(choco);
		GetHowlingSound(cheeze);
	}

	// 메서드
	public static void GetHowlingSound(Animal a) {
		a.howling();
	}
		
}

위 코드는 상위 클래스인 Animal, Animal을 상속받은 DogCat에서 각각 howling메서드를 오버라이딩하고 있습니다. 그리고 GetHowlingSound메서드에서 Animal 매개변수 a를 받고 howling을 요청하고 있습니다.

GetHowlingSound이라는 동일한 명령을 내렸을 때 Dog인 choco는 멍멍Cat인 cheeze는 야옹을 수행합니다. 즉 하위 클래스별로 서로 다른 동작을 진행한다는 걸 확인할 수 있습니다.

덕분에 Animal을 상속받은 Cow가 추가되더라도 GetHowlingSound메서드는 아무런 변화가 없습니다. 이처럼 다형성을 통해 우리는 코드의 재사용성을 높일 수 있습니다.

다형성을 통해 우리는 조상 타입의 참조변수로 자손 타입의 객체를 다룰 수 있습니다.

List<Integer> lst = new ArrayList<Integer>();

이 역시 lst변수를 간단히 LinkedList로 바꿀 수 있기 때문에 재사용성 및 유지보수성이 높아집니다. 또한 ArrayList의 여러 기능 중 List에 정의되어 있는 최소 기능만 사용할 수 있게 되므로 안전성또한 높아진다 할 수 있습니다.

동적바인딩

프로그래밍에서 컴파일 시점에 값이 확정되는 걸 정적바인딩, 그렇지 않고 런타임 시점에 값이 확정되는 동적바인딩이라고 합니다.

// 상위 클래스
class Animal{
	void howling(){
		System.out.println("Grrr");
	}
}		

// 상속받은 클래스
class Cat extends Animal{
	@Override
	void howling(){
		System.out.println("야옹");
	}	
}

// 실행
public class Main {	
	public static void main(String[] args) {
		Animal unidentified;
		// unidentifiedAnimal = new Cat();
		GetHowlingSound(unidentified); // Grrrr? 야옹?
	}

	// 메서드
	public static void GetHowlingSound(Animal a) {
		a.howling();
	}		
}

다형성이나 상속을 활용할 수 있는 이유는 자바가 동적바인딩을 활용하기 때문입니다. 컴파일 시점을 기준으로 본다면 unidentified는 단순히 Animal일 겁니다. 하지만 코드가 실행되는 과정에서 unidentifiedcat이라는 게 밝혀진다면 우리는 이를 반영해 unidentified의 울음소리를 야옹으로 변경하는 게 적절해 보입니다. 자바는 동적바인딩을 활용하기 때문에 위와 같은 상황에서 Grrr가 아닌 야옹을 반환합니다.

참고) static 메서드

static 메서드는 오버라이딩이 불가능 합니다. 그 이유는 static 메서드가 컴파일 시점에 메모리에 값이 할당되기 때문입니다(정적바인딩). 이미 값이 정해졌기 때문에 실행 시점에 값을 결정해야 하는 오버라이딩이 불가능하게 됩니다.

static

•  static 키워드가 붙은 메서드 및 멤버변수는 `컴파일시점`에 값이 할당됩니다. 

•  static변수 및 메서드는 JVM의 Method Area에 저장됩니다.

•  static이 붙은 멤버변수는 인스턴스를 생성하지 않아도 사용할 수 있습니다.

•  모든 인스턴스가 공통적으로 값을 유지해야 하는 변수에는 static을 붙입니다. 

•  static이 붙은 메서드는 인스턴스 변수를 사용할 수 없습니다.

다형성을 구성하는 방법으로는 오버라이딩오버로딩이 있습니다.

오버라이딩: 상속받은 조상의 메서드를 자신에 맞게 변경하는 걸말합니다.

• 오버라이딩 성립 조건

• 해당 메서드의 선언부가 조상 클래스 메서드와 동일해야 합니다.

• 조상 클레스의 메서드보다 더 좁은 범위의 접근 제어자를 사용할 수 없습니다.

• 조상 클레스의 메서드보다 더 큰 예외를 선언할 수 없습니다. 

오버로딩: 한 클래스 안에서 같은 이름의 메서드를 여러개 정의하는 걸말합니다.

• 오버로딩 성립 조건

• 메서드 이름이 같아야 합니다.

• 매개변수의 개수 또는 매개변수의 타입이 달라야 합니다.

• 반환 타입은 상관이 없습니다.

• 매개변수 이름 또는 매개변수의 순서는 상관이 없습니다.

상속

상속은 중복을 줄이기 위해사용하는 개념으로 생각되기 쉽습니다. 중복을 줄이는 것도 맞지만 상속의 더 큰 의의는 클래스간의 관계를 만들어 주는 것입니다. 그렇다면 관계는 왜 정의할까요? 관계를 가짐으로써 우리는 앞서 공부한 다형성을 구현할 수 있게 됩니다. 즉, 상속은 다형성을 구현하기 위한 전재조건과도 같습니다.

CatDog를 관찰하다 보면 ‘발이 4개며 이빨이 있고 새끼를 낳으며…’ 등의 공통적인 특징이 존재한다는 걸 알 수 있습니다. 이러한 공통적인 특징을 가진 객체를 그룹화하기 위해 공통속성을 Animal이라는 객체에 담고 이들을 CatDog에 상속시키게 됩니다. 여기서 우리가 기억해야 할 내용은 Cat과 Dog를 하나의 집합으로 묶기 위해서 Animal을 만들었다는 것입니다.


캡슐화

캡슐화는 관련있는 변수와 함수를 하나의 클래스로 묶고 외부에서 쉽게 접근하지 못하게 정보를 은닉하는방법을 말합니다.

캡슐화는 객체에 직접적으로 접근하거나, 내부의 정보를 외부에서 직접적으로 접근하거나 변경하는 걸 막습니다. 캡슐화된 정보는 객체가 제공하는 필드에만 접근할 수 있으며, 객체가 정의한 메서드를 통해서만 접근이 가능합니다.

캡슐화를 통해 우리는 정보를 보호하고 유지보수나 확장 시 오류의 범위를 최소화하며 데이터가 변경되어도 다른 객체에 영향을 주지 않게 해 독립성을 높여줍니다.

캡슐화는 접근제어자 설정을 통해 이뤄집니다.

•  클래스 접근 제어자

•  `default`: 동일한 패키지의 클래스에서만 접근할 수 있습니다. 

•  `public`: 다른 패키지에서 접근할 수 있습니다.

•  변수, 메서드 접근 제어자

•  `private`: 동일한 클래스 안에서만 접근이 가능합니다. 

•  `default`: 동일한 패키지 내에서만 접근이 가능합니다.

•  `protected`: 동일한 패키지에서, 다른 패키지라면 이를 상속 받은 클래스에서만 접근이 가능합니다.

•  `public`: 모든 객체에서 접근이 가능합니다.

응집도

밀접하게 연관된 작업만을 수행하며 연관성이 없는 작업은 다른 객체에게 위임하는 객체를 응집도가 높다고 합니다. 캡슐화를 잘 진행해 자신의 데이터를 스스로 처리하는 자율적인 객체를 만든다면 결합도는 낮추고 응집도는 높인좋은 코드를 만들 수 있습니다.


객체 지향 프로그래밍의 장점과 단점

장점

• 객체에 데이터와 데이터를 다루는 방법에 대해 정의해둔 뒤 필요할 때마다 호출을 통해 사용할 수 있기 때문에 코드의 재사용성이 좋습니다.

• 객체의 상호작용을 통해 동작하며 객체들은 각자의 고유한 역할이 존재하는 결합도가 낮은 상태가 되어 수정이 필요한 코드가 있을 경우 다른 코드를 신경쓰지 않고 해당 부분만을 수정하면 되기 때문에 유지보수성이 좋아집니다.

• 각자 담당한 모듈만을 개발한 뒤 마지막에 이를 결합하는 방식으로 개발을 진행할 수 있어 협업에 용이합니다.

단점

• 객체 지향 프로그래밍의 장점은 객체 간의 결합도가 낮을 때 발휘됩니다. 그렇기 때문에 설계 단계에서 결합도를 낮추기 위한 많은 시간과 비용이 소모됩니다.

• 객체지향은 실행될 코드를 필요할 때마다 호출해야 하기 때문에 실행처리속도가 느립니다.


좋은 객체 지향 설계를 위한 5가지 원칙 - SOLID원칙

SRP(Single Responsibility Principle)

단일 책임 원칙은 모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화 해야 한다는 원칙입니다. 하나의 책임만 가진다는 의미는 쉽게 말하면 하나의 일만 처리해야 한다는 말과 동일합니다.

SOLID 원칙을 얘기한 Robert Martin은 같이 수정되어야 할 내용이라면 하나로 묶고, 그렇지 않다면 따로 분리해야 한다고 얘기했습니다. 이러한 관점에서 SRP는 클래스를 변경해야 할 이유는 단 한가지여야 한다라고도 얘기할 수 있습니다.

이는 하나의 메서드만 가져야 한다는 의미가 아닙니다. 여러 메서드라도 같은 원인에 의해 바뀐다면 이들 모두 하나의 클래스에 묶일 수 있습니다.

OCP(Open-Closed Principle)

개방-폐쇄 원칙은 ‘개발에 필요한 개체는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다’는 원칙입니다. 조금 더 쉽게 얘기하면 손쉽게 기능을 확장할 수 있어야 하며, 그 확장이 기존의 코드를 변경하지 않으면서 이뤄지도록 설계되어야 한다는 의미입니다.

초반에 다형성을 설명할 때 활용한 예시를 살펴보면 DogCatAnimal로 추상화했기 때문에 새롭게 Rabbit이 추가되더라도 GetHowlingSound메서드는 아무런 수정이 필요없습니다. 이처럼 추상화 또는 상속을 활용하면 OCP원칙을 준수할 수 있습니다.

LSP(Liskov Substitution Principle)

리스코프 치환 원칙은 상위 타입의 객체를 하위 타입의 객체로 치환하더라도 상위 타입을 사용하는 프로그램은 아무런 이상이 없어야 한다는 원칙입니다. 즉, 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체할 수 있어야 한다는 원칙으로 부모의 속성 및 특징을 자식은 빠짐없이 가지고 있어야 한다는 말입니다.

객체의 치환이 자유롭다는 건 확장에 대해 열려 있다는 의미이기 때문에 LSP는 OCP를 위한 전재조건과도 같습니다.

타입을 확인하는 instanceof를 사용한다는 건 객체의 치환이 자연스럽지 않다는 의미입니다. instanceof와 downcasting를 사용할 필요가 없는 코드가 LSP원칙을 잘 준수하는 코드입니다.

ISP(Interface Segregation Principle)

인터페이스 분리 원칙은 자신이 사용하지 않는 인터페이스에 강제적으로 의존하고 있어서는 안된다는 원칙입니다. 인터페이스를 너무 큰 단위로 만들면 ISP원칙을 위반하게 됩니다.

Animal이라는 인터페이스에 howling외에 grooming이라는 메서드가 함께 들어있다고 생각해 봅시다. 이 때 Animal을 상속 받는 Dog는 본의 아니게 grooming까지 함께 구현해야 하므로 효율적이지 못합니다.

DIP(Dependency Inversion Principle)

의존성 역전 원칙은 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안되며, 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다는 원칙입니다. 보다 구체적으로는 상위 모듈과 하위 모듈은 모두 추상화에 의존해야 한다, 추상화는 세부사항(구현)에 의존해서는 안된다는 두 가지 내용을 가리켜 의존성 역전 원칙이라고 합니다.

A를 실행하기 위해 B가 필요하다면 A는 B에 의존한다라고 말합니다.

일반적으로 고수준 모듈이란 유의미한 기능을 제공하는 모듈, 저수준 모듈은 고수준 모듈을 제공하기 위해 필요한 모듈 입니다.

주식 가격을 예측하는 모델을 고수준 모듈, 주식 가격 데이터를 DB에서 가져오는 기능을 저수준 모듈로 정의했을 때 주식 가격을 예측하기 위해 주식 가격 데이터를 가져오는 상황은 자연스럽습니다. 이처럼 고수준 모듈이 저수준 모듈에 의존하는 관계가 일반적이지만, 저수준 모듈은 일반적으로 사용되는 곳이 많고 변경이 잦기 때문에 고수준 모듈이 저수준 모듈에 의존하는 상황은 좋지 않습니다. 이러한 의존 관계를 벗어나 고수준 모듈과 저수준 모듈 모두 추상화에 의존해야 한다는 게 바로 DIP입니다.

조금 더 쉽게 얘기하면 변하기 쉽고 자주 변화하는 것에 의존하지 말고 변화하기 어렵거나 변하지 않는 것에 의존해라, 객체에 의존하기보다 인터페이스에 의존해라는 말입니다.

이렇듯 의존성 역전 원칙은 추상화를 통해 실현될 수 있습니다. DIP에서는 추상화를 할 때 유의사항으로 세부사항에 의존(여기의 의존은 참고의 의미에 가깝습니다)하지 말라고 했습니다.

1.어떤 클래스 A를 인터페이스 없이 만들었다가 
2.뒤늦게 인테페이스를 만들 일이 생겨서 
3.완성된 A코드를 99% 참고한 I인터페이스를 만들었는데 
4.I를 상속받지만 A와는 다른 B클래스를 만들어보니 생각지 못한 문제들이 발생했던
경험이 있다면 이 말의 의미를 잘 이해할 거라 생각합니다. 
추상화가 구현에 의존하게 되면 새로운 구현이 추가되면 추상화 역시 같이 변경될 것이며 이는 추상화를 한 의미가 없어지게 됩니다. 

마무리

객체 지향 프로그래밍이란 데이터(변수)와 행위(메서드)를 하나로 묶어 객체를 만들고, 객체들 간의 상호작용으로 로직을 구현하는 방법 입니다.

객체 지향 프로그래밍은 Abstraction, Polymorphism, Inheritance, Encapsulation 라는 특성을 가지고 있으며

이러한 특성으로 인해 코드의 재사용성유지보수성이 좋아지며 여러 사람이 협업하는 대규모 프로젝트를 효율적으로 진행할 수 있습니다.

하지만 객체 지향 프로그래밍은 초기 설계가 복잡한 편이며 설계가 잘 짜여졌을 때 긍정적인 효과를 발휘합니다. 그래서 일반적으로 SOLID라는 5가지 원칙을 준수해가며 객체 지향 프로그래밍을 설계합니다.

profile
기록하고 정리하는 걸 좋아하는 개발자.

0개의 댓글