JustDoEat
[테커톡] 좋은 객체 지향이란 무엇인가? 본문
객체란?
객체란 현실세계의 개체나 개념에 대한 걸 프로그램 언어로 모델링 한 것 입니다.
이러한 객체들은, 객체에 대한 데이터, 데이터를 다루는 메서드의 집합으로 이루어저 있습니다.
public class Member {
private Long id;
private String name;
private Grade grade;
public Member(Long id, Grade grade, String name) {
this.id = id;
this.grade = grade;
this.name = name;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
public void setName(String name) {
this.name = name;
}
public void setId(Long id) {
this.id = id;
}
public Grade getGrade() {
return grade;
}
public String getName() {
return name;
}
public Long getId() {
return id;
}
}
(객체가 무엇인지 알고는 있으나 글로 녹이거나, 설명을 해보라고 했을때 깔끔하게 정의를 하기 힘들었습니다.)
(위 코드는 Member 객체에 대한 클레스 입니다.)
객체지향이란?
객체에 대해 알아봤으니 객체지향이 뭔지 알아볼까요
객체들간의 관계,협력을 중요시 한다, 해서 객체지향이라는 이름이 붙었습니다.
객체지향의 특징
1. 상속성
클레스는 다른 클레스를 상속받아 다른 클레스의 속성, 메서드를 사용 할 수 있습니다.
2.다형성
구현과 역할을 구분함으로써 다형성을 만족시킨다, 즉 구현을 interface, 역할을 구현체라고 생각하면 편함.
3.캡슐화
"객체는 객체에대한 데이터, 그 데이터를 다루는 메서드로 이루어진다"
라고 설명을 했었습니다. 즉 캡슐화는 데이터와,메서드가 하나로 묶여있는 것을 강조를 합니다.
이러한 특성으로 외부에서 바로 접근을 못하도록, 외부에서 내부의 구현 및 동작구조를 볼 수 없도록 하는 특징이 있습니다.
대표적으로 Getter,Setter를 이용해 값을 수정하는 예시가 캡슐화의 대표적인 예시 입니다.
(private로 선언되어진 데이터에 바로 접근이 불가)
좋은 객체지향이란?
좋은 객체지향이란 무엇이고 어떻게 해야 할까?
위에서 설명한 객체지향의 특징과 장점을 잘 살리기 위해.
로버트 마틴이라는 사람이 객체지향 설계의 5원칙, SOLID원칙을 제시 했습니다.
그럼 SOLID원칙이 무엇인지 알아볼까요?
SOLID원칙
SRP(Single Responsibility Principle) 단일 책임 원칙
하나의 클레스는 하나의 책임만을 가져야한다.
요약을 하자면 하나의 클레스는 하나의 역할을 분담해야한다는것이다, 아래 예시를 통해 알아볼게요
public class Member {
private Long id;
private String name;
private Grade grade;
//생성자 생략
//getter, setter 생략
public saveMember(){
//Member를 DB에 저장하는 메서드
}
}
객체를 설명하면서 예시를 들었던 코드에 Member를 DB에 저장하는 메서드도 Member 클레스 안에 구현을 하였습니다.
이 코드는 단일책임원칙을 위반 한 코드 입니다. 왜 인지 볼까요?
Member라는 클레스는 Member의 데이터에 대한 관리만 하면 되는데, 추가적으로 DB에 Member를 저장하는 역할을 넣어줌으로
하나의 역할이 아닌 다중역할을 가지므로 단일책임원칙을 위반하였습니다.
OCP(Open/Close Principle) 개방패쇄 원칙
확장에는 열려있으나 변경에는 닫혀있어야 한다.
다형성을 잘 활용해서 이 원칙을 지킬 수 있음.
LSP(Liskov substitution Princile) 리스코프 치환 원칙
프로그램의 객체는 프로그램의 정확성을 깨트리지 않으면서, 하위 타입의 인스턴스로 바꿀수있어야함.
즉 인터페이스를 구현하는 하위 클레스 구현체들은 인터페이스의 규약을 전부 지켜야 한다.
쉽게 예시를 들어보겠습니다.
자동차의 모든기능을 정의한 인터페이스가 있고, 기능을 구현한 구현체가 있다고 가정을 했을 시.
인터페이스에 엑셀부분은, 엑셀을 밞으면 차가 앞으로 가야합니다.
하지만 구현체에서는 엑셀을 밞으면 뒤로 가도록 구현을 했다면 인터페이스의 규약을 지키지 않음으로
LSP를 위반했다고 볼 수 있습니다.
ISP(Interface Segregation Principle) 인터페이스 분리 원칙
하나의 인터페이스에 모든 기능을 명시하기 보다, 범용적으로 여러개를 만드는 방식이 좋다.
쉽게 예시를 들어보겠습니다.
LSP를 설명하면서 예시를 들었던 자동차 인터페이스를 다시 떠올려보자면, 제가 분명 "자동차의 모든기능을 정의한 인터페이스"
라고 예시를 들었습니다, 이부분은 ISP를 위반한 예시 입니다. 이유를 생각해볼까요?
ISP의 정의인 "모든 기능을 명시하기 보다, 범용적으로 여러개를 만드는 방식이 좋다" 에서
자동차의 기능에도 사용자가 조작하는 조작 부, 전기적인 장치들이 조작하는 부분처럼 여러 기능들이 있는데 이러한 기능들을 단지
"자동차"라는 인터페이스에 전부 넣기 보다, 조작부 인터페이스, 전자장치 인터페이스 등으로 나누어서 구현하는게 좋습니다.
DIP(Dependency Inversion Principle) 의존관계 역전 원칙
고수준 모듈은 저수준 모듈에 의존하면 안되고, 둘다 추상화에 의존해야한다.
쉽게 생각하면 고수준 모듈(인터페이스), 저수준 모듈(구현체)라고 생각하면 편할것 같습니다.
OCP, DIP에 대한 부가 설명
1. 인삿말을 출력하는 인터페이스 HelloPolicy
2. 각 언어에 따른 인삿말을 출력하는 구현체를.
- 한국어로 "안녕하세요"를 출력하는 koreanHelloPolicy
- 영어로 "Hello"를 출력하는 EnglishHelloPolicy
3. 인삿말을 실행시킬 메인 클레스 HelloApp
인삿말을 출력하는 인터페이스 HelloPolicy
public interface HelloPolicy {
void sayHello();
}
한국어로 "안녕하세요"를 출력하는 koreanHelloPolicy
public class koreanHelloPolicy implements HelloPolicy {
@Override
public void sayHello() {
System.out.println("안녕하세요");
}
}
영어로 "Hello"를 출력하는 EnglishHelloPolicy
public class englishHelloPolicy implements HelloPolicy{
@Override
public void sayHello() {
System.out.println("Hello");
}
}
인삿말을 실행시킬 메인 클레스 HelloApp
public class HelloApp {
public static void main(String[] args) {
//final HelloPolicy helloPolicy = new englishHelloPolicy();
final HelloPolicy helloPolicy = new koreanHelloPolicy();
helloPolicy.sayHello();
}
}
HelloService클레스에서 helloPolicy.sayHello();를 입력한다면. 내가 설정한 구현체가 무엇인지에 따라 결과값이 다르게 됩니다.
(현제는 koreanHelloPolicy를 사용하고 있네요, 그럼 출력값으로 "안녕하세요"가 나오겠죠?)
final HelloPolicy helloPolicy = new englishHelloPolicy();
// final HelloPolicy helloPolicy = new koreanHelloPolicy();
하지만 영어로 인삿말을 출력하고 싶을때는 어떻해야 할까요?
위 처럼 koreanHelloPolicy를 주석처리 하고 englishHelloPolicy를 사용해야 합니다.
자! 여기서 왜 OCP와 DIP를 위반할까요?
OCP
확장에는 열려있으나 변경에는 닫혀있어야 한다.
지금 인삿말을 바꾸려면 직접 자바코드에 손을 대야합니다. 즉 변경에 열려있으므로 OCP를 위반합니다.
DIP
고수준의 모듈은 저수준의 모듈에 의존적이면 안된다.
코드를 보면 HelloPolicy 라는 인터페이스(고수준)가 englishHelloPolicy,koreanHelloPolicy(저수준)에 의존을 하고 있습니다.
이러한 이유로 OCP와 DIP를 모두 위반하고 있습니다.
결론적으로 객체지향의 다형성만으로는 OCP와 DIP문제를 해결하기 어렵습니다.
이러한 문제를 해결하려면 어떻게 해야할까?
외부에 설정파일을 두고 설정파일에 의존성에 대한 정보를 주면 클라이언트 코드의 수정이 일어나지 않아
OCP와 DIP를 모두 지킬 수 있습니다.
많은 객체지향 언어와, 프레임 워크가 있자만 여기서는 스프링을 사용하여 위와같은 문제를 해결하는 방법을 알아보겠습니다.
프레임워크에 대한 깊이있는 설명과 이해를 하는것이 아닌 이런식으로 원칙들을 만족시키는구나 정도로 봐주셨으면 좋겠습니다.
final HelloPolicy helloPolicy = new englishHelloPolicy();
// final HelloPolicy helloPolicy = new koreanHelloPolicy();
위에서 의존성 주입을 위 처럼 필요할때마다 코드를 바꾸면서 진행을 하였으나.
이러한 부분을 해결하기 위해 외부에 AppConfig라는 설정파일을 두었습니다.
AppConfig
@Configuration
public class AppConfig {
@Bean
public HelloPolicy helloPolicy() {
return new koreanHelloPolicy();
// return new englishHelloPolicy();
}
}
여기서 이런 의문이 들었습니다.
AppConfig라는 파일에서 어쨋던 코드의 수정이 이루어지는데 OCP가 만족이 안되는거 아닌가? 코드 수정이 되면 안되지 않나?
-> 조사 해본 결과 코드의 수정이라는 단어는 클라이언트 코드(사용자나 다른 시스템과 상호 작용하여 기능을 실행하는 코드)에서의 수정이 일어나면 안되는거라고 합니다.
만약 새로운 인사 방식을 추가하고자 한다면, 새로운 HelloPolicy 구현체를 작성하고 AppConfig에 해당 구현체를 추가하기만 하면 됩니다. 그러면 HelloApp은 수정 없이도 새로운 인사 방식을 사용할 수 있습니다. 이렇게 변경에 대해 폐쇄적인 구조를 갖추고 있으므로 개방-폐쇄 원칙을 준수합니다.
HelloApp
public class HelloApp {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
HelloPolicy helloPolicy = ac.getBean("helloPolicy", HelloPolicy.class);
helloPolicy.sayHello();
}
}
final HelloPolicy helloPolicy = new koreanHelloPolicy();
final HelloPolicy helloPolicy = ac.getBean("helloPolicy", HelloPolicy.class);
바뀐 부분은 위 코드입니다.
기존에는 직접 구현체를 클라이언트 코드내부에서 바꾸면서 구현체에 대한 정의를 해주었지만.
바뀐 코드를 보면 클라이언트 코드에서는 구현체를 어떤걸 쓰는지 알 필요가 없고, 직접 수정 할 필요가 없어졌습니다.
의존 역전 원칙은 고수준 모듈이 저수준 모듈에 의존하지 않도록 하고, 추상화에 의존하도록 하는 원칙입니다. 이 코드에서는 HelloApp이 HelloPolicy라는 인터페이스에만 의존하고 있습니다. 실제 구현은 AppConfig에서 결정되고, 이에 따라 어떤 구현체가 사용될지는 런타임 시점에 결정됩니다. 이로 인해 DIP를 만족합니다.