카테고리 없음

34.5일차 PSA걍추상화

바론고 2022. 6. 14. 14:33
  • POJO(Plain Old Java Object)
    • POJO의 의미 순수한 Java 객체
    • POJO가 필요한 이유 다른 기술이나 환경에 종속되지 않도록
    • 한줄요약  ㅡ플레인(포조맛)
  • IoC(Inversioin of Control)/ DI(Dependency Injection)
    • IoC/DI의 의미    Ioc=흐름의 주도권이 뒤바뀐 것   / DI=생성자 등을 통해 외부에서 다른 클래스의 객체를 전달 받기
    • IoC/DI가 필요한 이유  느슨한 결합만듬->요구 사항에 유연하게 대처
    • 한줄요약 - new객체생성금지 ->생성자매개변수통해ㄱ IoC
    •  

 

  • AOP(Aspect Oriented Programming)
    • AOP의 의미 관심 지향 프로그래밍 공통 관심사 공통 기능//로깅, 보안, 트랜잭션, 모니터링, 트레이싱
    • AOP가 필요한 이유 -코드의 간결성 유지 코드의 재사용
    • 한줄요약 - AOP기본공통기능
    • (공통옵)

 

  • PSA(Portable Service Abstraction)
    • PSA의 의미 상위 클래스를 일관되게 바라보며 하위 클래스의 기능을 사용
    • PSA가 필요한 이유 일관된 방식으로 기술이 변경되더라도 최소한의 변경
    •  추상화인터페이스PSA

 

 

 

PSA(Portable Service Abstraction)란?

추상화(Abstraction)의 개념

객체지향 프로그래밍 세계에서 추상화(Abstraction)의 의미는 피카소가 그린 추상화 그림의 의미와 별반 다르지 않습니다.

 

피카소의 그림처럼 객체지향 프로그래밍 세계에서는 어떤 클래스의 본질적인 특성만을 추출해서 일반화 하는것을 바로 추상화(Abstraction)라고 합니다.

 

어떤 용어든 간에 추상화의 의미를 이해하는데 더 적합한 언어를 사용하면 되겠지만 추상화보다는 일반화라는 표현이 설계 관점에서 더 적절한 표현인 것 같습니다.

 

추상화를 표현할 수 있는 대표적인 방법이 바로 추상 클래스와 인터페이스입니다.

 

추상화의 예

예를 들어, 미취학 아동을 관리하는 애플리케이션을 설계하면서 아이 클래스를 일반화(추상화) 한다라고 가정해봅시다.

 

먼저 미취학 아동을 관리하기 위해 필요한 아이의 일반적인 특징에는 뭐가 있는지 한번 살펴보겠습니다.

Java의 클래스는 속성을 나타내는 멤버 변수와 동작을 나타내는 메서드로 구성되므로, 아기의 속성과 동작을 일반화해서 멤버 변수와 메서드로 표현해 보도록 하겠습니다.

 

먼저 아이를 관리하는 관점에서 아이의 일반적인 속성으로는 이름, 키, 몸무게, 혈액형, 나이 등이 있을 것 같습니다.

그리고 일반적으로 아이가 할 수 있는 동작으로는 웃다, 울다, 자다, 먹다 등이 있을테구요.

public abstract class Child {
    protected String childType;
    protected double height;
    protected double weight;
    protected String bloodType;
    protected int age;

    protected abstract void smile();

    protected abstract void cry();

    protected abstract void sleep();

    protected abstract void eat();
}

[코드 2-13]은 미취학 아동을 관리하기 위한 관점에서 아이의 일반적인 특징을 추상 클래스로 작성했습니다.

 

 

추상화를 하는 이유

그렇다면 추상화는 왜 하는 걸까요?

[코드 2-13]을 확장하는 예제 코드를 통해서 추상화가 필요한 이유를 설명하도록 하겠습니다.

// NewBornBaby.java(신생아)
public class NewBornBaby extends Child {
    @Override
    protected void smile() {
        System.out.println("신생아는 가끔 웃어요");
    }

    @Override
    protected void cry() {
        System.out.println("신생아는 자주 울어요");
    }

    @Override
    protected void sleep() {
        System.out.println("신생아는 거의 하루 종일 자요");
    }

    @Override
    protected void eat() {
        System.out.println("신생아는 분유만 먹어요");
    }
}

// Infant.java(2개월 ~ 1살)
public class Infant extends Child {
    @Override
    protected void smile() {
        System.out.println("영아는 많이 웃어요");
    }

    @Override
    protected void cry() {
        System.out.println("영아는 종종 울어요");
    }

    @Override
    protected void sleep() {
        System.out.println("영아부터는 밤에 잠을 자기 시작해요");
    }

    @Override
    protected void eat() {
        System.out.println("영아부터는 이유식을 시작해요");
    }
}

