1. 팩토리 메소드 패턴이란

GoF의 디자인 패턴에서 제시한 디자인 패턴 중 생성 패턴에 속한다. 해당 서적에서는 팩토리 메소드 패턴을 이렇게 정의하고 있다.

객체를 생성하기 위한 인터페이스를 정의하고,
어떤 클래스의 인스턴스를 생성할지에 대한 처리는 서브클래스가 결정하는 디자인 패턴

요약해서 말하자면, 객체 생성을 대신 수행해주는 일종의 팩토리가 존재하는 것이다. 이때 객체를 생성하는 메소드를 팩토리 메소드라고 하며, 이를 구현 팩토리 객체에서 Override하여 원하는 객체를 생성하게 된다. 클라이언트에서 직접 객체를 생성하는 것이 아니라, 간접적으로 객체를 생성한 뒤 반환해주는 방식이다.

즉, 해당 디자인 패턴은 상위 클래스에 알려지지 않은 구체 클래스를 생성하는 패턴이며, 하위 클래스가 직접 어떤 객체 인스턴스를 생성할지 결정하게 된다. 이를 UML 형태로 나타내면 아래의 그림과 같다.

출처 : 위키피디아

이때 Creator의 factoryMethod()는 추상 메소드이며, 이를 ConcreteCreator에서 Override하여 구현 메소드를 작성한다. 결국 실질적으로는 ConcreteCreator를 통해서 Client에게 Product를 생성하여 전달하게 된다.

2. 팩토리 메소드 패턴의 사용

팩토리 메소드 패턴을 사용하는 주된 이유는 클래스의 생성과 사용의 처리 로직을 분리하여 결합도를 낮추기 위해서이다. 팩토리 메소드 패턴을 사용하여 직접 객체를 생성해 사용하는 것을 방지하고, 서브클래스에 생성 로직을 위임함으로써 보다 효율적으로 코드를 제어할 수 있고, 의존성을 제거해 주는 역할을 한다.

또한, 객체 생성을 캡슐화하고 한 곳(팩토리 클래스)에서 관리하기 때문에 수정에는 닫혀 있고, 객체의 자료형이 하위클래스에 의하여 결정된다는 점 때문에 확장이 용이하다는 장점이 있다. 이는 객체지향의 SOLID 원칙 중 OCP에 해당한다.

3. 팩토리 메소드 패턴 예제 코드(by. Java)

Computer 추상 클래스

