객체 지향이 무엇인가요? 라는 질문을 받는다고 생각해보면 참 막막하다. 이러한 기분이 드는 이유는 아무래도 객체 지향이라는 것이 굉장히 방대한 개념여서 그런게 아닐까라는 생각을 한다.
목차
1. 객체 지향 프로그래밍이란? 그런데 왜 사용하는가?
2. 객체 지향 특징 1 (추상화/다형성/캡슐화)
3. 객체 지향 특징 2 (SOILD)
1. 객체 지향 프로그래밍이란? 그런데 왜 사용하는가?
객체 지향 프로그램이란?
프로그램을 여러개의 독립 된 단위, 객체들의 모임으로 파악하고자 하는 프로그래밍 패러다임.각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있다.
객체 지향 프로그래밍은 왜 사용하는가?
OOP는 프로그램을 유연하고 변경이 용이하게 만들기 때문이다. 그래서 대규모 소프트웨어 개발에 많이 사용된다.
가장 핵심이 되는 단어는 변경에 용이하게 일 것이다.
2. 객체 지향 특징 1
일반적으로 자바 서적에서 객체 지향에 대해 말하는 4가지를 설명하려고 한다.
- 추상화
- 캡슐화
- 상속(이번 포스팅에서는 설명하지 않겠습니다.)
- 다형성
주목해야하는 것은 이 4가지가 변경에 용이하도록 만들어 준다는 것이다.
추상화/다형성
먼저 추상화/다형성에 대해 알아보자. 이는 어려운 개념이니 예를 들어 설명해도록 하겠다.
음식점을 운영하는 A씨는 다음과 같은 메뉴얼을 만들었다.
메뉴얼 1. 식재료를 자를 때는 스테인리스 칼을 사용한다.
어느 날 A씨는 스테인리스 칼이 고장나서 플라스틱 칼을 주방에 구비했다. 하지만 메뉴얼을 꼼꼼히 살피는 직원 B는 스테인리스 칼이 없어 손질을 하지 않았고 이로 인해 그 날 장사를 하지 못하게 되었다. 고민 끝에 A씨는 메뉴얼을 다음과 같이 변경했다.
메뉴얼 1. 식재료를 자를 때는 칼을 사용한다.
메뉴얼을 변경했더니 장사가 잘되 직원C를 채용했다. 하지만 C는 칼질을 잘하지 못했다. 그래서 사장 A는 식재료 손질을 위한 가위를 구비했다. C가 가위로 손질을 하려고 하자 B는 가위를 사용하는것은 메뉴얼에 어긋났다며 C를 중재한다. 이 상황을 지켜본 A씨는 메뉴얼은 다음과 같이 변경한다.
메뉴얼 1. 식재료를 자를 때는 자를 수 있는 도구를 사용하면 된다.
위 예시는 메뉴얼 변경에 따른 추상화(스테인리스 칼 → 칼 → 자를 수 있는 도구)를 보여주고 있다. 이를 반대로 생각하면
다형성이다. 자를 수 있는 도구(추상화)라는 개념을 구체화(스테인리스 칼, 플라스틱 칼, 가위)시키면 다형성이다.
이러한 예시를 든 이유는 인터페이스를 설명하기 위함이다. 인터페이스는 정확히 추상화 개념을 지원하고 이에 따라 구현체들이 구현하면 다형성이 부여됩니다.
앞서 객체 지향은 변경에 용이하다고 했습니다. 위 예시에서 처음부터 메뉴얼을 메뉴얼 1. 식재료를 자를 때는 자를 수 있는 도구를 사용하면 된다. 로 설정했다면 스테인리스 칼, 플라스틱 칼, 가위 등 무엇을 사용하더라도 메뉴얼에 변경이 없었을 것입니다.
캡슐화
캡슐화의 장점을 말할 때 보통 정보 은닉을 말하곤 합니다. 이 또한 캡슐화를 사용하는 매우 중요한 이유입니다. 하지만 현재 말하고 싶은 것은 변경에 용이하다는 점이기 때문에 정보 은닉에 대한 설명은 제외하겠습니다.
아래는 영화 예매와 관련된 코드입니다. 아래 코드에서 캡슐화 할 부분은 빨간 박스를 친 부분외에도 더 있지만 지금은 그것이 중요한게 아니기 때문에 빨간 박스 부분 위주로 보겠습니다.
예를 들어 청소년에 대한 정의가 19세에서 15세로 변경될 경우, 위 코드에서는 빨간색 박스 두 부분을 변경해야 합니다. 단순히 예제라서 2번이지 최대 N번 변경이 필요합니다.
하지만 아래와 같이 캡슐화를 진행한다면 변경이 1번만 일어납니다.
하지만 위와 같이 캡슐화를 진행한다면 청소년에 대한 정의가 변경되더라도 한 부분에서만 변경이 일어납니다.
캡슐화가 변경에 용이하다는것은 이러한 이유 때문입니다.
3. 객체 지향 특징 2
SOILD 원칙에 대해 아주 가볍게 이야기 하겠습니다.
SRP 단일책임원칙 - 한 클래스에는 한 가지 책임만 부여한다.
OCP 개방폐쇄원칙 - 확장에는 열려있고 변경에는 닫혀있어야 한다.
LSP 리스코프치환원칙 - 특정 메소드가 상위 타입을 인자로 사용한다고 할 때, 그 타입의 하위 타입도 문제 없이 정상적으로 작동을 해야 한다는 것입니다.
ISP 인터페이스 분리 원칙 - 사용하지 않는 메서드가 있는 인터페이스에 의존하지 않아야 한다.
DIP 의존 역전 원칙 - 추상화(인터페이스)에 의존해야한다. 구현체에 의존하면 안된다.
스프링에서 사용하는 DI 패턴을 이해하면 SOILD 원칙의 많은 부분을 이해할 수 있다. DI가 무엇인지 안다는 전제하에 설명하려고 한다.
DI X
public class ReserveService {
private final MemberRepository memberRepository = new SimpleMemberRepository();
public Integer reserve(Movie movie, String name) {
Member member = memberRepository.find(name);
if (AudienceType.ADULT.equals(movie.getAudienceType()) && member.isNotAdult()) {
throw new TeenagerNotAvailableException();
}
if (member.isNotAdult()) {
return movie.getMovieOrder().getPrice() - 2000;
}
return movie.getMovieOrder().getPrice();
}
}
생성자 주입을 통한 DI
public class ReserveService {
private final MemberRepository memberRepository;
public ReserveService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public Integer reserve(Movie movie, String name) {
Member member = memberRepository.find(name);
if (AudienceType.ADULT.equals(movie.getAudienceType()) && member.isNotAdult()) {
throw new TeenagerNotAvailableException();
}
if (member.isNotAdult()) {
return movie.getMovieOrder().getPrice() - 2000;
}
return movie.getMovieOrder().getPrice();
}
}
SRP, OCP, DIP 원칙은 위 예제 코드를 통해 설명할 것이다.
SRP 단일 책임 원칙
책임은 정의하기 나름, 생각하기 나름이지만 DI 패턴을 통해 단일 책임 원칙을 조금 더 준수할 수 있다.
DI를 적용하지 않은 클래스의 경우, 역할이 2개다.
1. memberRepository 구현체를 결정한다.
2. reserve() 메서드를 실행한다.
하지만 DI 패턴을 적용하면 역할은 1개로 축소 된다.
1. reserve() 메서드를 실행한다.
책임 축소된 것을 확인할 수 있다. 이게 SRP가 말하는 한 클래스에는 한 가지 책임만 있어야 된다는 의미이다. 그렇다면 왜 단일 책임 원칙을 준수하라고 하는 것일까? 여러 책임을 두게 되면 변경이 일어났을 때 변경의 대상이 될 확률이 높아지기 때문이다.
DIP 의존 역전 원칙
일단 DIP를 준수해야 OCP를 준수할 수 있기 때문에 먼저 설명하겠다. 추상화에 의존하라는 말은 객체의 타입을 정할 때 구현체(class)가 아니라 interface에 의존하라는 말이다.
// 추상화(MemberRepository) // 구현체(SimpleRepository) 둘 다 의존
private final MemberRepository memberRepository = new SimpleMemberRepository();
위 예제 코드에서는 추상화에도 의존하긴 하는데 객체 생성을 구현체로 해서 추상화/구현체 둘 다 의존한다. 이 또한 DI 패턴으로 DIP를 준수하게 된다.
그렇다면 왜 DIP를 준수하라고 말할까? 이 또한 변경이 일어났을 떄 용이하기 때문이다.
예를 들어 멤버를 찾을 때 SimpleMemberRepository 에서 ComplexMemberRepository 로 변경되었다고 해보자.
private final MemberRepository memberRepository = new SimpleMemberRepository();
해당 코드를 모두 찾아 아래와 같이 변경해야 한다. N곳에 변경이 일어난다.
private final MemberRepository memberRepository = new ComplexMemberRepository();
OCP 개방 폐쇄 원칙
확장에는 열려있고 변경에는 닫혀있어야 한다는 말이 이해가 되지 않을수도 있다.
MemberRepository에서 Member 를 관리하는 방식이 SimpleMemberRepository 하나에서 ComplexMemberRepository 두 개로 확장되었다. 이것이 확장의 의미이다. 변경이라는 것은 해당 클래스(ComplexMemberRepository)를 사용하는 곳에서 변경이 일어나면 안된다는 것이다. DIP 원칙을 미준수할 경우 OCP가 자동으로 위반되었다. DIP를 지키게 되면 OCP도 따라오게 된다.
ISP 인터페이스 분리 원칙
여기서부터는 위 예시를 사용하지 않는다. ISP는 너무 쉽고 명료한 개념이다. 핵심은 인터페이스를 잘 설계하라는 것이다. 인터페이스를 잘 설계하지 않으면 구현체들이 필요 없는 메서드들을 구현해야할 필요가 생기기 때문이다. 이는 구현체를 사용하는 개발자로 하여금 혼란을 줄 수 있다.
LSP 리스코프 치환 원칙
어렵게 생각하면 한도 끝도 없이 어려워진다. 인터페이스의 메서드들을 구현할 때 의도에 맞게 구현하라는 것이다.
예를 들어 아래와 같이 자동차의 기능을 추상화한 인터페이스가 있다고 해보자.
각각의 의도에 맞게 재정의되어야 한다. 예를 들면 run() 메서드 파라미터 스피드는 양수여야 한다. 그런데 이를 0미만으로 설정할 수 있도록 구현한다면 의도에 맞지 않는다. 또한 stop() 은 자동차가 멈출 것을 기대한다.
예를 들어 누군가가 Car 인터페이스를 상속받아 아래와 같이 구현했다고 하자.
run() 메서드는 speed가 음수가 될 수 있다는 문제가 존재한다. stop() 또한 예상한대로 동작하지 않습니다. 이럴 경우 해당 클래스를 인자로 받을 경우 문제가 생기게 됩니다. 리스코프치환원칙은 준수하지 않으면 기능 자체가 잘 못 동작될 수 있기 때문에 변경에 용이하다는 관점보다는 소프트웨어의 안정성 측면에 더 적합한 객체 지향 원칙인 것 같습니다.
'Java' 카테고리의 다른 글
record class는 정말로 불변할까 (0) | 2023.05.25 |
---|---|
제네릭이란 무엇일까? 타입의 안정성과 다형성 (0) | 2023.03.29 |
Java - 애너테이션 (0) | 2023.03.21 |
자바 - 람다식 (0) | 2022.11.26 |
자바 - 제네릭 (0) | 2022.11.26 |