// Toddler.java(1살 ~ 4살)
public class Toddler extends Child {
    @Override
    protected void smile() {
        System.out.println("유아는 웃길 때 웃어요");
    }

    @Override
    protected void cry() {
        System.out.println("유아는 화가나면 울어요");
    }

    @Override
    protected void sleep() {
        System.out.println("유아는 낮잠을 건너뛰고 밤잠만 자요");
    }

    @Override
    protected void eat() {
        System.out.println("유아는 딱딱한 걸 먹기 시작해요");
    }
}

[코드 2-14]는 Child 클래스를 확장한 하위 클래스의 예입니다.

 

그렇다면 해당 연령에 맞는 NewBornBaby, Infant, Toddler 클래스를 사용하기 위해서 어떤 방식으로 접근하면 될까요?

public class ChildManageApplication {
    public static void main(String[] args) {
        Child newBornBaby = new NewBornBaby(); // (1)
        Child infant = new Infant(); // (2)
        Child toddler = new Toddler(); // (3)

        newBornBaby.sleep();
        infant.sleep();
        toddler.sleep();
    }
}

실행 결과
=========================================
신생아는 거의 하루 종일 자요
영아부터는 밤에 잠을 자기 시작해요
유아는 낮잠을 건너뛰고 밤잠만 자요

[코드 2-15] 상위 클래스에 정의된 일반화 된 특징을 하위 클래스의 특징에 맞게 사용하는 예

[코드 2-15]에서는 Child라는 상위 클래스에 일반화 시켜 놓은 아이의 동작을 NewBornBaby, Infant, Toddler 라는 클래스로 연령별 아이의 동작으로 구체화 시켜서 사용을 하고 있습니다.

 

여기서 중요한 것은 클라이언트(여기서는 ChildManageApplication 클래스의 main() 메서드)는NewBornBaby, Infant, Toddler 를 사용할 때 구체화 클래스의 객체를 자신의 타입에 할당하지 않고, (1) ~ (3)과 같이 Child 클래스 변수에 할당을 해서 접근을 합니다.

 

이렇게 되면 클라이언트 입장에서는 Child 라는 추상 클래스만 일관되게 바라보며 하위 클래스의 기능을 사용할 수 있습니다.

이처럼 클라이언트가 추상화 된 상위 클래스를 일관되게 바라보며 하위 클래스의 기능을 사용하는 것이 바로 일관된 서비스 추상화(PSA)의 기본 개념입니다.

 

일반적으로 서버 / 클라이언트 측면에서는 서버 측 기능을 이용하는 쪽을 클라이언트라고 합니다. 우리가 알고 있는 대표적인 클라이언트는 바로 웹 브라우저입니다.

그런데 코드 레벨에서 어떤 클래스의 기능을 사용하는 측 역시 클라이언트라고 부른다는 사실을 기억하길 바랍니다.

 

서비스에 적용하는 일관된 서비스 추상화 (PSA)기법

서비스 추상화란 위와 같은 추상화의 개념을 애플리케이션에서 사용하는 서비스에 적용하는 기법입니다.

[그림 2-14]는 Java 콘솔 애플리케이션에서 클라이언트가 데이터베이스에 연결하기 위해 JdbcConnector를 사용하기 위한 서비스 추상화의 예입니다.

 

즉, JdbcConnector가 애플리케이션에서 이용하는 하나의 서비스가 되는 것입니다.

 

Java에서 특정 데이터베이스에 연결하기 위해서는 해당 데이터베이스의 JDBC 구현체로부터 Connection을 얻어야 하는데 [그림 2-14]는 이 동작을 재현해보기 위한 클래스 다이어그램이라고 생각하면 되겠습니다.

 

[그림 2-14]에서 DbClient는 OracleJdbcConnector, MariaDBJdbcConnector, SQLiteJdbcConnector 같은 JdbcConnector 인터페이스의 구현체에 직접적으로 연결해서 Connection을 얻는 것이 아니라 JdbcConnector 인터페이스를 통해 간접적으로 연결되어(느슨한 결합) Connection 객체를 얻는 것을 볼 수 있습니다.

 

그런데 DbClient에서 어떤 JdbcConnector 구현체를 사용하더라도 Connection을 얻는 방식은 getConnection() 메서드를 사용해야 하기 때문에 동일합니다.

즉, 일관된 방식으로 해당 서비스의 기능을 사용할 수 있다는 의미입니다.

 

이처럼 애플리케이션에서 특정 서비스를 이용할 때, 서비스의 기능을 접근하는 방식 자체를 일관되게 유지하면서 기술 자체를 유연하게 사용할 수 있도록 하는 것을 PSA(일관된 서비스 추상화)라고 합니다.

 

 

// DbClient.java
public class DbClient {
    public static void main(String[] args) {
        // Spring DI로 대체 가능
        JdbcConnector connector = new SQLiteJdbcConnector(); // (1)

        // Spring DI로 대체 가능
        DataProcessor processor = new DataProcessor(connector); // (2)
        processor.insert();
    }
}

