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

 

참고문헌

반응형

소프트웨어 개발을 위한 여러 디자인 패턴들 중, 웹 서비스에 자주 사용되는 패턴으로 MVC 디자인 패턴이 있다. Spring MVC, Django 등에서 활용되고 있는 패턴이다.

MVC란 Model, View, Controller의 앞글자를 따서 MVC 패턴이라고 부르며, 애플리케이션을 세 가지의 역할로 구분하는 개발 방법론이다. 그림처럼, 사용자는 Controller만 조작하고, Controller는 Model을 통하여 데이터를 가져오게 되며, 그 정보를 바탕으로 시각적인 표현을 담당하는 View를 통해 사용자에게 전달하게 된다.

MVC (출처 : 생활코딩)

간단히 말해서, 사용자는 Controller를 통해 입력하고, View를 통해 출력을 받는다고 볼 수 있다. 일종의 추상화 개념이라고 볼 수 있다. 위의 그림의 경우, Controller와 View의 연관 관계는 표현되어 있지 않는데, 실제로는 Controller에서 View에도 영향을 줄 수 있고, Model이 Controller에도 정보를 전달할 수 있다..

각각의 구성 요소들은 각자의 역할을 수행해야 하며, 각각의 구성 요소가 다른 요소들에게 영향을 미치지 않고 각자의 역할에 충실해야 한다는 것이 MVC 패턴의 핵심이다.

Controller는 Model에 명령을 보냄으로써 Model의 상태를 변경할 수 있어야 한다(Manipulate). 또한, Controller는 관련된 View에도 명령을 보냄으로써, Model의 표시 방법을 바꿀 수 있어야 한다. 

Model은 Model의 상태에 변화가 있을 때, Controller와 View에 해당 내용을 전달한다. 이와 같은 행위를 통해, View는 최신의 결과를 보여주고, Controller는 Model의 변화에 따라 명령을 추가하거나, 제거하거나, 수정할 수도 있다. 특정 MVC 구현에서는 Model에서 전달하는 것이 아니라, View나 Controller가 직접 Model의 상태를 읽어 오는 경우도 있다.

View는 사용자가 볼 결과물을 생성하기 위해 Model로부터 정보를 얻어 온다.

Django에서는 Model, View, Controller를 각각 Model, Template, View로 사용하고 있다.

일반적으로 Model의 경우 클래스 형태로 표현되며, 이 클래스는 하나의 데이터베이스 테이블이라고 볼 수 있다. Django의 경우 ORM(Object Relational Mapping)이라는 데이터베이스 기능을 지원하기 때문에, 파이썬 코드를 통해서 DB Handling이 가능하다.

Template의 경우 HTML 형태로 구현되며, View에게 받은 데이터를 동적으로 적용한다. 이때 Django의 경우, 자체적인 Django Template 문법을 지원하며, 이 문법을 통해 동적으로 적용이 가능하다.

View의 경우, MVC 패턴에서의 컨트롤러에 대응된다. 요청에 따라, 적절한 로직을 수행하고, 결과를 Template에게 전달하는 역할을 한다.

 

참고자료

반응형

웹 애플리케이션(web application) 또는 웹 앱 소프트웨어 공학적 관점에서 인터넷이나 인트라넷을 통해 웹 브라우저에서 이용할 수 있는 응용 소프트웨어를 말한다. (위키백과)

사용자의 브라우저에서 웹이 작동하는 원리는 기본적으로 클라이언트 - 서버 구조로 이루어지는데, 그 원리는 다음과 같다.

  1. 사용자가 웹 브라우저(클라이언트)를 통해 웹 서버에 요청(Request)을 보낸다
  2. 웹 서버는 받은 요청을 기반으로 웹 애플리케이션 서버, 데이터베이스 서버를 통해 처리를 진행한다.
  3. 처리가 완료된 후, 웹 브라우저에게 응답(Response)을 보낸다.
  4. 받은 응답을 기반으로 웹 브라우저는 해당 내용을 해석하여 사용자에게 보여준다.

 

실무에서는 프론트엔드(FE), 백엔드(BE)로 업무를 분담하고 있다.

프론트엔드

프론트엔드란 Front라는 말의 뜻에 맞게, 사용자가 직접 보고, 확인할 수 있는 부분을 의미한다. 클라이언트-서버 구조에서는 클라이언트 부분을 의미하며, 일반적으로 웹 브라우저를 통해 사용자에게 보여지는 부분이다. 부가적으로, 웹 브라우저를 통해 요청을 보내는 것과, 응답을 받아서 해독하는 부분까지 프론트엔드로 본다. 대표적으로 HTML, CSS, JS등을 이용한다. 최근에는 웹 브라우저가 아닌 모바일 어플리케이션 부분도 프론트엔드로 보는데, 모바일 애플리케이션 개발 토대로, 리액트라는 프론트엔드 라이브러리가 많이 사용되고 있다.

백엔드

백엔드란 프론트엔드 개념의 반대되는 의미로, 사용자의 눈에 보이지 않는 부분을 의미한다. 프론트엔드에서 보내준 요청을 처리하여 다시 응답을 제공하는 부분으로, 클라이언트-서버 구조에서는 서버를 의미하며, 해당 부분에는 다양한 서버가 맞물려서 구성될 수 있다. 웹 서버, 웹 애플리케이션 서버, 데이터베이스가 있으며, 최근 웹 서버용 프레임워크로 Java 기반의 Spring, Python 기반의 Django, Javascript 기반의 node.js와 nest.js 등이 사용되고 있다. 데이터베이스는 Oracle, Mysql, Mariadb, Mongodb 등이 있다.

 

반응형

+ Recent posts