abstract class Computer {
    public void on() { // 컴퓨터 전원버튼으로 켜기 }
    public void off() { // 컴퓨터 전원버튼으로 끄기 }
    
    abstract void compose(); // 컴퓨터 조립하는 방법
    abstract void use(); // 컴퓨터 사용하는 방법
}

Computer는 on, off, compose, use라는 공통적인 메소드를 갖는 인터페이스이다. 컴퓨터라는 종류로 추상화되어 있는 상태이다. 이때, on과 off 메소드의 경우 어느 컴퓨터든 전원버튼을 이용하여 켜고 끈다는 동일한 행위를 수행하기 때문에 구체 클래스에서의 구현을 줄이기 위해 미리 구현해 두었다.

Computer 구체 클래스

public class Macbook extends Computer {
    @Override
    void compose() { // 맥북이 조립되어 나오는 과정 }
    
    @Override
    void use() { // 트랙패드로 사용하기 }
}

public class Galaxy extends Computer {
    @Override
    void compose() { // 갤럭시북이 조립되어 나오는 과정 }
    
    @Override
    void use() { // 360도로 꺾어서 사용하기 }
}

public class Asus extends Computer {
    @Override
    void compose() { // Asus 노트북이 조립되어 나오는 과정 }
    
    @Override
    void use() { // 평범하게 마우스로 사용하기 }
}

public class Lenova extends Computer {
    @Override
    void compose() { // Lenova 노트북이 조립되어 나오는 과정 }
    
    @Override
    void use() { // 빨콩으로 사용하기 }
}

Computer라는 추상화된 인터페이스를 활용하여 구현한 구체 클래스이다. 각 클래스마다 조립되어 나오는 과정과 사용 방식을 구현해 둔 상태이다.

ComputerFactory 추상 클래스

abstract class ComputerFactory {
    public Computer orderComputer(String type){
        Computer computer = createComputer(String); // Factory Method
        computer.compose();
        computer.on();
        computer.off();
        return computer;
    }
    
    abstract Computer createComputer(String type); // Factory Method
}

ComputerFactory는 위의 그림에서 Creator 역할을 한다. 이때 createComputer 부분이 팩토리 메소드가 되고, 이 부분을 구체 클래스에서 실질적으로 구현하여 어떤 객체 인스턴스를 반환할지 결정하게 된다. 그리고 추상 객체에서는 orderComputer 메소드 내부에서 팩토리 메소드를 활용하여 Computer를 생성하고, 기능들을 사용하고 있다.

ComputerFactory 구체 클래스

class ComputerFactoryImpl extends ComputerFactory {
    @Override
    Computer createComputer(String type) {
        if ("Macbook".equals(type)) {
            return new Macbook();
        } else if ("Galaxy".equals(type)) {
            return new Galaxy();
        } else if ("Asus".equals(type)) {
        	return new Asus();
        } else if ("Lenova".equals(type)) {
        	return new Lenova();
        }
        return null;
    }
}

위 두 클래스는 ComputerFactory를 구현한 구체 클래스이다. 객체의 종류가 추가되는 경우, 구체 클래스에 추가해줌으로써 새로운 객체를 반환할 수 있다. 기능의 추가가 필요한 경우 해당 구체 클래스만 변경하고, 추상 클래스는 변경하지 않아도 된다.

팩토리 메소드에 String 값을 인자로 받아 리턴되는 객체의 종류를 변화시킨다. 해당 구체 클래스를 실제로 클라이언트에서 사용함으로써 클라이언트 단에서 리턴되는 객체의 종류를 제어할 수 있다.

클라이언트 코드

public class FactoryPatternTest {
    public static void main(String[] args) {
        ComputerFactory computerFactory = new ComputerFactoryImpl();
        
        Computer macbook = computerFactory.orderComputer("Macbook");
        Computer galaxy = computerFactory.orderComputer("Galaxy");
        Computer asus = computerFactory.orderComputer("Asus");
        Computer lenova = computerFactory.orderComputer("Lenova");
    }
}

위와 같은 형태로 팩토리를 통해 생성되는 객체들의 종류를 제어할 수 있다.

반응형

Spring은 정말 어려운 프레임워크이다. 왜냐하면 개발자들이 정말 오랜 기간 동안 고민하면서 개발 패러다임의 격변을 가져왔던 객체지향에 관한 생각, 그리고 끊임없이 연구되어 왔던 자바를 활용한 다양한 디자인 패턴들이 모두 함축되어 있는 프레임워크이기 때문이다. 대략적으로만 생각해보더라도 스프링의 핵심 기술에는 IoC/DI, AOP, PSA 등이 있고, 그 외에도 데이터베이스 용도로 응용하여 활용할 수 있는 기술들인 JPA, QueryDSL 등도 존재한다. 또한 아직 사용해보거나 접해보지 못한 부분들에는 더 많은 내용들이 녹아 있을 것이다.

그중에서도 스프링에서의 객체지향 설계의 기본이 되는, IoC와 DI에 대해 간단히 정리해 보았다.

 

IoC (Inversion of Control, 제어의 역전)

간단하게 설명하면 말 그대로이다. 제어의 역전. 제어권이 역전되어 있는 상황을 의미한다.

그렇다면 제어의 역전이란 곧 기존의 제어 방식을 뒤집었다는 말인 것 같은데, 기존의 방식은 과연 어떠했을까?

기존 프로그램의 경우 클라이언트의 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행하는 등 프로그램의 제어 흐름을 스스로 조종했다. 즉, 코드의 제어권은 개발자가 갖고 있는 것이다.

그러나 Spring에서는 코드에서 제어권을 프레임워크가 갖고 있고, 개발자는 일종의 config 파일만을 활용하여 프레임워크에게 제어할 수 있는 방법을 제공한다. 이후 프로그램의 제어 흐름 자체는 개발자가 직접 제어하지 못하고, 프레임워크 단에서만 관리가 가능해진다. 사용자가 작성하는 부분은 프레임워크의 라이프사이클 속에서 일종의 콜백 형식으로 적절한 위치에서 자동으로 호출되게 된다. 이러한 상황을 제어의 역전, IoC라고 한다.

Spring에서는 구현 객체를 생성하고 연결하는 책임을 AppConfig 단에서만 담당하게 되어, SRP(단일 책임 원칙)에도 부합하는 소프트웨어를 개발할 수 있게 해준다.

 

DI (Dependency Injection, 의존관계 주입)

DI는 스프링에서 클래스 간 또는 클래스와 인터페이스 간의 의존 관계들을 정의해주기 위해 필요한 개념이다. 의존 관계에는 정적인 클래스 의존 관계와 런타임에 결정되는 동적인 객체(인스턴스) 의존 관계 두 가지 종류가 존재한다.

정적인 클래스 의존 관계의 경우, 클래스에서 사용하는 코드만을 통해서도 의존관계를 쉽게 판단할 수 있다. 예를 들어 주문 관련 Service가 존재한다면, 해당 Service내에서 어떤 Repository 인터페이스를 사용할지 등의 내용은 이미 요구사항 분석과 클래스 다이어그램을 작성하면서 어느정도 구상이 끝나 있었을 것이다. 하지만, 이러한 클래스 의존 관계만을 통해서는 실질적으로 어떤 객체가 Service에 주입될지, 즉 각 인터페이스 내에 어떤 구현체가 사용될지에 관한 내용을 파악하기는 어렵다.

이를 위해 동적인 객체 인스턴스 의존 관계 파악이 필요하다. 동적인 객체 인스턴스 의존 관계는 애플리케이션의 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계를 의미한다.

이때 애플리케이션 실행 시점, 즉 런타임에 실제 구현 객체를 생성하고, 클라이언트에 전달하여 실제 의존 관계가 연결되는 것의존관계 주입, DI라고 한다. 의존관계 주입을 통하여 클라이언트 코드를 변경하지 않고 호출 대상의 타입 인스턴스를 변경할 수 있으며, 정적인 클래스 의존 관계를 변경하지 않고 동적인 객체 인스턴스 의존 관계를 쉽게 변경할 수 있다.

스프링에서는 IoC를 구현하기 위하여 의존 관계 주입을 사용하고 있고, 이는 AppConfig 파일 내에서 객체를 생성하고 관리하면서 의존 관계를 연결해 주고 있다. 이러한 것을 IoC 컨테이너 또는 DI 컨테이너라고 부른다.

Spring에서는 DI 컨테이너를 이용하여 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존 관계를 주입하고 있다. 즉, 클라이언트 코드는 추상화에 의존하여 개발할 수 있고 이를 통해 DIP(의존관계 역전 원칙)에도 부합하는 소프트웨어를 개발할 수 있게 해준다.

또한 DI 컨테이너만을 이용하여 의존 관계를 변경할 수 있게 되면서 애플리케이션을 사용 영역과 구성 영역으로 나누어 주기 때문에 클라이언트 코드는 변경하지 않아도 되며, 결국 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있도록 구현이 가능해진다. 이는 OCP(개방 폐쇄 원칙)에 부합하는 소프트웨어를 개발할 수 있게 해준다.

반응형

최근 Node.js와 PostgreSQL을 이용하여 프로젝트를 진행했다.

Node.js에서 PostgreSQL을 사용하는 방법은 여타 RDBMS를 사용하는 방법과 유사하다.

커넥션 풀을 생성하고, 해당 커넥션 풀에서 커넥션을 빌려오고, 빌려온 커넥션을 통해서 쿼리문을 수행하는 과정을 ES7의 async await를 이용하여 비동기식 프로그램의 순차 처리를 진행해서 데이터를 가져온다.

이때 커넥션 풀 생성 및 커넥션을 빌려오는 부분은 별도로 모듈화하여 사용하고 있었고, 쿼리문을 수행하는 과정만 코드로 확인해 보면, 다음과 같은 형식으로 활용했다.

const getLectures = async (client, lectureId) => {
  const { rows } = await client.query(
    `
    SELECT * from lecture
    WHERE id = $1
    `,
    [lectureId]
  );
  return convertSnakeToCamel.keysToCamel(rows[0]);
};

다음 코드는 lecture 테이블에서 id=${lectureId} 인 값을 가져오는 코드이다. 위 코드와 같이, 백틱(`)과 ${num} 형태를 이용하여 쿼리문을 작성하고 차후에 값을 넣어주는 방식을 Node.js 에서의 Prepared Statement라고 한다.

Prepared Statement란?

위키백과에서는 Prepared Statement를 다음과 같이 정의하고 있다.

Prepared Statement란
데이터베이스 관리 시스템에서 동일하거나 비슷한 데이터베이스 문을
효율적이고 반복적으로 실행하기 위해 사용하는 기능이다

(위키백과)

실제로 Prepared Statement는 쿼리문을 반복 사용하기 위해 사용되는 방식이었으나, 현재는 해당 용도 외에 보안적인 요소로 프로그래밍에 사용된다. 주로 SQL Injection 공격을 막기 위해 활용되는데, 방어가 가능한 원리는 아래 SK인포섹 블로그 게시글을 보면 자세히 설명되어 있다.

https://m.blog.naver.com/PostView.nhn?blogId=skinfosec2000&logNo=220482240245 

 

[SQL인젝션] Prepared Statement를 쓰면 SQL 인젝션 공격이 되지 않는 이유는?

Prepared Statement의 내부 원리 개요 SQL인젝션 취약점에 대한 대응방안으로 Prepared Statement와...

blog.naver.com

우리는 SQL Injection 공격을 막기 위해 Prepared Statement를 활용한다는 점만 확인하고 넘어가자.

문제 발생

프로젝트를 진행하던 중, 문제점이 발생했다.

order by를 이용해서 정렬을 수행해야 하는데, 정렬을 위한 값들이 사용자에 의해 프론트에서 파라미터 형태로 들어오기 때문에, 평소와 같이 Prepared Statement를 활용하여 order by 문에 파라미터를 넣어 줬다.

const getLectures = async (client, lectureId, order) => {
  const { rows } = await client.query(
    `
    SELECT * from lecture
    WHERE id = $1
    order by $2
    `,
    [lectureId, order]
  );
  return convertSnakeToCamel.keysToCamel(rows[0]);
};

하지만, 쿼리문이 정상적으로 작동하지 않았고, order by 문이 없는 것처럼 동작되었다.

해당 문제점에 대해 확인해 본 결과,

이러한 스택오버플로우 게시글들을 확인할 수 있었다.

게시글들을 확인해 본 결과, 해당 문제점이 나에게만 발생하는 문제가 아니라는 것을 확인할 수 있었고, Prepared Statement를 사용하는 경우 쿼리의 파라미터로 컬럼 또는 테이블 이름을 넣을 수는 없다는 것을 확인할 수 있었고, 직접 적절한 order by 구문을 작성해 주는 방법밖에 없었다.

해당 문제를 해결하기 위해, pg-format이라는 npm 모듈을 활용하였다.

pg-format이란

pg-format이란 dynamic SQL Query를 안전하게 작성하기 위한 postgreSQL용 formatter로, 포맷을 identifier와 literal, string으로 구분하여 SQL Injection을 회피할 수 있게 해 준다. order by 구문의 경우 해당 컬럼의 값을 이용해서 직접적인 데이터 조회는 불가능하기 때문에 파라미터를 단순 string으로 넣어 줘도 괜찮은데, 이러한 상황을 위해 여러 가지 포맷 스트링을 이용하여 구분해서 데이터를 넣어 주는 모듈이다.

pg-format의 사용법은 기본적으로 npm 모듈 페이지에 잘 작성되어 있다. 해당 모듈의 사용법을 간단히 정리해 보았다.

포맷의 기본값으로, %I, %L, %s 3가지의 포맷이 존재한다.

%I : SQL Identifier - 식별자, 개체의 이름으로 활용되는 포맷(컬럼명, 테이블명)

%L : SQL Literal - 리터럴, Dynamic SQL에서 변수 형태로 활용되는 포맷(WHERE문의 조건)

%s : simple string - 일반적인 String 값에 활용되는 포맷(LIMIT 조건 등)

기본 사용법

const format = require('pg-format');
const getLectures = async (client, lectureId, order) => {
  const { rows } = await client.query(
    format(
      `
      SELECT * from lecture
      WHERE id = %L
      order by %I
      `,
      lectureId, order
    )
  );
  return convertSnakeToCamel.keysToCamel(rows[0]);
};

위의 코드 형태처럼, 기존에 Prepared Statement의 ${num} 부분에 적절한 포맷 스트링을 넣어 주고, 뒤에 순서대로 각 포맷에 해당하는 값들을 넣어 주면 된다. 다른 언어에서의 포맷 스트링 사용법과 유사하다.

보다 자세한 사용법은 https://www.npmjs.com/package/pg-format 을 참고하면 된다.

 

pg-format

Node.js implementation of PostgreSQL's format() to safely create dynamic SQL queries.

www.npmjs.com

 

참고문헌

반응형

자바에서는 자료구조를 사용할 때, Collection이라는 객체를 이용한다.

Collection이라는 인터페이스를 기반으로 내부에 객체를 저장하고, 조작할 수 있느 구조를 제공하고 있으며, Collection 인터페이스를 기반으로 만들어지기 때문에 Java Collection Framework라고 명명하고 있다.

Iterable 인터페이스와 Collection 인터페이스를 기반으로, List, Queue, Set이 존재하고, 그 외에도 Collection 인터페이스 기반은 아니지만, 데이터를 효율적으로 관리한다는 측면에서 일반적으로 Map도 함께 이야기되고 있다.

Collection 프레임워크의 경우 여러 가지 형태의 구현체로 만들어져 있기 때문에, 다양한 자료 구조의 특징을 알고 자신이 작성하고자 하는 프로그램에서 유용하거나 효율적인 자료구조가 어떤 것일지를 선택하는 것이 중요하다.

 

Java Collection Framework Hierarchy
Java Map Interface

 

각 인터페이스들은 인터페이스 하위의 구현체로 구현되고, Java 코드 내에서 사용하기 위해 인터페이스에 구현 객체를 생성하여 부여하는 식으로 사용된다. 아래의 코드는 Integer를 원소로 갖는 arraylist라는 이름의 List 인터페이스를 ArrayList를 이용하여 구현한다는 의미이다.

List<Integer> arraylist = new ArrayList<>();

이때, 다양한 프로그래밍을 하다 보면 인터페이스 자체는 동일하지만 구현체만 변경해 줘야 할 필요성이 있는 경우가 있다. 따라서 인터페이스와 구현체를 분리해 주는 것을 권장한다.

ArrayList<Integer> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();

list1과 list2의 경우, 코드를 사용함에 있어서 구현상 차이는 존재하지 않는다. 하지만 성능을 측정해 본 결과, ArrayList가 아닌 LinkedList로 구현체를 변경할 필요가 있을 때, list2의 경우 인터페이스가 아닌 구현체 부분만 변경해 주면 되지만, list1의 경우 인터페이스와 구현체를 모두 변경해 주어야 한다. 따라서, 객체 지향프로그래밍에서의 가장 큰 장점인 확장성을 이용하지 못하게 되는 것이다. 따라서 객체지향 프로그래밍 언어의 대표격인 Java에서는 list2와 같이 구현하는 것을 권장하고 있다.

 

1. List

List 인터페이스는 순서가 존재하는 것이 가장 큰 특징이다. 각 구현체별 특징은 아래와 같다.

구현체 특징
ArrayList 단방향 포인터 구조로, 각 데이터에 대한 인덱스를 갖고 있기 때문에 데이터의 접근 연산에 유리하지만, 데이터의 삽입/삭제 연산의 경우 불리하다.
LinkedList 양방향 포인터 구조이기 때문에 데이터의 삽입/삭제 연산에 있어서 유리하지만 데이터의 인덱스는 존재하지 않기 때문에 데이터 접근 연산의 경우 불리하다.
Vector ArrayList와 유사한 구현체이지만, Thread-safe하다.
Stack 자료 구조에서의 Stack을 의미한다. List 인터페이스에 포함되긴 하지만, Stack을 사용하는 경우를 제외하면 사용하지 않는다.

 

2. Queue

순서가 있는 데이터 중, 선입선출을 보장하는 리스트로 볼 수 있다.  Queue의 하위 인터페이스로 Deque가 존재하는데, Deque 인터페이스의 경우 양쪽에서 요소를 제거 및 추가할 수 있다. Deque는 ArrayQueue와 LinkedList 두 가지 구현체가 존재하는데, 이는 역시 Queue의 구현체로 사용될 수 있다. 각 구현체별 특징은 아래와 같다.

구현체 특징
PriorityQueue 우선순위 큐를 의미한다. Null 값이 들어갈 수 없다.
ArrayQueue Deque 인터페이스의 구현체로, ArrayList와 유사한 특징을 갖고 있는 큐 자료구조이다.

 

3. Set

순서가 없는 데이터의 집합을 의미한다. 데이터의 중복을 허용하지 않는다. 위의 사진에서, Set이라는 인터페이스 하위에 SortedSet이라는 인터페이스가 존재하는 것을 볼 수 있는데, SortedSet의 경우 요소에 대한 순서를 제공하는 Set 자료구조를 의미한다. SortedSet의 경우 구현체로 TreeSet만 존재한다. Set의 구현체별 특징은 아래와 같다.

구현체 특징
HashSet 데이터의 저장을 위해 해시 테이블을 사용한다. 해싱을 통하여 요소를 저장한다.
LinkedHashSet HashSet 클래스의 확장으로, LinkedList를 이용하여 구현된 HashSet을 의미한다. 따라서 삽입 순서를 알 수 있고, Null 요소도 삽입이 가능하다.
TreeSet Set의 저장을 위해 트리를 사용한다. 액세스 및 검색 속도가 매우 빠르다는 특징이 있다.

 

4. Map

키-밸류 쌍을 기반으로 데이터를 저장하는 자료 구조를 의미한다. 키의 경우 중복을 허용하지 않고, 밸류의 경우 중복이 가능하다. 맵은 일반적으로 키를 기반으로 밸류를 검색/갱신/삭제해야 하는 경우에 유용하게 사용될 수 있다. Map의 하위 인터페이스로 SortedMap이 존재하는데, 키 값에 대한 순서가 존재하는 Map 자료구조를 의미한다. SortedMap의 경우 구현체로 TreeMap만 존재한다. Map의 구현체별 특징은 아래와 같다.

구현체 특징
HashMap 데이터의 저장을 위해 해시 테이블을 사용한다. 키 값의 해싱을 통하여 밸류를 저장한다.
LinkedHashMap HashMap 클래스의 확장으로, LinkedList를 이용하여 구현된 HashMap을 의미한다. 따라서 삽입 순서를 알 수 있다.
TreeMap Map의 저장을 위해 트리를 사용한다. 오름차순을 유지한다.

 

참고문헌

- https://www.javatpoint.com/collections-in-java

- https://www.javatpoint.com/java-map

 

 

반응형

'Development > Algorithm' 카테고리의 다른 글

[programmers] Greedy - 구명보트  (0) 2021.03.24
[programmers] Greedy - 큰 수 만들기  (0) 2021.03.24
[programmers] Greedy - 조이스틱  (0) 2021.03.24
[programmers] Greedy - 체육복  (0) 2021.03.23
[boj] 2504 - 괄호의 값  (0) 2021.03.14

스프링 프레임워크에 대해 공부하면서, 스프링의 가장 큰 두가지 특징으로 DI와 IoC가 존재한다는 것을 배웠었다.

그런데 스프링에서 DI와 IoC를 도입하게 된 이유로 되돌아 올라가 보면, 싱글톤 패턴을 적용하면서 객체지향에서의 SOLID 원칙을 따르기 위해 해당 개념을 사용했다고 한다. 그러나 해당 개념에 대해서 이해하기 어려웠던 점도 있고, 최근 코딩 테스트 이후 CS 시험들을 2차 테스트로 보면서 SOLID 원칙에 대해 어느정도 상세히 이해하고 있는지에 대해서 묻는 문제들이 자주 출제되었다. 그래서 스프링 공부를 하면서 겸사겸사 SOLID 원칙에 대해 간단하게 정리해 보았다.

SOLID란 로버트 마틴의 좋은 객체 지향 설계의 5가지 원칙으로, 해당 원칙들의 맨 앞글자들을 따서 만든 용어이다. 이 원칙을 알아야 하는 이유는 시스템의 기능이 확장되거나 변동사항이 있을 때, 객체지향적인 설계가 추구하는 점인 기존의 시스템들이 영향을 받지 않는 방향을 갖기 위해서이다.

 

1. SRP(Single Responsibility Principle, 단일 책임 원칙)

소프트웨어의 한 객체는 단 하나의 책임만 가질 수 있다.

변경이 있을 때 파급 효과가 작은 경우 SRP를 잘 따랐다고 할 수 있으며, 책임의 범위를 적절하게 조절할 필요가 있다.

객체 간의 응집도를 최대화하고, 객체 간의 결합도를 최소화하는 것이 좋은 프로그램이라고 볼 수 있다.

 

 

2. OCP(Open/Closed Principle, 개방 폐쇄 원칙)

소프트웨어가 기존의 코드를 변경하지 않고(Closed) 확장할 수 있다(Open).

확장에는 열려 있고, 변경에는 닫혀 있어야 한다.

이때 자바의 경우 다형성을 사용하는데, 인터페이스를 구현한 새로운 클래스를 만듬으로써 새로운 기능을 구현한다.

그러나 구현 객체를 변경하려면 결국 클라이언트 코드를 변경해야 하는 문제점이 있고, 이때 OCP가 깨지게 된다. 따라서 스프링에서는 이 원칙을 DI를 이용하여 지원하고 있다.

 

 

3. LSP(Liskov Substitution Principle, 리스코프 치환 원칙

객체는 프로그램의 정확성을 깨트리지 않으면서 하위 타입의 인스턴스로 변환할 수 있어야 한다.

클래스를 상속하는 자식 클래스들은 부모 클래스의 규약을 지켜야 한다는 것으로, 기능적으로 보장이 필요하다는 의미이다. 인터페이스에 대한 구현체를 구현할 때, 인터페이스의 규약을 지켜줘야 한다.

컴파일 단계에서 문제가 생기지 않을수는 있지만, 부모 클래스가 규정하고 있는 기능을 무시하거나 오버라이딩을 자제해야 한다는 의미이다.

 

 

4. ISP(Interface Segregation Principle, 인터페이스 분리 원칙)

특정 클라이언트를 위한 인터페이스 여러개를 사용하는 것이 범용 인터페이스를 하나 사용하는 것보다 낫다.

여러 개의 인터페이스를 구현함으로써 대체 가능성을 향상시키고, 인터페이스의 기능을 명확하게 표현할 수 있다.

일반적인 인터페이스를 구체적인 여러 인터페이스로 나눠줌으로써 ISP를 만족하도록 설계할 수 있다.

 

 

5. DIP(Dependency Inversion Principle, 의존 관계 역전 원칙)

추상화에 의존해야지, 구체화에 의존하면 안된다.

구현체보다는 인터페이스나 추상 클래스에 의존하는 편이 좋다는 의미로, 클라이언트 코드가 구현 클래스를 바라보는 것이 아닌, 인터페이스를 바라보도록 하는 것을 의미한다. 즉, 역할에 의존하게 한다는 의미로써, 클래스에서 역할의 의미를 갖는 인터페이스에 의존해야 한다는 것이다.

그러나 일반적인 경우, 인터페이스가 구현 클래스를 선택하게 되기 때문에 이때, DIP의 위반이 발생할 수 있다. 따라서 이 문제점도 스프링에서는 DI를 이용하여 클라이언트 코드 변경 없이 기능을 확장할 수 있도록 제공하고 있다.

 

 

반응형

1. RDT란

 

RDT란 Reliable data transfer의 약자로, TCP에서 신뢰성 있는 데이터 전송을 보장하기 위한 개념이다. RDT는 TCP가 UDP와 구분되는 가장 큰 특징이라고 볼 수 있으며, 사실상 TCP 네트워킹에서 가장 중요한 개념으로 봐도 무방하다.

네트워크는 공식적으로 7개의 계층으로 이루어져 있다(OSI 7 Layer 참고). RDT란 이때, 상위 계층에서 전송한 데이터가 손상되거나 손실되지 않도록 보장하는 개념으로써, 하위 계층에서 신뢰성을 보장할 수 없기 때문에 특정 계층에서 신뢰성을 보장하도록 하면, 그 상위 계층의 신뢰성은 모두 보장된다는 개념으로 볼 수 있다.

RDT 개념

패킷을 송신하는 경우, 상위레이어에서 rdt_send()를 이용하여 RDT 프로토콜로 데이터를 전송한다. 이후 RDT 프로토콜에서 신뢰할 수 없는 채널인 하위 레이어로 보낼 때, udt_send()를 이용하여 패킷을 전송한다.

패킷을 수신하는 경우, rdt_rcv()를 통하여 하위 레이어에서 RDT 프로토콜로 데이터를 전송하고, RDT 프로토콜에서 상위 레이어로 데이터를 보낼 때 deliver_data()를 호출하여 데이터를 전송한다.

즉, 하위 Unreliable Channel을 통과하더라도, 신뢰성 있는 전달을 보장하는 것이 RDT 프로토콜의 기본이라고 볼 수 있다.

 

2. RDT 1.0

 

RDT 1.0은 네트워크의 모든 채널이 완벽하게 신뢰성 있다고 가정할 때 사용하는 방식으로, 단순히 데이터의 송신 및 수신만 이루어지는 구조이다. 채널이 안정적이기 때문에, 패킷 손실 등의 에러가 전혀 발생하지 않는다고 가정한다.

RDT 1.0

rdt_sender는 상위 계층에서 데이터를 받고 데이터를 포함한 패킷을 생성한 뒤, 이를 송신한다.

rdt_receiver는 하위 채널에서 패킷을 수신하고, 데이터를 추출한 뒤, 이를 상위 계층으로 전달한다.

이때 만약 하위 채널이 안정적이지 못하다면, 그래서 비트 오류가 발생할 수 있다는 것을 가정하여 RDT 2.0으로 발전하게 된다. 

 

3. RDT 2.0

 

RDT 2.0부터는 비트 에러가 발생할 수 있다고 생각하고 이때의 에러 처리에 대해 생각하고 설계를 진행하였다. 따라서 ACK, NAK 라는 신호를 보내는데, 이를 통해 수신된 패킷이 손상되었는지 여부를 수신자가 송신자에게 반환해 준다.

RDT 2.0

이때 rdt_sender는 두 가지의 상태를 갖는다. sender는 파일을 보낸 후, ACK/NAK 신호를 기다린다. 이후 오류가 없다면 상위 계층에서 데이터를 기다리는 상태로 돌아간다. NAK가 수신된다면 데이터를 재전송하고, 또다시 ACK/NAK 신호를 기다린다.

rdt_receiver는 단일 상태를 갖는다. 패킷이 도착했을 때, 수신된 패킷의 손상 여부에 따라 ACK/NAK로 응답하고, NAK를 응답한 뒤에는 다시 패킷이 도착하는 것을 기다린다.

RDT 2.0은 이론상 잘 동작하는 것처럼 보이지만, 치명적인 결함이 존재한다. ACK와 NAK 패킷 자체의 손상을 가정하지 않는다는 점이다. 예를 들어, 수신자가 ACK를 보내야 하는데, 이것이 손상되어 제대로 전달이 되지 않는다면 송신자는 패킷을 수신 측으로 다시 보내야 할 것이다. 이때 수신 측은 혼란이 있을 것이다. 아까 받은 데이터에 대한 ACK를 보냈는데, 동일한 데이터가 다시 왔으니 해당 데이터가 다음 데이터인지 중복으로 온 이상 있는 데이터인지 확인이 불가능하다. 따라서 이런 문제점들을 해결하기 위해 RDT 2.1이 도입되었다.

 

4. RDT 2.1

 

RDT 2.1은 RDT 2.0에 시퀀스 넘버를 추가한 방식이다. RDT 2.0에 비해 이런 순서 번호를 갖고 있는 이유는 각 패킷에 대한 순서를 정함으로써 해당 패킷이 새로운 데이터를 전송하는 것인지 NAK를 받아 재전송한 것인지를 구분하기 위한 것이다. 만약 송신자가 NAK 1이라는 데이터를 받으면, 송신자는 1번 패킷이 문제가 있다는 것을 확인할 수 있다. 그림에서는 편의상 시퀀스 넘버가 0, 1 두가지만 있다고 가정한다.

RDT 2.1 Sender

rdt_sender는 총 4가지 상태가 존재한다. 패킷을 만들고 시퀀스 0을 넣어서 전송하고, 상태를 전환한 뒤 ACK/NAK를 기다린다. 이후 rdt_receiver가 0번 패킷에 대한 ACK/NAK를 전송해 주고, ACK를 받으면 시퀀스 1을 넣은 새로운 패킷을 보내 주며, NAK를 받거나 오류가 있는 패킷이 들어온다면(corrupt 발생), 시퀀스 0을 넣은 기존 패킷을 보낸다. 따라서 0번 패킷을 보내기 전 상태, 0번 패킷을 보내고 ACK/NAK를 기다리는 상태, 1번 패킷을 보내기 전 상태, 1번 패킷을 보내고 ACK/NAK를 기다리는 상태 총 4가지 상태가 존재하게 된다.

RDT 2.1 Receiver

rdt_receiver는 2가지 상태가 존재한다. 0번 패킷을 보내기 전 상태, 1번 패킷을 보내기 전 상태이다. 1번 패킷을 보내기 전 상태에서 1번 패킷이 수신되면 ACK를 보내면 되고, 0번 패킷이 도착하면 0번 패킷이 중복도착했으므로 무언가 중간에 문제가 생겼음을 확인하고, 0번 패킷에 대한 ACK를 보낸다. 그 외 NAK를 보내는 상황은 RDT 2.0과 동일하게 이루어진다.

 

5. RDT 2.2

 

RDT 2.2는 RDT 2.1에서 NAK 신호를 삭제한 프로토콜이다. NAK 대신 가장 최근에 잘 받은 패킷에 대한 ACK를 보냄으로써 NAK를 보내는 것과 동일한 효과를 얻을 수 있다.

RDT 2.2 Sender
RDT 2.2 Receiver

RDT 2.1과 동일하게 이루어지지만, 송신 측이 1번 패킷을 보냈을 때, 수신 측에서 1번 패킷에 대한 ACK가 아니라 0번 패킷에 대한 ACK가 돌아온다면 송신 측은 1번 패킷의 전송에 문제가 있다는 것을 확인하고 재전송하게 된다.

그러나 RDT 2.2까지 적용하더라도 전송 도중 패킷이 유실될 수 있는 상황은 확인이 불가능하다. 모종의 이유로 데이터 패킷 자체가 유실될 수 있는데, 이러한 문제를 해결하기 위해 RDT 3.0이 등장했다.

 

6. RDT 3.0

 

RDT 3.0에서는 일정 시간을 기다리는 timer를 이용하여 패킷 손실을 검출하고, 손실이 발생했을 때 어떤 행동을 해야 하는지 판단할 수 있다. 타이머는 100% 패킷 손실이 일어났다고 보장할 수는 없지만, 손실이 일어났다고 판단할 수 있는 시간을 선택할 수 있다. 만일 ACK가 특정 시간 안에 수신되지 않는 경우 패킷이 재전송될 수 있으며, 이 경우는 특정 패킷이 전송 딜레이가 너무 커져서 타이머 시간보다 늦게 도착하는 경우에도 패킷이 재전송될 수 있다는 것을 의미한다. 따라서 중복 데이터 패킷이 존재할 수 있으며, 이는 RDT 2.2에서 시퀀스 넘버를 이용하여 이미 해결한 부분이다.

RDT 3.0 Sender

RDT 3.0의 receiver의 경우, RDT 2.2의 receiver와 동일한 역할을 한다.

 

RDT 3.0까지 발전하면서 네트워크에서 신뢰성 있는 전송에 대한 성능은 향상되었지만, 기본적으로 RDT는 전송 후 대기하는 방식의 프로토콜이기 때문에, 현대 고속 네트워크에 대한 요구를 모두 만족시키지는 못했다. 따라서 이러한 문제점들을 해결하기 위해, ACK를 기다리지 않고 여러 패킷을 전송하도록 허용함으로써 성능 향상을 가져올 수 있다. 이러한 기술을 파이프라이닝이라고 하며, 파이프라이닝 기술을 위해서 고려해야 할 다양한 사항들이 생겨났다.

- Sequence Number 범위의 증가 : 여러 패킷을 보내야 하므로 0과 1로는 부족함

- 버퍼 필요 : 송/수신측에서 모두 각각 여러 패킷을 담을 수 있는 버퍼가 필요함

- 파이프라이닝에서의 오류 회복 방법 : 파이프라이닝 시스템에서 패킷 손실 및 지연 패킷에 대한 처리 방식이 필요

따라서 해당 방법을 처리하기 위해 Go-Back-N 방식과 Selective Repeat 방식을 이용하여 패킷 손실 및 지연 패킷에 대한 처리를 진행하게 된다.

반응형

'Development > Network' 카테고리의 다른 글

내 PC에 구글 웹 페이지가 보이기까지  (0) 2021.03.03

+ Recent posts