// DataProcessor.java
public class DataProcessor {
    private Connection connection;

    public DataProcessor(JdbcConnector connector) {
        this.connection = connector.getConnection();
    }

    public void insert() {
        // 실제로는 connection 객체를 이용해서 데이터를 insert 할 수 있다.
        System.out.println("inserted data");
    }
}

// JdbcConnector.java
public interface JdbcConnector {
    Connection getConnection();
}

// MariaDBJdbcConnector.java
public class MariaDBJdbcConnector implements JdbcConnector {
    @Override
    public Connection getConnection() {
        return null;
    }
}

// OracleJdbcConnector.java
public class OracleJdbcConnector implements JdbcConnector {
    @Override
    public Connection getConnection() {
        return null;
    }
}

// SQLiteJdbcConnector.java
public class SQLiteJdbcConnector implements JdbcConnector {
    @Override
    public Connection getConnection() {
        return null;
    }
}

[코드 2-16] JdbcConnector 서비스를 사용하기 위한 PSA 예제 코드

[코드 2-16]은 [그림 2-14]의 클래스 다이어그램을 기반으로 JdbcConnector 서비스를 사용하는 예제 코드인데, 편의상 6개의 .java 파일을 하나로 표시했습니다.

 

DbClient 클래스의 (1)에서 SQLiteJdbcConnector 구현체의 객체를 생성해서 JdbcConnector 인터페이스 타입의 변수에 할당(업캐스팅)하고 있는 것을 볼 수 있습니다.

 

그리고 (2)에서 실제로 데이터를 데이터베이스에 저장하는 기능을 하는 DataProcessor 클래스의 생성자로 JdbcConnector 객체를 전달하고 있습니다(의존성 주입).

 

만약에 다른 애플리케이션에서 SQLite 데이터베이스를 사용하는 것이 아니라 Oracle 데이터베이스를 사용해야 한다면, JdbcConnector 서비스 모듈을 그대로 가져와서 (1)의 new SQLiteJdbcConnector()를 new OracleJdbcConnector()로 바꿔서 사용하면 될 것입니다.

 

[코드 2-16]은 PSA의 개념을 이해하기 위한 용도로 작성된 것이지 실제 Java에서 JDBC를 이용하는 API 사용법이 코드 2-16과 동일하지 않습니다.

우리가 앞으로 Spring 기술을 사용하면서 내부적으로 JDBC 기술을 이용하긴하지만 Java JDBC 기술을 로우 레벨에서 사용하는 일은 없을테니 개념 이해 수준에서 넘어가면 되겠습니다.

 

PSA가 필요한 이유

PSA가 필요한 주된 이유는 어떤 서비스를 이용하기 위한 접근 방식을 일관된 방식으로 유지함으로써 애플리케이션에서 사용하는 기술이 변경되더라도 최소한의 변경만으로 변경된 요구 사항을 반영하기 위함입니다.

즉, PSA를 통해서 애플리케이션의 요구 사항 변경에 유연하게 대처할 수 있습니다.

 

Spring은 상황에 따라 기술이 바뀌더라도 변경된 기술에 일관된 방식으로 접근할 수 있는 PSA를 적극적으로 지원하고 있습니다.

Spring에서 PSA가 적용된 분야로는 트랜잭션 서비스, 메일 서비스, Spring Data 서비스 등이 있습니다.

 

여러분들이 당장에 직접적으로 PSA를 이용해 어떤 서비스를 구현할일은 없을거라 생각합니다. 하지만 PSA의 기본 개념을 이해함으로써 Spring이 지원하는 기술을 적절하게 사용할 수 있고, 필요하다면 미래에 PSA를 통한 서비스를 직접 개발할 수 있는 원동력이 될 것이라고 생각합니다.

 


 

핵심 포인트

  • 객체지향 프로그래밍 세계에서 어떤 클래스의 본질적인 특성만을 추출해서 일반화 하는것을 추상화(Abstraction)라고 한다.
  • 클라이언트가 추상화 된 상위 클래스를 일관되게 바라보며 하위 클래스의 기능을 사용하는 것이 바로 일관된 서비스 추상화(PSA)의 기본 개념이다.
  • 애플리케이션에서 특정 서비스를 이용할 때, 서비스의 기능을 접근하는 방식 자체를 일관되게 유지하면서 기술 자체를 유연하게 사용할 수 있도록 하는 것을 PSA(일관된 서비스 추상화)라고 한다.
  • PSA가 필요한 주된 이유는 어떤 서비스를 이용하기 위한 접근 방식을 일관된 방식으로 유지함으로써 애플리케이션에서 사용하는 기술이 변경되더라도 최소한의 변경만으로 변경된 요구 사항을 반영하기 위함이다.