인프런 - 객체 지향 프로그래밍 입문


인프런 객체 지향 프로그래밍 입문 강의를 듣고 정리한 내용입니다


들어가며

  • 비용
    • 코드 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


이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)




© 2017. by Seongyun Byeon

Powered by zzsza