7장 스프링 핵심 기술의 응용

Soonwoo Kwon·2022년 4월 9일
0

토비의 스프링

목록 보기
8/11

7.1 SQL과 DAO의 분리

UserDao는 충분히 개선되었지만 SQL을 분리하지 않았다는 문제가 있다. SQL을 변경하는 작업은 빈번히 일어나고 실수가 자주 일어날 수 있는 작업이기 때문에 SQL을 분리하여 다른 파일에 두어 관리하면 좋을 것이다.

XML 설정을 이용한 분리

개별 SQL 프로퍼티 방식

  • SQL구문 마다 설정 파일에 프로퍼티를 추가하여 DAO에서 DI를 받아 사용한다
  • DI를 받기 위한 수정자 메소드가 필요하고 많은 프로퍼티를 생성해야하는 장점이 있다.

SQL 맵 프로퍼티 방식

  • Map을 사용하여 키 값을 이용하여 원하는 SQL구문을 얻어온다.
  • 하나의 sqlMap 프로퍼티만 등록하면 된다.
  • Map을 프로퍼티 태그를 통해 정의할 수 없으므로 <map> 태그를 통해 map를 넣어주고, <entity> 태그를 통해 key, value를 정의한다.

SQL 제공 서비스

SQL을 설정 파일에 두어 관리하는 것은 다음과 같은 문제점들이 있다.

  • DI와 SQL에 대한 정보를 함께 관리하는 것은 바람직하지 않다.
  • SQL을 반드시 XML에 저장할 필요가 없으며 다양한 방법을 통해 SQL을 저장하고 로드하는 방법이 존재한다.
  • 설정파일로부터 생성된 오브젝트는 애플리케이션을 다시 실행하지 않고 변경이 어렵다. 즉 동적인 갱신이 어렵다.

위와 같은 이유로 SQL은 독립적인 제공 서비스가 필요하다. SQL을 독립적인 서비스로 분리하여 동적인 갱신을 가능하게 하고 확장성을 높일 수 있다.

SQL 서비스 인터페이스

SQL 서비스 인터페이스를 설계하여 독립시키는 방법이 있다. SqlService 인터페이스를 생성하여 key 값을 통해 SQL 구문을 받아오는 getSql() 메소드를 정의한다. SQL 구문을 받아오지 못했을 때 예외를 정의한다.
UserDao에서는 SqlService를 DI 받아 이 오브젝트를 통해 SQL 구문을 얻는다.

스프링 설정을 사용하는 단순 SQL 서비스

SqlService를 구현하여 SimpleSqlService를 정의한다. Map 프로퍼티를 가지고 있고 설정파일 정보를 통해 map을 얻는다. getSql() 메소드는 map을 통해 SQL 구문을 얻는다.
여전히 설정파일 정보를 통해 SQL 구문을 얻어오고 있다. 하지만 DAO를 SQL과 완전히 분리하였고 SqlService는 DAO와 관련없이 확장 가능하므로 의미를 갖는다.

7.2 인터페이스의 분리와 자기참조 빈

XML 파일 매핑

설정파일에서 bean 태그 안에 SQL 정보를 넣어놓는 것은 좋지 않다. 별도의 XML 파일을 생성하여 SQL에 관한 정보를 저장한다.

JAXB

XML을 편집하는 방법 중 하나로 전통적 방식인 DOM보다 여러 장점이 존재한다. SQL Map XML 문서(sqlmap.xml)와 SQL Map 문서에 대한 스키마(sqlmap.xsd)를 생성하여 저장한다. 이 스키마 파일을 프로젝트 루트에 두고 컴파일 한다.

마샬링과 언마샬링

바인딩 오브젝트를 XML 문서로 변환하는 것을 마샬링(marshalling)이라고 하고, 반대로 XML 문서를 읽어 자바의 오브젝트로 변환하는 것을 언마샬링(unmarsharlling)이라고 한다.

XML 파일을 이용하는 SQL 서비스

SQL 맵 XML 파일

SQL map에 관한 정보가 저장된 sqlmap.xml 파일을 생성한다.

XML SQL 서비스

SqlService가 XML 파일을 읽어 SQL Map 정보를 언마샬링 하는 기능을 수행해야 한다. 일단 생성자에서 JAXB를 통해 SQL의 정의된 XML문서를 읽어 HashMap으로 저장한다. getSql() 메소드에서는 이 저장된 HashMap을 통해 원하는 SQL 구문을 얻을 수 있다.

빈의 초기화 작업

XMLSqlService 코드는 생성자에서 예외가 발생할 수 있는 복잡한 초기화 작업이 존재한다는 문제점이 존재한다. 먼저 초기 상태를 가진 오브젝트를 생성한 뒤 초기화 메소드를 통해 초기화 하는 것이 바람직한다.
또한 XML 파일의 위치와 이름이 고정되어있다. DI를 통해 받는 것이 바람직하다.

7.4 인터페이스 상속을 통한 안전한 기능 확장

DI와 기능의 확장

DI를 의식하는 설계

DI를 적용하는 것은 어렵지 않은 일이다. 사용할 오브젝트를 직접 만들지 않고 프로퍼티로 먼저 생성한 뒤 수정자 메소드를 통해 주입을 받는다. XML 빈 설정을 이용해 DI와 관련된 설정도 필요하다. DI를 적용함으로써 런타임 시에 의존 오브젝트를 다이내믹 하게 연결할 수 있고 유연한 확장과 재사용이 가능한 설계를 할 수 있다.

