객체 지향 프로그래밍 기본


객체 지향 프로그래밍에 대해 정리한 내용입니다


들어가며

  • 비용
    • 코드 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
      • 메서드에서 생성한 객체의 메서드만 호출
      • 파라미터로 받은 객체의 메서드만 호출
      • 필드로 참조하는 개체의 메서드만 호출
  • 정리
    • 캡슐화 : 기능의 구현을 외부에 감춤
    • 캡슐화를 통해 기능을 사용하는 코드에 영향을 주지 않고(또는 최소화) 내부 구현을 변경할 수 있는 유연함을 가지게 됨

캡슐화 연습

  • 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

역할 분리와 테스트

  • 역할 분리가 잘 되면 테스트도 용이

의존과 DI


의존

  • 기능 구현을 위해 다른 구성 요소를 사용하는 것
    • 의존의 예 : 객체 생성, 메서드 호출, 데이터 사용
  • 의존은 변경이 전파될 가능성을 의미
    • 의존하는 대상이 바뀌면 바뀔 가능성이 높아짐
      • 예 : 호출하는 메서드의 파라미터가 변경
      • 예 : 호출하는 메서드가 발생할 수 있는 익셉션 타입이 추가

순환 의존

  • 변경 연쇄 전파 가능성 존재
    • 클래스, 패키지, 모듈 등 모든 수준에서 순환 의존이 없도록

의존하는 대상이 많다면?

  • 의존하는 대상이 많으면 변경될 확률이 높음
  • 의존하는 대상은 적을수록 좋음
  • 의존 대상이 많을 경우
    • 1) 기능이 많은 경우
      • 한 클래스에서 많은 기능을 제공하는 경우
      • 각 기능마다 의존하는 대상이 다를 수 있고
      • 한 기능 변경이 다른 기능에 영향을 줄 수 있음
      • 기능 별로 분리 고려!!
        • 클래스 개수는 증가하나 클래스마다 필요로 하는 의존이 적어짐
    • 2) 묶어보기
      • 몇 가지 의존 대상을 단일 기능으로 묶어서 생각하면 의존 대상을 줄일 수 있음
      • 기능 구현을 추상화!
  • 의존 대상 객체를 직접 생성하면?
    • 생성 클래스가 바뀌면 의존하는 코드도 바뀜(추상화 때 언급)
    • 의존 대상 객체를 직접 생성하지 않는 방법
      • 팩토리, 빌더
      • 의존 주입(Depedency injection)
      • 서비스 로케이터(Service Locator)

의존 주입(Depedency Injection)

  • 외부에서 의존 객체를 주입
    • 생성자나 메서드를 이용해 주입
  • 조립기
    • 조립기가 객체 생성, 의존 주입을 처리
      • 예 : 스프링 프레임워크
  • DI의 장점
    • 1) 상위 타입을 사용할 경우 의존 대상이 바뀌면 조립기(설정)만 변경하면 됨
    • 2) 의존하는 객체 없이 대역 객체를 사용해서 테스트 가능

DI를 습관처럼 사용하기

  • 의존 객체는 주입받도록 코드 작성하는 습관!
  • 지금은 아니여도 점점 사용해보기

다음 학습 추천

  • 강의 복습
  • TDD (개발 속도, 좋은 설계 가능성 높여줌)
  • 함수형 프로그래밍 기초 (비용을 낮춰주는 다른 방법)
  • 각 패러다임의 설계 패턴(지식/지혜 재사용)
  • UML(도식화)

부록(DIP)


고수준 모듈, 저수준 모듈

  • 고수준 모듈
    • 의미있는 단일 기능을 제공
    • 상위 수준의 정책 구현
  • 저수준 모듈
    • 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현
  • 그러나 고수준이 저수준에 직접 의존하면
    • 저수준 모듈 변경이 고수준 모듈에 영향
    • 고수준 정책이 바뀌지 않았으나 저수준 구현 변경으로 코드 변경 발생

Dependency Inversion Principle

  • 의존 역전 원칙
    • 고수준 모듈은 저수준 모듈의 구현에 의존하면 안됨
    • 저수준 모듈이 고수준 모듈에서 정의한 추상타입에 의존해야 함
  • 고수준 관점에서 추상화
    • 고수준 입장에서 저수준 모듈을 추상화
      • 구현 입장에서 추상화하지 말 것
  • DIP는 유연함을 높임
    • 고수준 모듈의 변경을 최소화하면서 저수준 모듈의 변경 유연함을 높임
  • 부단한 추상화 노력 필요
    • 처음부터 바로 좋은 설계가 나오진 않음
      • 요구사항/업무 이해가 높아지며 저수준 모듈을 인지하고 상위 수준 관점에서 저수준 모듈에 대한 추상화 시도

Reference


카일스쿨 유튜브 채널을 만들었습니다. 데이터 분석, 커리어에 대한 내용을 공유드릴 예정입니다.

PM을 위한 데이터 리터러시 강의를 만들었습니다. 문제 정의, 지표, 실험 설계, 문화 만들기, 로그 설계, 회고 등을 담은 강의입니다

이 글이 도움이 되셨거나 의견이 있으시면 댓글 남겨주셔요.

Buy me a coffeeBuy me a coffee