객체 지향 프로그래밍
컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러개의 독립된 단위.
즉 "객체" 들의 모임으로 파악하고자 하는 것
각각의 객체는 메세지를 주고받고, 데이터를 처리할 수 있다. = 협력
또 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 유용하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다.
유연 변경? 다시 말해 레고 블럭 조립하듯 컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법
이것이 다형성(Polymorphism)이라고 한다.
객체 지향의 가장 중요한 개념 중 하나는 다형성이다.
다형성은 역할과 구현으로 구분을 한다. 역할은 인터페이스고 구현은 그 인터페이스를 구현한 객체라고 보면된다!
다형성을 운전자-자동차로 예를 들어보자
운전자(역할)는 자동차(역할)를 운전한다
'자동차 역할'을 '자동차 구현'으로 분리해보면 K3, 아반떼, 테슬라..(인스턴스)
'자동차 구현' 이 바뀌어도 운전자는 역시나 운전할 수 있다.
이 말은 즉슨, 운전자는 자동차 인터페이스 즉 '자동차 역할' 에 대해서만 의존을 하고있다.
'자동차 구현' 처럼 다른 대상으로 변환이 가능하고(새로운 기능들 제공) 무한확장이 가능하나 '자동차 역할'에 영향을 주지 않으며 운전자(클라이언트)는 바꿀 필요가 없다.
이것이 바로 '유연하고 변경용이하다' 라고 할 수 있다.
정리
역할과 구현을 분리
- 역할과 구현으로 구분하면 유연해지며 변경이 편리해진다
- 클라이언트는 대상의 역할(인터페이스)만 알면 된다.
- 클라이언트는 구현 대상의 내부 구조를 몰라도 된다 (자동차 디테일 구조를 몰라도 됨 )
- 클라이언트는 내부 구조가 변경되어도 영향을 받지 않는다. (엑셀, 브레이크만 알아도 된다)
- 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않는다 (K2 에서 테슬라로 바꿔도 운전자는 영향X)
핵심은 역할이 중요하다!
- 자바 언어
- 역할 = 인터페이스
- 구현 = 인터페이스를 구현한 클래스, 구현 객체
객체를 설계할 때 역할과 구현을 명확히 분리
객체 설계시 역할(인터페이스)를 먼저 부여하고 , 그 역할을 수행하는 구현 객체 만들기
이론으로는 알겠는데 이제 코드로 예를 들어보자
- 역할 : MemberRepository 인터페이스
- 구현 : MemoryMemberRepository 클래스, JdbcMemberRepository 클래스
- 인터페이스를 구현한 객체 인스턴스
우선 MemberService 에서 save()를 호출하면
MemberRepository 인터페이스를 구현 한 MemoryMemberRepository 클래스는 save() 메서드를 호출된다.
MemoryMemberRepository 클래스는 save()를 오버라이딩을 하여 자체적인 동작(구현)을 제공할 수 있다.
다시 말해 MemberService 는 MemberRepository 인터페이스를 통해 MemoryMemberRepository 클래스의 메서드 save() 를 호출할 수 있다.
- MemberService > MemberRepository > MemoryMemberRepository 의 save()호출
[MemberService]
public interface MemberService {
//회원 가입
void join(Member member);
//회원 조회
Member findMember(Long memberId);
}
[MemberRepository]
public interface MemberRepository {
//회원 저장
void save(Member member);
//회원 id를 찾는기능
Member findById(Long memberId);
}
[MemoryMemberRepository]
@Component
public class MemoryMemberRepository implements MemberRepository{ //MemberRepository를 구현
private static Map<Long, Member> store = new HashMap<>();
//MemberRepository의 save()를 재정의(오버라이딩)
@Override
public void save(Member member) {
store.put(member.getId(), member);
}
@Override
public Member findById(Long memberId) {
return store.get(memberId);
}
}
여기서 다형성의 본질을 배울 수 있다.
- 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다.
- MemberService 는 MemberRepository 인터페이스의 메서드를 호출하기 때문에 인터페이스에만 의존한다.
- 의존한다는 뜻은 "내가 쟤를 알고 있다" 라는 말
- MemberService 는 MemoryMemberRepository 의 구체적인 코드를 몰라도 된다.
- 인터페이스를 구현한 객체 인스턴스( MemoryMemberRepository )를 실행시점에서 유연하게 변경할 수 있다.
- 구현체( MemoryMemberRepository ) 변경가능 = JdbcMemberRepository 클래스를 사용해도 된다.
대신 인터페이스가 깨지면 모두 깨짐
그래서 인터페이스를 안정적으로 잘 설계하는게 가장 중요하다!
스프링과 객체 지향
- 스프링은 다형성이 가장 중요하다.
- 스프링을 사용하면 구현을 편리하게 변경할 수 있다.
- 다형성을 극대화해서 이용할 수 있게 도와주고 다형성을 편리하게 사용할 수 있도록 지원하는 개념이라고도 할 수 있다.
- 제어의 역전(IoC), 의존관계 주입(DI)은 다형성을 활용하여 역할과 구현을 편리하게 다룰 수 있도록 지원한다
SOLID
: 좋은 객체 지향 설계의 5가지 원칙
- SRP (Single responsibility principle) 단일 책임 원칙
- 한 클래스는 하나의 책임만 가진다
- 변경이 있을 때 파급효과가 작으면 단일 책임 원칙을 잘 따른것
- ex) UI 변경, 객체 생성과 사용을 분리
- OCP (Open / cloased principle) 개방-폐쇄 원칙
- 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀 있어야한다 (코드에 변경할 필요없이 기능추가 가능)
- 역할과 구현의 분리같은 거라고 생각하면된다
- 다형성을 활용해보면 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현 추가
- 새로운 JdbcMemberRepository 클래스 만들어서 MemoryMemberRepository 클래스를 변경
- 기존 코드를 변경을 하지않은 것!
- 클라이언트 코드인 MemberService 에서 구현 클래스를 직접 선택시
- MemberRepository m = new MemoryMemberRepository(); //기존코드
- MemberRepository m = new JdbcMemberRepository(); //변경코드
- OCP 위반 (코드가 변경됐기 때문)
- OCP 원칙을 지키려면 다형성 만으로 지킬 수 없고 DI, IOC 컨테이너가 필요함
- LSP (Liskov substitution principle) 리스코프 치환 원칙
- 자동차가 엑셀 구현(앞으로 간다의 기능) 하도록 구현한다.
- 뒤로가는 기능을 만들면 리스코프 치환 원칙을 위반
- 자동차가 엑셀 구현(앞으로 간다의 기능) 하도록 구현한다.
- ISP (Interface segregation principle) 인터페이스 분리 원칙
- 특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다
- 자동차 인터페이스 => 운전 인터페이스 , 정비 인터페이스로 분리하면
- 사용자 클라이언트 => 운전자 클라이언트 , 정비사 클라이언트로 분리가 가능하다
- 이렇게 분리하면 정비 인터페이스를 변경해도 운전자 클라이언트에 영향을 주지 않아서 인터페이스가 명확하고 대체 가능성이 높아진다.
- 특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다
- DIP (Dependency inversion principle) 의존관계 역전 원칙
- 추상화에 의존하고 구체화에 의존하면 안된다
- 다시 말해 클라이언트 코드가 구현 클래스에 의존하지 않고 인터페이스에 의존해야한다 (객체도 인터페이스에 의존하면 유연하게 구현체 변경가능)
- 예를들어 운전자는 자동차의 역할만 알면되지 k3에 대해 알 필요가 없다.
- DIP원칙을 지키려면 다형성 만으로 지킬 수 없고 DI, IOC 컨테이너가 필요함
- OCP 처럼 클라이언트 코드인 MemberService 에서 구현 클래스를 직접 선택시
- MemberRepository m = new MemoryMemberRepository(); //기존코드
- MemberRepository m = new JdbcMemberRepository(); //변경코드
- MemberRepository 인터페이스에 의존하나 구현클래스까지 동시에 의존한다.
- DIP위반 (구체화에 의존했기 때문)
정리
: 다형성만으로는 OCP, DIP 를 지킬 수 없다.
어떻게 해결? 스프링으로!
- 스프링은 DI , DI 컨테이너 제공으로 다형성 + OCP,DIP 를 가능하게 지원한다.
- 클라이언트 코드 변경없이 기능확장
'공부' 카테고리의 다른 글
IoC, DI 컨테이너 (0) | 2023.11.21 |
---|---|
Spring boot | 스프링 부트 프로젝트 생성 (1) | 2023.11.20 |
연관관계 | 단방향, 양방향, 연관관계의 주인 (0) | 2023.10.24 |
엔티티 매핑 (2) | @Id 기본 키 매핑 (0) | 2023.09.22 |
엔티티 매핑 (1) | @Entity, @Table, @Column, @Enumerated.. (0) | 2023.09.22 |