DI와 인터페이스 프로그래밍

DI를 받는 오브젝트는 인터페이스를 구현한 오브젝트이다. 인터페이스를 사용하지 않고 구체 클래스로도 DI가 가능하지만 인터페이스를 사용하면 얻을 수 있는 장점이 있다.

  • 다형성을 얻을 수 있다
    하나의 인터페이스를 다르게 구현하며 의존 오브젝트의 핵심 기능 외에 프록시, 데코레이터, 어댑터와 같은 개념을 적용할 수 있다.
  • 클라이언트별 다중 인터페이스 구현
    상속과는 다르게 인터페이스 구현에는 여러 인터페이스를 동시에 구현할 수 있다. 여러 인터페이스를 구현한다는 것은 클라이언트가 의존 오브젝트에 접근하는 여러 창구가 생긴다는 의미이다. 목적과 관심이 다른 클라이언트는 인터페이스를 통해 적절하게 분리해줄 필요가 있다.

인터페이스 상속

현재 BaseSQLServiceSqlReader,SqlResister두 인터페이스에 의존하고 있다. SqlResister는 SQL 초기 등록과 검색 두가지 기능을 갖고 있다. 이 상태에서 새로운 클라이언트가 SQL 수정 기능이 필요하다면 이 기능을 추가해야한다.

SqlResister를 직접 바꾸는 것도 방법이지만 BaseSQLService 클라이언트는 기존 인터페이스의 기능만으로 충분히 동작하고 새로운 클라이언트를 위한 새로운 인터페이스를 만들 필요가 있다.

이 새로운 인터페이스 역시 기존 SqlResister 가 지닌 초기 SQL 등록과 검색 기능을 지녀야 하므로 인터페이스 상속을 이용하는 것이 좋다.

SQL 수정 기능이 필요한 클라이언트를 SqlAdminService라고 하자. BaseSQLServiceSqlAdminService 모두 UpdatableSqlResigtry를 구현한 오브젝트에 의존하고 있지만 SqlResitryUpdatableSqlResigtry라는 다른 인터페이스에 의존하고 있다.

7.5 DI를 이용해 다양한 구현 방법 적용하기

ConcurrentHashMap을 이용한 수정 가능 SQL 레지스트리

기존 SqlResister는 초기 등록과 조회 두 가지 기능만 존재하여 멀티스레드 환경에서 문제가 되지 않았다. SQL 수정 기능이 추가된 UpdatableSqlResigtry는 멀티스레드 환경에서 동시에 수정이 진행되면 문제가 발생할 수 있다.

지금까지는 HashMap를 사용하여 SQL을 관리하였다. 이 HashMap는 멀티스레드 환경에서 안전하게 사용되기 위해 Collections.synchronizeMap()이라는 외부에서의 동기화 작업이 추가로 필요하다. 이 작업은 오버헤드가 커 권장되지 않는다.
따라서 동기화된 해시 데이터 조작에 특화된 ConcurrentHashMap를 사용한다.

내장형 데이터베이스를 이용한 SQL 레지스트리 만들기

ConcurrentHashMap을 이용하여 멀티스레드 환경에서의 SQL 수정을 할 수 있지만 데이터의 양이 늘어나고, 조회와 변경이 잦아지게 되면 성능이 충분하지 않다. 이를 위해 동시에 많은 요청을 처리하고 관리할 수 있는 데이터베이스를 적용할 수 있다. 하지만 SQL 저장만을 위해 데이터베이스를 구성하는 것은 과하므로 내장형 데이터베이스를 통해 SQL을 저장하고 관리하는 것이 좋다.

내장형 데이터베이스는 애플리케이션과 함께 시작되고 종료된다. 내장형 데이터베이스를 사용하면 SQL을 관리하면 별도의 IO가 없어 성능이 뛰어나다.

스프링의 내장형 DB 지원 기능

자바에서 많이 사용되는 내장형 데이터베이스는 Derby, HSQL, H2가 있다. 모두 JDBC 드라이버를 제공한다. JDBC 접근방식이라고 하여 기존 DataSource와 DAO를 사용하는 모델을 그대로 사용하는 것은 좋지 않다. 별도의 작업을 통해 애플리케이션 내에서 DB를 기동시키고 초기화 해야 한다.

DataSource 인터페이스를 상속하여 shutdown() 메소드를 추가한 EmbeddedDatabase 인터페이스가 이용된다.

트랜잭션 적용

EmbeddedSqlResitry는 내장형 DB를 사용하기 때문에 멀티스레드 환경에서 수정 작업이 안전하게 수행될 수 있다. 하지만 updateSql() 메소드에서 여러개의 SQL을 map로 받아 수정하는 경우 하나의 키 값이 발견되지 않으면 예외가 발생하게 된다.
각각의 수정은 별도의 트랜잭션으로 이루어져 예외가 발생한 부분 외에는 수정이 적용되고 이는 비즈니스 로직의 맞지 않다.

HashMap을 사용했다면 트랜잭션 경계 설정이 매우 복잡하지만 내장형 DB의 경우 트랜잭션 기반 작업에 충실하게 설계되어 트랜잭션 경계설정을 적용하기가 용이하다. SQL 레지스트리는 제한된 오브젝트만을 다루는 작업이므로 AOP처럼 거창한 방법 보다는 트랜잭션 추상화 API를 사용하는 것이 편리하다.

0개의 댓글