객체 지향 프로그래밍 기본
in Development on Python
객체 지향 프로그래밍에 대해 정리한 내용입니다
들어가며
- 비용
- 코드 1줄 만드는데 비용이 최초 릴리즈엔 조금 들지만, 그 이후엔 비용이 많이 듬
- 첫 출시 이후엔, 거의 증가하지 않는 경우는 1줄 만드는 비용이 증가되는 것
- 예시 : 위 코드에서 아래 코드로 변경될 경우, 코드를 찾아 헤매는 시간이 점점 늘어남(비슷한 코드가 여러 곳에 있음)
long start = System.currentTimeMillis(); ... long end = System.currentTimeMillis(); long elapsed = end - start;
long start = System.nanoTime(); ... long end = System.nanoTime(); long elapsednano = end - start;
- 주요 원인
- 코드 분석 시간 증가
- 코드 변경 시간 증가
- 낮은 비용으로 변화할 수 있는 방법
- 패러다임
- 객체 지향, 함수형, 리액티브
- 코드, 설계, 아키텍처
- TDD, DDD, 클린 아키텍처
- 업무 프로세스/문화
- 애자일, DevOps
- 패러다임
- 객체 지향과 비용
- 캡슐화 + 다형성(추상화)
객체
절차 지향
- 데이터를 여러 프로시저가 공유하는 방식
- 처음엔 쉬움. 변수 선언하고, 필드 조작하고 등등
- 시간이 흘러갈수록 데이터를 공유하는 방식은 구조를 복잡하게 만들고, 수정을 어렵게 만드는 요인이 됨
객체 지향
- 데이터와 프로시저를 객체라는 단위로 묶음
- 특정 객체가 가지고 있는 데이터는 그 객체의 프로시저만 접근할 수 있음
- 다른 객체는 (다른 객체의) 데이터에 바로 접근할 수 없고, (다른 객체의) 프로시저를 호출하는 방식으로 연결
- 시간이 흐를수록 코드를 수정하기 수월해짐 (캡슐화에 대한 이야기)
객체
- 객체의 핵심
- 객체는 기능을 제공
- 내부적으로 가진 필드(데이터)로 정의하지 않음
- 예시) 회원 객체. 암호 변경하기 기능, 차단 여부 확인하기 기능
- 기능 명세
- 메서드(오퍼레이션)를 이용해 기능 명세
- 이름, 파라미터, 결과로 구성
- 메서드(오퍼레이션)를 이용해 기능 명세
- 객체와 객체
- 기능을 사용해 연결
- 기능 사용 = 메서드 호출
VolumnController volCont = new VolumnController(); volCont.increase(4); volCont.decrease(3); int currentVol = volCont.volume();
- 메세지
- 객체와 객체 상호 작용을 메세지를 주고 받는다고 표현
- 메서드를 호출하는 메세지, 리턴하는 메세지, 익셉션 메세지 등
캡슐화(Encapsulation)
- 데이터 + 관련 기능 묶기
- 객체가 기능을 어떻게 구현했는지는 외부에 감춤
- 구현에 사용된 데이터의 상세 내용을 외부에 감춤
- 정보 은닉(Information Hiding) 의미 포함
- 외부에 영향 없이 객체 내부 구현 변경 가능
- 캡슐화하지 않으면
- 요구사항의 변화가 데이터 구조 및 사용에 변화를 발생
- 데이터를 사용하는 코드 A, B, C 수정 필요
- 요구사항 예시
- 자익 사용자에게 특정 기능 실행 권장 연장
- 계정 차단하면 모든 실행 권한 없음
- Data를 LocalDateTime으로 변경
- 예시
- 왼쪽 코드는 변경되지 않음. Account 클래스 내부만 변경됨
- 캡슐화는 연쇄적인 변경 전파를 최소화
- 요구 사항의 변화가 내부 구현을 변경
- 캡슐화된 기능을 사용하는 코드 영향 최소화
- 캡슐화를 잘하면 기능에 대한 이해를 잘할 수 있음!
- 캡슐화를 위한 규칙
- Tell, Don’t Ask
- 데이터를 달라하지 말고 해달라고 하기
- 데이터를 가지고 와서 확인하지 말고 데이터를 확인을 해주세요!
- Demeter’s Law
- 메서드에서 생성한 객체의 메서드만 호출
- 파라미터로 받은 객체의 메서드만 호출
- 필드로 참조하는 개체의 메서드만 호출
- Tell, Don’t Ask
- 정리
- 캡슐화 : 기능의 구현을 외부에 감춤
- 캡슐화를 통해 기능을 사용하는 코드에 영향을 주지 않고(또는 최소화) 내부 구현을 변경할 수 있는 유연함을 가지게 됨
캡슐화 연습
- 1번째 예시
public AuthResult authenticate(String id, String pw) {
Member mem = findOne(id);
if (mem == null) return AuthResult.NO_MATCH;
if (mem.getVerificationEmailStatus() != 2) {
return AuthResult.NO_EMAIL_VERIFIED;
}
// 판단을 바꿔보기!
// if(!mem.isEmailVerified()) {
// return AuthResult.NO_EMAIL_VERIFIED;
//}
// public class Member {
// private int verificationEmail Status;
// public boolean isEmailVerified() {
// return verificationEmailStatus == 2;
// }
if (passwordEncoder.isPasswordValid(mem.getPassword(), pw, mem.getId())) {
return AuthResult.SUCCESS;
}
return AuthResult.NO_MATCH;
}
- 2번째 예시
- 데이터를 들고 있는 쪽에 기능을 추가하며, 기능에 필요한 다른 값을 파라미터로 받는 예시!
- 3번째 예시
- 4번째 예시
- 데이터를 가지고 와서 판단한 후, 판단의 결과로 데이터를 다시 바꾸는 코드
추상화
다형성
- 여러(poly) 모습(morph)을 갖는 것
- 객체 지향에서는 한 객체가 여러 타입을 갖는 것
- 즉 한 객체가 여러 타입의 기능을 제공
- 타입 상속으로 다형성 구현
- 하위 타입은 상위 타입도 됨
- 예시
추상화
- 데이터나 프로세스 등을 의미가 비슷한 개념이나 의미있는 표현으로 정의하는 과정
- 두 가지 방식의 추상화
- 특정한 성질, 공통 성질(일반화)
- 간단한 예
- DB의 USER 테이블 : 아이디, 이름, 이메일
- Mony 클래스 : 통화, 금액
- 프린터 : HP, 삼성
- GPU : 지포스, 라데온
- 서로 다른 구현 추상화
- SCP 파일 업로드, HTTP 데이터 전송, DB 테이블에 삽입
- 이것들을 추상화하면 푸시 발송 요청으로 표현 가능
- 타입 추상화
- 여러 구현 클래스를 대표하는 상위 타입 도출
- 흔히 인터페이스 타입으로 추상화
- 기능에 대한 의미를 제공하고, 구현은 제공하지 않음
- 추상화 타입과 구현은 타입 상속으로 연결
- EmailNotifier, SMSNotifier, KakaoNotifier 등이 콘크리트 클래스
- 흔히 인터페이스 타입으로 추상화
- 여러 구현 클래스를 대표하는 상위 타입 도출
- 추상 타입 사용
- 추상 타입을 이용한 프로그래밍
- 추상 타입은 구현을 감춤
- 기능의 구현이 아닌 의도를 더 잘 드러냄
- 추상 타입의 이점 : 유연함
- 콘크리트 클래스를 직접 사용하면
- 통제 방식이 바뀌면서 코드가 점점 변경됨
- 공통점을 도출해 추상 타입 사용
- 콘크리트 클래스를 직접 사용하면
- 추상화는 의존 대상이 변경하는 시점에 진행
- 추상화 -> 추상 타입 증가 -> 복잡도 증가
- 아직 존재하지 않는 기능에 대한 추상화는 주의!
- 잘못된 추상화 가능성이 있고, 복잡도만 증가
- 실제 변경 및 확장이 발생할 때 추상화 시도
- 추상화를 잘 하려면
- 구현을 한 이유가 무엇 때문인지 생각하기
- 추상화 예제는 강의 참고!
상속보단 조립
상속
- 상위 클래스의 기능을 재사용, 확장하는 방법으로 활용
- 그러나 상속을 통해 기능 재사용의 단점
- 상위 클래스 변경 어려움
- 상위 클래스를 조금만 변경해도 하위 클래스가 모두 영향을 받음
- 클래스 증가
- 새로운 조합이 생기면 하위 클래스가 증가
- 어떤 것을 상속 받아야 하는지 애매해짐
- 상속 오용
- 불필요한 기능까지 모두 상속되서 꼬일 수 있음
- 상위 클래스 변경 어려움
조립
- 상속의 단점 해결 방법
- 여러 객체를 묶어서 더 복잡한 기능을 제공
- 보통 필드로 다른 객체를 참조하는 방식으로 조립 또는 객체를 필요 시점에 생성/구함
- 상속보다는 조립(Composition over inferitance)
- 상속하기에 앞서 조립으로 풀 수 없는지 검토
- 진짜 하위 타입인 경우에만 상속 사용
기능과 책임 분리
기능 분해
- 기능은 하위 기능으로 분해
- 기능은 곧 책임
- 분리한 각 기능을 알맞게 분배
- 하위 기능 사용
큰 클래스, 큰 메서드
- 클래스나 메서드가 커지면 절차 지향의 문제 발생
- 큰 클래스 : 많은 필드를 많은 메서드가 공유
- 큰 메서드 : 많은 변수를 많은 코드가 공유
- 여러 기능이 한 클래스/메서드에 섞여 있을 가능성
- 책임에 따라 알맞게 코드 분리 필요
- 책임 분배/분리 방법
- 패턴 적용
- 계산 기능 분리
- 외부 연동 분리
- 조건별 분기는 추상화
패턴 적용
- 전형적인 역할 분리
- 간단한 웹
- 컨트롤러, 서비스, DAO
- 복잡한 도메인
- 엔티티, 밸류, 리포지토리, 도메인 서비스
- AOP
- Aspect(공통 기능)
- GoF
- 팩토리, 빌더, 전략, 템플릿 메서드, 프록시/데코레이터 등
- 간단한 웹
계산 분리
- 포인트를 계산하는 부분을 별도의 클래스로 사용
연동 분리
- 네트워크, 메시징, 파일 등 연동 처리 코드 분리
조건 분기는 추상화
- 연속적인 if-else는 추상화 고민
역할 분리시 주의할 점
- 의도가 잘 드러나는 이름을 사용해야 함
- HTTP로 추천 데이터 읽어오는 기능 분리시
- RecommendService > HttpDataService
- HTTP로 추천 데이터 읽어오는 기능 분리시
역할 분리와 테스트
- 역할 분리가 잘 되면 테스트도 용이
의존과 DI
의존
- 기능 구현을 위해 다른 구성 요소를 사용하는 것
- 의존의 예 : 객체 생성, 메서드 호출, 데이터 사용
- 의존은 변경이 전파될 가능성을 의미
- 의존하는 대상이 바뀌면 바뀔 가능성이 높아짐
- 예 : 호출하는 메서드의 파라미터가 변경
- 예 : 호출하는 메서드가 발생할 수 있는 익셉션 타입이 추가
- 의존하는 대상이 바뀌면 바뀔 가능성이 높아짐
순환 의존
- 변경 연쇄 전파 가능성 존재
- 클래스, 패키지, 모듈 등 모든 수준에서 순환 의존이 없도록
의존하는 대상이 많다면?
- 의존하는 대상이 많으면 변경될 확률이 높음
- 의존하는 대상은 적을수록 좋음
- 의존 대상이 많을 경우
- 1) 기능이 많은 경우
- 한 클래스에서 많은 기능을 제공하는 경우
- 각 기능마다 의존하는 대상이 다를 수 있고
- 한 기능 변경이 다른 기능에 영향을 줄 수 있음
- 기능 별로 분리 고려!!
- 클래스 개수는 증가하나 클래스마다 필요로 하는 의존이 적어짐
- 2) 묶어보기
- 몇 가지 의존 대상을 단일 기능으로 묶어서 생각하면 의존 대상을 줄일 수 있음
- 기능 구현을 추상화!
- 1) 기능이 많은 경우
- 의존 대상 객체를 직접 생성하면?
- 생성 클래스가 바뀌면 의존하는 코드도 바뀜(추상화 때 언급)
- 의존 대상 객체를 직접 생성하지 않는 방법
- 팩토리, 빌더
- 의존 주입(Depedency injection)
- 서비스 로케이터(Service Locator)
의존 주입(Depedency Injection)
- 외부에서 의존 객체를 주입
- 생성자나 메서드를 이용해 주입
- 조립기
- 조립기가 객체 생성, 의존 주입을 처리
- 예 : 스프링 프레임워크
- 조립기가 객체 생성, 의존 주입을 처리
- DI의 장점
- 1) 상위 타입을 사용할 경우 의존 대상이 바뀌면 조립기(설정)만 변경하면 됨
- 2) 의존하는 객체 없이 대역 객체를 사용해서 테스트 가능
- 1) 상위 타입을 사용할 경우 의존 대상이 바뀌면 조립기(설정)만 변경하면 됨
DI를 습관처럼 사용하기
- 의존 객체는 주입받도록 코드 작성하는 습관!
- 지금은 아니여도 점점 사용해보기
다음 학습 추천
- 강의 복습
- TDD (개발 속도, 좋은 설계 가능성 높여줌)
- 함수형 프로그래밍 기초 (비용을 낮춰주는 다른 방법)
- 각 패러다임의 설계 패턴(지식/지혜 재사용)
- UML(도식화)
부록(DIP)
고수준 모듈, 저수준 모듈
- 고수준 모듈
- 의미있는 단일 기능을 제공
- 상위 수준의 정책 구현
- 저수준 모듈
- 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현
- 그러나 고수준이 저수준에 직접 의존하면
- 저수준 모듈 변경이 고수준 모듈에 영향
- 고수준 정책이 바뀌지 않았으나 저수준 구현 변경으로 코드 변경 발생
Dependency Inversion Principle
- 의존 역전 원칙
- 고수준 모듈은 저수준 모듈의 구현에 의존하면 안됨
- 저수준 모듈이 고수준 모듈에서 정의한 추상타입에 의존해야 함
- 고수준 관점에서 추상화
- 고수준 입장에서 저수준 모듈을 추상화
- 구현 입장에서 추상화하지 말 것
- 고수준 입장에서 저수준 모듈을 추상화
- DIP는 유연함을 높임
- 고수준 모듈의 변경을 최소화하면서 저수준 모듈의 변경 유연함을 높임
- 부단한 추상화 노력 필요
- 처음부터 바로 좋은 설계가 나오진 않음
- 요구사항/업무 이해가 높아지며 저수준 모듈을 인지하고 상위 수준 관점에서 저수준 모듈에 대한 추상화 시도
- 처음부터 바로 좋은 설계가 나오진 않음
Reference
- 인프런 - 객체 지향 프로그래밍 입문
카일스쿨 유튜브 채널을 만들었습니다. 데이터 사이언스, 성장, 리더십, BigQuery 등을 이야기할 예정이니, 관심 있으시면 구독 부탁드립니다 :)
PM을 위한 데이터 리터러시 강의를 만들었습니다. 문제 정의, 지표, 실험 설계, 문화 만들기, 로그 설계, 회고 등을 담은 강의입니다
이 글이 도움이 되셨거나 다양한 의견이 있다면 댓글 부탁드립니다 :)