일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 백엔드로드맵
- 코드스테이츠 #회고록
- Java환경
- 에러해결
- 코딩테스트
- 네트워크 기초
- 회고
- Git #회고록
- git #intellij
- 사용자계정 #WSL2
- String
- 회고록_코드스테이츠
- mapstruct
- 코드스테이츠 #
- 롤중독
- WSL #Linux #ubuntu
- 시작 #꾸준히 #과하지않게
- jvm
- 공부방식
- 인터넷 #ISP
- WSL2 #권한
- 피드백
- 회고록
- 회고 #주간회고
- JAVA기본
- OOP
- 몰입학습 #몰입
- 정리글 작성법
- 호스트주소 #Host주소
- 글 foramt
- Today
- Total
느리더라도 꾸준히
[OOP]객체 지향 프로그래밍(OOP) - (2)추상화 & 다형성 & DI 본문
목표
객체 지향적이지 않은 코드를 객체 지향으로 바꾸면서 객체지향의 4가지 특징을 이해한다.
최종적으로 객체 지향적인 코드를 만들어보고, 이를 미리 구현 해 놓은 Springframework를 이해해보자.
(1) 상속
(2) 추상화 및 다형성를 통한 DI(Dependency Injection)
(3) 캡슐화
(4) SRP(Single-Relationship Principle)
정리
추상화
- 클래스 간의 공통적인 부분을 뽑아서 상위 클래스나 인터페이스로 만드는 개념이다.
- 이중 인터페이스는 역할 과 구현을 분리시켜 인터페이스를 사용하는 입장(객체)에게 복잡한 구현의 내용 또는 변경과 상관없이 기능을 사용하도록 만들 수 있다.
다형성
- 하나의 객체가 여러가지 형태를 가질 수 있는 성질을 말하며, 상위 클래스 타입의 참조 변수로 여러 하위 클래스의 객체를 참조 할 수 있도록 허용한 것이다.
추상화와 다형성을 구현하고 생성자 주입을 사용하면, 변화와 확장에 유연한 객체지향적 코드를 작성 할 수 있다. 그리고 이러한 테크닉을 의존성 주입이라고 부른다.
의존성 주입(DI)
- 추상화와 다형성을 구현하고 생성자 주입을 사용하면, 변화와 확장에 유연한 객체지향적 코드를 작성 할 수 있다. 그리고 이러한 개념을 의존성 주입이라고 부른다.
- 객체가 자신이 의존할 객체를 스스로 만들도록 하는 것이 아니라 외부에서 주입해주는 것을 의미한다.
본문
할인 로직 시스템의 문제점
추상화와 다형성을 가진 코드를 작성하기 전에 이전에 설계한 할인 로직 시스템의 문제를 확인해 봅시다.
public class MartService {
private Customer[] customers;
int originalPrice;
// 10퍼 센트 할인
StudentDiscountCondition studentDiscountCondition
= new StudentDiscountCondition(new RatePolicy(10));
// 10000원 할인
EmployeeDiscountCondition employeeDiscountCondition
= new EmployeeDiscountCondition(new AmountPolicy(10000));
public MartService(Customer[] customers, int originalPrice) {
this.customers = customers;
this.originalPrice = originalPrice;
}
HashMap<Customer, Integer> discountedHashMap = new HashMap<>();
public void service(){
// 각 고객마다 할인을 적용해본다.
DiscountAllCustomers();
// 고객마다의 할인 요금 출력하기
printDiscountedPrice();
}
private void DiscountAllCustomers() {
for(Customer customer : customers){
String customerType = customer.getCustomerType();
if(customerType == "일반") discountedHashMap.put(customer, originalPrice);
else if(customerType == "직원") {
discountedHashMap.put(customer,
employeeDiscountCondition.applyDiscountPolicy(originalPrice));
}else if(customerType == "학생") {
discountedHashMap.put(customer,
studentDiscountCondition.applyDiscountPolicy(originalPrice));
}else {
System.out.println("처음보는 유형입니다.");
}
}
}
private void printDiscountedPrice() {
System.out.println("할인 요금을 출력합니다.");
Set<Customer> keySet = discountedHashMap.keySet();
Iterator<Customer> iter = keySet.iterator();
while(iter.hasNext()){
Customer customer = iter.next();
System.out.printf("고객이름 : %s , 할인된요금 : %d\n",
customer.getName(),
discountedHashMap.get(customer));
}
}
}
객체지향코드는 유연한 확장이 가능해야합니다. 즉 새로운 할인 조건 또는 할인 정책이 생성되거나, 할인 조건 속 할인 정책이 변경했을때 등 다양한 경우에 유연한 확장이 가능해야하는 것입니다.
예시
현재 학생 - 정률 / 직원 - 정액 의 할인에서 학생-정액 / 직원 - 정률로 바꾼다고 가정해봅시다.
그리고 20% 할인해주는 유아용 할인을 추가해준다고 가정해봅시다. 그렇다면 우리는 MartApp 의 소스코드를 수정해야합니다.
public class MartService {
private Customer[] customers;
int originalPrice;
StudentDiscountCondition studentDiscountCondition
// = new StudentDiscountCondition(new RatePolicy(10));
= new StudentDiscountCondition(new AmountPolicy(10000));
// 10000원 할인
EmployeeDiscountCondition employeeDiscountCondition
//= new EmployeeDiscountCondition(new AmountPolicy(10000));
= new EmployeeDiscountCondition(new RatePolicy(10));
// 만약 유아용 할인이 추가된다면?
// BabyDiscountCondition babyDiscountCondition
// = new BabyDiscountCondition(new RatePolicy(20));
....
private void DiscountAllCustomers() {
for(Customer customer : customers){
String customerType = customer.getCustomerType();
if(customerType == "일반") discountedHashMap.put(customer, originalPrice);
else if(customerType == "직원") {
discountedHashMap.put(customer,
employeeDiscountCondition.applyDiscountPolicy(originalPrice));
}else if(customerType == "학생") {
discountedHashMap.put(customer,
studentDiscountCondition.applyDiscountPolicy(originalPrice));
// else if(customerType == "유아") {
// discountedHashMap.put(customer,
// BabyDiscountCondition.applyDiscountPolicy(originalPrice));
}else {
System.out.println("처음보는 유형입니다.");
}
}
}
위 코드의 문제는 아래의 문제를 가집니다.
- 요구 사항이 변화함에 따라 기존에 작성해둔 코드의 여러 군데를 수정해주어야만 한다는 점입니다.
- applyDiscountPolicy()를 호출하는 중복적인 코드가 보이고 있습니다.
- 각 고객에 맞는 할인가격을 가져와서 출력해주기만 하면 되는 MartService에 할인조건 생성이라는 책임을 더해주고 있습니다.
현재는 프로그램의 규모가 크지않기때문에 손쉽게 수정할 수 있지만, 몇천줄 되는 코드에서 수정될 부분될 부분을 찾아 몇백줄을 고쳐준다고 생각해보면 해당 코드와 변화와 확장에 유연하지 않음을 알 수 있습니다.
할인 로직에 추상화와 다형성을 적용시켜 보자.
추상화란?
이전 상속이 상위 클래스와 하위 클래스의 공통의 부분을 재사용하고 확장하는 개념이었다면, 추상화는 클래스들의 공통된 부분을 뽑아서 상위 클래스 또는 인터페이스를 만들어내는 개념입니다.
인터페이스란?
서로 다른 두 시스템, 장치, 소프트웨어 따위를 서로 이어주는 부분 또는 그런 접속 장치" 라 정의할 수 있습니다.
좀더 쉽게 말해 Java 에서는 객체와 객체를 이어주는 부분이라고 이해하시면 될 것 같습니다.
public interface InterfaceEx {
public static final int rock = 1; // 인터페이스 인스턴스 변수 정의
final int scissors = 2; // public static 생략
static int paper = 3; // public & final 생략
public abstract String getPlayingNum();
void call() //public abstract 생략
}
인터페이스에서 생성한 필드는 자동적으로 public static final 제어자가 붙습니다.
인터페이스에서는 추상메서드만 생성 할 수 있습니다.
인터페이스는 역할 과 구현을 분리시켜 인터페이스를 사용하는 입장(객체)에게 복잡한 구현의 내용 또는 변경과 상관없이 기능을 사용하도록 만들수 있습니다.( 다형성 아래 예시를 통해 확인 )
다형성이란?
일반적으로는 하나의 객체가 여러 가지 형태를 가질 수 있는 성질을 말합니다. 좀 더 구체적인 사례를 말하자면 상위 클래스 타입의 참조변수를 통해서 하위 클래스의 객체를 참조할 수 있도록 허용한 것을 말합니다.
ex ) 오버라이딩, 오버로딩, 참조변수의 타입 변환등
인터페이스와 참조변수의 타입 변환을 적용한 예시
interface Cover { // 인터페이스 정의 - 역할
public abstract void call();
}
public class Interface4 {
public static void main(String[] args) {
User user = new User();
// Provider provider = new Provider();
// user.callProvider(new Provider()); // 무야호~
user.callProvider(new Provider2()); // 야호~
}
}
class User {
public void callProvider(Cover cover) { // 매개변수의 다형성 활용
cover.call();
}
}
// 구현체들 - 구현
class Provider implements Cover {
public void call() {
System.out.println("무야호~");
}
}
// 구현체들 - 구현
class Provider2 implements Cover {
public void call() {
System.out.println("야호~");
}
}
//출력값
야호~
각 구현체들의 공통적인 부분을 추출하여 Cover 인터페이스를 선언합니다.(추상화)
callProvider() 의 매개변수 속 참조변수는 인터페이스 타입으로 사용되어 여러 구현체들이 참조 될수 있는 다형성의 모습을 볼 수 있습니다.
이제 문제가 되는 할인 조건과 할인 정책에 추상화와 다형성을 적용해 봅시다.
DiscountCondition 할인 조건
public interface DiscountCondition {
int applyDiscountPolicy(int price);
}
public class EmployeeDiscountCondition implements DiscountCondition{
AmountPolicy amountPolicy;
public EmployeeDiscountCondition(AmountPolicy amountPolicy) {
this.amountPolicy = amountPolicy;
}
public int applyDiscountPolicy(int price){
return amountPolicy.discount(price);
}
}
public class StudentDiscountCondition implements DiscountCondition{
RatePolicy ratePolicy;
public StudentDiscountCondition(RatePolicy ratePolicy) {
this.ratePolicy = ratePolicy;
}
// 할인 정책을 적용해서 할인값을 리턴하는 메서드
public int applyDiscountPolicy(int price){
return ratePolicy.discount(price);
}
}
두 할인 조건의 공통적인 부분을 DiscountCondition 인터페이스 속에 선언하고 implements keyword로 구현체로 선언해 주었습니다.(추상화)
DiscountPolicy 할인 정책
public interface DiscountPolicy {
int discountAmount=0;
int discount(int price);
}
public class AmountPolicy implements DiscountPolicy{
private int discountAmount;
public AmountPolicy(int discountAmount) {
this.discountAmount = discountAmount;
}
public int discount(int price){
return price - discountAmount;
}
}
public class RatePolicy implements DiscountPolicy{
private int discountRate;
public RatePolicy(int discountRate) {
this.discountRate = discountRate;
}
public int discount(int price){
return price - (price * discountRate / 100);
}
}
마찬가지로 두 할인 정책의 공통적인 부분을 DiscountPolicy 인터페이스 속에 선언하고, 각 클래스들이 구현하도록 만들었습니다.(추상화)
할인 정책에 다형성 구현하기
StudentDiscountCondition
public class StudentDiscountCondition implements DiscountCondition{
// RatePolicy ratePolicy;
DiscountPolicy discountPolicy;
// public StudentDiscountCondition(RatePolicy ratePolicy) {
// this.ratePolicy = ratePolicy;
// }
public StudentDiscountCondition(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
// 할인 정책을 적용해서 할인값을 리턴하는 메서드
// public int applyDiscountPolicy(int price){
// return ratePolicy.discount(price);
// }
public int applyDiscountPolicy(int price){
return discountPolicy.discount(price);
}
}
StudentDiscountCondition 는 RatePolicy 객체를 의존하지 않고, 역할을 담당하는 DiscountPolicy 인터페이스를 의존하도록 수정해줍니다.
이제 StudentDiscountCondition 객체는 참조변수를 통해 DiscountPolicy 의 구현체인 RatePolicy
, AmountPolicy
중 하나의 구현체를 참조해올수 있습니다.
public class EmployeeDiscountCondition implements DiscountCondition{
// AmountPolicy amountPolicy;
DiscountPolicy discountPolicy;
public EmployeeDiscountCondition(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
public int applyDiscountPolicy(int price){
return discountPolicy.discount(price);
}
}
인터페이스를 의존하도록 다형적인 표현을 입력해 줍시다. 마찬가지로 EmployeeDiscountCondition 도 똑같이 동작합니다.
**MartService
할인 로직 수정하기**
public class MartService {
private Customer[] customers;
int originalPrice;
// Map 생성
HashMap<String, DiscountCondition> discountConditionHashMap;
// 생성자 주입
public MartService(Customer[] customers, int originalPrice
, HashMap<String, DiscountCondition> discountConditionHashMap) { // 추가
this.customers = customers;
this.originalPrice = originalPrice;
this.discountConditionHashMap = discountConditionHashMap;
}
...
// 이전코드-------------------------------------
private void DiscountAllCustomers() {
for(Customer customer : customers){
String customerType = customer.getCustomerType();
if(customerType == "일반") discountedHashMap.put(customer, originalPrice);
else if(customerType == "직원") {
discountedHashMap.put(customer,
employeeDiscountCondition.applyDiscountPolicy(originalPrice));
}else if(customerType == "학생") {
discountedHashMap.put(customer,
studentDiscountCondition.applyDiscountPolicy(originalPrice));
}else {
System.out.println("처음보는 유형입니다.");
}
}
}
// 변화된 코드-------------------------------------
private void DiscountAllCustomers() {
for(Customer customer : customers){
String customerType = customer.getCustomerType();
if(customerType == "일반") discountedHashMap.put(customer, originalPrice);
else {
discountedHashMap.put(customer,
discountConditionHashMap.get(customerType).applyDiscountPolicy(originalPrice));
}
}
}
...
MartService 클래스는 인터페이스를 구현하는 구현체, 즉 할인조건들을 모두 가져와야합니다. 또한 고객의 유형에 따라 할인정도를 다르게 합니다.
이를 구현하기 위해 필드에 고객을 key로 값을 할인 조건으로하는 Map 자료구조 형식으로 필드를 선언했습니다. 또한 구현체의 주입을 받기 위해 생성자로 주입해주고 있습니다.
변수 discountConditionHashMap는 생성자로 주입을 받으므로, MarSerive 객체를 생성하는 MartApp에서 HashMap<String, DiscountCondition>을 생성해주어야합니다.
public class MartApp {
// 고객정보
CustomerRepository customerRepository = new CustomerRepository();
Customer[] customers = customerRepository.findAll();
int originalPrice = new PhoneInfo(1000000, "iphone").getPrice();
// ------------ 추가
HashMap<String, DiscountCondition> discountConditionHashMap = new HashMap<String, DiscountCondition>(){{
put("학생",new StudentDiscountCondition(new RatePolicy(10)));
put("직원",new EmployeeDiscountCondition(new AmountPolicy(10000)));
// 유아 할인조건 추가시
// put("유아",new BabyDiscountCondition(new RatePolicy(20)));
}};
// ----------------
MartService martService
= new MartService(customers, originalPrice, discountConditionHashMap);
public void start(){
// 할인 로직 시작
martService.service();
}
}
이제는 만약 할인 조건이 추가되거나 할인 정책이 변경되더라도 MartService의 코드는 변경할 필요가 없습니다. 단지 MartApp 클래스에서 생성자속 매개변수만 수정해주면 됩니다.
결국 추상화를 통해 역할과 구현을 분리시키고, 다형성을 통해 인터페이스 타입으로 여러 구현체를 참조하면서 기능 변경과 확장에 유연한 객체지향 코드를 완성할수 있습니다.
최종 의존 관계도
Dependency Injection (DI)
앞에서 작업한 추상화와 다형성 성질을 구현하고 생성자 주입을 사용하면, 변화와 확장에 유연한 객체지향적 코드를 작성 할 수 있습니다. 이러한 테크닉을 의존성 주입이라고 부릅니다.
의존성 주입은 객체가 자신이 의존할 객체를 스스로 만들도록 하는 것이 아니라 외부에서 주입해주는 것을 의미합니다. 그리고 이러한 테크닉은 추상화와 다형성을 기반으로 동작 시킬 수 있습니다.
- 인터페이스를 통해 공통된 메서드들을 추상화하여 추상메서드로 정의하였고, 인터페이스를 타입으로 사용한 필드를 정의함으로써 다형성을 통해 구현 클래스의 객체를 할당받을 수 있기 때문입니다.
- 예를 들어 이제는 구현체 StudentDiscountCondition가 스스로 EmployeeDiscountPolicy 객체를 생성해서 사용하지 않습니다. 그리고 StudentDiscountCondition객체가 생성되는 시점에 EmployeeDiscountPolicy 역할을 수행할 객체를 외부로부터 생성자를 통해 주입받고 있습니다.
'Java' 카테고리의 다른 글
[Stream]map() 그리고 mapToInt(); (0) | 2022.11.20 |
---|---|
[OOP]객체지향 프로그래밍(OOP) - (3) 캡슐화 (0) | 2022.11.17 |
[OOP]객체 지향 프로그래밍(OOP) - (0)설계 (0) | 2022.11.16 |
[OOP]객체 지향 프로그래밍(OOP) - (1) 상속 (0) | 2022.11.16 |
[Java기본]가상 함수(메서드)란? (0) | 2022.11.15 |